From 9aeda3c380b0cac4547c5ddfb941fc81fd047bdc Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Tue, 27 Jul 2021 07:26:02 -0700 Subject: [PATCH 01/31] Add github workflows and prepare for publishing to PyPI --- .github/workflows/publish-to-test-pypi.yml | 45 ++++++++++++++++++++++ .gitignore | 1 - README.md | 2 +- setup.py | 14 +++---- 4 files changed, 53 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/publish-to-test-pypi.yml diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml new file mode 100644 index 0000000..9278144 --- /dev/null +++ b/.github/workflows/publish-to-test-pypi.yml @@ -0,0 +1,45 @@ +# adapted from https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/ + +name: Publish Python distributions to PyPI and TestPyPI + +on: push + +jobs: + build-n-publish: + name: Build and publish Python distributions to PyPI and TestPyPI + runs-on: ubuntu-18.04 + + steps: + - uses: actions/checkout@master + with: + fetch-depth: 0 + - run: | + git fetch origin +refs/tags/*:refs/tags/* + - name: Set up Python 3.7 + uses: actions/setup-python@v1 + with: + python-version: 3.7 + - name: Install pep517 + run: >- + python -m + pip install + pep517 + --user + - name: Build a binary wheel and a source tarball + run: >- + python -m + pep517.build + --binary + --out-dir dist/ + . + - name: Publish distribution to Test PyPI + uses: pypa/gh-action-pypi-publish@master + with: + skip_existing: true + password: ${{ secrets.test_pypi_password }} + repository_url: https://test.pypi.org/legacy/ + - name: Publish distribution to PyPI + if: startsWith(github.event.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@master + with: + password: ${{ secrets.pypi_password }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2acf4fe..19c9e4c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -/test_project /chia_dev_tools.egg-info /venv /dist diff --git a/README.md b/README.md index b446a2e..591641b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -Chialisp Dev Utility +Chia Dev Tools ======= Install diff --git a/setup.py b/setup.py index 68a547e..6e2d344 100644 --- a/setup.py +++ b/setup.py @@ -32,24 +32,24 @@ package_data={ "": ["*.clvm", "*.clvm.hex", "*.clib", "*.clsp", "*.clsp.hex"], }, - author_email="quexington@gmail.com", + author_email="m.hauff@chia.net", setup_requires=["setuptools_scm"], install_requires=dependencies, - url="https://github.com/Quexington", - license="https://mit-license.org/", - description="Chialisp development utility", + url="https://github.com/Chia-Network", + license="https://opensource.org/licenses/Apache-2.0", + description="Chia development commands", long_description=long_description, long_description_content_type="text/markdown", classifiers=[ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", - "License :: OSI Approved :: MIT License", + "License :: OSI Approved :: Apache Software License", "Topic :: Security :: Cryptography", ], extras_require=dict(dev=dev_dependencies,), project_urls={ - "Bug Reports": "https://github.com/Quexington/chialisp_dev_utility", - "Source": "https://github.com/Quexington/chialisp_dev_utility", + "Bug Reports": "https://github.com/Chia-Network/chia-dev-tools", + "Source": "https://github.com/Chia-Network/chia-dev-tools", }, ) From 6f26e453f737c99d91edf0909fb7e099b9199af0 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Tue, 27 Jul 2021 20:06:10 -0700 Subject: [PATCH 02/31] Fixed a broken include command --- cdv/cmds/clsp.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cdv/cmds/clsp.py b/cdv/cmds/clsp.py index 461b577..273f674 100644 --- a/cdv/cmds/clsp.py +++ b/cdv/cmds/clsp.py @@ -109,8 +109,7 @@ def retrieve_cmd(libraries): include_path = Path(os.getcwd()).joinpath("include") if not include_path.exists(): os.mkdir("include") - retrieve_path = include_path.joinpath(f"{lib}.clib") - if retrieve_path.exists(): - shutil.copyfile(src_path, retrieve_path) + if src_path.exists(): + shutil.copyfile(src_path, include_path.joinpath(f"{lib}.clib")) else: print(f"Could not find {lib}.clib. You can always create it in ./include yourself.") \ No newline at end of file From 2fa8b1acf77b96b9f4197e031d62faec7cf04418 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Mon, 2 Aug 2021 08:58:58 -0700 Subject: [PATCH 03/31] Added an __init__.py file when creating test env --- cdv/cmds/cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cdv/cmds/cli.py b/cdv/cmds/cli.py index 15cffe7..2e24e81 100644 --- a/cdv/cmds/cli.py +++ b/cdv/cmds/cli.py @@ -57,6 +57,8 @@ def test_cmd(tests: str, discover: bool, init: str): src_path = Path(testlib.__file__).parent.joinpath("test_skeleton.py") dest_path = test_dir.joinpath("test_skeleton.py") shutil.copyfile(src_path, dest_path) + dest_path_init = test_dir.joinpath("__init__.py") + open(dest_path_init,"w") if discover: pytest.main(["--collect-only",*test_paths]) elif not init: From f7e926a4caed6a49c8ab4503a6f09bea20c457bc Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Mon, 2 Aug 2021 09:20:34 -0700 Subject: [PATCH 04/31] Update python package version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6e2d344..e00ea80 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ setup( name="chia_dev_tools", - version="0.1.0", + version="1.0.1", packages=find_packages(), author="Quexington", entry_points={ From fcd15a91f29766956c413a2768459c698b7e4a24 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Wed, 4 Aug 2021 10:01:50 -0700 Subject: [PATCH 05/31] Added some new flags to the spendbundle inspection --- cdv/cmds/chia_inspect.py | 48 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/cdv/cmds/chia_inspect.py b/cdv/cmds/chia_inspect.py index 48667b8..fd4a377 100644 --- a/cdv/cmds/chia_inspect.py +++ b/cdv/cmds/chia_inspect.py @@ -183,15 +183,22 @@ def do_inspect_coin_spend_cmd(ctx, spends, print_results=True, **kwargs): @click.argument("bundles", nargs=-1, required=False) @click.option("-s","--spend", multiple=True, help="A coin spend object to add to the bundle") @click.option("-as","--aggsig", multiple=True, help="A BLS signature to aggregate into the bundle (can be used more than once)") +@click.option("-db","--debug", is_flag=True, help="Show debugging information about the bundles") +@click.option("-sd","--signable_data", is_flag=True, help="Print the data that needs to be signed in the bundles") +@click.option("-n","--network", default="mainnet", show_default=True, help="The network this spend bundle will be pushed to (for AGG_SIG_ME)") @click.pass_context def inspect_spend_bundle_cmd(ctx, bundles, **kwargs): do_inspect_spend_bundle_cmd(ctx, bundles, **kwargs) def do_inspect_spend_bundle_cmd(ctx, bundles, print_results=True, **kwargs): - if kwargs and (len(kwargs['spend']) > 0) and (len(kwargs['aggsig']) > 0): + if kwargs and (len(kwargs['spend']) > 0): + if (len(kwargs['aggsig']) > 0): + sig = AugSchemeMPL.aggregate([G2Element(bytes.fromhex(sanitize_bytes(sig))) for sig in kwargs["aggsig"]]) + else: + sig = G2Element() spend_bundle_objs = [SpendBundle( do_inspect_coin_spend_cmd(ctx, kwargs["spend"], print_results=False), - AugSchemeMPL.aggregate([G2Element(bytes.fromhex(sanitize_bytes(aggsig))) for aggsig in kwargs["aggsig"]]) + sig )] else: spend_bundle_objs = [] @@ -205,5 +212,42 @@ def do_inspect_spend_bundle_cmd(ctx, bundles, print_results=True, **kwargs): if print_results: inspect_callback(spend_bundle_objs, ctx, id_calc=(lambda e: e.name()), type='SpendBundle') + if kwargs: + if kwargs["debug"]: + print(f"") + print(f"Debugging Information") + print(f"---------------------") + for bundle in spend_bundle_objs: + print(bundle.debug()) + if kwargs["signable_data"]: + print(f"") + print(f"Public Key/Message Pairs") + print(f"------------------------") + for obj in spend_bundle_objs: + for coin_spend in obj.coin_spends: + err, conditions_dict, _ = conditions_dict_for_solution( + coin_spend.puzzle_reveal, coin_spend.solution, INFINITE_COST + ) + if err or conditions_dict is None: + print(f"Generating conditions failed, con:{conditions_dict}, error: {err}") + else: + from chia.util.default_root import DEFAULT_ROOT_PATH + from chia.util.config import load_config + config = load_config(DEFAULT_ROOT_PATH, "config.yaml") + genesis_challenge = config["network_overrides"]["constants"][kwargs["network"]]["GENESIS_CHALLENGE"] + pkm_dict = {} + for pk, msg in pkm_pairs_for_conditions_dict( + conditions_dict, + coin_spend.coin.name(), + bytes.fromhex(genesis_challenge), + ): + if str(pk) in pkm_dict: + pkm_dict[str(pk)].append(msg) + else: + pkm_dict[str(pk)] = [msg] + for pk, msgs in pkm_dict.items(): + print(f"{pk}:") + for msg in msgs: + print(f"\t- {msg.hex()}") else: return spend_bundle_objs \ No newline at end of file From 897c114a8ab33824009080d9929fc00a092e16d5 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Wed, 4 Aug 2021 10:03:56 -0700 Subject: [PATCH 06/31] Added a cost flag to the spends and spendbundles inspection --- cdv/cmds/chia_inspect.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/cdv/cmds/chia_inspect.py b/cdv/cmds/chia_inspect.py index fd4a377..d694665 100644 --- a/cdv/cmds/chia_inspect.py +++ b/cdv/cmds/chia_inspect.py @@ -5,10 +5,15 @@ from blspy import AugSchemeMPL, G2Element +from chia.types.blockchain_format.program import INFINITE_COST, Program from chia.types.blockchain_format.coin import Coin from chia.types.coin_spend import CoinSpend from chia.types.spend_bundle import SpendBundle +from chia.consensus.cost_calculator import calculate_cost_of_program +from chia.full_node.mempool_check_conditions import get_name_puzzle_conditions +from chia.full_node.bundle_tools import simple_solution_generator from chia.util.ints import uint64 +from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict from cdv.cmds.clsp import parse_program @@ -137,11 +142,20 @@ def do_inspect_coin_cmd(ctx, coins, print_results=True, **kwargs): @click.option("-a","--amount", help="The amount of the coin being spent") @click.option("-pr","--puzzle-reveal", help="The program that is hashed into this coin") @click.option("-s","--solution", help="The attempted solution to the puzzle") +@click.option("-ec","--cost", is_flag=True, help="Print the CLVM cost of the spend") +@click.option("-bc","--cost-per-byte", default=12000, show_default=True, help="The cost per byte in the puzzle and solution reveal to use when calculating cost") @click.pass_context def inspect_coin_cmd(ctx, spends, **kwargs): do_inspect_coin_spend_cmd(ctx, spends, **kwargs) def do_inspect_coin_spend_cmd(ctx, spends, print_results=True, **kwargs): + cost_flag = False + cost_per_byte = 12000 + if kwargs: + cost_flag = kwargs["cost"] + cost_per_byte = kwargs["cost_per_byte"] + del kwargs["cost"] + del kwargs["cost_per_byte"] if kwargs and all([kwargs['puzzle_reveal'], kwargs['solution']]): if (not kwargs['coin']) and all([kwargs['parent_id'], kwargs['puzzle_hash'], kwargs['amount']]): coin_spend_objs = [CoinSpend( @@ -176,6 +190,12 @@ def do_inspect_coin_spend_cmd(ctx, spends, print_results=True, **kwargs): if print_results: inspect_callback(coin_spend_objs, ctx, id_calc=(lambda e: e.coin.name()), type='CoinSpend') + if cost_flag: + for coin_spend in coin_spend_objs: + program = simple_solution_generator(SpendBundle([coin_spend], G2Element())) + npc_result = get_name_puzzle_conditions(program, INFINITE_COST, cost_per_byte=cost_per_byte, safe_mode=True) + cost = calculate_cost_of_program(program, npc_result, cost_per_byte) + print(f"Cost: {cost}") else: return coin_spend_objs @@ -186,6 +206,8 @@ def do_inspect_coin_spend_cmd(ctx, spends, print_results=True, **kwargs): @click.option("-db","--debug", is_flag=True, help="Show debugging information about the bundles") @click.option("-sd","--signable_data", is_flag=True, help="Print the data that needs to be signed in the bundles") @click.option("-n","--network", default="mainnet", show_default=True, help="The network this spend bundle will be pushed to (for AGG_SIG_ME)") +@click.option("-ec","--cost", is_flag=True, help="Print the CLVM cost of the spend") +@click.option("-bc","--cost-per-byte", default=12000, show_default=True, help="The cost per byte in the puzzle and solution reveal to use when calculating cost") @click.pass_context def inspect_spend_bundle_cmd(ctx, bundles, **kwargs): do_inspect_spend_bundle_cmd(ctx, bundles, **kwargs) @@ -213,6 +235,12 @@ def do_inspect_spend_bundle_cmd(ctx, bundles, print_results=True, **kwargs): if print_results: inspect_callback(spend_bundle_objs, ctx, id_calc=(lambda e: e.name()), type='SpendBundle') if kwargs: + if kwargs["cost"]: + for spend_bundle in spend_bundle_objs: + program = simple_solution_generator(spend_bundle) + npc_result = get_name_puzzle_conditions(program, INFINITE_COST, cost_per_byte=kwargs["cost_per_byte"], safe_mode=True) + cost = calculate_cost_of_program(program, npc_result, kwargs["cost_per_byte"]) + print(f"Cost: {cost}") if kwargs["debug"]: print(f"") print(f"Debugging Information") From a0387a055d4bd0088f3a5edf1c0f15d7b80d5c42 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Wed, 4 Aug 2021 10:11:00 -0700 Subject: [PATCH 07/31] Added CoinRecords to be inspected --- cdv/cmds/chia_inspect.py | 66 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/cdv/cmds/chia_inspect.py b/cdv/cmds/chia_inspect.py index d694665..69cdf89 100644 --- a/cdv/cmds/chia_inspect.py +++ b/cdv/cmds/chia_inspect.py @@ -8,6 +8,7 @@ from chia.types.blockchain_format.program import INFINITE_COST, Program from chia.types.blockchain_format.coin import Coin from chia.types.coin_spend import CoinSpend +from chia.types.coin_record import CoinRecord from chia.types.spend_bundle import SpendBundle from chia.consensus.cost_calculator import calculate_cost_of_program from chia.full_node.mempool_check_conditions import get_name_puzzle_conditions @@ -86,7 +87,7 @@ def inspect_any_cmd(ctx, objects): input_objects = [] for obj in objects: in_obj = obj - for cls in [Coin, CoinSpend, SpendBundle]: + for cls in [Coin, CoinSpend, SpendBundle, CoinRecord]: try: in_obj = streamable_load(cls, [obj])[0] except Exception as e: @@ -102,6 +103,8 @@ def inspect_any_cmd(ctx, objects): do_inspect_coin_spend_cmd(ctx, [obj]) elif type(obj) == SpendBundle: do_inspect_spend_bundle_cmd(ctx, [obj]) + elif type(obj) == CoinRecord: + do_inspect_coin_record_cmd(ctx, [obj]) @inspect_cmd.command("coins", short_help="Various methods for examining and calculating coin objects") @@ -278,4 +281,63 @@ def do_inspect_spend_bundle_cmd(ctx, bundles, print_results=True, **kwargs): for msg in msgs: print(f"\t- {msg.hex()}") else: - return spend_bundle_objs \ No newline at end of file + return spend_bundle_objs + +@inspect_cmd.command("coinrecords", short_help="Various methods for examining and calculating CoinRecord objects") +@click.argument("records", nargs=-1, required=False) +@click.option("-c","--coin", help="The coin to spend (replaces -pid, -ph, -a)") +@click.option("-pid","--parent-id", help="The parent coin's ID") +@click.option("-ph","--puzzle-hash", help="The tree hash of the CLVM puzzle that locks the coin being spent") +@click.option("-a","--amount", help="The amount of the coin being spent") +@click.option("-cb","--coinbase", is_flag=True, help="Is this coin generated as a farming reward?") +@click.option("-ci","--confirmed-block-index", help="The block index in which this coin was created") +@click.option("-s","--spent", is_flag=True, help="Has the coin been spent?") +@click.option("-si","--spent-block-index", default=0, show_default=True, type=int, help="The block index in which this coin was spent") +@click.option("-t","--timestamp", help="The timestamp of the block in which this coin was created") +@click.pass_context +def inspect_coin_record_cmd(ctx, records, **kwargs): + do_inspect_coin_record_cmd(ctx, records, **kwargs) + +def do_inspect_coin_record_cmd(ctx, records, print_results=True, **kwargs): + if kwargs and all([kwargs['confirmed_block_index'], kwargs['timestamp']]): + if (not kwargs['coin']) and all([kwargs['parent_id'], kwargs['puzzle_hash'], kwargs['amount']]): + coin_record_objs = [CoinRecord( + Coin( + bytes.fromhex(kwargs['parent_id']), + bytes.fromhex(kwargs['puzzle_hash']), + uint64(kwargs['amount']), + ), + kwargs["confirmed_block_index"], + kwargs["spent_block_index"], + kwargs["spent"], + kwargs["coinbase"], + kwargs["timestamp"], + )] + elif kwargs['coin']: + coin_record_objs = [CoinRecord( + do_inspect_coin_cmd(ctx, [kwargs['coin']], print_results=False)[0], + kwargs["confirmed_block_index"], + kwargs["spent_block_index"], + kwargs["spent"], + kwargs["coinbase"], + kwargs["timestamp"], + )] + else: + print("Invalid arguments specified.") + elif not kwargs or not any([kwargs[key] for key in kwargs.keys()]): + coin_record_objs = [] + try: + if type(records[0]) == str: + coin_record_objs = streamable_load(CoinRecord, records) + else: + coin_record_objs = records + except: + print("One or more of the specified objects was not a coin record") + else: + print("Invalid arguments specified.") + return + + if print_results: + inspect_callback(coin_record_objs, ctx, id_calc=(lambda e: e.coin.name()), type='CoinRecord') + else: + return coin_record_objs From 717a7486bfbbb15d86dd79594db9c7c3811c34bc Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Wed, 4 Aug 2021 14:31:36 -0700 Subject: [PATCH 08/31] Added Programs to be inspected --- cdv/cmds/chia_inspect.py | 42 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/cdv/cmds/chia_inspect.py b/cdv/cmds/chia_inspect.py index 69cdf89..096ec30 100644 --- a/cdv/cmds/chia_inspect.py +++ b/cdv/cmds/chia_inspect.py @@ -31,10 +31,16 @@ def inspect_cmd(ctx, **kwargs): def inspect_callback(objs, ctx, id_calc=None, type='Unknown'): if not any([value for key, value in ctx.obj.items()]): - pprint([obj.to_json_dict() for obj in objs]) + if getattr(objs[0], "to_json_dict", None): + pprint([obj.to_json_dict() for obj in objs]) + else: + pprint(f"Object of type {type} cannot be serialized to JSON") else: if ctx.obj['json']: - pprint([obj.to_json_dict() for obj in objs]) + if getattr(obj, "to_json_dict", None): + pprint([obj.to_json_dict() for obj in objs]) + else: + pprint(f"Object of type {type} cannot be serialized to JSON") if ctx.obj['bytes']: final_output = [] for obj in objs: @@ -87,11 +93,18 @@ def inspect_any_cmd(ctx, objects): input_objects = [] for obj in objects: in_obj = obj + # Try it as Streamable types for cls in [Coin, CoinSpend, SpendBundle, CoinRecord]: try: in_obj = streamable_load(cls, [obj])[0] - except Exception as e: + except: pass + # Try it as a Program + try: + in_obj = parse_program(obj) + except: + pass + input_objects.append(in_obj) for obj in input_objects: @@ -105,6 +118,8 @@ def inspect_any_cmd(ctx, objects): do_inspect_spend_bundle_cmd(ctx, [obj]) elif type(obj) == CoinRecord: do_inspect_coin_record_cmd(ctx, [obj]) + elif type(obj) == Program: + do_inspect_program_cmd(ctx, [obj]) @inspect_cmd.command("coins", short_help="Various methods for examining and calculating coin objects") @@ -341,3 +356,24 @@ def do_inspect_coin_record_cmd(ctx, records, print_results=True, **kwargs): inspect_callback(coin_record_objs, ctx, id_calc=(lambda e: e.coin.name()), type='CoinRecord') else: return coin_record_objs + +@inspect_cmd.command("programs", short_help="Various methods for examining CLVM Program objects") +@click.argument("programs", nargs=-1, required=False) +@click.pass_context +def inspect_program_cmd(ctx, programs, **kwargs): + do_inspect_program_cmd(ctx, programs, **kwargs) + +def do_inspect_program_cmd(ctx, programs, print_results=True, **kwargs): + program_objs = [] + try: + if type(programs[0]) == str: + program_objs = [parse_program(prog) for prog in programs] + else: + program_objs = programs + except: + print("One or more of the specified objects was not a Program") + + if print_results: + inspect_callback(program_objs, ctx, id_calc=(lambda e: e.get_tree_hash()), type='Program') + else: + return program_objs From ec4e2970627997f104d4754c83e0509c29f20427 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Wed, 4 Aug 2021 14:32:25 -0700 Subject: [PATCH 09/31] Added functions for deriving all sorts of keys --- cdv/cmds/chia_inspect.py | 101 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 2 deletions(-) diff --git a/cdv/cmds/chia_inspect.py b/cdv/cmds/chia_inspect.py index 096ec30..765e7b8 100644 --- a/cdv/cmds/chia_inspect.py +++ b/cdv/cmds/chia_inspect.py @@ -2,8 +2,9 @@ import json from pprint import pprint +from secrets import token_bytes -from blspy import AugSchemeMPL, G2Element +from blspy import AugSchemeMPL, PrivateKey, G1Element, G2Element from chia.types.blockchain_format.program import INFINITE_COST, Program from chia.types.blockchain_format.coin import Coin @@ -13,7 +14,14 @@ from chia.consensus.cost_calculator import calculate_cost_of_program from chia.full_node.mempool_check_conditions import get_name_puzzle_conditions from chia.full_node.bundle_tools import simple_solution_generator -from chia.util.ints import uint64 +from chia.wallet.derive_keys import _derive_path +from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( + DEFAULT_HIDDEN_PUZZLE_HASH, + calculate_synthetic_secret_key, + calculate_synthetic_public_key, +) +from chia.util.keychain import mnemonic_to_seed, bytes_to_mnemonic +from chia.util.ints import uint64, uint32 from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict from cdv.cmds.clsp import parse_program @@ -99,6 +107,12 @@ def inspect_any_cmd(ctx, objects): in_obj = streamable_load(cls, [obj])[0] except: pass + # Try it as some key stuff + for cls in [G1Element, G2Element, PrivateKey]: + try: + in_obj = cls.from_bytes(bytes.fromhex(sanitize_bytes(obj))) + except: + pass # Try it as a Program try: in_obj = parse_program(obj) @@ -120,6 +134,10 @@ def inspect_any_cmd(ctx, objects): do_inspect_coin_record_cmd(ctx, [obj]) elif type(obj) == Program: do_inspect_program_cmd(ctx, [obj]) + elif type(obj) == G1Element: + do_inspect_keys_cmd(ctx, public_key=obj) + elif type(obj) == PrivateKey: + do_inspect_keys_cmd(ctx, secret_key=obj) @inspect_cmd.command("coins", short_help="Various methods for examining and calculating coin objects") @@ -377,3 +395,82 @@ def do_inspect_program_cmd(ctx, programs, print_results=True, **kwargs): inspect_callback(program_objs, ctx, id_calc=(lambda e: e.get_tree_hash()), type='Program') else: return program_objs + +@inspect_cmd.command("keys", short_help="Various methods for examining BLS Public Keys") +@click.option("-pk","--public-key", help="A BLS public key") +@click.option("-sk","--secret-key", help="The secret key from which to derive the public key") +@click.option("-m","--mnemonic", help="A 24 word mnemonic from which to derive the secret key") +@click.option("-pw","--passphrase", default="", show_default=True, help="A passphrase to use when deriving a secret key from mnemonic") +@click.option("-r","--random", is_flag=True, help="Generate a random set of keys") +@click.option("-hd","--hd-path", help="Enter the HD path in the form 'm/12381/8444/n/n'") +@click.option("-t","--key-type", type=click.Choice(["farmer","pool","wallet","local","backup","owner","auth"]), help="Automatically use a chia defined HD path for a specific service") +@click.option("-sy","--synthetic", is_flag=True, help="Use a hidden puzzle hash (-ph) to calculate a synthetic secret/public key") +@click.option("-ph","--hidden-puzhash", default=DEFAULT_HIDDEN_PUZZLE_HASH.hex(), show_default=False, help="The hidden puzzle to use when calculating a synthetic key") +@click.pass_context +def inspect_keys_cmd(ctx, **kwargs): + do_inspect_keys_cmd(ctx, **kwargs) + +def do_inspect_keys_cmd(ctx, print_results=True, **kwargs): + sk = None + pk = None + path = "m" + if len(kwargs) == 1: + if "secret_key" in kwargs: + sk = kwargs["secret_key"] + pk = sk.get_g1() + elif "public_key" in kwargs: + pk = kwargs["public_key"] + else: + condition_list = [kwargs["public_key"], kwargs["secret_key"], kwargs["mnemonic"], kwargs["random"]] + def one_or_zero(value): + return 1 if value else 0 + if sum([one_or_zero(condition) for condition in condition_list]) == 1: + if kwargs["public_key"]: + sk = None + pk = G1Element.from_bytes(bytes.fromhex(sanitize_bytes(kwargs["public_key"]))) + elif kwargs["secret_key"]: + sk = PrivateKey.from_bytes(bytes.fromhex(sanitize_bytes(kwargs["secret_key"]))) + pk = sk.get_g1() + elif kwargs["mnemonic"]: + seed = mnemonic_to_seed(kwargs["mnemonic"], kwargs["passphrase"]) + sk = AugSchemeMPL.key_gen(seed) + pk = sk.get_g1() + elif kwargs["random"]: + sk = AugSchemeMPL.key_gen(mnemonic_to_seed(bytes_to_mnemonic(token_bytes(32)),"")) + pk = sk.get_g1() + + if kwargs["hd_path"] and (kwargs["hd_path"] != "m"): + path = [uint32(int(i)) for i in kwargs["hd_path"].split("/") if i != "m"] + elif kwargs["key_type"]: + case = kwargs["key_type"] + if case == "farmer": + path = [12381, 8444, 0, 0] + if case == "pool": + path = [12381, 8444, 1, 0] + if case == "wallet": + path = [12381, 8444, 2, 0] + if case == "local": + path = [12381, 8444, 3, 0] + if case == "backup": + path = [12381, 8444, 4, 0] + if case == "owner": + path = [12381, 8444, 5, 0] + if case == "auth": + path = [12381, 8444, 6, 0] + if path != "m": + sk = _derive_path(sk, path) + pk = sk.get_g1() + path = "m/" + "/".join([str(e) for e in path]) + + if kwargs["synthetic"]: + if sk: + sk = calculate_synthetic_secret_key(sk, bytes.fromhex(kwargs["hidden_puzhash"])) + pk = calculate_synthetic_public_key(pk, bytes.fromhex(kwargs["hidden_puzhash"])) + else: + print("Invalid arguments specified.") + + if sk: + print(f"Secret Key: {bytes(sk).hex()}") + print(f"Public Key: {str(pk)}") + print(f"Fingerprint: {str(pk.get_fingerprint())}") + print(f"HD Path: {path}") From 4fa3ba51a0a7762e723ecd7858b80d2ecb4f6154 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Wed, 4 Aug 2021 15:17:18 -0700 Subject: [PATCH 10/31] Added command to construct signatures --- cdv/cmds/chia_inspect.py | 46 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/cdv/cmds/chia_inspect.py b/cdv/cmds/chia_inspect.py index 765e7b8..e04df48 100644 --- a/cdv/cmds/chia_inspect.py +++ b/cdv/cmds/chia_inspect.py @@ -474,3 +474,49 @@ def one_or_zero(value): print(f"Public Key: {str(pk)}") print(f"Fingerprint: {str(pk.get_fingerprint())}") print(f"HD Path: {path}") + + +class OrderedParamsCommand(click.Command): + _options = [] + + def parse_args(self, ctx, args): + # run the parser for ourselves to preserve the passed order + parser = self.make_parser(ctx) + opts, _, param_order = parser.parse_args(args=list(args)) + for param in param_order: + if param.name != "help": + type(self)._options.append((param, opts[param.name].pop(0))) + + # return "normal" parse results + return super().parse_args(ctx, args) + +@inspect_cmd.command("signatures", cls=OrderedParamsCommand, short_help="Various methods for examining BLS aggregated signatures") +@click.option("-sk","--secret-key", multiple=True, help="A secret key to sign a message with") +@click.option("-t","--utf-8", multiple=True, help="A UTF-8 message to be signed with the specified secret key") +@click.option("-b","--bytes", multiple=True, help="A hex message to be signed with the specified secret key") +@click.option("-sig","--aggsig", multiple=True, help="A signature to be aggregated") +@click.pass_context +def inspect_sigs_cmd(ctx, **kwargs): + do_inspect_sigs_cmd(ctx, **kwargs) + +def do_inspect_sigs_cmd(ctx, print_results=True, **kwargs): + base = G2Element() + sk = None + for param, value in OrderedParamsCommand._options: + if param.name == "secret_key": + sk = PrivateKey.from_bytes(bytes.fromhex(sanitize_bytes(value))) + elif param.name == "aggsig": + new_sig = G2Element.from_bytes(bytes.fromhex(sanitize_bytes(value))) + base = AugSchemeMPL.aggregate([base, new_sig]) + elif sk: + if param.name == "utf_8": + new_sig = AugSchemeMPL.sign(sk, bytes(value, "utf-8")) + base = AugSchemeMPL.aggregate([base, new_sig]) + if param.name == "bytes": + new_sig = AugSchemeMPL.sign(sk, bytes.fromhex(sanitize_bytes(value))) + base = AugSchemeMPL.aggregate([base, new_sig]) + + if print_results: + print(str(base)) + else: + return base From 7a67a51d3aff444b4c2f1172d13cdf0619f638f3 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Wed, 4 Aug 2021 15:18:09 -0700 Subject: [PATCH 11/31] Added a check to remove 0x if necessary in program parsing --- cdv/cmds/clsp.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cdv/cmds/clsp.py b/cdv/cmds/clsp.py index 273f674..609fbc6 100644 --- a/cdv/cmds/clsp.py +++ b/cdv/cmds/clsp.py @@ -28,17 +28,21 @@ def parse_program(program: str, include=[]): if '(' in program: prog = Program.to(assemble(program)) elif '.' not in program: - prog = Program.from_bytes(bytes.fromhex(program)) + #TODO: Replace this and the one below with sanitize_bytes (inspect) once that's not circular + sanitized = program[2:] if program[:2] == "0x" else program + prog = Program.from_bytes(bytes.fromhex(sanitized)) else: with open(program, "r") as file: filestring = file.read() if '(' in filestring: + # TODO: This should probably be more robust if re.compile('\(mod\s').search(filestring): prog = Program.to(compile_clvm_text(filestring, append_include(include))) else: prog = Program.to(assemble(filestring)) else: - prog = Program.from_bytes(bytes.fromhex(filestring)) + sanitized = filestring[2:] if filestring[:2] == "0x" else filestring + prog = Program.from_bytes(bytes.fromhex(sanitized)) return prog @clsp_cmd.command("build", short_help="Build all specified CLVM files (i.e mypuz.clsp or ./puzzles/*.clsp)") From 1dcdbe89c7621bce7cd583df98c116a43f20c0ab Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Thu, 5 Aug 2021 13:01:12 -0700 Subject: [PATCH 12/31] Added RPCs for the entire full node --- cdv/cmds/rpc.py | 119 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 108 insertions(+), 11 deletions(-) diff --git a/cdv/cmds/rpc.py b/cdv/cmds/rpc.py index e0cdec4..4b585ea 100644 --- a/cdv/cmds/rpc.py +++ b/cdv/cmds/rpc.py @@ -9,10 +9,11 @@ from chia.util.default_root import DEFAULT_ROOT_PATH from chia.util.config import load_config from chia.util.ints import uint16 +from chia.util.misc import format_bytes from chia.types.spend_bundle import SpendBundle from chia.types.blockchain_format.coin import Coin -from cdv.cmds.chia_inspect import fake_context, do_inspect_spend_bundle_cmd +from cdv.cmds.chia_inspect import fake_context, do_inspect_spend_bundle_cmd, sanitize_bytes @click.group("rpc", short_help="Make RPC requests to a Chia full node") def rpc_cmd(): @@ -46,14 +47,21 @@ async def do_command(): asyncio.get_event_loop().run_until_complete(do_command()) -@rpc_cmd.command("blocks", short_help="Gets blocks between two indexes (get_blocks)") -@click.option("-s","--start", required=True, help="The block index to start at (included)") -@click.option("-e","--end", required=True, help="The block index to end at (excluded)") -def rpc_blocks_cmd(start, end): +@rpc_cmd.command("blocks", short_help="Gets blocks between two indexes (get_block(s))") +@click.option("-hh","--header-hash", help="The header hash of the block to get") +@click.option("-s","--start", help="The block index to start at (included)") +@click.option("-e","--end", help="The block index to end at (excluded)") +def rpc_blocks_cmd(header_hash, start, end): async def do_command(): try: node_client = await get_client() - blocks = await node_client.get_all_block(start, end) + if header_hash: + blocks = [await node_client.get_block(bytes.fromhex(sanitize_bytes(header_hash)))] + elif start and end: + blocks = await node_client.get_all_block(start, end) + else: + print("Invalid arguments specified") + return pprint([block.to_json_dict() for block in blocks]) finally: node_client.close() @@ -61,14 +69,25 @@ async def do_command(): asyncio.get_event_loop().run_until_complete(do_command()) -@rpc_cmd.command("blockrecords", short_help="Gets block records between two indexes (get_block_records)") -@click.option("-s","--start", required=True, help="The block index to start at (included)") -@click.option("-e","--end", required=True, help="The block index to end at (excluded)") -def rpc_blockrecords_cmd(start, end): +@rpc_cmd.command("blockrecords", short_help="Gets block records between two indexes (get_block_record(s), get_block_record_by_height)") +@click.option("-hh","--header-hash", help="The header hash of the block to get") +@click.option("-i","--height", help="The height of the block to get") +@click.option("-s","--start", help="The block index to start at (included)") +@click.option("-e","--end", help="The block index to end at (excluded)") +def rpc_blockrecords_cmd(header_hash, height, start, end): async def do_command(): try: node_client = await get_client() - block_records = await node_client.get_block_records(start, end) + if header_hash: + block_record = await node_client.get_block_record(bytes.fromhex(sanitize_bytes(header_hash))) + block_records = block_record.to_json_dict() if block_record else [] + elif height: + block_record = await node_client.get_block_record_by_height(height) + block_records = block_record.to_json_dict() if block_record else [] + elif start and end: + block_records = await node_client.get_block_records(start, end) + else: + print("Invalid arguments specified") pprint(block_records) finally: node_client.close() @@ -76,6 +95,58 @@ async def do_command(): asyncio.get_event_loop().run_until_complete(do_command()) +@rpc_cmd.command("unfinished", short_help="Returns the current unfinished header blocks (get_unfinished_block_headers)") +def rpc_unfinished_cmd(): + async def do_command(): + try: + node_client = await get_client() + header_blocks = await node_client.get_unfinished_block_headers() + pprint([block.to_json_dict() for block in header_blocks]) + finally: + node_client.close() + await node_client.await_closed() + + asyncio.get_event_loop().run_until_complete(do_command()) + +@rpc_cmd.command("space", short_help="Gets the netspace of the network between two blocks (get_network_space)") +@click.option("-old","--older", help="The header hash of the older block") +@click.option("-new","--newer", help="The header hash of the newer block") +@click.option("-s","--start", help="The height of the block to start at") +@click.option("-e","--end", help="The height of the block to end at") +def rpc_space_cmd(older, newer, start, end): + async def do_command(): + try: + node_client = await get_client() + + if (older and start) or (newer and end): + pprint("Invalid arguments specified.") + else: + if start: + start_hash = (await node_client.get_block_record_by_height(start)).header_hash + elif older: + start_hash = bytes.fromhex(sanitize_bytes(older)) + else: + start_hash = (await node_client.get_block_record_by_height(0)).header_hash + + if end: + end_hash = (await node_client.get_block_record_by_height(end)).header_hash + elif newer: + end_hash = bytes.fromhex(sanitize_bytes(newer)) + else: + end_hash = (await node_client.get_block_record_by_height((await node_client.get_blockchain_state())["peak"].height)).header_hash + + netspace = await node_client.get_network_space(start_hash, end_hash) + if netspace: + pprint(format_bytes(netspace)) + else: + pprint("Invalid block range specified") + + finally: + node_client.close() + await node_client.await_closed() + + asyncio.get_event_loop().run_until_complete(do_command()) + @rpc_cmd.command("blockcoins", short_help="Gets the coins added and removed for a specific header hash (get_additions_and_removals)") @click.argument("headerhash", nargs=1, required=True) def rpc_addrem_cmd(headerhash): @@ -125,6 +196,32 @@ async def do_command(): asyncio.get_event_loop().run_until_complete(do_command()) +@rpc_cmd.command("mempool", short_help="Gets items that are currently sitting in the mempool (get_(all_)mempool_*)") +@click.option("-txid","--transaction-id", help="The ID of a spend bundle that is sitting in the mempool") +@click.option("--ids-only", is_flag=True, help="Only show the IDs of the retrieved spend bundles") +def rpc_mempool_cmd(transaction_id, ids_only): + async def do_command(): + try: + node_client = await get_client() + if transaction_id: + items = {} + items[transaction_id] = await node_client.get_mempool_item_by_tx_id(bytes.fromhex(sanitize_bytes(transaction_id))) + else: + b_items = await node_client.get_all_mempool_items() + items = {} + for key in b_items.keys(): + items[key.hex()] = b_items[key] + + if ids_only: + pprint(list(items.keys())) + else: + pprint(items) + finally: + node_client.close() + await node_client.await_closed() + + asyncio.get_event_loop().run_until_complete(do_command()) + @rpc_cmd.command("coinrecords", short_help="Gets coin records by a specified information (get_coin_records_by_*)") @click.argument("values", nargs=-1, required=True) @click.option("--by", help="The property to use (id, puzzlehash, parentid)") From 4b3c2e969ba95156a2aa99349c84f849c813ab0f Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Thu, 5 Aug 2021 13:36:04 -0700 Subject: [PATCH 13/31] Added a hash function --- cdv/cmds/cli.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cdv/cmds/cli.py b/cdv/cmds/cli.py index 2e24e81..cfcb9bc 100644 --- a/cdv/cmds/cli.py +++ b/cdv/cmds/cli.py @@ -7,6 +7,7 @@ from cdv import __version__ +from chia.util.hash import std_hash from chia.util.bech32m import encode_puzzle_hash, decode_puzzle_hash from cdv.cmds import ( @@ -64,6 +65,15 @@ def test_cmd(tests: str, discover: bool, init: str): elif not init: pytest.main([*test_paths]) +@cli.command("hash", short_help="SHA256 hash UTF-8 strings or bytes (use 0x prefix for bytes)") +@click.argument("data", nargs=1, required=True) +def hash_cmd(data): + if data[:2] == "0x": + hash_data = bytes.fromhex(data[2:]) + else: + hash_data = bytes(data, "utf-8") + print(std_hash(hash_data)) + @cli.command("encode", short_help="Encode a puzzle hash to a bech32m address") @click.argument("puzzle_hash", nargs=1, required=True) @click.option("-p", "--prefix", type=str, default="xch", show_default=True, required=False, help="The prefix to encode with") From 1d9a9e12f290c5c849a1997abfcb89b4d3085e5d Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Thu, 5 Aug 2021 14:04:25 -0700 Subject: [PATCH 14/31] Moved sanitize_bytes to hexstr_from_bytes --- cdv/cmds/chia_inspect.py | 26 ++++++++++++-------------- cdv/cmds/clsp.py | 8 +++----- cdv/cmds/rpc.py | 13 +++++++------ 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/cdv/cmds/chia_inspect.py b/cdv/cmds/chia_inspect.py index e04df48..23f33fc 100644 --- a/cdv/cmds/chia_inspect.py +++ b/cdv/cmds/chia_inspect.py @@ -23,6 +23,7 @@ from chia.util.keychain import mnemonic_to_seed, bytes_to_mnemonic from chia.util.ints import uint64, uint32 from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict +from chia.util.byte_types import hexstr_to_bytes from cdv.cmds.clsp import parse_program @@ -63,9 +64,6 @@ def inspect_callback(objs, ctx, id_calc=None, type='Unknown'): pprint([type for _ in objs]) # Utility functions -def sanitize_bytes(bytecode): - return bytecode[2:] if bytecode[:2] == "0x" else bytecode - def fake_context(): ctx = {} ctx["obj"] = {"json": True} @@ -110,7 +108,7 @@ def inspect_any_cmd(ctx, objects): # Try it as some key stuff for cls in [G1Element, G2Element, PrivateKey]: try: - in_obj = cls.from_bytes(bytes.fromhex(sanitize_bytes(obj))) + in_obj = cls.from_bytes(hexstr_to_bytes(obj)) except: pass # Try it as a Program @@ -200,14 +198,14 @@ def do_inspect_coin_spend_cmd(ctx, spends, print_results=True, **kwargs): bytes.fromhex(kwargs['puzzle_hash']), uint64(kwargs['amount']), ), - parse_program(sanitize_bytes(kwargs['puzzle_reveal'])), - parse_program(sanitize_bytes(kwargs['solution'])), + parse_program(kwargs['puzzle_reveal']), + parse_program(kwargs['solution']), )] elif kwargs['coin']: coin_spend_objs = [CoinSpend( do_inspect_coin_cmd(ctx, [kwargs['coin']], print_results=False)[0], - parse_program(sanitize_bytes(kwargs['puzzle_reveal'])), - parse_program(sanitize_bytes(kwargs['solution'])), + parse_program(kwargs['puzzle_reveal']), + parse_program(kwargs['solution']), )] else: print("Invalid arguments specified.") @@ -251,7 +249,7 @@ def inspect_spend_bundle_cmd(ctx, bundles, **kwargs): def do_inspect_spend_bundle_cmd(ctx, bundles, print_results=True, **kwargs): if kwargs and (len(kwargs['spend']) > 0): if (len(kwargs['aggsig']) > 0): - sig = AugSchemeMPL.aggregate([G2Element(bytes.fromhex(sanitize_bytes(sig))) for sig in kwargs["aggsig"]]) + sig = AugSchemeMPL.aggregate([G2Element(hexstr_to_bytes(sig)) for sig in kwargs["aggsig"]]) else: sig = G2Element() spend_bundle_objs = [SpendBundle( @@ -427,9 +425,9 @@ def one_or_zero(value): if sum([one_or_zero(condition) for condition in condition_list]) == 1: if kwargs["public_key"]: sk = None - pk = G1Element.from_bytes(bytes.fromhex(sanitize_bytes(kwargs["public_key"]))) + pk = G1Element.from_bytes(hexstr_to_bytes(kwargs["public_key"])) elif kwargs["secret_key"]: - sk = PrivateKey.from_bytes(bytes.fromhex(sanitize_bytes(kwargs["secret_key"]))) + sk = PrivateKey.from_bytes(hexstr_to_bytes(kwargs["secret_key"])) pk = sk.get_g1() elif kwargs["mnemonic"]: seed = mnemonic_to_seed(kwargs["mnemonic"], kwargs["passphrase"]) @@ -504,16 +502,16 @@ def do_inspect_sigs_cmd(ctx, print_results=True, **kwargs): sk = None for param, value in OrderedParamsCommand._options: if param.name == "secret_key": - sk = PrivateKey.from_bytes(bytes.fromhex(sanitize_bytes(value))) + sk = PrivateKey.from_bytes(hexstr_to_bytes(value)) elif param.name == "aggsig": - new_sig = G2Element.from_bytes(bytes.fromhex(sanitize_bytes(value))) + new_sig = G2Element.from_bytes(hexstr_to_bytes(value)) base = AugSchemeMPL.aggregate([base, new_sig]) elif sk: if param.name == "utf_8": new_sig = AugSchemeMPL.sign(sk, bytes(value, "utf-8")) base = AugSchemeMPL.aggregate([base, new_sig]) if param.name == "bytes": - new_sig = AugSchemeMPL.sign(sk, bytes.fromhex(sanitize_bytes(value))) + new_sig = AugSchemeMPL.sign(sk, hexstr_to_bytes(value)) base = AugSchemeMPL.aggregate([base, new_sig]) if print_results: diff --git a/cdv/cmds/clsp.py b/cdv/cmds/clsp.py index 609fbc6..aa2419f 100644 --- a/cdv/cmds/clsp.py +++ b/cdv/cmds/clsp.py @@ -13,6 +13,7 @@ from clvm_tools.binutils import disassemble, assemble from chia.types.blockchain_format.program import Program +from chia.util.byte_types import hexstr_to_bytes @click.group("clsp", short_help="Commands to use when developing with chialisp") def clsp_cmd(): @@ -28,9 +29,7 @@ def parse_program(program: str, include=[]): if '(' in program: prog = Program.to(assemble(program)) elif '.' not in program: - #TODO: Replace this and the one below with sanitize_bytes (inspect) once that's not circular - sanitized = program[2:] if program[:2] == "0x" else program - prog = Program.from_bytes(bytes.fromhex(sanitized)) + prog = Program.from_bytes(hexstr_to_bytes(program)) else: with open(program, "r") as file: filestring = file.read() @@ -41,8 +40,7 @@ def parse_program(program: str, include=[]): else: prog = Program.to(assemble(filestring)) else: - sanitized = filestring[2:] if filestring[:2] == "0x" else filestring - prog = Program.from_bytes(bytes.fromhex(sanitized)) + prog = Program.from_bytes(hexstr_to_bytes(filestring)) return prog @clsp_cmd.command("build", short_help="Build all specified CLVM files (i.e mypuz.clsp or ./puzzles/*.clsp)") diff --git a/cdv/cmds/rpc.py b/cdv/cmds/rpc.py index 4b585ea..2c20ead 100644 --- a/cdv/cmds/rpc.py +++ b/cdv/cmds/rpc.py @@ -10,10 +10,11 @@ from chia.util.config import load_config from chia.util.ints import uint16 from chia.util.misc import format_bytes +from chia.util.byte_types import hexstr_to_bytes from chia.types.spend_bundle import SpendBundle from chia.types.blockchain_format.coin import Coin -from cdv.cmds.chia_inspect import fake_context, do_inspect_spend_bundle_cmd, sanitize_bytes +from cdv.cmds.chia_inspect import fake_context, do_inspect_spend_bundle_cmd @click.group("rpc", short_help="Make RPC requests to a Chia full node") def rpc_cmd(): @@ -56,7 +57,7 @@ async def do_command(): try: node_client = await get_client() if header_hash: - blocks = [await node_client.get_block(bytes.fromhex(sanitize_bytes(header_hash)))] + blocks = [await node_client.get_block(hexstr_to_bytes(header_hash))] elif start and end: blocks = await node_client.get_all_block(start, end) else: @@ -79,7 +80,7 @@ async def do_command(): try: node_client = await get_client() if header_hash: - block_record = await node_client.get_block_record(bytes.fromhex(sanitize_bytes(header_hash))) + block_record = await node_client.get_block_record(hexstr_to_bytes(header_hash)) block_records = block_record.to_json_dict() if block_record else [] elif height: block_record = await node_client.get_block_record_by_height(height) @@ -124,14 +125,14 @@ async def do_command(): if start: start_hash = (await node_client.get_block_record_by_height(start)).header_hash elif older: - start_hash = bytes.fromhex(sanitize_bytes(older)) + start_hash = hexstr_to_bytes(older) else: start_hash = (await node_client.get_block_record_by_height(0)).header_hash if end: end_hash = (await node_client.get_block_record_by_height(end)).header_hash elif newer: - end_hash = bytes.fromhex(sanitize_bytes(newer)) + end_hash = hexstr_to_bytes(newer) else: end_hash = (await node_client.get_block_record_by_height((await node_client.get_blockchain_state())["peak"].height)).header_hash @@ -205,7 +206,7 @@ async def do_command(): node_client = await get_client() if transaction_id: items = {} - items[transaction_id] = await node_client.get_mempool_item_by_tx_id(bytes.fromhex(sanitize_bytes(transaction_id))) + items[transaction_id] = await node_client.get_mempool_item_by_tx_id(hexstr_to_bytes(transaction_id)) else: b_items = await node_client.get_all_mempool_items() items = {} From 5f61196081d2125b2fe634e8b3dfc7872778060f Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Thu, 5 Aug 2021 14:17:32 -0700 Subject: [PATCH 15/31] Eliminated cross command imports (except one) --- cdv/cmds/chia_inspect.py | 7 +------ cdv/cmds/clsp.py | 26 ++------------------------ cdv/cmds/rpc.py | 3 ++- cdv/cmds/util.py | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 31 deletions(-) create mode 100644 cdv/cmds/util.py diff --git a/cdv/cmds/chia_inspect.py b/cdv/cmds/chia_inspect.py index 23f33fc..4514dc5 100644 --- a/cdv/cmds/chia_inspect.py +++ b/cdv/cmds/chia_inspect.py @@ -25,7 +25,7 @@ from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict from chia.util.byte_types import hexstr_to_bytes -from cdv.cmds.clsp import parse_program +from cdv.cmds.util import parse_program @click.group("inspect", short_help="Inspect various data structures") @click.option("-j","--json", is_flag=True, help="Output the result as JSON") @@ -64,11 +64,6 @@ def inspect_callback(objs, ctx, id_calc=None, type='Unknown'): pprint([type for _ in objs]) # Utility functions -def fake_context(): - ctx = {} - ctx["obj"] = {"json": True} - return ctx - def json_and_key_strip(input): json_dict = json.loads(input) if len(json_dict.keys()) == 1: diff --git a/cdv/cmds/clsp.py b/cdv/cmds/clsp.py index aa2419f..f6c739b 100644 --- a/cdv/cmds/clsp.py +++ b/cdv/cmds/clsp.py @@ -15,34 +15,12 @@ from chia.types.blockchain_format.program import Program from chia.util.byte_types import hexstr_to_bytes +from cdv.cmds.util import parse_program, append_include + @click.group("clsp", short_help="Commands to use when developing with chialisp") def clsp_cmd(): pass -def append_include(search_paths): - if search_paths: - list(search_paths).append("./include") - else: - return ['./include'] - -def parse_program(program: str, include=[]): - if '(' in program: - prog = Program.to(assemble(program)) - elif '.' not in program: - prog = Program.from_bytes(hexstr_to_bytes(program)) - else: - with open(program, "r") as file: - filestring = file.read() - if '(' in filestring: - # TODO: This should probably be more robust - if re.compile('\(mod\s').search(filestring): - prog = Program.to(compile_clvm_text(filestring, append_include(include))) - else: - prog = Program.to(assemble(filestring)) - else: - prog = Program.from_bytes(hexstr_to_bytes(filestring)) - return prog - @clsp_cmd.command("build", short_help="Build all specified CLVM files (i.e mypuz.clsp or ./puzzles/*.clsp)") @click.argument("files", nargs=-1, required=True, default=None) @click.option("-i","--include", required=False, multiple=True, help="Paths to search for include files (./include will be searched automatically)") diff --git a/cdv/cmds/rpc.py b/cdv/cmds/rpc.py index 2c20ead..2c7b570 100644 --- a/cdv/cmds/rpc.py +++ b/cdv/cmds/rpc.py @@ -14,7 +14,8 @@ from chia.types.spend_bundle import SpendBundle from chia.types.blockchain_format.coin import Coin -from cdv.cmds.chia_inspect import fake_context, do_inspect_spend_bundle_cmd +from cdv.cmds.util import fake_context +from cdv.cmds.chia_inspect import do_inspect_spend_bundle_cmd @click.group("rpc", short_help="Make RPC requests to a Chia full node") def rpc_cmd(): diff --git a/cdv/cmds/util.py b/cdv/cmds/util.py new file mode 100644 index 0000000..ca892b7 --- /dev/null +++ b/cdv/cmds/util.py @@ -0,0 +1,36 @@ +import re + +from chia.types.blockchain_format.program import Program +from chia.util.byte_types import hexstr_to_bytes + +from clvm_tools.clvmc import compile_clvm_text +from clvm_tools.binutils import assemble + +def fake_context(): + ctx = {} + ctx["obj"] = {"json": True} + return ctx + +def append_include(search_paths): + if search_paths: + list(search_paths).append("./include") + else: + return ['./include'] + +def parse_program(program: str, include=[]): + if '(' in program: + prog = Program.to(assemble(program)) + elif '.' not in program: + prog = Program.from_bytes(hexstr_to_bytes(program)) + else: + with open(program, "r") as file: + filestring = file.read() + if '(' in filestring: + # TODO: This should probably be more robust + if re.compile('\(mod\s').search(filestring): + prog = Program.to(compile_clvm_text(filestring, append_include(include))) + else: + prog = Program.to(assemble(filestring)) + else: + prog = Program.from_bytes(hexstr_to_bytes(filestring)) + return prog \ No newline at end of file From 2a52d394d28d7e14e48af8db2e99c6c58e9fff9e Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Thu, 5 Aug 2021 15:14:09 -0700 Subject: [PATCH 16/31] Added testing for the cdv command --- setup.py | 2 +- tests/cmds/test_cdv.py | 53 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 tests/cmds/test_cdv.py diff --git a/setup.py b/setup.py index e00ea80..7fad5da 100644 --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ setup( name="chia_dev_tools", version="1.0.1", - packages=find_packages(), + packages=find_packages(exclude=("tests",)), author="Quexington", entry_points={ "console_scripts": [ diff --git a/tests/cmds/test_cdv.py b/tests/cmds/test_cdv.py new file mode 100644 index 0000000..4037832 --- /dev/null +++ b/tests/cmds/test_cdv.py @@ -0,0 +1,53 @@ +from pathlib import Path + +from click.testing import CliRunner + +from cdv.cmds.cli import cli + +class TestCdvCommands: + def test_encode_decode(self): + runner = CliRunner() + puzhash = '3d4237d9383a7b6e60d1bfe551139ec2d6e5468205bf179ed381e66bed7b9788' + + result = runner.invoke(cli, ['encode', puzhash]) + address = 'xch184pr0kfc8fakucx3hlj4zyu7cttw235zqkl308kns8nxhmtmj7yqxsnauc' + assert result.exit_code == 0 + assert address in result.output + result = runner.invoke(cli, ['decode', address]) + assert result.exit_code == 0 + assert puzhash in result.output + + result = runner.invoke(cli, ['encode', puzhash, '--prefix', 'txch']) + test_address = 'txch184pr0kfc8fakucx3hlj4zyu7cttw235zqkl308kns8nxhmtmj7yqth5tat' + assert result.exit_code == 0 + assert test_address in result.output + result = runner.invoke(cli, ['decode', test_address]) + assert result.exit_code == 0 + assert puzhash in result.output + + def test_hash(self): + runner = CliRunner() + str_msg = "chia" + b_msg = "0xcafef00d" + + result = runner.invoke(cli, ['hash', str_msg]) + assert result.exit_code == 0 + assert '3d4237d9383a7b6e60d1bfe551139ec2d6e5468205bf179ed381e66bed7b9788' in result.output + result = runner.invoke(cli, ['hash', b_msg]) + assert result.exit_code == 0 + assert '8f6e594e007ca1a1676ef64469c58f7ece8cddc9deae0faf66fbce2466519ebd' in result.output + + def test_test(self): + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke(cli, ['test', '--init']) + assert result.exit_code == 0 + assert Path('./tests').exists() and Path('./tests/test_skeleton.py').exists() + + result = runner.invoke(cli, ['test', '--discover']) + assert result.exit_code == 0 + assert 'TestSomething' in result.output + + result = runner.invoke(cli, ['test']) + assert result.exit_code == 0 + assert 'test_skeleton.py .' in result.output \ No newline at end of file From 41dd2e5d24ba267d1a68cee4f1359a7926572ee5 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Fri, 6 Aug 2021 15:15:54 -0700 Subject: [PATCH 17/31] Added testing for the clsp subcommand --- cdv/cmds/clsp.py | 2 +- cdv/cmds/util.py | 4 +- tests/cmds/test_clsp.py | 132 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 tests/cmds/test_clsp.py diff --git a/cdv/cmds/clsp.py b/cdv/cmds/clsp.py index f6c739b..f7a1d3d 100644 --- a/cdv/cmds/clsp.py +++ b/cdv/cmds/clsp.py @@ -46,7 +46,7 @@ def build_cmd(files, include) -> None: print("...Compilation finished") except Exception as e: print("Couldn't build "+filename.name+": "+str(e)) - pass + @clsp_cmd.command("disassemble", short_help="Disassemble serialized clvm into human readable form.") @click.argument("programs", nargs=-1, required=True) diff --git a/cdv/cmds/util.py b/cdv/cmds/util.py index ca892b7..b6561ae 100644 --- a/cdv/cmds/util.py +++ b/cdv/cmds/util.py @@ -13,7 +13,9 @@ def fake_context(): def append_include(search_paths): if search_paths: - list(search_paths).append("./include") + search_list = list(search_paths) + search_list.append("./include") + return search_list else: return ['./include'] diff --git a/tests/cmds/test_clsp.py b/tests/cmds/test_clsp.py new file mode 100644 index 0000000..fe0e63d --- /dev/null +++ b/tests/cmds/test_clsp.py @@ -0,0 +1,132 @@ +import os +import shutil + +from pathlib import Path +from click.testing import CliRunner + +from chia.types.blockchain_format.program import Program + +from clvm_tools.binutils import assemble, disassemble + +from cdv.cmds.cli import cli + +class TestCdvCommands: + program = '(q . 1)' + serialized = 'ff0101' + mod = '(mod () (include condition_codes.clib) CREATE_COIN)' + + #This comes before build because build is going to use retrieve + def test_retrieve(self): + runner = CliRunner() + with runner.isolated_filesystem(): + result = runner.invoke(cli, ["clsp", "retrieve", "condition_codes"]) + assert result.exit_code == 0 + assert Path("./include/condition_codes.clib").exists() + + def test_build(self): + runner = CliRunner() + with runner.isolated_filesystem(): + # Test building CLVM + program_file = open("program.clvm","w") + program_file.write(self.program) + program_file.close() + result = runner.invoke(cli, ["clsp","build","."]) + assert result.exit_code == 0 + assert Path("./program.clvm.hex").exists() + assert open("program.clvm.hex","r").read() == "01" + + # Use the retrieve command for the include file + runner.invoke(cli, ["clsp", "retrieve", "condition_codes"]) + + # Test building Chialisp (automatic include search) + mod_file = open("mod.clsp","w") + mod_file.write(self.mod) + mod_file.close() + result = runner.invoke(cli, ["clsp","build","."]) + assert result.exit_code == 0 + assert Path("./mod.clsp.hex").exists() + assert open("mod.clsp.hex","r").read() == "ff0133" + + # Test building Chialisp (specified include search) + os.remove(Path("./mod.clsp.hex")) + shutil.copytree("./include","./include_test") + shutil.rmtree("./include") + result = runner.invoke(cli, ["clsp","build",".","--include","./include_test"]) + assert result.exit_code == 0 + assert Path("./mod.clsp.hex").exists() + assert open("mod.clsp.hex","r").read() == "ff0133" + + def test_curry(self): + integer = 1 + hexadecimal = bytes.fromhex("aabbccddeeff") + string = "hello" + program = Program.to([2, 2, 3]) + mod = Program.from_bytes(bytes.fromhex(self.serialized)) + curried_mod = mod.curry(integer, hexadecimal, string, program) + + runner = CliRunner() + cmd = ["clsp","curry",str(mod), + "-a", str(integer), + "-a", "0x"+hexadecimal.hex(), + "-a", string, + "-a", disassemble(program), + ] + + result = runner.invoke(cli, cmd) + assert result.exit_code == 0 + assert disassemble(curried_mod) in result.output + + cmd.append("-x") + result = runner.invoke(cli, cmd) + assert result.exit_code == 0 + assert str(curried_mod) in result.output + + cmd.append("-H") + result = runner.invoke(cli, cmd) + assert result.exit_code == 0 + assert curried_mod.get_tree_hash().hex() in result.output + + # The following two functions aim to test every branch of the parse_program utility function between them + def test_disassemble(self): + runner = CliRunner() + # Test the program passed in as a hex string + result = runner.invoke(cli, ["clsp","disassemble",self.serialized]) + assert result.exit_code == 0 + assert self.program in result.output + # Test the program passed in as a hex file + with runner.isolated_filesystem(): + program_file = open("program.clvm.hex","w") + program_file.write(self.serialized) + program_file.close() + result = runner.invoke(cli, ["clsp","disassemble","program.clvm.hex"]) + assert result.exit_code == 0 + assert self.program in result.output + + def test_treehash(self): + # Test a program passed as a string + runner = CliRunner() + program = "(a 2 3)" + program_as_mod = "(mod (arg . arg2) (a arg arg2))" + program_hash = "530d1b3283c802be3a7bdb34b788c1898475ed76c89ecb2224e4b4f40c32d1a4" + result = runner.invoke(cli, ["clsp","treehash",program]) + assert result.exit_code == 0 + assert program_hash in result.output + + # Test a program passed in as a CLVM file + filename = "program.clvm" + with runner.isolated_filesystem(): + program_file = open(filename,"w") + program_file.write(program) + program_file.close() + result = runner.invoke(cli, ["clsp","treehash",filename]) + assert result.exit_code == 0 + assert program_hash in result.output + + # Test a program passed in as a Chialisp file + with runner.isolated_filesystem(): + program_file = open(filename,"w") + program_file.write(program_as_mod) + program_file.close() + result = runner.invoke(cli, ["clsp","treehash",filename]) + assert result.exit_code == 0 + assert program_hash in result.output \ No newline at end of file From 54ebcda8d232236ec0a3164421212afeb5e3f74d Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Mon, 9 Aug 2021 10:25:34 -0700 Subject: [PATCH 18/31] Added testing for the inspect subcommand --- cdv/cmds/chia_inspect.py | 46 +-- .../object_files/coinrecords/coinrecord.json | 8 + .../coinrecords/coinrecord_invalid.json | 8 + .../coinrecords/coinrecord_metadata.json | 5 + tests/cmds/object_files/coins/coin.json | 3 + .../cmds/object_files/coins/coin_invalid.json | 3 + .../object_files/coins/coin_metadata.json | 5 + .../spendbundles/spendbundle.json | 10 + .../spendbundles/spendbundle_invalid.json | 10 + .../spendbundles/spendbundle_metadata.json | 5 + tests/cmds/object_files/spends/spend.json | 5 + .../object_files/spends/spend_invalid.json | 5 + .../object_files/spends/spend_metadata.json | 5 + tests/cmds/test_inspect.py | 270 ++++++++++++++++++ 14 files changed, 365 insertions(+), 23 deletions(-) create mode 100644 tests/cmds/object_files/coinrecords/coinrecord.json create mode 100644 tests/cmds/object_files/coinrecords/coinrecord_invalid.json create mode 100644 tests/cmds/object_files/coinrecords/coinrecord_metadata.json create mode 100644 tests/cmds/object_files/coins/coin.json create mode 100644 tests/cmds/object_files/coins/coin_invalid.json create mode 100644 tests/cmds/object_files/coins/coin_metadata.json create mode 100644 tests/cmds/object_files/spendbundles/spendbundle.json create mode 100644 tests/cmds/object_files/spendbundles/spendbundle_invalid.json create mode 100644 tests/cmds/object_files/spendbundles/spendbundle_metadata.json create mode 100644 tests/cmds/object_files/spends/spend.json create mode 100644 tests/cmds/object_files/spends/spend_invalid.json create mode 100644 tests/cmds/object_files/spends/spend_metadata.json create mode 100644 tests/cmds/test_inspect.py diff --git a/cdv/cmds/chia_inspect.py b/cdv/cmds/chia_inspect.py index 4514dc5..d8bad93 100644 --- a/cdv/cmds/chia_inspect.py +++ b/cdv/cmds/chia_inspect.py @@ -81,9 +81,9 @@ def streamable_load(cls, inputs): if "{" in file_string: input_objs.append(cls.from_json_dict(json_and_key_strip(file_string))) else: - input_objs.append(cls.from_bytes(bytes.fromhex(file_string))) + input_objs.append(cls.from_bytes(hexstr_to_bytes(file_string))) else: - input_objs.append(cls.from_bytes(bytes.fromhex(input))) + input_objs.append(cls.from_bytes(hexstr_to_bytes(input))) return input_objs @@ -144,7 +144,7 @@ def inspect_coin_cmd(ctx, coins, **kwargs): def do_inspect_coin_cmd(ctx, coins, print_results=True, **kwargs): if kwargs and all([kwargs[key] for key in kwargs.keys()]): - coin_objs = [Coin(bytes.fromhex(kwargs['parent_id']), bytes.fromhex(kwargs['puzzle_hash']), uint64(kwargs['amount']))] + coin_objs = [Coin(hexstr_to_bytes(kwargs['parent_id']), hexstr_to_bytes(kwargs['puzzle_hash']), uint64(kwargs['amount']))] elif not kwargs or not any([kwargs[key] for key in kwargs.keys()]): coin_objs = [] try: @@ -159,7 +159,7 @@ def do_inspect_coin_cmd(ctx, coins, print_results=True, **kwargs): return if print_results: - inspect_callback(coin_objs, ctx, id_calc=(lambda e: e.name()), type='Coin') + inspect_callback(coin_objs, ctx, id_calc=(lambda e: e.name().hex()), type='Coin') else: return coin_objs @@ -189,8 +189,8 @@ def do_inspect_coin_spend_cmd(ctx, spends, print_results=True, **kwargs): if (not kwargs['coin']) and all([kwargs['parent_id'], kwargs['puzzle_hash'], kwargs['amount']]): coin_spend_objs = [CoinSpend( Coin( - bytes.fromhex(kwargs['parent_id']), - bytes.fromhex(kwargs['puzzle_hash']), + hexstr_to_bytes(kwargs['parent_id']), + hexstr_to_bytes(kwargs['puzzle_hash']), uint64(kwargs['amount']), ), parse_program(kwargs['puzzle_reveal']), @@ -218,7 +218,7 @@ def do_inspect_coin_spend_cmd(ctx, spends, print_results=True, **kwargs): return if print_results: - inspect_callback(coin_spend_objs, ctx, id_calc=(lambda e: e.coin.name()), type='CoinSpend') + inspect_callback(coin_spend_objs, ctx, id_calc=(lambda e: e.coin.name().hex()), type='CoinSpend') if cost_flag: for coin_spend in coin_spend_objs: program = simple_solution_generator(SpendBundle([coin_spend], G2Element())) @@ -235,7 +235,7 @@ def do_inspect_coin_spend_cmd(ctx, spends, print_results=True, **kwargs): @click.option("-db","--debug", is_flag=True, help="Show debugging information about the bundles") @click.option("-sd","--signable_data", is_flag=True, help="Print the data that needs to be signed in the bundles") @click.option("-n","--network", default="mainnet", show_default=True, help="The network this spend bundle will be pushed to (for AGG_SIG_ME)") -@click.option("-ec","--cost", is_flag=True, help="Print the CLVM cost of the spend") +@click.option("-ec","--cost", is_flag=True, help="Print the CLVM cost of the entire bundle") @click.option("-bc","--cost-per-byte", default=12000, show_default=True, help="The cost per byte in the puzzle and solution reveal to use when calculating cost") @click.pass_context def inspect_spend_bundle_cmd(ctx, bundles, **kwargs): @@ -262,7 +262,7 @@ def do_inspect_spend_bundle_cmd(ctx, bundles, print_results=True, **kwargs): print("One or more of the specified objects was not a spend bundle") if print_results: - inspect_callback(spend_bundle_objs, ctx, id_calc=(lambda e: e.name()), type='SpendBundle') + inspect_callback(spend_bundle_objs, ctx, id_calc=(lambda e: e.name().hex()), type='SpendBundle') if kwargs: if kwargs["cost"]: for spend_bundle in spend_bundle_objs: @@ -280,6 +280,7 @@ def do_inspect_spend_bundle_cmd(ctx, bundles, print_results=True, **kwargs): print(f"") print(f"Public Key/Message Pairs") print(f"------------------------") + pkm_dict = {} for obj in spend_bundle_objs: for coin_spend in obj.coin_spends: err, conditions_dict, _ = conditions_dict_for_solution( @@ -292,20 +293,19 @@ def do_inspect_spend_bundle_cmd(ctx, bundles, print_results=True, **kwargs): from chia.util.config import load_config config = load_config(DEFAULT_ROOT_PATH, "config.yaml") genesis_challenge = config["network_overrides"]["constants"][kwargs["network"]]["GENESIS_CHALLENGE"] - pkm_dict = {} for pk, msg in pkm_pairs_for_conditions_dict( conditions_dict, coin_spend.coin.name(), - bytes.fromhex(genesis_challenge), + hexstr_to_bytes(genesis_challenge), ): if str(pk) in pkm_dict: pkm_dict[str(pk)].append(msg) else: pkm_dict[str(pk)] = [msg] - for pk, msgs in pkm_dict.items(): - print(f"{pk}:") - for msg in msgs: - print(f"\t- {msg.hex()}") + for pk, msgs in pkm_dict.items(): + print(f"{pk}:") + for msg in msgs: + print(f"\t- {msg.hex()}") else: return spend_bundle_objs @@ -329,8 +329,8 @@ def do_inspect_coin_record_cmd(ctx, records, print_results=True, **kwargs): if (not kwargs['coin']) and all([kwargs['parent_id'], kwargs['puzzle_hash'], kwargs['amount']]): coin_record_objs = [CoinRecord( Coin( - bytes.fromhex(kwargs['parent_id']), - bytes.fromhex(kwargs['puzzle_hash']), + hexstr_to_bytes(kwargs['parent_id']), + hexstr_to_bytes(kwargs['puzzle_hash']), uint64(kwargs['amount']), ), kwargs["confirmed_block_index"], @@ -364,7 +364,7 @@ def do_inspect_coin_record_cmd(ctx, records, print_results=True, **kwargs): return if print_results: - inspect_callback(coin_record_objs, ctx, id_calc=(lambda e: e.coin.name()), type='CoinRecord') + inspect_callback(coin_record_objs, ctx, id_calc=(lambda e: e.coin.name().hex()), type='CoinRecord') else: return coin_record_objs @@ -385,11 +385,11 @@ def do_inspect_program_cmd(ctx, programs, print_results=True, **kwargs): print("One or more of the specified objects was not a Program") if print_results: - inspect_callback(program_objs, ctx, id_calc=(lambda e: e.get_tree_hash()), type='Program') + inspect_callback(program_objs, ctx, id_calc=(lambda e: e.get_tree_hash().hex()), type='Program') else: return program_objs -@inspect_cmd.command("keys", short_help="Various methods for examining BLS Public Keys") +@inspect_cmd.command("keys", short_help="Various methods for examining and generating BLS Keys") @click.option("-pk","--public-key", help="A BLS public key") @click.option("-sk","--secret-key", help="The secret key from which to derive the public key") @click.option("-m","--mnemonic", help="A 24 word mnemonic from which to derive the secret key") @@ -457,8 +457,8 @@ def one_or_zero(value): if kwargs["synthetic"]: if sk: - sk = calculate_synthetic_secret_key(sk, bytes.fromhex(kwargs["hidden_puzhash"])) - pk = calculate_synthetic_public_key(pk, bytes.fromhex(kwargs["hidden_puzhash"])) + sk = calculate_synthetic_secret_key(sk, hexstr_to_bytes(kwargs["hidden_puzhash"])) + pk = calculate_synthetic_public_key(pk, hexstr_to_bytes(kwargs["hidden_puzhash"])) else: print("Invalid arguments specified.") @@ -483,7 +483,7 @@ def parse_args(self, ctx, args): # return "normal" parse results return super().parse_args(ctx, args) -@inspect_cmd.command("signatures", cls=OrderedParamsCommand, short_help="Various methods for examining BLS aggregated signatures") +@inspect_cmd.command("signatures", cls=OrderedParamsCommand, short_help="Various methods for examining and creating BLS aggregated signatures") @click.option("-sk","--secret-key", multiple=True, help="A secret key to sign a message with") @click.option("-t","--utf-8", multiple=True, help="A UTF-8 message to be signed with the specified secret key") @click.option("-b","--bytes", multiple=True, help="A hex message to be signed with the specified secret key") diff --git a/tests/cmds/object_files/coinrecords/coinrecord.json b/tests/cmds/object_files/coinrecords/coinrecord.json new file mode 100644 index 0000000..9900cc5 --- /dev/null +++ b/tests/cmds/object_files/coinrecords/coinrecord.json @@ -0,0 +1,8 @@ +{"coin": {"amount": 0, + "parent_coin_info": "0x0000000000000000000000000000000000000000000000000000000000000000", + "puzzle_hash": "0000000000000000000000000000000000000000000000000000000000000000"}, + "coinbase": true, + "confirmed_block_index": 1, + "spent": true, + "spent_block_index": 1, + "timestamp": 909469800} \ No newline at end of file diff --git a/tests/cmds/object_files/coinrecords/coinrecord_invalid.json b/tests/cmds/object_files/coinrecords/coinrecord_invalid.json new file mode 100644 index 0000000..87ddcca --- /dev/null +++ b/tests/cmds/object_files/coinrecords/coinrecord_invalid.json @@ -0,0 +1,8 @@ +{"coin": {"amount": -1, + "parent_coin_info": "0x0000000000000000000000000000000000000000000000000000000000000000", + "puzzle_hash": "00000000000000000000000000000000000000000000000000000000000000001"}, + "coinbase": "yes", + "confirmed_block_index": -1, + "spent": "no", + "spent_block_index": -1, + "timestamp": -1} \ No newline at end of file diff --git a/tests/cmds/object_files/coinrecords/coinrecord_metadata.json b/tests/cmds/object_files/coinrecords/coinrecord_metadata.json new file mode 100644 index 0000000..07e8c85 --- /dev/null +++ b/tests/cmds/object_files/coinrecords/coinrecord_metadata.json @@ -0,0 +1,5 @@ +{ + "id": "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", + "bytes": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000101010000000036356868", + "type": "CoinRecord" +} \ No newline at end of file diff --git a/tests/cmds/object_files/coins/coin.json b/tests/cmds/object_files/coins/coin.json new file mode 100644 index 0000000..5bcf27e --- /dev/null +++ b/tests/cmds/object_files/coins/coin.json @@ -0,0 +1,3 @@ +{"amount": 0, +"parent_coin_info": "0x0000000000000000000000000000000000000000000000000000000000000000", +"puzzle_hash": "0000000000000000000000000000000000000000000000000000000000000000"} \ No newline at end of file diff --git a/tests/cmds/object_files/coins/coin_invalid.json b/tests/cmds/object_files/coins/coin_invalid.json new file mode 100644 index 0000000..cccb580 --- /dev/null +++ b/tests/cmds/object_files/coins/coin_invalid.json @@ -0,0 +1,3 @@ +{"amount": -1, +"parent_coin_info": "0x0000000000000000000000000000000000000000000000000000000000000000", +"puzzle_hash": "00000000000000000000000000000000000000000000000000000000000000001"} \ No newline at end of file diff --git a/tests/cmds/object_files/coins/coin_metadata.json b/tests/cmds/object_files/coins/coin_metadata.json new file mode 100644 index 0000000..3f8f940 --- /dev/null +++ b/tests/cmds/object_files/coins/coin_metadata.json @@ -0,0 +1,5 @@ +{ + "id": "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", + "bytes": "None", + "type": "Coin" +} \ No newline at end of file diff --git a/tests/cmds/object_files/spendbundles/spendbundle.json b/tests/cmds/object_files/spendbundles/spendbundle.json new file mode 100644 index 0000000..5f6f2c8 --- /dev/null +++ b/tests/cmds/object_files/spendbundles/spendbundle.json @@ -0,0 +1,10 @@ +{ + "coin_spends": [ + {"coin": {"amount": 0, + "parent_coin_info": "0x0000000000000000000000000000000000000000000000000000000000000000", + "puzzle_hash": "0000000000000000000000000000000000000000000000000000000000000000"}, + "puzzle_reveal": "0xff0101", + "solution": "80"} + ], + "aggregated_signature": "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +} \ No newline at end of file diff --git a/tests/cmds/object_files/spendbundles/spendbundle_invalid.json b/tests/cmds/object_files/spendbundles/spendbundle_invalid.json new file mode 100644 index 0000000..7c47ed7 --- /dev/null +++ b/tests/cmds/object_files/spendbundles/spendbundle_invalid.json @@ -0,0 +1,10 @@ +{ + "coin_spends": [ + {"coin": {"amount": -1, + "parent_coin_info": "0x00000000000000000000000000000000000000000000000000000000000000001", + "puzzle_hash": "z0000000000000000000000000000000000000000000000000000000000000000"}, + "puzzle_reveal": "0xff01018", + "solution": "801"} + ], + "aggregated_signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +} \ No newline at end of file diff --git a/tests/cmds/object_files/spendbundles/spendbundle_metadata.json b/tests/cmds/object_files/spendbundles/spendbundle_metadata.json new file mode 100644 index 0000000..e5135ce --- /dev/null +++ b/tests/cmds/object_files/spendbundles/spendbundle_metadata.json @@ -0,0 +1,5 @@ +{ + "id": "2c2f8b6a02ab83608baf6c976b3451f8d5bffca4c86683fc3bf793fea77b37d8", + "bytes": "00000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff010180c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "type": "SpendBundle" +} \ No newline at end of file diff --git a/tests/cmds/object_files/spends/spend.json b/tests/cmds/object_files/spends/spend.json new file mode 100644 index 0000000..d89760a --- /dev/null +++ b/tests/cmds/object_files/spends/spend.json @@ -0,0 +1,5 @@ +{"coin": {"amount": 0, + "parent_coin_info": "0x0000000000000000000000000000000000000000000000000000000000000000", + "puzzle_hash": "0000000000000000000000000000000000000000000000000000000000000000"}, + "puzzle_reveal": "0x01", + "solution": "ffff32ffb080df54b2a616f5c79baaed254134ae5dfc6e24e2d8e1165b251601ceb67b1886db50aacf946eb20f00adc303e7534dd0ff248080"} \ No newline at end of file diff --git a/tests/cmds/object_files/spends/spend_invalid.json b/tests/cmds/object_files/spends/spend_invalid.json new file mode 100644 index 0000000..b0c7865 --- /dev/null +++ b/tests/cmds/object_files/spends/spend_invalid.json @@ -0,0 +1,5 @@ +{"coin": {"amount": -1, + "parent_coin_info": "0x00000000000000000000000000000000000000000000000000000000000000001", + "puzzle_hash": "z000000000000000000000000000000000000000000000000000000000000000"}, + "puzzle_reveal": "0xff01018", + "solution": "801"} \ No newline at end of file diff --git a/tests/cmds/object_files/spends/spend_metadata.json b/tests/cmds/object_files/spends/spend_metadata.json new file mode 100644 index 0000000..b2d4a7f --- /dev/null +++ b/tests/cmds/object_files/spends/spend_metadata.json @@ -0,0 +1,5 @@ +{ + "id": "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b", + "bytes": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001ffff32ffb080df54b2a616f5c79baaed254134ae5dfc6e24e2d8e1165b251601ceb67b1886db50aacf946eb20f00adc303e7534dd0ff248080", + "type": "CoinSpend" +} \ No newline at end of file diff --git a/tests/cmds/test_inspect.py b/tests/cmds/test_inspect.py new file mode 100644 index 0000000..27ee7b2 --- /dev/null +++ b/tests/cmds/test_inspect.py @@ -0,0 +1,270 @@ +import json + +from pathlib import Path + +from click.testing import CliRunner + +from cdv.cmds.cli import cli + +class TestInspectCommands: + def test_any(self): + runner = CliRunner() + for class_type in ["coinrecord","coin","spendbundle","spend"]: + valid_json_path = Path(__file__).parent.joinpath(f"object_files/{class_type}s/{class_type}.json") + invalid_json_path = Path(__file__).parent.joinpath(f"object_files/{class_type}s/{class_type}_invalid.json") + metadata_path = Path(__file__).parent.joinpath(f"object_files/{class_type}s/{class_type}_metadata.json") + valid_json = json.loads(open(valid_json_path,"r").read()) + invalid_json = json.loads(open(invalid_json_path,"r").read()) + metadata_json = json.loads(open(metadata_path,"r").read()) + + # Try to load the invalid and make sure it fails + result = runner.invoke(cli, ["inspect", "any", str(invalid_json_path)]) + assert result.exit_code == 0 + # If this succeeds, there should be no file name, if it fails, the file name should be output as info + assert str(invalid_json_path) in result.output + + # Try to load the valid json + result = runner.invoke(cli, ["inspect", "any", str(valid_json_path)]) + assert result.exit_code == 0 + for key in valid_json.keys(): + key_type = type(valid_json[key]) + if (key_type is not dict) and (key_type is not list): + assert str(valid_json[key]) in result.output + + # Make sure the ID calculation is correct + result = runner.invoke(cli, ["inspect", "--id", "any", str(valid_json_path)]) + assert result.exit_code == 0 + assert metadata_json["id"] in result.output + + # Make sure the bytes encoding is correct + result = runner.invoke(cli, ["inspect", "--bytes", "any", str(valid_json_path)]) + assert result.exit_code == 0 + assert metadata_json["bytes"] in result.output + + # Make sure the type guessing is correct + result = runner.invoke(cli, ["inspect", "--type", "any", str(valid_json_path)]) + assert result.exit_code == 0 + assert metadata_json["type"] in result.output + + def test_coins(self): + pid = "0x0000000000000000000000000000000000000000000000000000000000000000" + ph = "0000000000000000000000000000000000000000000000000000000000000000" + amount = "0" + id = "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b" + + runner = CliRunner() + result = runner.invoke(cli, ["inspect","--id","coins","-pid",pid,"-ph",ph,"-a",amount]) + assert result.exit_code == 0 + assert id in result.output + + def test_spends(self): + coin_path = Path(__file__).parent.joinpath(f"object_files/coins/coin.json") + pid = "0x0000000000000000000000000000000000000000000000000000000000000000" + ph = "0000000000000000000000000000000000000000000000000000000000000000" + amount = "0" + id = "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b" + puzzle_reveal = "ff0101" + solution = "80" + cost = "63600" + cost_modifier = "1" + modified_cost = "53" + + runner = CliRunner() + + # Specify the coin file + result = runner.invoke(cli, ["inspect","--id","spends","-c",str(coin_path),"-pr",puzzle_reveal,"-s",solution]) + assert result.exit_code == 0 + assert id in result.output + + # Specify all of the arguments + base_command = ["inspect","--id","spends","-pid",pid,"-ph",ph,"-a",amount,"-pr",puzzle_reveal,"-s",solution] + result = runner.invoke(cli, base_command) + assert result.exit_code == 0 + assert id in result.output + + # Ask for the cost + base_command.append("-ec") + result = runner.invoke(cli, base_command) + assert result.exit_code == 0 + assert id in result.output + assert cost in result.output + + # Change the cost per byte + base_command.append("-bc") + base_command.append(cost_modifier) + result = runner.invoke(cli, base_command) + assert result.exit_code == 0 + assert id in result.output + assert modified_cost in result.output + + def test_spendbundles(self): + spend_path = Path(__file__).parent.joinpath(f"object_files/spends/spend.json") + pubkey = "80df54b2a616f5c79baaed254134ae5dfc6e24e2d8e1165b251601ceb67b1886db50aacf946eb20f00adc303e7534dd0" + signable_data = "24f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4bccd5bb71183532bff220ba46c268991a3ff07eb358e8255a65c30a2dce0e5fbb" + signed_data = "b77c3f7dbbc694a77ff3739c1f4ade1df6a3936fcb352005e4ca74ac4ed21009ad02ac98f2ecd6eacba2a007011471d508f29ee6456c6a7f13e15c81b4d0772351eb62770be47175a9a7f843698540761537ad1696ea0f7132bfa2710fb78f09" + agg_sig = "b83fe374efbc5776735df7cbfb7e27ede5079b41cd282091450e4de21c4b772e254ce906508834b0c2dcd3d58c47a96914c782f0baf8eaff7ece3b070d2035cd878f744deadcd6c6625c1d0a1b418437ee3f25c2df08ffe08bdfe06b8a83b514" + id_no_sig = "3ac222f0e8f19afcad367b3068273801ca21fe515311dae8d399a5baad9c3c73" + id_with_sig = "bd9acfbb344c006cf520f1265a9b611a20cd478f234f51cd31a978b2d3ad9bbb" + network_modifier = "testnet7" + modified_signable_data = "24f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b117816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015af" + cost = "4868283" + cost_modifier = "0" + modified_cost = "2408283" + + runner = CliRunner() + + # Build with only the spends + result = runner.invoke(cli, ["inspect","--id","spendbundles","-s",str(spend_path),"-s",str(spend_path)]) + assert result.exit_code == 0 + assert id_no_sig in result.output + + # Build with the aggsig as well + base_command = ["inspect","--id","spendbundles","-s",str(spend_path),"-s",str(spend_path),"-as",agg_sig] + result = runner.invoke(cli, base_command) + assert result.exit_code == 0 + assert id_with_sig in result.output + + # Test that debugging info comes up (disabled until breakpoints are removed from debug) + # base_command.append("-db") + # result = runner.invoke(cli, base_command) + # assert result.exit_code == 0 + # assert "Debugging Information" in result.output + + # Make sure our signable data comes out + base_command.append("-sd") + result = runner.invoke(cli, base_command) + assert result.exit_code == 0 + assert pubkey in result.output + assert result.output.count(signable_data) == 2 + + # Try a different network for different signable data + base_command.append("-n") + base_command.append(network_modifier) + result = runner.invoke(cli, base_command) + assert result.exit_code == 0 + assert modified_signable_data in result.output + + # Output the execution cost + base_command.append("-ec") + result = runner.invoke(cli, base_command) + assert result.exit_code == 0 + assert cost in result.output + + # Try a new cost per bytes + base_command.append("-bc") + base_command.append(cost_modifier) + result = runner.invoke(cli, base_command) + assert result.exit_code == 0 + assert modified_cost in result.output + + def test_coinrecords(self): + coin_path = Path(__file__).parent.joinpath(f"object_files/coins/coin.json") + pid = "0x0000000000000000000000000000000000000000000000000000000000000000" + ph = "0000000000000000000000000000000000000000000000000000000000000000" + amount = "0" + id = "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b" + coinbase = "False" + confirmed_block_index = "500" + spent = "True" + spent_block_index = "501" + timestamp = "909469800" + + runner = CliRunner() + + # Specify the coin file + result = runner.invoke(cli, ["inspect","--id","coinrecords","-c",str(coin_path),"-cb",coinbase,"-ci",confirmed_block_index,"-s",spent,"-si",spent_block_index,"-t",timestamp]) + assert result.exit_code == 0 + assert id in result.output + + # Specify all of the arguments + result = runner.invoke(cli, ["inspect","--id","coinrecords","-pid",pid,"-ph",ph,"-a",amount,"-cb",coinbase,"-ci",confirmed_block_index,"-s",spent,"-si",spent_block_index,"-t",timestamp]) + assert result.exit_code == 0 + assert id in result.output + + def test_programs(self): + program = "ff0101" + id = "69ae360134b1fae04326e5546f25dc794a19192a1f22a44a46d038e7f0d1ecbb" + + runner = CliRunner() + + result = runner.invoke(cli, ["inspect","--id","programs",program]) + assert result.exit_code == 0 + assert id in result.output + + def test_keys(self): + mnemonic = "chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia" + passphrase = "chia" + sk = "68cbc26a245903f3d20a405c0673a9f32b2382174abeeabadb7ba1478b162326" + pk = "b7531990662d3fbff22d073a08123ddeae70e0a118cecebf8f207b373da5a90aaefcfed2d9cab8fbe711d6b4f5c72e89" + hd_modifier = "m/12381/8444/0/0" + type_modifier = "farmer" # Should be same as above HD Path + farmer_sk = "5729513405cb68999711618aa02e317335cecdea63d666886bbb39c0fc487dae" + synthetic_sk = "6124dbc2580cbdc2f3c716572950434ecdb42f952ec2e947bd393643c13e8ec2" + ph_modifier = "69ae360134b1fae04326e5546f25dc794a19192a1f22a44a46d038e7f0d1ecbb" + modified_synthetic_sk = "66dff9a8d49d90029e5fb42378562d459e375965150bc72c3a7ea2c523ab49f5" + + runner = CliRunner() + + # Build the key from secret key + result = runner.invoke(cli, ["inspect","keys","-sk",sk]) + assert result.exit_code == 0 + assert sk in result.output + assert pk in result.output + key_output = result.output + + # Build the key from mnemonic + result = runner.invoke(cli, ["inspect","keys","-m",mnemonic,"-pw",passphrase]) + assert result.exit_code == 0 + assert result.output == key_output + + # Use only the public key + result = runner.invoke(cli, ["inspect","keys","-pk",pk]) + assert result.exit_code == 0 + assert result.output in key_output + + # Generate a random one + result = runner.invoke(cli, ["inspect","keys","--random"]) + assert result.exit_code == 0 + assert "Secret Key" in result.output + assert "Public Key" in result.output + + # Check the HD derivation is working + result = runner.invoke(cli, ["inspect","keys","-sk",sk,"-hd",hd_modifier]) + assert result.exit_code == 0 + assert farmer_sk in result.output + + # Check the type derivation is working + result = runner.invoke(cli, ["inspect","keys","-sk",sk,"-t",type_modifier]) + assert result.exit_code == 0 + assert farmer_sk in result.output + + # Check that synthetic calculation is working + result = runner.invoke(cli, ["inspect","keys","-sk",sk,"-sy"]) + assert result.exit_code == 0 + assert synthetic_sk in result.output + + # Check that using a non default puzzle hash is working + result = runner.invoke(cli, ["inspect","keys","-sk",sk,"-sy","-ph",ph_modifier]) + assert result.exit_code == 0 + assert modified_synthetic_sk in result.output + + def test_signatures(self): + empty_sig = "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + secret_key_1 = "70432627e84c13c1a6e6007bf6d9a7a0342018fdef7fc911757aad5a6929d20a" + secret_key_2 = "0f01f7f68935f8594548bca3892fec419c6b2aa7cff54c3353a2e9b1011f09c7" + text_message = "cafe food" + bytes_message = "0xcafef00d" + extra_signature = "b5d4e653ec9a737d19abe9af7050d37b0f464f9570ec66a8457fbdabdceb50a77c6610eb442ed1e4ace39d9ecc6d40560de239c1c8f7a115e052438385d594be7394df9287cf30c3254d39f0ae21daefc38d3d07ba3e373628bf8ed73f074a80" + final_signature = "b7a6ab2c825068eb40298acab665f95c13779e828d900b8056215b54e47d8b8314e8b61fbb9c98a23ef8a134155a35b109ba284bd5f1f90f96e0d41427132b3ca6a83faae0806daa632ee6b1602a0b4bad92f2743fdeb452822f0599dfa147c0" + + runner = CliRunner() + + # Test that an empty command returns an empty signature + result = runner.invoke(cli,["inspect","signatures"]) + assert result.exit_code == 0 + assert empty_sig in result.output + + # Test a complex signature calculation + result = runner.invoke(cli, ["inspect","signatures","-sk",secret_key_1,"-t",text_message,"-sk",secret_key_2,"-b",bytes_message,"-sig",extra_signature]) + assert result.exit_code == 0 + assert final_signature in result.output From b552bb69b64432eeca305824b5bd3f80e7c70f19 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Mon, 9 Aug 2021 13:25:40 -0700 Subject: [PATCH 19/31] Minor changes based on coverage report --- .gitignore | 1 + cdv/cmds/chia_inspect.py | 36 ++++++------- .../object_files/coinrecords/coinrecord.hex | 1 + .../object_files/spendbundles/spendbundle.hex | 1 + .../spendbundles/spendbundle.json | 18 ++++--- tests/cmds/object_files/spends/spend.hex | 1 + tests/cmds/test_clsp.py | 6 ++- tests/cmds/test_inspect.py | 50 ++++++++++++++++++- 8 files changed, 84 insertions(+), 30 deletions(-) create mode 100644 tests/cmds/object_files/coinrecords/coinrecord.hex create mode 100644 tests/cmds/object_files/spendbundles/spendbundle.hex create mode 100644 tests/cmds/object_files/spends/spend.hex diff --git a/.gitignore b/.gitignore index 19c9e4c..3d9a756 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /.eggs __pycache__/ main.sym +.coverage \ No newline at end of file diff --git a/cdv/cmds/chia_inspect.py b/cdv/cmds/chia_inspect.py index d8bad93..1b0730d 100644 --- a/cdv/cmds/chia_inspect.py +++ b/cdv/cmds/chia_inspect.py @@ -39,35 +39,29 @@ def inspect_cmd(ctx, **kwargs): ctx.obj[key] = value def inspect_callback(objs, ctx, id_calc=None, type='Unknown'): - if not any([value for key, value in ctx.obj.items()]): + if (not any([value for key, value in ctx.obj.items()])) or ctx.obj['json']: if getattr(objs[0], "to_json_dict", None): pprint([obj.to_json_dict() for obj in objs]) else: pprint(f"Object of type {type} cannot be serialized to JSON") - else: - if ctx.obj['json']: - if getattr(obj, "to_json_dict", None): - pprint([obj.to_json_dict() for obj in objs]) - else: - pprint(f"Object of type {type} cannot be serialized to JSON") - if ctx.obj['bytes']: - final_output = [] - for obj in objs: - try: - final_output.append(bytes(obj).hex()) - except AssertionError: - final_output.append(None) - pprint(final_output) - if ctx.obj['id']: - pprint([id_calc(obj) for obj in objs]) - if ctx.obj['type']: - pprint([type for _ in objs]) + if ctx.obj['bytes']: + final_output = [] + for obj in objs: + try: + final_output.append(bytes(obj).hex()) + except AssertionError: + final_output.append(None) + pprint(final_output) + if ctx.obj['id']: + pprint([id_calc(obj) for obj in objs]) + if ctx.obj['type']: + pprint([type for _ in objs]) # Utility functions def json_and_key_strip(input): json_dict = json.loads(input) if len(json_dict.keys()) == 1: - return json_dict[json_dict.keys()[0]] + return json_dict[list(json_dict.keys())[0]] else: return json_dict @@ -131,6 +125,8 @@ def inspect_any_cmd(ctx, objects): do_inspect_keys_cmd(ctx, public_key=obj) elif type(obj) == PrivateKey: do_inspect_keys_cmd(ctx, secret_key=obj) + elif type(obj) == G2Element: + print("That's a BLS aggregated signature") @inspect_cmd.command("coins", short_help="Various methods for examining and calculating coin objects") diff --git a/tests/cmds/object_files/coinrecords/coinrecord.hex b/tests/cmds/object_files/coinrecords/coinrecord.hex new file mode 100644 index 0000000..394fff1 --- /dev/null +++ b/tests/cmds/object_files/coinrecords/coinrecord.hex @@ -0,0 +1 @@ +000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000101010000000036356868 \ No newline at end of file diff --git a/tests/cmds/object_files/spendbundles/spendbundle.hex b/tests/cmds/object_files/spendbundles/spendbundle.hex new file mode 100644 index 0000000..dca0fc7 --- /dev/null +++ b/tests/cmds/object_files/spendbundles/spendbundle.hex @@ -0,0 +1 @@ +00000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ff010180c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 \ No newline at end of file diff --git a/tests/cmds/object_files/spendbundles/spendbundle.json b/tests/cmds/object_files/spendbundles/spendbundle.json index 5f6f2c8..79efbc5 100644 --- a/tests/cmds/object_files/spendbundles/spendbundle.json +++ b/tests/cmds/object_files/spendbundles/spendbundle.json @@ -1,10 +1,12 @@ { - "coin_spends": [ - {"coin": {"amount": 0, - "parent_coin_info": "0x0000000000000000000000000000000000000000000000000000000000000000", - "puzzle_hash": "0000000000000000000000000000000000000000000000000000000000000000"}, - "puzzle_reveal": "0xff0101", - "solution": "80"} - ], - "aggregated_signature": "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + "spend_bundle": { + "coin_spends": [ + {"coin": {"amount": 0, + "parent_coin_info": "0x0000000000000000000000000000000000000000000000000000000000000000", + "puzzle_hash": "0000000000000000000000000000000000000000000000000000000000000000"}, + "puzzle_reveal": "0xff0101", + "solution": "80"} + ], + "aggregated_signature": "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + } } \ No newline at end of file diff --git a/tests/cmds/object_files/spends/spend.hex b/tests/cmds/object_files/spends/spend.hex new file mode 100644 index 0000000..eb5f5d3 --- /dev/null +++ b/tests/cmds/object_files/spends/spend.hex @@ -0,0 +1 @@ +00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001ffff32ffb080df54b2a616f5c79baaed254134ae5dfc6e24e2d8e1165b251601ceb67b1886db50aacf946eb20f00adc303e7534dd0ff248080 \ No newline at end of file diff --git a/tests/cmds/test_clsp.py b/tests/cmds/test_clsp.py index fe0e63d..78c0489 100644 --- a/tests/cmds/test_clsp.py +++ b/tests/cmds/test_clsp.py @@ -23,6 +23,10 @@ def test_retrieve(self): assert result.exit_code == 0 assert Path("./include/condition_codes.clib").exists() + result = runner.invoke(cli, ["clsp", "retrieve", "sha256tree.clib"]) + assert result.exit_code == 0 + assert Path("./include/condition_codes.clib").exists() + def test_build(self): runner = CliRunner() with runner.isolated_filesystem(): @@ -42,7 +46,7 @@ def test_build(self): mod_file = open("mod.clsp","w") mod_file.write(self.mod) mod_file.close() - result = runner.invoke(cli, ["clsp","build","."]) + result = runner.invoke(cli, ["clsp","build","./mod.clsp"]) assert result.exit_code == 0 assert Path("./mod.clsp.hex").exists() assert open("mod.clsp.hex","r").read() == "ff0133" diff --git a/tests/cmds/test_inspect.py b/tests/cmds/test_inspect.py index 27ee7b2..5ec069f 100644 --- a/tests/cmds/test_inspect.py +++ b/tests/cmds/test_inspect.py @@ -9,6 +9,27 @@ class TestInspectCommands: def test_any(self): runner = CliRunner() + + # Try to inspect a program + result = runner.invoke(cli, ["inspect","any","ff0101"]) + assert result.exit_code == 0 + assert "guess" not in result.output + + # Try to inspect a private key + result = runner.invoke(cli, ["inspect","any","05ec9428fc2841a79e96631a633b154b57a45311c0602269a6500732093a52cd"]) + assert result.exit_code == 0 + assert "guess" not in result.output + + # Try to inspect a public key + result = runner.invoke(cli, ["inspect","any","b364a088d9df9423e54bff4c62e4bd854445fb8f5b8f6d80dea06773a4f828734a3a75318b180364ca8468836f0742db"]) + assert result.exit_code == 0 + assert "guess" not in result.output + + # Try to inspect an aggsig + result = runner.invoke(cli, ["inspect","any","c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"]) + assert result.exit_code == 0 + assert "guess" not in result.output + for class_type in ["coinrecord","coin","spendbundle","spend"]: valid_json_path = Path(__file__).parent.joinpath(f"object_files/{class_type}s/{class_type}.json") invalid_json_path = Path(__file__).parent.joinpath(f"object_files/{class_type}s/{class_type}_invalid.json") @@ -24,13 +45,28 @@ def test_any(self): assert str(invalid_json_path) in result.output # Try to load the valid json - result = runner.invoke(cli, ["inspect", "any", str(valid_json_path)]) + result = runner.invoke(cli, ["inspect", "any", json.dumps(valid_json)]) assert result.exit_code == 0 for key in valid_json.keys(): key_type = type(valid_json[key]) if (key_type is not dict) and (key_type is not list): assert str(valid_json[key]) in result.output + # Try to load bytes + if class_type != "coin": + valid_hex_path = Path(__file__).parent.joinpath(f"object_files/{class_type}s/{class_type}.hex") + + # From a file + result = runner.invoke(cli, ["inspect","--json","any", str(valid_hex_path)]) + assert result.exit_code == 0 + assert "'coin':" in result.output + + # From a string + valid_hex = open(valid_hex_path,"r").read() + result = runner.invoke(cli, ["inspect","--json","any", valid_hex]) + assert result.exit_code == 0 + assert "'coin':" in result.output + # Make sure the ID calculation is correct result = runner.invoke(cli, ["inspect", "--id", "any", str(valid_json_path)]) assert result.exit_code == 0 @@ -157,6 +193,12 @@ def test_spendbundles(self): assert result.exit_code == 0 assert modified_cost in result.output + # Try to use it the programmatic way (like cdv rpc pushtx does) + from cdv.cmds.chia_inspect import do_inspect_spend_bundle_cmd + from cdv.cmds.util import fake_context + bundle_path = Path(__file__).parent.joinpath(f"object_files/spendbundles/spendbundle.json") + assert len(do_inspect_spend_bundle_cmd(fake_context(), [str(bundle_path)], print_results=False)) > 0 + def test_coinrecords(self): coin_path = Path(__file__).parent.joinpath(f"object_files/coins/coin.json") pid = "0x0000000000000000000000000000000000000000000000000000000000000000" @@ -171,6 +213,12 @@ def test_coinrecords(self): runner = CliRunner() + # Try to load it from a file + record_path = Path(__file__).parent.joinpath(f"object_files/coinrecords/coinrecord.json") + result = runner.invoke(cli, ["inspect","coinrecords",str(record_path)]) + assert result.exit_code == 0 + assert "'coin'" in result.output + # Specify the coin file result = runner.invoke(cli, ["inspect","--id","coinrecords","-c",str(coin_path),"-cb",coinbase,"-ci",confirmed_block_index,"-s",spent,"-si",spent_block_index,"-t",timestamp]) assert result.exit_code == 0 From 67e32e86afbbb3834067036b62287ae51b6d4aea Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Mon, 9 Aug 2021 14:04:27 -0700 Subject: [PATCH 20/31] Added a workflow to run the test suite as part of CI --- .github/workflows/run-test-suite.yml | 46 ++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/run-test-suite.yml diff --git a/.github/workflows/run-test-suite.yml b/.github/workflows/run-test-suite.yml new file mode 100644 index 0000000..833d80e --- /dev/null +++ b/.github/workflows/run-test-suite.yml @@ -0,0 +1,46 @@ +name: Run Test Suite + +on: + workflow_dispatch: + push: + branches: + - main + tags: + - '**' + pull_request: + branches: + - '**' + +jobs: + build: + name: All tests + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + max-parallel: 4 + + steps: + - name: Cancel previous runs on the same branch + if: ${{ github.ref != 'refs/heads/main' }} + uses: styfle/cancel-workflow-action@0.9.1 + with: + access_token: ${{ github.token }} + + - name: Checkout Code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Setup Python environment + uses: actions/setup-python@v2 + with: + python-version: 3.9 + + - name: Test code with pytest + run: | + python3 -m venv venv + . ./venv/bin/activate + pip install . + ./venv/bin/chia init + ./venv/bin/py.test tests/ -s -v --durations 0 \ No newline at end of file From 7fb86ac7d7e4a23136e724d471ddd71d123d085b Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Mon, 9 Aug 2021 14:11:56 -0700 Subject: [PATCH 21/31] Update setup.py to new version --- setup.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 7fad5da..277f3fa 100644 --- a/setup.py +++ b/setup.py @@ -8,10 +8,7 @@ long_description = fh.read() dependencies = [ - "clvm_tools>=0.4.3", - "clvm_rs>=0.1.1", "chia-blockchain==1.2.3", - # "chia-blockchain@git+https://github.com/Chia-Network/chia-blockchain.git@fa2e66bc74d07a0d79d9a3762e5207aa6d38a0de", "pytest", "pytest-asyncio", "pytimeparse", @@ -21,7 +18,7 @@ setup( name="chia_dev_tools", - version="1.0.1", + version="1.0.2", packages=find_packages(exclude=("tests",)), author="Quexington", entry_points={ From 5b98fcc44acb4710ed3a24b34812f07bac3f5099 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Tue, 10 Aug 2021 08:54:53 -0700 Subject: [PATCH 22/31] Ran python black and flake8 --- cdv/__init__.py | 2 +- cdv/cmds/chia_inspect.py | 497 ++++++++++++++++------ cdv/cmds/cli.py | 47 +- cdv/cmds/clsp.py | 97 +++-- cdv/cmds/rpc.py | 217 +++++++--- cdv/cmds/util.py | 19 +- cdv/examples/drivers/piggybank_drivers.py | 14 +- cdv/examples/tests/test_piggybank.py | 75 ++-- cdv/test/__init__.py | 235 +++++----- cdv/test/test_skeleton.py | 1 + cdv/util/keys.py | 45 +- cdv/util/load_clvm.py | 26 +- setup.py | 6 +- tests/cmds/test_cdv.py | 43 +- tests/cmds/test_clsp.py | 70 +-- tests/cmds/test_inspect.py | 283 +++++++++--- 16 files changed, 1159 insertions(+), 518 deletions(-) diff --git a/cdv/__init__.py b/cdv/__init__.py index dfdf332..65a2efc 100644 --- a/cdv/__init__.py +++ b/cdv/__init__.py @@ -4,4 +4,4 @@ __version__ = get_distribution("chia-dev-tools").version except DistributionNotFound: # package is not installed - __version__ = "unknown" \ No newline at end of file + __version__ = "unknown" diff --git a/cdv/cmds/chia_inspect.py b/cdv/cmds/chia_inspect.py index 1b0730d..c267358 100644 --- a/cdv/cmds/chia_inspect.py +++ b/cdv/cmds/chia_inspect.py @@ -22,29 +22,34 @@ ) from chia.util.keychain import mnemonic_to_seed, bytes_to_mnemonic from chia.util.ints import uint64, uint32 -from chia.util.condition_tools import conditions_dict_for_solution, pkm_pairs_for_conditions_dict +from chia.util.condition_tools import ( + conditions_dict_for_solution, + pkm_pairs_for_conditions_dict, +) from chia.util.byte_types import hexstr_to_bytes from cdv.cmds.util import parse_program + @click.group("inspect", short_help="Inspect various data structures") -@click.option("-j","--json", is_flag=True, help="Output the result as JSON") -@click.option("-b","--bytes", is_flag=True, help="Output the result as bytes") -@click.option("-id","--id", is_flag=True, help="Output the id of the object") -@click.option("-t","--type", is_flag=True, help="Output the type of the object") +@click.option("-j", "--json", is_flag=True, help="Output the result as JSON") +@click.option("-b", "--bytes", is_flag=True, help="Output the result as bytes") +@click.option("-id", "--id", is_flag=True, help="Output the id of the object") +@click.option("-t", "--type", is_flag=True, help="Output the type of the object") @click.pass_context def inspect_cmd(ctx, **kwargs): ctx.ensure_object(dict) for key, value in kwargs.items(): ctx.obj[key] = value -def inspect_callback(objs, ctx, id_calc=None, type='Unknown'): - if (not any([value for key, value in ctx.obj.items()])) or ctx.obj['json']: + +def inspect_callback(objs, ctx, id_calc=None, type="Unknown"): + if (not any([value for key, value in ctx.obj.items()])) or ctx.obj["json"]: if getattr(objs[0], "to_json_dict", None): pprint([obj.to_json_dict() for obj in objs]) else: pprint(f"Object of type {type} cannot be serialized to JSON") - if ctx.obj['bytes']: + if ctx.obj["bytes"]: final_output = [] for obj in objs: try: @@ -52,11 +57,12 @@ def inspect_callback(objs, ctx, id_calc=None, type='Unknown'): except AssertionError: final_output.append(None) pprint(final_output) - if ctx.obj['id']: + if ctx.obj["id"]: pprint([id_calc(obj) for obj in objs]) - if ctx.obj['type']: + if ctx.obj["type"]: pprint([type for _ in objs]) + # Utility functions def json_and_key_strip(input): json_dict = json.loads(input) @@ -65,6 +71,7 @@ def json_and_key_strip(input): else: return json_dict + def streamable_load(cls, inputs): input_objs = [] for input in inputs: @@ -81,7 +88,10 @@ def streamable_load(cls, inputs): return input_objs -@inspect_cmd.command("any", short_help="Attempt to guess the type of the object before inspecting it") + +@inspect_cmd.command( + "any", short_help="Attempt to guess the type of the object before inspecting it" +) @click.argument("objects", nargs=-1, required=False) @click.pass_context def inspect_any_cmd(ctx, objects): @@ -92,18 +102,18 @@ def inspect_any_cmd(ctx, objects): for cls in [Coin, CoinSpend, SpendBundle, CoinRecord]: try: in_obj = streamable_load(cls, [obj])[0] - except: + except Exception: pass # Try it as some key stuff for cls in [G1Element, G2Element, PrivateKey]: try: in_obj = cls.from_bytes(hexstr_to_bytes(obj)) - except: + except Exception: pass # Try it as a Program try: in_obj = parse_program(obj) - except: + except Exception: pass input_objects.append(in_obj) @@ -129,18 +139,29 @@ def inspect_any_cmd(ctx, objects): print("That's a BLS aggregated signature") -@inspect_cmd.command("coins", short_help="Various methods for examining and calculating coin objects") +@inspect_cmd.command( + "coins", short_help="Various methods for examining and calculating coin objects" +) @click.argument("coins", nargs=-1, required=False) -@click.option("-pid","--parent-id", help="The parent coin's ID") -@click.option("-ph","--puzzle-hash", help="The tree hash of the CLVM puzzle that locks this coin") -@click.option("-a","--amount", help="The amount of the coin") +@click.option("-pid", "--parent-id", help="The parent coin's ID") +@click.option( + "-ph", "--puzzle-hash", help="The tree hash of the CLVM puzzle that locks this coin" +) +@click.option("-a", "--amount", help="The amount of the coin") @click.pass_context def inspect_coin_cmd(ctx, coins, **kwargs): do_inspect_coin_cmd(ctx, coins, **kwargs) + def do_inspect_coin_cmd(ctx, coins, print_results=True, **kwargs): if kwargs and all([kwargs[key] for key in kwargs.keys()]): - coin_objs = [Coin(hexstr_to_bytes(kwargs['parent_id']), hexstr_to_bytes(kwargs['puzzle_hash']), uint64(kwargs['amount']))] + coin_objs = [ + Coin( + hexstr_to_bytes(kwargs["parent_id"]), + hexstr_to_bytes(kwargs["puzzle_hash"]), + uint64(kwargs["amount"]), + ) + ] elif not kwargs or not any([kwargs[key] for key in kwargs.keys()]): coin_objs = [] try: @@ -148,31 +169,50 @@ def do_inspect_coin_cmd(ctx, coins, print_results=True, **kwargs): coin_objs = streamable_load(Coin, coins) else: coin_objs = coins - except: + except Exception: print("One or more of the specified objects was not a coin") else: print("Invalid arguments specified.") return if print_results: - inspect_callback(coin_objs, ctx, id_calc=(lambda e: e.name().hex()), type='Coin') + inspect_callback( + coin_objs, ctx, id_calc=(lambda e: e.name().hex()), type="Coin" + ) else: return coin_objs -@inspect_cmd.command("spends", short_help="Various methods for examining and calculating CoinSpend objects") + +@inspect_cmd.command( + "spends", + short_help="Various methods for examining and calculating CoinSpend objects", +) @click.argument("spends", nargs=-1, required=False) -@click.option("-c","--coin", help="The coin to spend (replaces -pid, -ph, -a)") -@click.option("-pid","--parent-id", help="The parent coin's ID") -@click.option("-ph","--puzzle-hash", help="The tree hash of the CLVM puzzle that locks the coin being spent") -@click.option("-a","--amount", help="The amount of the coin being spent") -@click.option("-pr","--puzzle-reveal", help="The program that is hashed into this coin") -@click.option("-s","--solution", help="The attempted solution to the puzzle") -@click.option("-ec","--cost", is_flag=True, help="Print the CLVM cost of the spend") -@click.option("-bc","--cost-per-byte", default=12000, show_default=True, help="The cost per byte in the puzzle and solution reveal to use when calculating cost") +@click.option("-c", "--coin", help="The coin to spend (replaces -pid, -ph, -a)") +@click.option("-pid", "--parent-id", help="The parent coin's ID") +@click.option( + "-ph", + "--puzzle-hash", + help="The tree hash of the CLVM puzzle that locks the coin being spent", +) +@click.option("-a", "--amount", help="The amount of the coin being spent") +@click.option( + "-pr", "--puzzle-reveal", help="The program that is hashed into this coin" +) +@click.option("-s", "--solution", help="The attempted solution to the puzzle") +@click.option("-ec", "--cost", is_flag=True, help="Print the CLVM cost of the spend") +@click.option( + "-bc", + "--cost-per-byte", + default=12000, + show_default=True, + help="The cost per byte in the puzzle and solution reveal to use when calculating cost", +) @click.pass_context -def inspect_coin_cmd(ctx, spends, **kwargs): +def inspect_coin_spend_cmd(ctx, spends, **kwargs): do_inspect_coin_spend_cmd(ctx, spends, **kwargs) + def do_inspect_coin_spend_cmd(ctx, spends, print_results=True, **kwargs): cost_flag = False cost_per_byte = 12000 @@ -181,23 +221,29 @@ def do_inspect_coin_spend_cmd(ctx, spends, print_results=True, **kwargs): cost_per_byte = kwargs["cost_per_byte"] del kwargs["cost"] del kwargs["cost_per_byte"] - if kwargs and all([kwargs['puzzle_reveal'], kwargs['solution']]): - if (not kwargs['coin']) and all([kwargs['parent_id'], kwargs['puzzle_hash'], kwargs['amount']]): - coin_spend_objs = [CoinSpend( - Coin( - hexstr_to_bytes(kwargs['parent_id']), - hexstr_to_bytes(kwargs['puzzle_hash']), - uint64(kwargs['amount']), - ), - parse_program(kwargs['puzzle_reveal']), - parse_program(kwargs['solution']), - )] - elif kwargs['coin']: - coin_spend_objs = [CoinSpend( - do_inspect_coin_cmd(ctx, [kwargs['coin']], print_results=False)[0], - parse_program(kwargs['puzzle_reveal']), - parse_program(kwargs['solution']), - )] + if kwargs and all([kwargs["puzzle_reveal"], kwargs["solution"]]): + if (not kwargs["coin"]) and all( + [kwargs["parent_id"], kwargs["puzzle_hash"], kwargs["amount"]] + ): + coin_spend_objs = [ + CoinSpend( + Coin( + hexstr_to_bytes(kwargs["parent_id"]), + hexstr_to_bytes(kwargs["puzzle_hash"]), + uint64(kwargs["amount"]), + ), + parse_program(kwargs["puzzle_reveal"]), + parse_program(kwargs["solution"]), + ) + ] + elif kwargs["coin"]: + coin_spend_objs = [ + CoinSpend( + do_inspect_coin_cmd(ctx, [kwargs["coin"]], print_results=False)[0], + parse_program(kwargs["puzzle_reveal"]), + parse_program(kwargs["solution"]), + ) + ] else: print("Invalid arguments specified.") elif not kwargs or not any([kwargs[key] for key in kwargs.keys()]): @@ -207,46 +253,92 @@ def do_inspect_coin_spend_cmd(ctx, spends, print_results=True, **kwargs): coin_spend_objs = streamable_load(CoinSpend, spends) else: coin_spend_objs = spends - except: + except Exception: print("One or more of the specified objects was not a coin spend") else: print("Invalid arguments specified.") return if print_results: - inspect_callback(coin_spend_objs, ctx, id_calc=(lambda e: e.coin.name().hex()), type='CoinSpend') + inspect_callback( + coin_spend_objs, + ctx, + id_calc=(lambda e: e.coin.name().hex()), + type="CoinSpend", + ) if cost_flag: for coin_spend in coin_spend_objs: - program = simple_solution_generator(SpendBundle([coin_spend], G2Element())) - npc_result = get_name_puzzle_conditions(program, INFINITE_COST, cost_per_byte=cost_per_byte, safe_mode=True) + program = simple_solution_generator( + SpendBundle([coin_spend], G2Element()) + ) + npc_result = get_name_puzzle_conditions( + program, INFINITE_COST, cost_per_byte=cost_per_byte, safe_mode=True + ) cost = calculate_cost_of_program(program, npc_result, cost_per_byte) print(f"Cost: {cost}") else: return coin_spend_objs -@inspect_cmd.command("spendbundles", short_help="Various methods for examining and calculating SpendBundle objects") + +@inspect_cmd.command( + "spendbundles", + short_help="Various methods for examining and calculating SpendBundle objects", +) @click.argument("bundles", nargs=-1, required=False) -@click.option("-s","--spend", multiple=True, help="A coin spend object to add to the bundle") -@click.option("-as","--aggsig", multiple=True, help="A BLS signature to aggregate into the bundle (can be used more than once)") -@click.option("-db","--debug", is_flag=True, help="Show debugging information about the bundles") -@click.option("-sd","--signable_data", is_flag=True, help="Print the data that needs to be signed in the bundles") -@click.option("-n","--network", default="mainnet", show_default=True, help="The network this spend bundle will be pushed to (for AGG_SIG_ME)") -@click.option("-ec","--cost", is_flag=True, help="Print the CLVM cost of the entire bundle") -@click.option("-bc","--cost-per-byte", default=12000, show_default=True, help="The cost per byte in the puzzle and solution reveal to use when calculating cost") +@click.option( + "-s", "--spend", multiple=True, help="A coin spend object to add to the bundle" +) +@click.option( + "-as", + "--aggsig", + multiple=True, + help="A BLS signature to aggregate into the bundle (can be used more than once)", +) +@click.option( + "-db", "--debug", is_flag=True, help="Show debugging information about the bundles" +) +@click.option( + "-sd", + "--signable_data", + is_flag=True, + help="Print the data that needs to be signed in the bundles", +) +@click.option( + "-n", + "--network", + default="mainnet", + show_default=True, + help="The network this spend bundle will be pushed to (for AGG_SIG_ME)", +) +@click.option( + "-ec", "--cost", is_flag=True, help="Print the CLVM cost of the entire bundle" +) +@click.option( + "-bc", + "--cost-per-byte", + default=12000, + show_default=True, + help="The cost per byte in the puzzle and solution reveal to use when calculating cost", +) @click.pass_context def inspect_spend_bundle_cmd(ctx, bundles, **kwargs): do_inspect_spend_bundle_cmd(ctx, bundles, **kwargs) + def do_inspect_spend_bundle_cmd(ctx, bundles, print_results=True, **kwargs): - if kwargs and (len(kwargs['spend']) > 0): - if (len(kwargs['aggsig']) > 0): - sig = AugSchemeMPL.aggregate([G2Element(hexstr_to_bytes(sig)) for sig in kwargs["aggsig"]]) + if kwargs and (len(kwargs["spend"]) > 0): + if len(kwargs["aggsig"]) > 0: + sig = AugSchemeMPL.aggregate( + [G2Element(hexstr_to_bytes(sig)) for sig in kwargs["aggsig"]] + ) else: sig = G2Element() - spend_bundle_objs = [SpendBundle( - do_inspect_coin_spend_cmd(ctx, kwargs["spend"], print_results=False), - sig - )] + spend_bundle_objs = [ + SpendBundle( + do_inspect_coin_spend_cmd(ctx, kwargs["spend"], print_results=False), + sig, + ) + ] else: spend_bundle_objs = [] try: @@ -254,28 +346,40 @@ def do_inspect_spend_bundle_cmd(ctx, bundles, print_results=True, **kwargs): spend_bundle_objs = streamable_load(SpendBundle, bundles) else: spend_bundle_objs = bundles - except: + except Exception: print("One or more of the specified objects was not a spend bundle") if print_results: - inspect_callback(spend_bundle_objs, ctx, id_calc=(lambda e: e.name().hex()), type='SpendBundle') + inspect_callback( + spend_bundle_objs, + ctx, + id_calc=(lambda e: e.name().hex()), + type="SpendBundle", + ) if kwargs: if kwargs["cost"]: for spend_bundle in spend_bundle_objs: program = simple_solution_generator(spend_bundle) - npc_result = get_name_puzzle_conditions(program, INFINITE_COST, cost_per_byte=kwargs["cost_per_byte"], safe_mode=True) - cost = calculate_cost_of_program(program, npc_result, kwargs["cost_per_byte"]) + npc_result = get_name_puzzle_conditions( + program, + INFINITE_COST, + cost_per_byte=kwargs["cost_per_byte"], + safe_mode=True, + ) + cost = calculate_cost_of_program( + program, npc_result, kwargs["cost_per_byte"] + ) print(f"Cost: {cost}") if kwargs["debug"]: - print(f"") - print(f"Debugging Information") - print(f"---------------------") + print("") + print("Debugging Information") + print("---------------------") for bundle in spend_bundle_objs: print(bundle.debug()) if kwargs["signable_data"]: - print(f"") - print(f"Public Key/Message Pairs") - print(f"------------------------") + print("") + print("Public Key/Message Pairs") + print("------------------------") pkm_dict = {} for obj in spend_bundle_objs: for coin_spend in obj.coin_spends: @@ -283,12 +387,17 @@ def do_inspect_spend_bundle_cmd(ctx, bundles, print_results=True, **kwargs): coin_spend.puzzle_reveal, coin_spend.solution, INFINITE_COST ) if err or conditions_dict is None: - print(f"Generating conditions failed, con:{conditions_dict}, error: {err}") + print( + f"Generating conditions failed, con:{conditions_dict}, error: {err}" + ) else: from chia.util.default_root import DEFAULT_ROOT_PATH from chia.util.config import load_config + config = load_config(DEFAULT_ROOT_PATH, "config.yaml") - genesis_challenge = config["network_overrides"]["constants"][kwargs["network"]]["GENESIS_CHALLENGE"] + genesis_challenge = config["network_overrides"][ + "constants" + ][kwargs["network"]]["GENESIS_CHALLENGE"] for pk, msg in pkm_pairs_for_conditions_dict( conditions_dict, coin_spend.coin.name(), @@ -305,45 +414,80 @@ def do_inspect_spend_bundle_cmd(ctx, bundles, print_results=True, **kwargs): else: return spend_bundle_objs -@inspect_cmd.command("coinrecords", short_help="Various methods for examining and calculating CoinRecord objects") + +@inspect_cmd.command( + "coinrecords", + short_help="Various methods for examining and calculating CoinRecord objects", +) @click.argument("records", nargs=-1, required=False) -@click.option("-c","--coin", help="The coin to spend (replaces -pid, -ph, -a)") -@click.option("-pid","--parent-id", help="The parent coin's ID") -@click.option("-ph","--puzzle-hash", help="The tree hash of the CLVM puzzle that locks the coin being spent") -@click.option("-a","--amount", help="The amount of the coin being spent") -@click.option("-cb","--coinbase", is_flag=True, help="Is this coin generated as a farming reward?") -@click.option("-ci","--confirmed-block-index", help="The block index in which this coin was created") -@click.option("-s","--spent", is_flag=True, help="Has the coin been spent?") -@click.option("-si","--spent-block-index", default=0, show_default=True, type=int, help="The block index in which this coin was spent") -@click.option("-t","--timestamp", help="The timestamp of the block in which this coin was created") +@click.option("-c", "--coin", help="The coin to spend (replaces -pid, -ph, -a)") +@click.option("-pid", "--parent-id", help="The parent coin's ID") +@click.option( + "-ph", + "--puzzle-hash", + help="The tree hash of the CLVM puzzle that locks the coin being spent", +) +@click.option("-a", "--amount", help="The amount of the coin being spent") +@click.option( + "-cb", + "--coinbase", + is_flag=True, + help="Is this coin generated as a farming reward?", +) +@click.option( + "-ci", + "--confirmed-block-index", + help="The block index in which this coin was created", +) +@click.option("-s", "--spent", is_flag=True, help="Has the coin been spent?") +@click.option( + "-si", + "--spent-block-index", + default=0, + show_default=True, + type=int, + help="The block index in which this coin was spent", +) +@click.option( + "-t", + "--timestamp", + help="The timestamp of the block in which this coin was created", +) @click.pass_context def inspect_coin_record_cmd(ctx, records, **kwargs): do_inspect_coin_record_cmd(ctx, records, **kwargs) + def do_inspect_coin_record_cmd(ctx, records, print_results=True, **kwargs): - if kwargs and all([kwargs['confirmed_block_index'], kwargs['timestamp']]): - if (not kwargs['coin']) and all([kwargs['parent_id'], kwargs['puzzle_hash'], kwargs['amount']]): - coin_record_objs = [CoinRecord( - Coin( - hexstr_to_bytes(kwargs['parent_id']), - hexstr_to_bytes(kwargs['puzzle_hash']), - uint64(kwargs['amount']), - ), - kwargs["confirmed_block_index"], - kwargs["spent_block_index"], - kwargs["spent"], - kwargs["coinbase"], - kwargs["timestamp"], - )] - elif kwargs['coin']: - coin_record_objs = [CoinRecord( - do_inspect_coin_cmd(ctx, [kwargs['coin']], print_results=False)[0], - kwargs["confirmed_block_index"], - kwargs["spent_block_index"], - kwargs["spent"], - kwargs["coinbase"], - kwargs["timestamp"], - )] + if kwargs and all([kwargs["confirmed_block_index"], kwargs["timestamp"]]): + if (not kwargs["coin"]) and all( + [kwargs["parent_id"], kwargs["puzzle_hash"], kwargs["amount"]] + ): + coin_record_objs = [ + CoinRecord( + Coin( + hexstr_to_bytes(kwargs["parent_id"]), + hexstr_to_bytes(kwargs["puzzle_hash"]), + uint64(kwargs["amount"]), + ), + kwargs["confirmed_block_index"], + kwargs["spent_block_index"], + kwargs["spent"], + kwargs["coinbase"], + kwargs["timestamp"], + ) + ] + elif kwargs["coin"]: + coin_record_objs = [ + CoinRecord( + do_inspect_coin_cmd(ctx, [kwargs["coin"]], print_results=False)[0], + kwargs["confirmed_block_index"], + kwargs["spent_block_index"], + kwargs["spent"], + kwargs["coinbase"], + kwargs["timestamp"], + ) + ] else: print("Invalid arguments specified.") elif not kwargs or not any([kwargs[key] for key in kwargs.keys()]): @@ -353,23 +497,32 @@ def do_inspect_coin_record_cmd(ctx, records, print_results=True, **kwargs): coin_record_objs = streamable_load(CoinRecord, records) else: coin_record_objs = records - except: + except Exception: print("One or more of the specified objects was not a coin record") else: print("Invalid arguments specified.") return if print_results: - inspect_callback(coin_record_objs, ctx, id_calc=(lambda e: e.coin.name().hex()), type='CoinRecord') + inspect_callback( + coin_record_objs, + ctx, + id_calc=(lambda e: e.coin.name().hex()), + type="CoinRecord", + ) else: return coin_record_objs -@inspect_cmd.command("programs", short_help="Various methods for examining CLVM Program objects") + +@inspect_cmd.command( + "programs", short_help="Various methods for examining CLVM Program objects" +) @click.argument("programs", nargs=-1, required=False) @click.pass_context def inspect_program_cmd(ctx, programs, **kwargs): do_inspect_program_cmd(ctx, programs, **kwargs) + def do_inspect_program_cmd(ctx, programs, print_results=True, **kwargs): program_objs = [] try: @@ -377,28 +530,65 @@ def do_inspect_program_cmd(ctx, programs, print_results=True, **kwargs): program_objs = [parse_program(prog) for prog in programs] else: program_objs = programs - except: + except Exception: print("One or more of the specified objects was not a Program") if print_results: - inspect_callback(program_objs, ctx, id_calc=(lambda e: e.get_tree_hash().hex()), type='Program') + inspect_callback( + program_objs, + ctx, + id_calc=(lambda e: e.get_tree_hash().hex()), + type="Program", + ) else: return program_objs -@inspect_cmd.command("keys", short_help="Various methods for examining and generating BLS Keys") -@click.option("-pk","--public-key", help="A BLS public key") -@click.option("-sk","--secret-key", help="The secret key from which to derive the public key") -@click.option("-m","--mnemonic", help="A 24 word mnemonic from which to derive the secret key") -@click.option("-pw","--passphrase", default="", show_default=True, help="A passphrase to use when deriving a secret key from mnemonic") -@click.option("-r","--random", is_flag=True, help="Generate a random set of keys") -@click.option("-hd","--hd-path", help="Enter the HD path in the form 'm/12381/8444/n/n'") -@click.option("-t","--key-type", type=click.Choice(["farmer","pool","wallet","local","backup","owner","auth"]), help="Automatically use a chia defined HD path for a specific service") -@click.option("-sy","--synthetic", is_flag=True, help="Use a hidden puzzle hash (-ph) to calculate a synthetic secret/public key") -@click.option("-ph","--hidden-puzhash", default=DEFAULT_HIDDEN_PUZZLE_HASH.hex(), show_default=False, help="The hidden puzzle to use when calculating a synthetic key") + +@inspect_cmd.command( + "keys", short_help="Various methods for examining and generating BLS Keys" +) +@click.option("-pk", "--public-key", help="A BLS public key") +@click.option( + "-sk", "--secret-key", help="The secret key from which to derive the public key" +) +@click.option( + "-m", "--mnemonic", help="A 24 word mnemonic from which to derive the secret key" +) +@click.option( + "-pw", + "--passphrase", + default="", + show_default=True, + help="A passphrase to use when deriving a secret key from mnemonic", +) +@click.option("-r", "--random", is_flag=True, help="Generate a random set of keys") +@click.option( + "-hd", "--hd-path", help="Enter the HD path in the form 'm/12381/8444/n/n'" +) +@click.option( + "-t", + "--key-type", + type=click.Choice(["farmer", "pool", "wallet", "local", "backup", "owner", "auth"]), + help="Automatically use a chia defined HD path for a specific service", +) +@click.option( + "-sy", + "--synthetic", + is_flag=True, + help="Use a hidden puzzle hash (-ph) to calculate a synthetic secret/public key", +) +@click.option( + "-ph", + "--hidden-puzhash", + default=DEFAULT_HIDDEN_PUZZLE_HASH.hex(), + show_default=False, + help="The hidden puzzle to use when calculating a synthetic key", +) @click.pass_context def inspect_keys_cmd(ctx, **kwargs): do_inspect_keys_cmd(ctx, **kwargs) + def do_inspect_keys_cmd(ctx, print_results=True, **kwargs): sk = None pk = None @@ -410,9 +600,16 @@ def do_inspect_keys_cmd(ctx, print_results=True, **kwargs): elif "public_key" in kwargs: pk = kwargs["public_key"] else: - condition_list = [kwargs["public_key"], kwargs["secret_key"], kwargs["mnemonic"], kwargs["random"]] + condition_list = [ + kwargs["public_key"], + kwargs["secret_key"], + kwargs["mnemonic"], + kwargs["random"], + ] + def one_or_zero(value): return 1 if value else 0 + if sum([one_or_zero(condition) for condition in condition_list]) == 1: if kwargs["public_key"]: sk = None @@ -425,11 +622,15 @@ def one_or_zero(value): sk = AugSchemeMPL.key_gen(seed) pk = sk.get_g1() elif kwargs["random"]: - sk = AugSchemeMPL.key_gen(mnemonic_to_seed(bytes_to_mnemonic(token_bytes(32)),"")) + sk = AugSchemeMPL.key_gen( + mnemonic_to_seed(bytes_to_mnemonic(token_bytes(32)), "") + ) pk = sk.get_g1() if kwargs["hd_path"] and (kwargs["hd_path"] != "m"): - path = [uint32(int(i)) for i in kwargs["hd_path"].split("/") if i != "m"] + path = [ + uint32(int(i)) for i in kwargs["hd_path"].split("/") if i != "m" + ] elif kwargs["key_type"]: case = kwargs["key_type"] if case == "farmer": @@ -453,8 +654,12 @@ def one_or_zero(value): if kwargs["synthetic"]: if sk: - sk = calculate_synthetic_secret_key(sk, hexstr_to_bytes(kwargs["hidden_puzhash"])) - pk = calculate_synthetic_public_key(pk, hexstr_to_bytes(kwargs["hidden_puzhash"])) + sk = calculate_synthetic_secret_key( + sk, hexstr_to_bytes(kwargs["hidden_puzhash"]) + ) + pk = calculate_synthetic_public_key( + pk, hexstr_to_bytes(kwargs["hidden_puzhash"]) + ) else: print("Invalid arguments specified.") @@ -479,15 +684,33 @@ def parse_args(self, ctx, args): # return "normal" parse results return super().parse_args(ctx, args) -@inspect_cmd.command("signatures", cls=OrderedParamsCommand, short_help="Various methods for examining and creating BLS aggregated signatures") -@click.option("-sk","--secret-key", multiple=True, help="A secret key to sign a message with") -@click.option("-t","--utf-8", multiple=True, help="A UTF-8 message to be signed with the specified secret key") -@click.option("-b","--bytes", multiple=True, help="A hex message to be signed with the specified secret key") -@click.option("-sig","--aggsig", multiple=True, help="A signature to be aggregated") + +@inspect_cmd.command( + "signatures", + cls=OrderedParamsCommand, + short_help="Various methods for examining and creating BLS aggregated signatures", +) +@click.option( + "-sk", "--secret-key", multiple=True, help="A secret key to sign a message with" +) +@click.option( + "-t", + "--utf-8", + multiple=True, + help="A UTF-8 message to be signed with the specified secret key", +) +@click.option( + "-b", + "--bytes", + multiple=True, + help="A hex message to be signed with the specified secret key", +) +@click.option("-sig", "--aggsig", multiple=True, help="A signature to be aggregated") @click.pass_context def inspect_sigs_cmd(ctx, **kwargs): do_inspect_sigs_cmd(ctx, **kwargs) + def do_inspect_sigs_cmd(ctx, print_results=True, **kwargs): base = G2Element() sk = None diff --git a/cdv/cmds/cli.py b/cdv/cmds/cli.py index cfcb9bc..cb27e2a 100644 --- a/cdv/cmds/cli.py +++ b/cdv/cmds/cli.py @@ -1,7 +1,6 @@ import click import pytest import os -import io import shutil from pathlib import Path @@ -18,6 +17,7 @@ CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) + def monkey_patch_click() -> None: # this hacks around what seems to be an incompatibility between the python from `pyinstaller` # and `click` @@ -35,7 +35,7 @@ def monkey_patch_click() -> None: @click.group( - help=f"\n Dev tooling for Chia development \n", + help="\n Dev tooling for Chia development \n", context_settings=CONTEXT_SETTINGS, ) @click.version_option(__version__) @@ -43,10 +43,23 @@ def monkey_patch_click() -> None: def cli(ctx: click.Context) -> None: ctx.ensure_object(dict) + @cli.command("test", short_help="Run the local test suite (located in ./tests)") @click.argument("tests", default="./tests", required=False) -@click.option("-d", "--discover", is_flag=True, type=bool, help="List the tests without running them") -@click.option("-i","--init", is_flag=True, type=bool, help="Create the test directory and/or add a new test skeleton") +@click.option( + "-d", + "--discover", + is_flag=True, + type=bool, + help="List the tests without running them", +) +@click.option( + "-i", + "--init", + is_flag=True, + type=bool, + help="Create the test directory and/or add a new test skeleton", +) def test_cmd(tests: str, discover: bool, init: str): test_paths = Path.cwd().glob(tests) test_paths = list(map(lambda e: str(e), test_paths)) @@ -55,17 +68,21 @@ def test_cmd(tests: str, discover: bool, init: str): if not test_dir.exists(): os.mkdir("tests") import cdv.test as testlib + src_path = Path(testlib.__file__).parent.joinpath("test_skeleton.py") dest_path = test_dir.joinpath("test_skeleton.py") shutil.copyfile(src_path, dest_path) dest_path_init = test_dir.joinpath("__init__.py") - open(dest_path_init,"w") + open(dest_path_init, "w") if discover: - pytest.main(["--collect-only",*test_paths]) + pytest.main(["--collect-only", *test_paths]) elif not init: pytest.main([*test_paths]) -@cli.command("hash", short_help="SHA256 hash UTF-8 strings or bytes (use 0x prefix for bytes)") + +@cli.command( + "hash", short_help="SHA256 hash UTF-8 strings or bytes (use 0x prefix for bytes)" +) @click.argument("data", nargs=1, required=True) def hash_cmd(data): if data[:2] == "0x": @@ -74,21 +91,33 @@ def hash_cmd(data): hash_data = bytes(data, "utf-8") print(std_hash(hash_data)) + @cli.command("encode", short_help="Encode a puzzle hash to a bech32m address") @click.argument("puzzle_hash", nargs=1, required=True) -@click.option("-p", "--prefix", type=str, default="xch", show_default=True, required=False, help="The prefix to encode with") +@click.option( + "-p", + "--prefix", + type=str, + default="xch", + show_default=True, + required=False, + help="The prefix to encode with", +) def encode_cmd(puzzle_hash, prefix): print(encode_puzzle_hash(bytes.fromhex(puzzle_hash), prefix)) + @cli.command("decode", short_help="Decode a bech32m address to a puzzle hash") @click.argument("address", nargs=1, required=True) -def encode_cmd(address): +def decode_cmd(address): print(decode_puzzle_hash(address).hex()) + cli.add_command(clsp.clsp_cmd) cli.add_command(chia_inspect.inspect_cmd) cli.add_command(rpc.rpc_cmd) + def main() -> None: monkey_patch_click() cli() # pylint: disable=no-value-for-parameter diff --git a/cdv/cmds/clsp.py b/cdv/cmds/clsp.py index f7a1d3d..2c0b5ca 100644 --- a/cdv/cmds/clsp.py +++ b/cdv/cmds/clsp.py @@ -1,71 +1,109 @@ import click import os -import io import shutil -import re from pathlib import Path -from clvm.SExp import SExp -from clvm.serialize import sexp_from_stream - -from clvm_tools.clvmc import compile_clvm, compile_clvm_text +from clvm_tools.clvmc import compile_clvm from clvm_tools.binutils import disassemble, assemble -from chia.types.blockchain_format.program import Program -from chia.util.byte_types import hexstr_to_bytes - from cdv.cmds.util import parse_program, append_include + @click.group("clsp", short_help="Commands to use when developing with chialisp") def clsp_cmd(): pass -@clsp_cmd.command("build", short_help="Build all specified CLVM files (i.e mypuz.clsp or ./puzzles/*.clsp)") + +@clsp_cmd.command( + "build", + short_help="Build all specified CLVM files (i.e mypuz.clsp or ./puzzles/*.clsp)", +) @click.argument("files", nargs=-1, required=True, default=None) -@click.option("-i","--include", required=False, multiple=True, help="Paths to search for include files (./include will be searched automatically)") +@click.option( + "-i", + "--include", + required=False, + multiple=True, + help="Paths to search for include files (./include will be searched automatically)", +) def build_cmd(files, include) -> None: project_path = Path.cwd() clvm_files = [] for glob in files: for path in Path(project_path).rglob(glob): if path.is_dir(): - for clvm_path in Path(path).rglob('*.cl[vs][mp]'): + for clvm_path in Path(path).rglob("*.cl[vs][mp]"): clvm_files.append(clvm_path) else: clvm_files.append(path) for filename in clvm_files: - hex_file_name = (filename.name + ".hex") + hex_file_name = filename.name + ".hex" full_hex_file_name = Path(filename.parent).joinpath(hex_file_name) - if not (full_hex_file_name.exists() and full_hex_file_name.stat().st_mtime > filename.stat().st_mtime): + if not ( + full_hex_file_name.exists() + and full_hex_file_name.stat().st_mtime > filename.stat().st_mtime + ): outfile = str(filename) + ".hex" try: - print("Beginning compilation of "+filename.name+"...") - compile_clvm(str(filename),outfile, search_paths=append_include(include)) + print("Beginning compilation of " + filename.name + "...") + compile_clvm( + str(filename), outfile, search_paths=append_include(include) + ) print("...Compilation finished") except Exception as e: - print("Couldn't build "+filename.name+": "+str(e)) + print("Couldn't build " + filename.name + ": " + str(e)) -@clsp_cmd.command("disassemble", short_help="Disassemble serialized clvm into human readable form.") +@clsp_cmd.command( + "disassemble", short_help="Disassemble serialized clvm into human readable form." +) @click.argument("programs", nargs=-1, required=True) def disassemble_cmd(programs): for program in programs: print(disassemble(parse_program(program))) -@clsp_cmd.command("treehash", short_help="Return the tree hash of a clvm file or string") + +@clsp_cmd.command( + "treehash", short_help="Return the tree hash of a clvm file or string" +) @click.argument("program", nargs=1, required=True) -@click.option("-i","--include", required=False, multiple=True, help="Paths to search for include files (./include will be searched automatically)") +@click.option( + "-i", + "--include", + required=False, + multiple=True, + help="Paths to search for include files (./include will be searched automatically)", +) def treehash_cmd(program: str, include): print(parse_program(program, include).get_tree_hash()) + @clsp_cmd.command("curry", short_help="Curry a program with specified arguments") @click.argument("program", required=True) -@click.option("-a","--args", multiple=True, help="An argument to be curried in (i.e. -a 0xdeadbeef -a '(a 2 3)')") -@click.option("-H","--treehash", is_flag=True, help="Output the tree hash of the curried puzzle") -@click.option("-x","--dump", is_flag=True, help="Output the hex serialized program rather that the CLVM form") -@click.option("-i","--include", required=False, multiple=True, help="Paths to search for include files (./include will be searched automatically)") +@click.option( + "-a", + "--args", + multiple=True, + help="An argument to be curried in (i.e. -a 0xdeadbeef -a '(a 2 3)')", +) +@click.option( + "-H", "--treehash", is_flag=True, help="Output the tree hash of the curried puzzle" +) +@click.option( + "-x", + "--dump", + is_flag=True, + help="Output the hex serialized program rather that the CLVM form", +) +@click.option( + "-i", + "--include", + required=False, + multiple=True, + help="Paths to search for include files (./include will be searched automatically)", +) def curry_cmd(program, args, treehash, dump, include): prog = parse_program(program, include) curry_args = [assemble(arg) for arg in args] @@ -78,10 +116,15 @@ def curry_cmd(program, args, treehash, dump, include): else: print(disassemble(prog_final)) -@clsp_cmd.command("retrieve", short_help="Copy the specified .clib file to the current directory (for example sha256tree)") + +@clsp_cmd.command( + "retrieve", + short_help="Copy the specified .clib file to the current directory (for example sha256tree)", +) @click.argument("libraries", nargs=-1, required=True) def retrieve_cmd(libraries): import cdv.clibs as clibs + for lib in libraries: if lib[-5:] == ".clib": lib = lib[:-5] @@ -92,4 +135,6 @@ def retrieve_cmd(libraries): if src_path.exists(): shutil.copyfile(src_path, include_path.joinpath(f"{lib}.clib")) else: - print(f"Could not find {lib}.clib. You can always create it in ./include yourself.") \ No newline at end of file + print( + f"Could not find {lib}.clib. You can always create it in ./include yourself." + ) diff --git a/cdv/cmds/rpc.py b/cdv/cmds/rpc.py index 2c7b570..3a62722 100644 --- a/cdv/cmds/rpc.py +++ b/cdv/cmds/rpc.py @@ -1,7 +1,6 @@ import click import aiohttp import asyncio -import json from pprint import pprint @@ -11,37 +10,45 @@ from chia.util.ints import uint16 from chia.util.misc import format_bytes from chia.util.byte_types import hexstr_to_bytes -from chia.types.spend_bundle import SpendBundle from chia.types.blockchain_format.coin import Coin from cdv.cmds.util import fake_context from cdv.cmds.chia_inspect import do_inspect_spend_bundle_cmd + @click.group("rpc", short_help="Make RPC requests to a Chia full node") def rpc_cmd(): pass + async def get_client(): try: config = load_config(DEFAULT_ROOT_PATH, "config.yaml") self_hostname = config["self_hostname"] full_node_rpc_port = config["full_node"]["rpc_port"] - full_node_client = await FullNodeRpcClient.create(self_hostname, uint16(full_node_rpc_port), DEFAULT_ROOT_PATH, config) + full_node_client = await FullNodeRpcClient.create( + self_hostname, uint16(full_node_rpc_port), DEFAULT_ROOT_PATH, config + ) return full_node_client except Exception as e: if isinstance(e, aiohttp.ClientConnectorError): - pprint(f"Connection error. Check if full node is running at {full_node_rpc_port}") + pprint( + f"Connection error. Check if full node is running at {full_node_rpc_port}" + ) else: pprint(f"Exception from 'harvester' {e}") return None -@rpc_cmd.command("state", short_help="Gets the status of the blockchain (get_blockchain_state)") + +@rpc_cmd.command( + "state", short_help="Gets the status of the blockchain (get_blockchain_state)" +) def rpc_state_cmd(): async def do_command(): try: node_client = await get_client() state = await node_client.get_blockchain_state() - state['peak'] = state['peak'].to_json_dict() + state["peak"] = state["peak"].to_json_dict() pprint(state) finally: node_client.close() @@ -49,10 +56,11 @@ async def do_command(): asyncio.get_event_loop().run_until_complete(do_command()) + @rpc_cmd.command("blocks", short_help="Gets blocks between two indexes (get_block(s))") -@click.option("-hh","--header-hash", help="The header hash of the block to get") -@click.option("-s","--start", help="The block index to start at (included)") -@click.option("-e","--end", help="The block index to end at (excluded)") +@click.option("-hh", "--header-hash", help="The header hash of the block to get") +@click.option("-s", "--start", help="The block index to start at (included)") +@click.option("-e", "--end", help="The block index to end at (excluded)") def rpc_blocks_cmd(header_hash, start, end): async def do_command(): try: @@ -71,17 +79,23 @@ async def do_command(): asyncio.get_event_loop().run_until_complete(do_command()) -@rpc_cmd.command("blockrecords", short_help="Gets block records between two indexes (get_block_record(s), get_block_record_by_height)") -@click.option("-hh","--header-hash", help="The header hash of the block to get") -@click.option("-i","--height", help="The height of the block to get") -@click.option("-s","--start", help="The block index to start at (included)") -@click.option("-e","--end", help="The block index to end at (excluded)") + +@rpc_cmd.command( + "blockrecords", + short_help="Gets block records between two indexes (get_block_record(s), get_block_record_by_height)", +) +@click.option("-hh", "--header-hash", help="The header hash of the block to get") +@click.option("-i", "--height", help="The height of the block to get") +@click.option("-s", "--start", help="The block index to start at (included)") +@click.option("-e", "--end", help="The block index to end at (excluded)") def rpc_blockrecords_cmd(header_hash, height, start, end): async def do_command(): try: node_client = await get_client() if header_hash: - block_record = await node_client.get_block_record(hexstr_to_bytes(header_hash)) + block_record = await node_client.get_block_record( + hexstr_to_bytes(header_hash) + ) block_records = block_record.to_json_dict() if block_record else [] elif height: block_record = await node_client.get_block_record_by_height(height) @@ -97,7 +111,11 @@ async def do_command(): asyncio.get_event_loop().run_until_complete(do_command()) -@rpc_cmd.command("unfinished", short_help="Returns the current unfinished header blocks (get_unfinished_block_headers)") + +@rpc_cmd.command( + "unfinished", + short_help="Returns the current unfinished header blocks (get_unfinished_block_headers)", +) def rpc_unfinished_cmd(): async def do_command(): try: @@ -110,11 +128,15 @@ async def do_command(): asyncio.get_event_loop().run_until_complete(do_command()) -@rpc_cmd.command("space", short_help="Gets the netspace of the network between two blocks (get_network_space)") -@click.option("-old","--older", help="The header hash of the older block") -@click.option("-new","--newer", help="The header hash of the newer block") -@click.option("-s","--start", help="The height of the block to start at") -@click.option("-e","--end", help="The height of the block to end at") + +@rpc_cmd.command( + "space", + short_help="Gets the netspace of the network between two blocks (get_network_space)", +) +@click.option("-old", "--older", help="The header hash of the older block") +@click.option("-new", "--newer", help="The header hash of the newer block") +@click.option("-s", "--start", help="The height of the block to start at") +@click.option("-e", "--end", help="The height of the block to end at") def rpc_space_cmd(older, newer, start, end): async def do_command(): try: @@ -124,18 +146,28 @@ async def do_command(): pprint("Invalid arguments specified.") else: if start: - start_hash = (await node_client.get_block_record_by_height(start)).header_hash + start_hash = ( + await node_client.get_block_record_by_height(start) + ).header_hash elif older: start_hash = hexstr_to_bytes(older) else: - start_hash = (await node_client.get_block_record_by_height(0)).header_hash + start_hash = ( + await node_client.get_block_record_by_height(0) + ).header_hash if end: - end_hash = (await node_client.get_block_record_by_height(end)).header_hash + end_hash = ( + await node_client.get_block_record_by_height(end) + ).header_hash elif newer: end_hash = hexstr_to_bytes(newer) else: - end_hash = (await node_client.get_block_record_by_height((await node_client.get_blockchain_state())["peak"].height)).header_hash + end_hash = ( + await node_client.get_block_record_by_height( + (await node_client.get_blockchain_state())["peak"].height + ) + ).header_hash netspace = await node_client.get_network_space(start_hash, end_hash) if netspace: @@ -149,30 +181,50 @@ async def do_command(): asyncio.get_event_loop().run_until_complete(do_command()) -@rpc_cmd.command("blockcoins", short_help="Gets the coins added and removed for a specific header hash (get_additions_and_removals)") + +@rpc_cmd.command( + "blockcoins", + short_help="Gets the coins added and removed for a specific header hash (get_additions_and_removals)", +) @click.argument("headerhash", nargs=1, required=True) def rpc_addrem_cmd(headerhash): async def do_command(): try: node_client = await get_client() - additions, removals = await node_client.get_additions_and_removals(bytes.fromhex(headerhash)) + additions, removals = await node_client.get_additions_and_removals( + bytes.fromhex(headerhash) + ) additions = [rec.to_json_dict() for rec in additions] removals = [rec.to_json_dict() for rec in removals] - pprint({'additions': additions, 'removals': removals}) + pprint({"additions": additions, "removals": removals}) finally: node_client.close() await node_client.await_closed() asyncio.get_event_loop().run_until_complete(do_command()) -@rpc_cmd.command("blockspends", short_help="Gets the puzzle and solution for a coin spent at the specified block height (get_puzzle_and_solution)") -@click.option("-id","--coinid", required=True, help="The id of the coin that was spent") -@click.option("-h","--block-height", required=True, type=int, help="The block height in which the coin was spent") + +@rpc_cmd.command( + "blockspends", + short_help="Gets the puzzle and solution for a coin spent at the specified block height (get_puzzle_and_solution)", +) +@click.option( + "-id", "--coinid", required=True, help="The id of the coin that was spent" +) +@click.option( + "-h", + "--block-height", + required=True, + type=int, + help="The block height in which the coin was spent", +) def rpc_puzsol_cmd(coinid, block_height): async def do_command(): try: node_client = await get_client() - coin_spend = await node_client.get_puzzle_and_solution(bytes.fromhex(coinid), block_height) + coin_spend = await node_client.get_puzzle_and_solution( + bytes.fromhex(coinid), block_height + ) pprint(coin_spend) finally: node_client.close() @@ -180,13 +232,16 @@ async def do_command(): asyncio.get_event_loop().run_until_complete(do_command()) + @rpc_cmd.command("pushtx", short_help="Pushes a spend bundle to the network (push_tx)") @click.argument("spendbundles", nargs=-1, required=True) def rpc_pushtx_cmd(spendbundles): async def do_command(): try: node_client = await get_client() - for bundle in do_inspect_spend_bundle_cmd(fake_context(), spendbundles, print_results=False): + for bundle in do_inspect_spend_bundle_cmd( + fake_context(), spendbundles, print_results=False + ): try: result = await node_client.push_tx(bundle) pprint(result) @@ -198,16 +253,28 @@ async def do_command(): asyncio.get_event_loop().run_until_complete(do_command()) -@rpc_cmd.command("mempool", short_help="Gets items that are currently sitting in the mempool (get_(all_)mempool_*)") -@click.option("-txid","--transaction-id", help="The ID of a spend bundle that is sitting in the mempool") -@click.option("--ids-only", is_flag=True, help="Only show the IDs of the retrieved spend bundles") + +@rpc_cmd.command( + "mempool", + short_help="Gets items that are currently sitting in the mempool (get_(all_)mempool_*)", +) +@click.option( + "-txid", + "--transaction-id", + help="The ID of a spend bundle that is sitting in the mempool", +) +@click.option( + "--ids-only", is_flag=True, help="Only show the IDs of the retrieved spend bundles" +) def rpc_mempool_cmd(transaction_id, ids_only): async def do_command(): try: node_client = await get_client() if transaction_id: items = {} - items[transaction_id] = await node_client.get_mempool_item_by_tx_id(hexstr_to_bytes(transaction_id)) + items[transaction_id] = await node_client.get_mempool_item_by_tx_id( + hexstr_to_bytes(transaction_id) + ) else: b_items = await node_client.get_all_mempool_items() items = {} @@ -224,31 +291,75 @@ async def do_command(): asyncio.get_event_loop().run_until_complete(do_command()) -@rpc_cmd.command("coinrecords", short_help="Gets coin records by a specified information (get_coin_records_by_*)") + +@rpc_cmd.command( + "coinrecords", + short_help="Gets coin records by a specified information (get_coin_records_by_*)", +) @click.argument("values", nargs=-1, required=True) @click.option("--by", help="The property to use (id, puzzlehash, parentid)") -@click.option("-nd","--as-name-dict", is_flag=True, help="Return the records as a dictionary with ids as the keys") -@click.option("-ou","--only-unspent", is_flag=True, help="Exclude already spent coins from the search") -@click.option("-s","--start", type=int, help="The block index to start at (included)") -@click.option("-e","--end", type=int, help="The block index to end at (excluded)") +@click.option( + "-nd", + "--as-name-dict", + is_flag=True, + help="Return the records as a dictionary with ids as the keys", +) +@click.option( + "-ou", + "--only-unspent", + is_flag=True, + help="Exclude already spent coins from the search", +) +@click.option("-s", "--start", type=int, help="The block index to start at (included)") +@click.option("-e", "--end", type=int, help="The block index to end at (excluded)") def rpc_coinrecords_cmd(values, by, as_name_dict, **kwargs): async def do_command(_kwargs): try: node_client = await get_client() - clean_values = map(lambda value: value[2:] if value[:2] == "0x" else value, values) + clean_values = map( + lambda value: value[2:] if value[:2] == "0x" else value, values + ) clean_values = [bytes.fromhex(value) for value in clean_values] - if by in ["name","id"]: - coin_records = [await node_client.get_coin_record_by_name(value) for value in clean_values] + if by in ["name", "id"]: + coin_records = [ + await node_client.get_coin_record_by_name(value) + for value in clean_values + ] if not kwargs["include_spent_coins"]: - coin_records = list(filter(lambda record: record.spent == False, coin_records)) + coin_records = list( + filter(lambda record: record.spent is False, coin_records) + ) if kwargs["start_height"] is not None: - coin_records = list(filter(lambda record: record.confirmed_block_index >= kwargs["start_height"], coin_records)) + coin_records = list( + filter( + lambda record: record.confirmed_block_index + >= kwargs["start_height"], + coin_records, + ) + ) if kwargs["end_height"] is not None: - coin_records = list(filter(lambda record: record.confirmed_block_index < kwargs["end_height"], coin_records)) - elif by in ["puzhash","puzzle_hash","puzzlehash"]: - coin_records = await node_client.get_coin_records_by_puzzle_hashes(clean_values,**_kwargs) - elif by in ["parent_id","parent_info","parent_coin_info","parentid","parentinfo","parent"]: - coin_records = await node_client.get_coin_records_by_parent_ids(clean_values,**_kwargs) + coin_records = list( + filter( + lambda record: record.confirmed_block_index + < kwargs["end_height"], + coin_records, + ) + ) + elif by in ["puzhash", "puzzle_hash", "puzzlehash"]: + coin_records = await node_client.get_coin_records_by_puzzle_hashes( + clean_values, **_kwargs + ) + elif by in [ + "parent_id", + "parent_info", + "parent_coin_info", + "parentid", + "parentinfo", + "parent", + ]: + coin_records = await node_client.get_coin_records_by_parent_ids( + clean_values, **_kwargs + ) coin_records = [rec.to_json_dict() for rec in coin_records] @@ -266,4 +377,4 @@ async def do_command(_kwargs): kwargs["include_spent_coins"] = not kwargs.pop("only_unspent") kwargs["start_height"] = kwargs.pop("start") kwargs["end_height"] = kwargs.pop("end") - asyncio.get_event_loop().run_until_complete(do_command(kwargs)) \ No newline at end of file + asyncio.get_event_loop().run_until_complete(do_command(kwargs)) diff --git a/cdv/cmds/util.py b/cdv/cmds/util.py index b6561ae..0d7ffb7 100644 --- a/cdv/cmds/util.py +++ b/cdv/cmds/util.py @@ -6,33 +6,38 @@ from clvm_tools.clvmc import compile_clvm_text from clvm_tools.binutils import assemble + def fake_context(): ctx = {} ctx["obj"] = {"json": True} return ctx + def append_include(search_paths): if search_paths: search_list = list(search_paths) search_list.append("./include") return search_list else: - return ['./include'] + return ["./include"] + def parse_program(program: str, include=[]): - if '(' in program: + if "(" in program: prog = Program.to(assemble(program)) - elif '.' not in program: + elif "." not in program: prog = Program.from_bytes(hexstr_to_bytes(program)) else: with open(program, "r") as file: filestring = file.read() - if '(' in filestring: + if "(" in filestring: # TODO: This should probably be more robust - if re.compile('\(mod\s').search(filestring): - prog = Program.to(compile_clvm_text(filestring, append_include(include))) + if re.compile(r"\(mod\s").search(filestring): + prog = Program.to( + compile_clvm_text(filestring, append_include(include)) + ) else: prog = Program.to(assemble(filestring)) else: prog = Program.from_bytes(hexstr_to_bytes(filestring)) - return prog \ No newline at end of file + return prog diff --git a/cdv/examples/drivers/piggybank_drivers.py b/cdv/examples/drivers/piggybank_drivers.py index fe1b367..cda8e1d 100644 --- a/cdv/examples/drivers/piggybank_drivers.py +++ b/cdv/examples/drivers/piggybank_drivers.py @@ -9,16 +9,24 @@ from cdv.util.load_clvm import load_clvm -PIGGYBANK_MOD = load_clvm("piggybank.clsp","cdv.examples.clsp") +PIGGYBANK_MOD = load_clvm("piggybank.clsp", "cdv.examples.clsp") + # Create a piggybank def create_piggybank_puzzle(amount: uint64, cash_out_puzhash: bytes32): return PIGGYBANK_MOD.curry(amount, cash_out_puzhash) + # Generate a solution to contribute to a piggybank def solution_for_piggybank(pb_coin: Coin, contrib_amount: uint64): - return Program.to([pb_coin.puzzle_hash, pb_coin.amount, (pb_coin.amount + contrib_amount)]) + return Program.to( + [pb_coin.puzzle_hash, pb_coin.amount, (pb_coin.amount + contrib_amount)] + ) + # Return the condition to assert the announcement def piggybank_announcement_assertion(pb_coin: Coin, contrib_amount: uint64): - return [ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, std_hash(pb_coin.name() + int_to_bytes(pb_coin.amount + contrib_amount))] \ No newline at end of file + return [ + ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, + std_hash(pb_coin.name() + int_to_bytes(pb_coin.amount + contrib_amount)), + ] diff --git a/cdv/examples/tests/test_piggybank.py b/cdv/examples/tests/test_piggybank.py index 6557e6f..becb311 100644 --- a/cdv/examples/tests/test_piggybank.py +++ b/cdv/examples/tests/test_piggybank.py @@ -10,6 +10,7 @@ ) from cdv.test import setup as setup_test + class TestStandardTransaction: @pytest.fixture(scope="function") async def setup(self): @@ -21,11 +22,13 @@ async def make_and_spend_piggybank(self, network, alice, bob, CONTRIBUTION_AMOUN await network.farm_block(farmer=alice) # This will use one mojo to create our piggybank on the blockchain. - piggybank_coin = await alice.launch_smart_coin(create_piggybank_puzzle(1000000000000, bob.puzzle_hash)) + piggybank_coin = await alice.launch_smart_coin( + create_piggybank_puzzle(1000000000000, bob.puzzle_hash) + ) # This retrieves us a coin that is at least 500 mojos. contribution_coin = await alice.choose_coin(CONTRIBUTION_AMOUNT) - #This is the spend of the piggy bank coin. We use the driver code to create the solution. + # This is the spend of the piggy bank coin. We use the driver code to create the solution. piggybank_spend = await alice.spend_coin( piggybank_coin, pushtx=False, @@ -37,9 +40,15 @@ async def make_and_spend_piggybank(self, network, alice, bob, CONTRIBUTION_AMOUN pushtx=False, amt=(contribution_coin.amount - CONTRIBUTION_AMOUNT), custom_conditions=[ - [ConditionOpcode.CREATE_COIN, contribution_coin.puzzle_hash, (contribution_coin.amount - CONTRIBUTION_AMOUNT)], - piggybank_announcement_assertion(piggybank_coin.as_coin(), CONTRIBUTION_AMOUNT) - ] + [ + ConditionOpcode.CREATE_COIN, + contribution_coin.puzzle_hash, + (contribution_coin.amount - CONTRIBUTION_AMOUNT), + ], + piggybank_announcement_assertion( + piggybank_coin.as_coin(), CONTRIBUTION_AMOUNT + ), + ], ) # Aggregate them to make sure they are spent together @@ -56,11 +65,18 @@ async def test_piggybank_contribution(self, setup): assert "error" not in result - filtered_result = list(filter( - lambda addition: - (addition.amount == 501) and - (addition.puzzle_hash == create_piggybank_puzzle(1000000000000, bob.puzzle_hash).get_tree_hash()) - ,result["additions"])) + filtered_result = list( + filter( + lambda addition: (addition.amount == 501) + and ( + addition.puzzle_hash + == create_piggybank_puzzle( + 1000000000000, bob.puzzle_hash + ).get_tree_hash() + ), + result["additions"], + ) + ) assert len(filtered_result) == 1 finally: await network.close() @@ -69,22 +85,33 @@ async def test_piggybank_contribution(self, setup): async def test_piggybank_completion(self, setup): network, alice, bob = setup try: - result = await self.make_and_spend_piggybank(network, alice, bob, 1000000000000) + result = await self.make_and_spend_piggybank( + network, alice, bob, 1000000000000 + ) assert "error" not in result - filtered_result = list(filter( - lambda addition: - (addition.amount == 0) and - (addition.puzzle_hash == create_piggybank_puzzle(1000000000000, bob.puzzle_hash).get_tree_hash()) - ,result["additions"])) + filtered_result = list( + filter( + lambda addition: (addition.amount == 0) + and ( + addition.puzzle_hash + == create_piggybank_puzzle( + 1000000000000, bob.puzzle_hash + ).get_tree_hash() + ), + result["additions"], + ) + ) assert len(filtered_result) == 1 - filtered_result = list(filter( - lambda addition: - (addition.amount == 1000000000001) and - (addition.puzzle_hash == bob.puzzle_hash) - ,result["additions"])) + filtered_result = list( + filter( + lambda addition: (addition.amount == 1000000000001) + and (addition.puzzle_hash == bob.puzzle_hash), + result["additions"], + ) + ) assert len(filtered_result) == 1 finally: await network.close() @@ -94,7 +121,7 @@ async def test_piggybank_stealing(self, setup): network, alice, bob = setup try: result = await self.make_and_spend_piggybank(network, alice, bob, -100) - assert 'error' in result - assert 'GENERATOR_RUNTIME_ERROR' in result['error'] + assert "error" in result + assert "GENERATOR_RUNTIME_ERROR" in result["error"] finally: - await network.close() \ No newline at end of file + await network.close() diff --git a/cdv/test/__init__.py b/cdv/test/__init__.py index 366e566..d6d6830 100644 --- a/cdv/test/__init__.py +++ b/cdv/test/__init__.py @@ -1,12 +1,7 @@ -import io import datetime import pytimeparse from typing import Dict -from unittest import TestCase -from blspy import AugSchemeMPL, G1Element, G2Element, PrivateKey - -from clvm.serialize import sexp_from_stream -from clvm import SExp +from blspy import AugSchemeMPL, G1Element, G2Element from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.blockchain_format.coin import Coin @@ -14,14 +9,12 @@ from chia.types.spend_bundle import SpendBundle from chia.types.coin_spend import CoinSpend from chia.util.ints import uint64 -from chia.util.condition_tools import ConditionOpcode, conditions_by_opcode +from chia.util.condition_tools import ConditionOpcode from chia.util.hash import std_hash from chia.wallet.sign_coin_spends import sign_coin_spends -from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( # standard_transaction +from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( # standard_transaction puzzle_for_pk, - solution_for_delegated_puzzle, calculate_synthetic_secret_key, - DEFAULT_HIDDEN_PUZZLE, DEFAULT_HIDDEN_PUZZLE_HASH, ) from chia.clvm.spend_sim import SpendSim, SimClient @@ -33,8 +26,9 @@ block_time = (600.0 / 32.0) / duration_div # Allowed subdivisions of 1 coin + class SpendResult: - def __init__(self,result): + def __init__(self, result): """Constructor for internal use. error - a string describing the error or None @@ -42,25 +36,29 @@ def __init__(self,result): outputs - a list of new Coin objects surviving the transaction """ self.result = result - if 'error' in result: - self.error = result['error'] + if "error" in result: + self.error = result["error"] self.outputs = [] else: self.error = None - self.outputs = result['additions'] + self.outputs = result["additions"] - def find_standard_coins(self,puzzle_hash): + def find_standard_coins(self, puzzle_hash): """Given a Wallet's puzzle_hash, find standard coins usable by it. These coins are recognized as changing the Wallet's chia balance and are usable for any purpose.""" return list(filter(lambda x: x.puzzle_hash == puzzle_hash, self.outputs)) + class CoinWrapper(Coin): """A class that provides some useful methods on coins.""" - def __init__(self, parent : Coin, puzzle_hash : bytes32, amt : uint64, source : Program): + + def __init__( + self, parent: Coin, puzzle_hash: bytes32, amt: uint64, source: Program + ): """Given parent, puzzle_hash and amount, give an object representing the coin""" - super().__init__(parent,puzzle_hash,amt) + super().__init__(parent, puzzle_hash, amt) self.source = source def puzzle(self) -> Program: @@ -104,11 +102,16 @@ def create_standard_spend(self, priv, conditions): # Create a signature for each of these. We'll aggregate them at the end. signature = AugSchemeMPL.sign( calculate_synthetic_secret_key(priv, DEFAULT_HIDDEN_PUZZLE_HASH), - (delegated_puzzle_solution.get_tree_hash() + self.name() + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA) + ( + delegated_puzzle_solution.get_tree_hash() + + self.name() + + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA + ), ) return coin_solution_object, signature + # We have two cases for coins: # - Wallet coins which contribute to the "wallet balance" of the user. # They enable a user to "spend money" and "take actions on the network" @@ -121,7 +124,7 @@ def create_standard_spend(self, priv, conditions): # into wallet coins. We should ensure that value contained in a smart coin # coin is never destroyed. class ContractWrapper: - def __init__(self,genesis_challenge,source): + def __init__(self, genesis_challenge, source): """A wrapper for a smart coin carrying useful methods for interacting with chia.""" self.genesis_challenge = genesis_challenge self.source = source @@ -134,17 +137,18 @@ def puzzle_hash(self): """Give this smart coin's puzzle hash""" return self.source.get_tree_hash() - def custom_coin(self, parent : Coin, amt : uint64): + def custom_coin(self, parent: Coin, amt: uint64): """Given a parent and an amount, create the Coin object representing this smart coin as it would exist post launch""" return CoinWrapper(parent.name(), self.puzzle_hash(), amt, self.source) + # Used internally to accumulate a search for coins we can combine to the # target amount. # Result is the smallest set of coins whose sum of amounts is greater # than target_amount. class CoinPairSearch: - def __init__(self,target_amount): + def __init__(self, target_amount): self.target = target_amount self.total = 0 self.max_coins = [] @@ -152,7 +156,7 @@ def __init__(self,target_amount): def get_result(self): return self.max_coins, self.total - def insort(self,coin,s,e): + def insort(self, coin, s, e): for i in range(len(self.max_coins)): if self.max_coins[i].amount < coin.amount: self.max_coins.insert(i, coin) @@ -160,26 +164,32 @@ def insort(self,coin,s,e): else: self.max_coins.append(coin) - def process_coin_for_combine_search(self,coin): + def process_coin_for_combine_search(self, coin): if self.target == 0: breakpoint() self.total += coin.amount if len(self.max_coins) == 0: self.max_coins.append(coin) else: - self.insort(coin,0,len(self.max_coins)-1) - while (len(self.max_coins) > 0) and \ - (self.total - self.max_coins[-1].amount >= self.target) and \ - ((self.total - self.max_coins[-1].amount > 0) or (len(self.max_coins) > 1)): + self.insort(coin, 0, len(self.max_coins) - 1) + while ( + (len(self.max_coins) > 0) + and (self.total - self.max_coins[-1].amount >= self.target) + and ( + (self.total - self.max_coins[-1].amount > 0) + or (len(self.max_coins) > 1) + ) + ): self.total -= self.max_coins[-1].amount self.max_coins = self.max_coins[:-1] + # A basic wallet that knows about standard coins. # We can use this to track our balance as an end user and keep track of # chia that is released by smart coins, if the smart coins interact # meaningfully with them, as many likely will. class Wallet: - def __init__(self,parent,name,pk,priv): + def __init__(self, parent, name, pk, priv): """Internal use constructor, use Network::make_wallet Fields: @@ -204,23 +214,25 @@ def __init__(self,parent,name,pk,priv): self.pk_to_sk_dict = {str(self.pk_): self.sk_, str(synth_sk.get_g1()): synth_sk} def __repr__(self): - return f'' + return ( + f"" + ) # Make this coin available to the user it goes with. - def add_coin(self,coin): + def add_coin(self, coin): self.usable_coins[coin.name()] = coin def pk_to_sk(self, pk: G1Element): assert str(pk) in self.pk_to_sk_dict return self.pk_to_sk_dict[str(pk)] - def compute_combine_action(self,amt,actions,usable_coins): + def compute_combine_action(self, amt, actions, usable_coins): # No one coin is enough, try to find a best fit pair, otherwise combine the two # maximum coins. searcher = CoinPairSearch(amt) # Process coins for this round. - for k,c in usable_coins.items(): + for k, c in usable_coins.items(): searcher.process_coin_for_combine_search(c) max_coins, total = searcher.get_result() @@ -248,10 +260,10 @@ def compute_combine_action(self,amt,actions,usable_coins): # - A list of signatures. Each coin has its own signature requirements, but standard # coins are signed like this: # - # AugSchemeMPL.sign( - # calculate_synthetic_secret_key(self.sk_,DEFAULT_HIDDEN_PUZZLE_HASH), - # (delegated_puzzle_solution.get_tree_hash() + c.name() + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA) - # ) + # AugSchemeMPL.sign( + # calculate_synthetic_secret_key(self.sk_,DEFAULT_HIDDEN_PUZZLE_HASH), + # (delegated_puzzle_solution.get_tree_hash() + c.name() + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA) + # ) # # where c.name is the coin's "name" (in the code) or coinID (in the # chialisp docs). delegated_puzzle_solution is a clvm program that @@ -290,7 +302,7 @@ def compute_combine_action(self,amt,actions,usable_coins): # signature = AugSchemeMPL.aggregate(signatures) # spend_bundle = SpendBundle(coin_solutions, signature) # - async def combine_coins(self,coins): + async def combine_coins(self, coins): # Overall structure: # Create len-1 spends that just assert that the final coin is created with full value. # Create 1 spend for the final coin that asserts the other spends occurred and @@ -304,7 +316,7 @@ async def combine_coins(self,coins): coins[-1].name(), self.puzzle_hash, sum(map(lambda x: x.amount, coins)), - self.puzzle + self.puzzle, ) destroyed_coin_solutions = [] @@ -317,26 +329,24 @@ async def combine_coins(self,coins): # Each coin expects the final coin creation announcement [ ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, - std_hash(coins[-1].name() + final_coin.name()) + std_hash(coins[-1].name() + final_coin.name()), ] ] - coin_solution, signature = c.create_standard_spend(self.sk_, announce_conditions) + coin_solution, signature = c.create_standard_spend( + self.sk_, announce_conditions + ) destroyed_coin_solutions.append(coin_solution) signatures.append(signature) final_coin_creation = [ - [ - ConditionOpcode.CREATE_COIN_ANNOUNCEMENT, - final_coin.name() - ], - [ - ConditionOpcode.CREATE_COIN, - self.puzzle_hash, final_coin.amount - ], + [ConditionOpcode.CREATE_COIN_ANNOUNCEMENT, final_coin.name()], + [ConditionOpcode.CREATE_COIN, self.puzzle_hash, final_coin.amount], ] - coin_solution, signature = coins[-1].create_standard_spend(self.sk_, final_coin_creation) + coin_solution, signature = coins[-1].create_standard_spend( + self.sk_, final_coin_creation + ) destroyed_coin_solutions.append(coin_solution) signatures.append(signature) @@ -354,7 +364,7 @@ async def combine_coins(self,coins): # Find a coin containing amt we can use as a parent. # Synthesize a coin with sufficient funds if possible. - async def choose_coin(self,amt) -> CoinWrapper: + async def choose_coin(self, amt) -> CoinWrapper: """Given an amount requirement, find a coin that contains at least that much chia""" start_balance = self.balance() coins_to_spend = self.compute_combine_action(amt, [], dict(self.usable_coins)) @@ -371,13 +381,10 @@ async def choose_coin(self,amt) -> CoinWrapper: result = await self.combine_coins( list( map( - lambda x:CoinWrapper( - x.parent_coin_info, - x.puzzle_hash, - x.amount, - self.puzzle + lambda x: CoinWrapper( + x.parent_coin_info, x.puzzle_hash, x.amount, self.puzzle ), - coins_to_spend + coins_to_spend, ) ) ) @@ -393,16 +400,16 @@ async def choose_coin(self,amt) -> CoinWrapper: # - allow use of more than one coin to launch smart coin # - ensure input chia = output chia. it'd be dumb to just allow somebody # to lose their chia without telling them. - async def launch_smart_coin(self,source,**kwargs) -> CoinWrapper: + async def launch_smart_coin(self, source, **kwargs) -> CoinWrapper: """Create a new smart coin based on a parent coin and return the smart coin's living coin to the user or None if the spend failed.""" amt = 1 - if 'amt' in kwargs: - amt = kwargs['amt'] + if "amt" in kwargs: + amt = kwargs["amt"] found_coin = await self.choose_coin(amt) if found_coin is None: - raise ValueError(f'could not find available coin containing {amt} mojo') + raise ValueError(f"could not find available coin containing {amt} mojo") # Create a puzzle based on the incoming smart coin cw = ContractWrapper(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, source) @@ -411,11 +418,7 @@ async def launch_smart_coin(self,source,**kwargs) -> CoinWrapper: ] if amt < found_coin.amount: condition_args.append( - [ - ConditionOpcode.CREATE_COIN, - self.puzzle_hash, - found_coin.amount - amt - ] + [ConditionOpcode.CREATE_COIN, self.puzzle_hash, found_coin.amount - amt] ) delegated_puzzle_solution = Program.to((1, condition_args)) @@ -423,22 +426,26 @@ async def launch_smart_coin(self,source,**kwargs) -> CoinWrapper: # Sign the (delegated_puzzle_hash + coin_name) with synthetic secret key signature = AugSchemeMPL.sign( - calculate_synthetic_secret_key(self.sk_,DEFAULT_HIDDEN_PUZZLE_HASH), - (delegated_puzzle_solution.get_tree_hash() + found_coin.name() + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA) + calculate_synthetic_secret_key(self.sk_, DEFAULT_HIDDEN_PUZZLE_HASH), + ( + delegated_puzzle_solution.get_tree_hash() + + found_coin.name() + + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA + ), ) spend_bundle = SpendBundle( [ CoinSpend( - found_coin.as_coin(), # Coin to spend - self.puzzle, # Puzzle used for found_coin - solution, # The solution to the puzzle locking found_coin + found_coin.as_coin(), # Coin to spend + self.puzzle, # Puzzle used for found_coin + solution, # The solution to the puzzle locking found_coin ) - ] - , signature + ], + signature, ) pushed = await self.parent.push_tx(spend_bundle) - if 'error' not in pushed: + if "error" not in pushed: return cw.custom_coin(found_coin, amt) else: return None @@ -466,32 +473,40 @@ def balance(self): # Automatically takes care of signing, etc. # Result is an object representing the actions taken when the block # with this transaction was farmed. - async def spend_coin(self, coin : CoinWrapper, pushtx=True, **kwargs): + async def spend_coin(self, coin: CoinWrapper, pushtx=True, **kwargs): """Given a coin object, invoke it on the blockchain, either as a standard coin if no arguments are given or with custom arguments in args=""" amt = 1 - if 'amt' in kwargs: - amt = kwargs['amt'] + if "amt" in kwargs: + amt = kwargs["amt"] delegated_puzzle_solution = None - if not 'args' in kwargs: + if "args" not in kwargs: target_puzzle_hash = self.puzzle_hash # Allow the user to 'give this much chia' to another user. - if 'to' in kwargs: - target_puzzle_hash = kwargs['to'].puzzle_hash + if "to" in kwargs: + target_puzzle_hash = kwargs["to"].puzzle_hash # Automatic arguments from the user's intention. - if not 'custom_conditions' in kwargs: + if "custom_conditions" not in kwargs: solution_list = [[ConditionOpcode.CREATE_COIN, target_puzzle_hash, amt]] else: - solution_list = kwargs['custom_conditions'] - if 'remain' in kwargs: - remainer = kwargs['remain'] + solution_list = kwargs["custom_conditions"] + if "remain" in kwargs: + remainer = kwargs["remain"] remain_amt = coin.amount - amt if isinstance(remainer, ContractWrapper): - solution_list.append([ConditionOpcode.CREATE_COIN, remainer.puzzle_hash(), remain_amt]) + solution_list.append( + [ + ConditionOpcode.CREATE_COIN, + remainer.puzzle_hash(), + remain_amt, + ] + ) elif isinstance(remainer, Wallet): - solution_list.append([ConditionOpcode.CREATE_COIN, remainer.puzzle_hash, remain_amt]) + solution_list.append( + [ConditionOpcode.CREATE_COIN, remainer.puzzle_hash, remain_amt] + ) else: raise ValueError("remainer is not a wallet or a smart coin") @@ -499,7 +514,7 @@ async def spend_coin(self, coin : CoinWrapper, pushtx=True, **kwargs): # Solution is the solution for the old coin. solution = Program.to([[], delegated_puzzle_solution, []]) else: - delegated_puzzle_solution = Program.to(kwargs['args']) + delegated_puzzle_solution = Program.to(kwargs["args"]) solution = delegated_puzzle_solution solution_for_coin = CoinSpend( @@ -516,9 +531,9 @@ async def spend_coin(self, coin : CoinWrapper, pushtx=True, **kwargs): [solution_for_coin], self.pk_to_sk, DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA, - DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM + DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, ) - except: + except ValueError: spend_bundle = SpendBundle( [solution_for_coin], G2Element(), @@ -530,6 +545,7 @@ async def spend_coin(self, coin : CoinWrapper, pushtx=True, **kwargs): else: return spend_bundle + # A user oriented (domain specific) view of the chia network. class Network: """An object that owns a simulation, responsible for managing Wallet actors, @@ -543,11 +559,13 @@ class Network: @classmethod async def create(cls): self = cls() - self.time = datetime.timedelta(days=18750, seconds=61201) # Past the initial transaction freeze + self.time = datetime.timedelta( + days=18750, seconds=61201 + ) # Past the initial transaction freeze self.sim = await SpendSim.create() self.sim_client = SimClient(self.sim) self.wallets = {} - self.nobody = self.make_wallet('nobody') + self.nobody = self.make_wallet("nobody") self.wallets[str(self.nobody.pk())] = self.nobody return self @@ -555,15 +573,15 @@ async def close(self): await self.sim.close() # Have the system farm one block with a specific beneficiary (nobody if not specified). - async def farm_block(self,**kwargs): + async def farm_block(self, **kwargs): """Given a farmer, farm a block with that actor as the beneficiary of the farm reward. Used for causing chia balance to exist so the system can do things. """ farmer = self.nobody - if 'farmer' in kwargs: - farmer = kwargs['farmer'] + if "farmer" in kwargs: + farmer = kwargs["farmer"] farm_duration = datetime.timedelta(block_time) farmed = await self.sim.farm_block(farmer.puzzle_hash) @@ -572,9 +590,11 @@ async def farm_block(self,**kwargs): w._clear_coins() for kw, w in self.wallets.items(): - coin_records = await self.sim_client.get_coin_records_by_puzzle_hash(w.puzzle_hash) + coin_records = await self.sim_client.get_coin_records_by_puzzle_hash( + w.puzzle_hash + ) for coin_record in coin_records: - if coin_record.spent == False: + if coin_record.spent is False: w.add_coin(CoinWrapper.from_coin(coin_record.coin, w.puzzle)) self.time += farm_duration @@ -589,7 +609,7 @@ def _alloc_key(self): # Allow the user to create a wallet identity to whom standard coins may be targeted. # This results in the creation of a wallet that tracks balance and standard coins. # Public and private key from here are used in signing. - def make_wallet(self,name): + def make_wallet(self, name): """Create a wallet for an actor. This causes the actor's chia balance in standard coin to be tracked during the simulation. Wallets have some domain specific methods that behave in similar ways to other blockchains.""" @@ -599,10 +619,12 @@ def make_wallet(self,name): return w # Skip real time by farming blocks until the target duration is achieved. - async def skip_time(self,target_duration,**kwargs): + async def skip_time(self, target_duration, **kwargs): """Skip a duration of simulated time, causing blocks to be farmed. If a farmer is specified, they win each block""" - target_time = self.time + datetime.timedelta(pytimeparse.parse(target_duration) / duration_div) + target_time = self.time + datetime.timedelta( + pytimeparse.parse(target_duration) / duration_div + ) while target_time > self.get_timestamp(): await self.farm_block(**kwargs) self.sim.pass_time(20) @@ -612,27 +634,28 @@ async def skip_time(self,target_duration,**kwargs): def get_timestamp(self): """Return the current simualtion time in seconds.""" - return datetime.timedelta(seconds = self.sim.timestamp) + return datetime.timedelta(seconds=self.sim.timestamp) # Given a spend bundle, farm a block and analyze the result. - async def push_tx(self,bundle): + async def push_tx(self, bundle): """Given a spend bundle, try to farm a block containing it. If the spend bundle didn't validate, then a result containing an 'error' key is returned. The reward for the block goes to Network::nobody""" status, error = await self.sim_client.push_tx(bundle) if error: - return { "error": str(error) } + return {"error": str(error)} # Common case that we want to farm this right away. additions, removals = await self.farm_block() return { - 'additions': additions, - 'removals':removals, + "additions": additions, + "removals": removals, } + async def setup(): network = await Network.create() - alice = network.make_wallet('alice') - bob = network.make_wallet('bob') - return network, alice, bob \ No newline at end of file + alice = network.make_wallet("alice") + bob = network.make_wallet("bob") + return network, alice, bob diff --git a/cdv/test/test_skeleton.py b/cdv/test/test_skeleton.py index d01554a..7595163 100644 --- a/cdv/test/test_skeleton.py +++ b/cdv/test/test_skeleton.py @@ -2,6 +2,7 @@ from cdv.test import setup as setup_test + class TestSomething: @pytest.fixture(scope="function") async def setup(self): diff --git a/cdv/util/keys.py b/cdv/util/keys.py index da7a51f..012a5a1 100644 --- a/cdv/util/keys.py +++ b/cdv/util/keys.py @@ -1,18 +1,10 @@ from typing import List, Dict from chia.util.hash import std_hash -from chia.util.ints import uint32 -from chia.wallet.derive_keys import ( - _derive_path, - master_sk_to_farmer_sk, - master_sk_to_pool_sk, - master_sk_to_wallet_sk, - master_sk_to_local_sk, - master_sk_to_backup_sk, -) from blspy import BasicSchemeMPL, PrivateKey, G1Element, AugSchemeMPL, G2Element + def secret_exponent_for_index(index: int) -> int: blob = index.to_bytes(32, "big") hashed_blob = BasicSchemeMPL.key_gen(std_hash(b"foo" + blob)) @@ -39,42 +31,9 @@ def sign_messages_with_indexes(sign_ops: List[Dict[int, str]]) -> G2Element: for _ in sign_ops: for index, message in _.items(): sk = private_key_for_index(index) - signatures.append(AugSchemeMPL.sign(sk,bytes(message, "utf-8"))) + signatures.append(AugSchemeMPL.sign(sk, bytes(message, "utf-8"))) return AugSchemeMPL.aggregate(signatures) def aggregate_signatures(signatures: List[G2Element]) -> G2Element: return AugSchemeMPL.aggregate(signatures) - - -# EIP 2334 bls key derivation -# https://eips.ethereum.org/EIPS/eip-2334 -# 12381 = bls spec number -# 8444 = Chia blockchain number and port number -# 0, 1, 2, 3, 4, farmer, pool, wallet, local, backup key numbers - - -def _derive_path(sk: PrivateKey, path: List[int]) -> PrivateKey: - for index in path: - sk = AugSchemeMPL.derive_child_sk(sk, index) - return sk - - -def master_sk_to_farmer_sk(master: PrivateKey) -> PrivateKey: - return _derive_path(master, [12381, 8444, 0, 0]) - - -def master_sk_to_pool_sk(master: PrivateKey) -> PrivateKey: - return _derive_path(master, [12381, 8444, 1, 0]) - - -def master_sk_to_wallet_sk(master: PrivateKey, index: uint32) -> PrivateKey: - return _derive_path(master, [12381, 8444, 2, index]) - - -def master_sk_to_local_sk(master: PrivateKey) -> PrivateKey: - return _derive_path(master, [12381, 8444, 3, 0]) - - -def master_sk_to_backup_sk(master: PrivateKey) -> PrivateKey: - return _derive_path(master, [12381, 8444, 4, 0]) diff --git a/cdv/util/load_clvm.py b/cdv/util/load_clvm.py index 30e755f..1eca87b 100644 --- a/cdv/util/load_clvm.py +++ b/cdv/util/load_clvm.py @@ -6,7 +6,9 @@ from chia.types.blockchain_format.program import Program, SerializedProgram -def load_serialized_clvm(clvm_filename, package_or_requirement=__name__) -> SerializedProgram: +def load_serialized_clvm( + clvm_filename, package_or_requirement=__name__ +) -> SerializedProgram: """ This function takes a .clvm file in the given package and compiles it to a .clvm.hex file if the .hex file is missing or older than the .clvm file, then @@ -20,18 +22,32 @@ def load_serialized_clvm(clvm_filename, package_or_requirement=__name__) -> Seri try: if pkg_resources.resource_exists(package_or_requirement, clvm_filename): - full_path = pathlib.Path(pkg_resources.resource_filename(package_or_requirement, clvm_filename)) + full_path = pathlib.Path( + pkg_resources.resource_filename(package_or_requirement, clvm_filename) + ) output = full_path.parent / hex_filename - compile_clvm(full_path, output, search_paths=[full_path.parent, pathlib.Path.cwd().joinpath("include")]) + compile_clvm( + full_path, + output, + search_paths=[full_path.parent, pathlib.Path.cwd().joinpath("include")], + ) except NotImplementedError: # pyinstaller doesn't support `pkg_resources.resource_exists` # so we just fall through to loading the hex clvm pass - clvm_hex = pkg_resources.resource_string(package_or_requirement, hex_filename).decode("utf8") + clvm_hex = pkg_resources.resource_string( + package_or_requirement, hex_filename + ).decode("utf8") clvm_blob = bytes.fromhex(clvm_hex) return SerializedProgram.from_bytes(clvm_blob) def load_clvm(clvm_filename, package_or_requirement=__name__) -> Program: - return Program.from_bytes(bytes(load_serialized_clvm(clvm_filename, package_or_requirement=package_or_requirement))) \ No newline at end of file + return Program.from_bytes( + bytes( + load_serialized_clvm( + clvm_filename, package_or_requirement=package_or_requirement + ) + ) + ) diff --git a/setup.py b/setup.py index 277f3fa..2814cb4 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,11 @@ "pytimeparse", ] -dev_dependencies = [] +dev_dependencies = [ + "flake8", + "mypy", + "black", +] setup( name="chia_dev_tools", diff --git a/tests/cmds/test_cdv.py b/tests/cmds/test_cdv.py index 4037832..a6e25ef 100644 --- a/tests/cmds/test_cdv.py +++ b/tests/cmds/test_cdv.py @@ -4,24 +4,25 @@ from cdv.cmds.cli import cli + class TestCdvCommands: def test_encode_decode(self): runner = CliRunner() - puzhash = '3d4237d9383a7b6e60d1bfe551139ec2d6e5468205bf179ed381e66bed7b9788' + puzhash = "3d4237d9383a7b6e60d1bfe551139ec2d6e5468205bf179ed381e66bed7b9788" - result = runner.invoke(cli, ['encode', puzhash]) - address = 'xch184pr0kfc8fakucx3hlj4zyu7cttw235zqkl308kns8nxhmtmj7yqxsnauc' + result = runner.invoke(cli, ["encode", puzhash]) + address = "xch184pr0kfc8fakucx3hlj4zyu7cttw235zqkl308kns8nxhmtmj7yqxsnauc" assert result.exit_code == 0 assert address in result.output - result = runner.invoke(cli, ['decode', address]) + result = runner.invoke(cli, ["decode", address]) assert result.exit_code == 0 assert puzhash in result.output - result = runner.invoke(cli, ['encode', puzhash, '--prefix', 'txch']) - test_address = 'txch184pr0kfc8fakucx3hlj4zyu7cttw235zqkl308kns8nxhmtmj7yqth5tat' + result = runner.invoke(cli, ["encode", puzhash, "--prefix", "txch"]) + test_address = "txch184pr0kfc8fakucx3hlj4zyu7cttw235zqkl308kns8nxhmtmj7yqth5tat" assert result.exit_code == 0 assert test_address in result.output - result = runner.invoke(cli, ['decode', test_address]) + result = runner.invoke(cli, ["decode", test_address]) assert result.exit_code == 0 assert puzhash in result.output @@ -30,24 +31,32 @@ def test_hash(self): str_msg = "chia" b_msg = "0xcafef00d" - result = runner.invoke(cli, ['hash', str_msg]) + result = runner.invoke(cli, ["hash", str_msg]) assert result.exit_code == 0 - assert '3d4237d9383a7b6e60d1bfe551139ec2d6e5468205bf179ed381e66bed7b9788' in result.output - result = runner.invoke(cli, ['hash', b_msg]) + assert ( + "3d4237d9383a7b6e60d1bfe551139ec2d6e5468205bf179ed381e66bed7b9788" + in result.output + ) + result = runner.invoke(cli, ["hash", b_msg]) assert result.exit_code == 0 - assert '8f6e594e007ca1a1676ef64469c58f7ece8cddc9deae0faf66fbce2466519ebd' in result.output + assert ( + "8f6e594e007ca1a1676ef64469c58f7ece8cddc9deae0faf66fbce2466519ebd" + in result.output + ) def test_test(self): runner = CliRunner() with runner.isolated_filesystem(): - result = runner.invoke(cli, ['test', '--init']) + result = runner.invoke(cli, ["test", "--init"]) assert result.exit_code == 0 - assert Path('./tests').exists() and Path('./tests/test_skeleton.py').exists() + assert ( + Path("./tests").exists() and Path("./tests/test_skeleton.py").exists() + ) - result = runner.invoke(cli, ['test', '--discover']) + result = runner.invoke(cli, ["test", "--discover"]) assert result.exit_code == 0 - assert 'TestSomething' in result.output + assert "TestSomething" in result.output - result = runner.invoke(cli, ['test']) + result = runner.invoke(cli, ["test"]) assert result.exit_code == 0 - assert 'test_skeleton.py .' in result.output \ No newline at end of file + assert "test_skeleton.py ." in result.output diff --git a/tests/cmds/test_clsp.py b/tests/cmds/test_clsp.py index 78c0489..9896968 100644 --- a/tests/cmds/test_clsp.py +++ b/tests/cmds/test_clsp.py @@ -6,16 +6,17 @@ from chia.types.blockchain_format.program import Program -from clvm_tools.binutils import assemble, disassemble +from clvm_tools.binutils import disassemble from cdv.cmds.cli import cli + class TestCdvCommands: - program = '(q . 1)' - serialized = 'ff0101' - mod = '(mod () (include condition_codes.clib) CREATE_COIN)' + program = "(q . 1)" + serialized = "ff0101" + mod = "(mod () (include condition_codes.clib) CREATE_COIN)" - #This comes before build because build is going to use retrieve + # This comes before build because build is going to use retrieve def test_retrieve(self): runner = CliRunner() with runner.isolated_filesystem(): @@ -31,34 +32,36 @@ def test_build(self): runner = CliRunner() with runner.isolated_filesystem(): # Test building CLVM - program_file = open("program.clvm","w") + program_file = open("program.clvm", "w") program_file.write(self.program) program_file.close() - result = runner.invoke(cli, ["clsp","build","."]) + result = runner.invoke(cli, ["clsp", "build", "."]) assert result.exit_code == 0 assert Path("./program.clvm.hex").exists() - assert open("program.clvm.hex","r").read() == "01" + assert open("program.clvm.hex", "r").read() == "01" # Use the retrieve command for the include file runner.invoke(cli, ["clsp", "retrieve", "condition_codes"]) # Test building Chialisp (automatic include search) - mod_file = open("mod.clsp","w") + mod_file = open("mod.clsp", "w") mod_file.write(self.mod) mod_file.close() - result = runner.invoke(cli, ["clsp","build","./mod.clsp"]) + result = runner.invoke(cli, ["clsp", "build", "./mod.clsp"]) assert result.exit_code == 0 assert Path("./mod.clsp.hex").exists() - assert open("mod.clsp.hex","r").read() == "ff0133" + assert open("mod.clsp.hex", "r").read() == "ff0133" # Test building Chialisp (specified include search) os.remove(Path("./mod.clsp.hex")) - shutil.copytree("./include","./include_test") + shutil.copytree("./include", "./include_test") shutil.rmtree("./include") - result = runner.invoke(cli, ["clsp","build",".","--include","./include_test"]) + result = runner.invoke( + cli, ["clsp", "build", ".", "--include", "./include_test"] + ) assert result.exit_code == 0 assert Path("./mod.clsp.hex").exists() - assert open("mod.clsp.hex","r").read() == "ff0133" + assert open("mod.clsp.hex", "r").read() == "ff0133" def test_curry(self): integer = 1 @@ -69,11 +72,18 @@ def test_curry(self): curried_mod = mod.curry(integer, hexadecimal, string, program) runner = CliRunner() - cmd = ["clsp","curry",str(mod), - "-a", str(integer), - "-a", "0x"+hexadecimal.hex(), - "-a", string, - "-a", disassemble(program), + cmd = [ + "clsp", + "curry", + str(mod), + "-a", + str(integer), + "-a", + "0x" + hexadecimal.hex(), + "-a", + string, + "-a", + disassemble(program), ] result = runner.invoke(cli, cmd) @@ -94,15 +104,15 @@ def test_curry(self): def test_disassemble(self): runner = CliRunner() # Test the program passed in as a hex string - result = runner.invoke(cli, ["clsp","disassemble",self.serialized]) + result = runner.invoke(cli, ["clsp", "disassemble", self.serialized]) assert result.exit_code == 0 assert self.program in result.output # Test the program passed in as a hex file with runner.isolated_filesystem(): - program_file = open("program.clvm.hex","w") + program_file = open("program.clvm.hex", "w") program_file.write(self.serialized) program_file.close() - result = runner.invoke(cli, ["clsp","disassemble","program.clvm.hex"]) + result = runner.invoke(cli, ["clsp", "disassemble", "program.clvm.hex"]) assert result.exit_code == 0 assert self.program in result.output @@ -111,26 +121,28 @@ def test_treehash(self): runner = CliRunner() program = "(a 2 3)" program_as_mod = "(mod (arg . arg2) (a arg arg2))" - program_hash = "530d1b3283c802be3a7bdb34b788c1898475ed76c89ecb2224e4b4f40c32d1a4" - result = runner.invoke(cli, ["clsp","treehash",program]) + program_hash = ( + "530d1b3283c802be3a7bdb34b788c1898475ed76c89ecb2224e4b4f40c32d1a4" + ) + result = runner.invoke(cli, ["clsp", "treehash", program]) assert result.exit_code == 0 assert program_hash in result.output # Test a program passed in as a CLVM file filename = "program.clvm" with runner.isolated_filesystem(): - program_file = open(filename,"w") + program_file = open(filename, "w") program_file.write(program) program_file.close() - result = runner.invoke(cli, ["clsp","treehash",filename]) + result = runner.invoke(cli, ["clsp", "treehash", filename]) assert result.exit_code == 0 assert program_hash in result.output # Test a program passed in as a Chialisp file with runner.isolated_filesystem(): - program_file = open(filename,"w") + program_file = open(filename, "w") program_file.write(program_as_mod) program_file.close() - result = runner.invoke(cli, ["clsp","treehash",filename]) + result = runner.invoke(cli, ["clsp", "treehash", filename]) assert result.exit_code == 0 - assert program_hash in result.output \ No newline at end of file + assert program_hash in result.output diff --git a/tests/cmds/test_inspect.py b/tests/cmds/test_inspect.py index 5ec069f..383f59e 100644 --- a/tests/cmds/test_inspect.py +++ b/tests/cmds/test_inspect.py @@ -6,37 +6,66 @@ from cdv.cmds.cli import cli +EMPTY_SIG = "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" # noqa + + class TestInspectCommands: def test_any(self): runner = CliRunner() # Try to inspect a program - result = runner.invoke(cli, ["inspect","any","ff0101"]) + result = runner.invoke(cli, ["inspect", "any", "ff0101"]) assert result.exit_code == 0 assert "guess" not in result.output # Try to inspect a private key - result = runner.invoke(cli, ["inspect","any","05ec9428fc2841a79e96631a633b154b57a45311c0602269a6500732093a52cd"]) + result = runner.invoke( + cli, + [ + "inspect", + "any", + "05ec9428fc2841a79e96631a633b154b57a45311c0602269a6500732093a52cd", + ], + ) assert result.exit_code == 0 assert "guess" not in result.output # Try to inspect a public key - result = runner.invoke(cli, ["inspect","any","b364a088d9df9423e54bff4c62e4bd854445fb8f5b8f6d80dea06773a4f828734a3a75318b180364ca8468836f0742db"]) + result = runner.invoke( + cli, + [ + "inspect", + "any", + "b364a088d9df9423e54bff4c62e4bd854445fb8f5b8f6d80dea06773a4f828734a3a75318b180364ca8468836f0742db", + ], + ) assert result.exit_code == 0 assert "guess" not in result.output # Try to inspect an aggsig - result = runner.invoke(cli, ["inspect","any","c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"]) + result = runner.invoke( + cli, + [ + "inspect", + "any", + EMPTY_SIG, + ], + ) assert result.exit_code == 0 assert "guess" not in result.output - for class_type in ["coinrecord","coin","spendbundle","spend"]: - valid_json_path = Path(__file__).parent.joinpath(f"object_files/{class_type}s/{class_type}.json") - invalid_json_path = Path(__file__).parent.joinpath(f"object_files/{class_type}s/{class_type}_invalid.json") - metadata_path = Path(__file__).parent.joinpath(f"object_files/{class_type}s/{class_type}_metadata.json") - valid_json = json.loads(open(valid_json_path,"r").read()) - invalid_json = json.loads(open(invalid_json_path,"r").read()) - metadata_json = json.loads(open(metadata_path,"r").read()) + for class_type in ["coinrecord", "coin", "spendbundle", "spend"]: + valid_json_path = Path(__file__).parent.joinpath( + f"object_files/{class_type}s/{class_type}.json" + ) + invalid_json_path = Path(__file__).parent.joinpath( + f"object_files/{class_type}s/{class_type}_invalid.json" + ) + metadata_path = Path(__file__).parent.joinpath( + f"object_files/{class_type}s/{class_type}_metadata.json" + ) + valid_json = json.loads(open(valid_json_path, "r").read()) + metadata_json = json.loads(open(metadata_path, "r").read()) # Try to load the invalid and make sure it fails result = runner.invoke(cli, ["inspect", "any", str(invalid_json_path)]) @@ -54,31 +83,41 @@ def test_any(self): # Try to load bytes if class_type != "coin": - valid_hex_path = Path(__file__).parent.joinpath(f"object_files/{class_type}s/{class_type}.hex") + valid_hex_path = Path(__file__).parent.joinpath( + f"object_files/{class_type}s/{class_type}.hex" + ) # From a file - result = runner.invoke(cli, ["inspect","--json","any", str(valid_hex_path)]) + result = runner.invoke( + cli, ["inspect", "--json", "any", str(valid_hex_path)] + ) assert result.exit_code == 0 assert "'coin':" in result.output # From a string - valid_hex = open(valid_hex_path,"r").read() - result = runner.invoke(cli, ["inspect","--json","any", valid_hex]) + valid_hex = open(valid_hex_path, "r").read() + result = runner.invoke(cli, ["inspect", "--json", "any", valid_hex]) assert result.exit_code == 0 assert "'coin':" in result.output # Make sure the ID calculation is correct - result = runner.invoke(cli, ["inspect", "--id", "any", str(valid_json_path)]) + result = runner.invoke( + cli, ["inspect", "--id", "any", str(valid_json_path)] + ) assert result.exit_code == 0 assert metadata_json["id"] in result.output # Make sure the bytes encoding is correct - result = runner.invoke(cli, ["inspect", "--bytes", "any", str(valid_json_path)]) + result = runner.invoke( + cli, ["inspect", "--bytes", "any", str(valid_json_path)] + ) assert result.exit_code == 0 assert metadata_json["bytes"] in result.output # Make sure the type guessing is correct - result = runner.invoke(cli, ["inspect", "--type", "any", str(valid_json_path)]) + result = runner.invoke( + cli, ["inspect", "--type", "any", str(valid_json_path)] + ) assert result.exit_code == 0 assert metadata_json["type"] in result.output @@ -89,12 +128,14 @@ def test_coins(self): id = "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b" runner = CliRunner() - result = runner.invoke(cli, ["inspect","--id","coins","-pid",pid,"-ph",ph,"-a",amount]) + result = runner.invoke( + cli, ["inspect", "--id", "coins", "-pid", pid, "-ph", ph, "-a", amount] + ) assert result.exit_code == 0 assert id in result.output def test_spends(self): - coin_path = Path(__file__).parent.joinpath(f"object_files/coins/coin.json") + coin_path = Path(__file__).parent.joinpath("object_files/coins/coin.json") pid = "0x0000000000000000000000000000000000000000000000000000000000000000" ph = "0000000000000000000000000000000000000000000000000000000000000000" amount = "0" @@ -108,12 +149,39 @@ def test_spends(self): runner = CliRunner() # Specify the coin file - result = runner.invoke(cli, ["inspect","--id","spends","-c",str(coin_path),"-pr",puzzle_reveal,"-s",solution]) + result = runner.invoke( + cli, + [ + "inspect", + "--id", + "spends", + "-c", + str(coin_path), + "-pr", + puzzle_reveal, + "-s", + solution, + ], + ) assert result.exit_code == 0 assert id in result.output # Specify all of the arguments - base_command = ["inspect","--id","spends","-pid",pid,"-ph",ph,"-a",amount,"-pr",puzzle_reveal,"-s",solution] + base_command = [ + "inspect", + "--id", + "spends", + "-pid", + pid, + "-ph", + ph, + "-a", + amount, + "-pr", + puzzle_reveal, + "-s", + solution, + ] result = runner.invoke(cli, base_command) assert result.exit_code == 0 assert id in result.output @@ -134,15 +202,14 @@ def test_spends(self): assert modified_cost in result.output def test_spendbundles(self): - spend_path = Path(__file__).parent.joinpath(f"object_files/spends/spend.json") + spend_path = Path(__file__).parent.joinpath("object_files/spends/spend.json") pubkey = "80df54b2a616f5c79baaed254134ae5dfc6e24e2d8e1165b251601ceb67b1886db50aacf946eb20f00adc303e7534dd0" - signable_data = "24f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4bccd5bb71183532bff220ba46c268991a3ff07eb358e8255a65c30a2dce0e5fbb" - signed_data = "b77c3f7dbbc694a77ff3739c1f4ade1df6a3936fcb352005e4ca74ac4ed21009ad02ac98f2ecd6eacba2a007011471d508f29ee6456c6a7f13e15c81b4d0772351eb62770be47175a9a7f843698540761537ad1696ea0f7132bfa2710fb78f09" - agg_sig = "b83fe374efbc5776735df7cbfb7e27ede5079b41cd282091450e4de21c4b772e254ce906508834b0c2dcd3d58c47a96914c782f0baf8eaff7ece3b070d2035cd878f744deadcd6c6625c1d0a1b418437ee3f25c2df08ffe08bdfe06b8a83b514" + signable_data = "24f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4bccd5bb71183532bff220ba46c268991a3ff07eb358e8255a65c30a2dce0e5fbb" # noqa + agg_sig = "b83fe374efbc5776735df7cbfb7e27ede5079b41cd282091450e4de21c4b772e254ce906508834b0c2dcd3d58c47a96914c782f0baf8eaff7ece3b070d2035cd878f744deadcd6c6625c1d0a1b418437ee3f25c2df08ffe08bdfe06b8a83b514" # noqa id_no_sig = "3ac222f0e8f19afcad367b3068273801ca21fe515311dae8d399a5baad9c3c73" id_with_sig = "bd9acfbb344c006cf520f1265a9b611a20cd478f234f51cd31a978b2d3ad9bbb" network_modifier = "testnet7" - modified_signable_data = "24f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b117816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015af" + modified_signable_data = "24f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b117816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015af" # noqa cost = "4868283" cost_modifier = "0" modified_cost = "2408283" @@ -150,12 +217,33 @@ def test_spendbundles(self): runner = CliRunner() # Build with only the spends - result = runner.invoke(cli, ["inspect","--id","spendbundles","-s",str(spend_path),"-s",str(spend_path)]) + result = runner.invoke( + cli, + [ + "inspect", + "--id", + "spendbundles", + "-s", + str(spend_path), + "-s", + str(spend_path), + ], + ) assert result.exit_code == 0 assert id_no_sig in result.output # Build with the aggsig as well - base_command = ["inspect","--id","spendbundles","-s",str(spend_path),"-s",str(spend_path),"-as",agg_sig] + base_command = [ + "inspect", + "--id", + "spendbundles", + "-s", + str(spend_path), + "-s", + str(spend_path), + "-as", + agg_sig, + ] result = runner.invoke(cli, base_command) assert result.exit_code == 0 assert id_with_sig in result.output @@ -196,11 +284,21 @@ def test_spendbundles(self): # Try to use it the programmatic way (like cdv rpc pushtx does) from cdv.cmds.chia_inspect import do_inspect_spend_bundle_cmd from cdv.cmds.util import fake_context - bundle_path = Path(__file__).parent.joinpath(f"object_files/spendbundles/spendbundle.json") - assert len(do_inspect_spend_bundle_cmd(fake_context(), [str(bundle_path)], print_results=False)) > 0 + + bundle_path = Path(__file__).parent.joinpath( + "object_files/spendbundles/spendbundle.json" + ) + assert ( + len( + do_inspect_spend_bundle_cmd( + fake_context(), [str(bundle_path)], print_results=False + ) + ) + > 0 + ) def test_coinrecords(self): - coin_path = Path(__file__).parent.joinpath(f"object_files/coins/coin.json") + coin_path = Path(__file__).parent.joinpath("object_files/coins/coin.json") pid = "0x0000000000000000000000000000000000000000000000000000000000000000" ph = "0000000000000000000000000000000000000000000000000000000000000000" amount = "0" @@ -214,18 +312,62 @@ def test_coinrecords(self): runner = CliRunner() # Try to load it from a file - record_path = Path(__file__).parent.joinpath(f"object_files/coinrecords/coinrecord.json") - result = runner.invoke(cli, ["inspect","coinrecords",str(record_path)]) + record_path = Path(__file__).parent.joinpath( + "object_files/coinrecords/coinrecord.json" + ) + result = runner.invoke(cli, ["inspect", "coinrecords", str(record_path)]) assert result.exit_code == 0 assert "'coin'" in result.output # Specify the coin file - result = runner.invoke(cli, ["inspect","--id","coinrecords","-c",str(coin_path),"-cb",coinbase,"-ci",confirmed_block_index,"-s",spent,"-si",spent_block_index,"-t",timestamp]) + result = runner.invoke( + cli, + [ + "inspect", + "--id", + "coinrecords", + "-c", + str(coin_path), + "-cb", + coinbase, + "-ci", + confirmed_block_index, + "-s", + spent, + "-si", + spent_block_index, + "-t", + timestamp, + ], + ) assert result.exit_code == 0 assert id in result.output # Specify all of the arguments - result = runner.invoke(cli, ["inspect","--id","coinrecords","-pid",pid,"-ph",ph,"-a",amount,"-cb",coinbase,"-ci",confirmed_block_index,"-s",spent,"-si",spent_block_index,"-t",timestamp]) + result = runner.invoke( + cli, + [ + "inspect", + "--id", + "coinrecords", + "-pid", + pid, + "-ph", + ph, + "-a", + amount, + "-cb", + coinbase, + "-ci", + confirmed_block_index, + "-s", + spent, + "-si", + spent_block_index, + "-t", + timestamp, + ], + ) assert result.exit_code == 0 assert id in result.output @@ -235,84 +377,111 @@ def test_programs(self): runner = CliRunner() - result = runner.invoke(cli, ["inspect","--id","programs",program]) + result = runner.invoke(cli, ["inspect", "--id", "programs", program]) assert result.exit_code == 0 assert id in result.output def test_keys(self): - mnemonic = "chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia" + mnemonic = "chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia" # noqa passphrase = "chia" sk = "68cbc26a245903f3d20a405c0673a9f32b2382174abeeabadb7ba1478b162326" pk = "b7531990662d3fbff22d073a08123ddeae70e0a118cecebf8f207b373da5a90aaefcfed2d9cab8fbe711d6b4f5c72e89" hd_modifier = "m/12381/8444/0/0" - type_modifier = "farmer" # Should be same as above HD Path + type_modifier = "farmer" # Should be same as above HD Path farmer_sk = "5729513405cb68999711618aa02e317335cecdea63d666886bbb39c0fc487dae" - synthetic_sk = "6124dbc2580cbdc2f3c716572950434ecdb42f952ec2e947bd393643c13e8ec2" + synthetic_sk = ( + "6124dbc2580cbdc2f3c716572950434ecdb42f952ec2e947bd393643c13e8ec2" + ) ph_modifier = "69ae360134b1fae04326e5546f25dc794a19192a1f22a44a46d038e7f0d1ecbb" - modified_synthetic_sk = "66dff9a8d49d90029e5fb42378562d459e375965150bc72c3a7ea2c523ab49f5" + modified_synthetic_sk = ( + "66dff9a8d49d90029e5fb42378562d459e375965150bc72c3a7ea2c523ab49f5" + ) runner = CliRunner() # Build the key from secret key - result = runner.invoke(cli, ["inspect","keys","-sk",sk]) + result = runner.invoke(cli, ["inspect", "keys", "-sk", sk]) assert result.exit_code == 0 assert sk in result.output assert pk in result.output key_output = result.output # Build the key from mnemonic - result = runner.invoke(cli, ["inspect","keys","-m",mnemonic,"-pw",passphrase]) + result = runner.invoke( + cli, ["inspect", "keys", "-m", mnemonic, "-pw", passphrase] + ) assert result.exit_code == 0 assert result.output == key_output # Use only the public key - result = runner.invoke(cli, ["inspect","keys","-pk",pk]) + result = runner.invoke(cli, ["inspect", "keys", "-pk", pk]) assert result.exit_code == 0 assert result.output in key_output # Generate a random one - result = runner.invoke(cli, ["inspect","keys","--random"]) + result = runner.invoke(cli, ["inspect", "keys", "--random"]) assert result.exit_code == 0 assert "Secret Key" in result.output assert "Public Key" in result.output # Check the HD derivation is working - result = runner.invoke(cli, ["inspect","keys","-sk",sk,"-hd",hd_modifier]) + result = runner.invoke(cli, ["inspect", "keys", "-sk", sk, "-hd", hd_modifier]) assert result.exit_code == 0 assert farmer_sk in result.output # Check the type derivation is working - result = runner.invoke(cli, ["inspect","keys","-sk",sk,"-t",type_modifier]) + result = runner.invoke(cli, ["inspect", "keys", "-sk", sk, "-t", type_modifier]) assert result.exit_code == 0 assert farmer_sk in result.output # Check that synthetic calculation is working - result = runner.invoke(cli, ["inspect","keys","-sk",sk,"-sy"]) + result = runner.invoke(cli, ["inspect", "keys", "-sk", sk, "-sy"]) assert result.exit_code == 0 assert synthetic_sk in result.output # Check that using a non default puzzle hash is working - result = runner.invoke(cli, ["inspect","keys","-sk",sk,"-sy","-ph",ph_modifier]) + result = runner.invoke( + cli, ["inspect", "keys", "-sk", sk, "-sy", "-ph", ph_modifier] + ) assert result.exit_code == 0 assert modified_synthetic_sk in result.output def test_signatures(self): - empty_sig = "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - secret_key_1 = "70432627e84c13c1a6e6007bf6d9a7a0342018fdef7fc911757aad5a6929d20a" - secret_key_2 = "0f01f7f68935f8594548bca3892fec419c6b2aa7cff54c3353a2e9b1011f09c7" + secret_key_1 = ( + "70432627e84c13c1a6e6007bf6d9a7a0342018fdef7fc911757aad5a6929d20a" + ) + secret_key_2 = ( + "0f01f7f68935f8594548bca3892fec419c6b2aa7cff54c3353a2e9b1011f09c7" + ) text_message = "cafe food" bytes_message = "0xcafef00d" - extra_signature = "b5d4e653ec9a737d19abe9af7050d37b0f464f9570ec66a8457fbdabdceb50a77c6610eb442ed1e4ace39d9ecc6d40560de239c1c8f7a115e052438385d594be7394df9287cf30c3254d39f0ae21daefc38d3d07ba3e373628bf8ed73f074a80" - final_signature = "b7a6ab2c825068eb40298acab665f95c13779e828d900b8056215b54e47d8b8314e8b61fbb9c98a23ef8a134155a35b109ba284bd5f1f90f96e0d41427132b3ca6a83faae0806daa632ee6b1602a0b4bad92f2743fdeb452822f0599dfa147c0" + extra_signature = "b5d4e653ec9a737d19abe9af7050d37b0f464f9570ec66a8457fbdabdceb50a77c6610eb442ed1e4ace39d9ecc6d40560de239c1c8f7a115e052438385d594be7394df9287cf30c3254d39f0ae21daefc38d3d07ba3e373628bf8ed73f074a80" # noqa + final_signature = "b7a6ab2c825068eb40298acab665f95c13779e828d900b8056215b54e47d8b8314e8b61fbb9c98a23ef8a134155a35b109ba284bd5f1f90f96e0d41427132b3ca6a83faae0806daa632ee6b1602a0b4bad92f2743fdeb452822f0599dfa147c0" # noqa runner = CliRunner() # Test that an empty command returns an empty signature - result = runner.invoke(cli,["inspect","signatures"]) + result = runner.invoke(cli, ["inspect", "signatures"]) assert result.exit_code == 0 - assert empty_sig in result.output + assert EMPTY_SIG in result.output # Test a complex signature calculation - result = runner.invoke(cli, ["inspect","signatures","-sk",secret_key_1,"-t",text_message,"-sk",secret_key_2,"-b",bytes_message,"-sig",extra_signature]) + result = runner.invoke( + cli, + [ + "inspect", + "signatures", + "-sk", + secret_key_1, + "-t", + text_message, + "-sk", + secret_key_2, + "-b", + bytes_message, + "-sig", + extra_signature, + ], + ) assert result.exit_code == 0 assert final_signature in result.output From 990f0e04e339672d9d29f9783873d550ef8e4c1f Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Wed, 11 Aug 2021 13:35:38 -0700 Subject: [PATCH 23/31] Added mypy linting --- cdv/cmds/chia_inspect.py | 220 ++++++++++++---------- cdv/cmds/cli.py | 19 +- cdv/cmds/clsp.py | 23 ++- cdv/cmds/rpc.py | 132 +++++++------ cdv/cmds/util.py | 43 +++-- cdv/examples/drivers/__init__.py | 0 cdv/examples/drivers/piggybank_drivers.py | 10 +- cdv/examples/tests/test_piggybank.py | 42 +++-- cdv/test/__init__.py | 218 ++++++++++++--------- mypy.ini | 5 + tests/cmds/test_cdv.py | 22 ++- tests/cmds/test_clsp.py | 45 ++--- tests/cmds/test_inspect.py | 139 +++++++------- 13 files changed, 523 insertions(+), 395 deletions(-) create mode 100644 cdv/examples/drivers/__init__.py create mode 100644 mypy.ini diff --git a/cdv/cmds/chia_inspect.py b/cdv/cmds/chia_inspect.py index c267358..34df46c 100644 --- a/cdv/cmds/chia_inspect.py +++ b/cdv/cmds/chia_inspect.py @@ -1,6 +1,8 @@ +import sys import click import json +from typing import List, Any, Callable, Dict, Iterable, Union, Optional, Tuple from pprint import pprint from secrets import token_bytes @@ -11,7 +13,8 @@ from chia.types.coin_spend import CoinSpend from chia.types.coin_record import CoinRecord from chia.types.spend_bundle import SpendBundle -from chia.consensus.cost_calculator import calculate_cost_of_program +from chia.types.generator_types import BlockGenerator +from chia.consensus.cost_calculator import calculate_cost_of_program, NPCResult from chia.full_node.mempool_check_conditions import get_name_puzzle_conditions from chia.full_node.bundle_tools import simple_solution_generator from chia.wallet.derive_keys import _derive_path @@ -37,13 +40,18 @@ @click.option("-id", "--id", is_flag=True, help="Output the id of the object") @click.option("-t", "--type", is_flag=True, help="Output the type of the object") @click.pass_context -def inspect_cmd(ctx, **kwargs): +def inspect_cmd(ctx: click.Context, **kwargs): ctx.ensure_object(dict) for key, value in kwargs.items(): ctx.obj[key] = value -def inspect_callback(objs, ctx, id_calc=None, type="Unknown"): +def inspect_callback( + objs: List[Any], + ctx: click.Context, + id_calc: Callable = (lambda: None), + type: str = "Unknown", +): if (not any([value for key, value in ctx.obj.items()])) or ctx.obj["json"]: if getattr(objs[0], "to_json_dict", None): pprint([obj.to_json_dict() for obj in objs]) @@ -55,7 +63,7 @@ def inspect_callback(objs, ctx, id_calc=None, type="Unknown"): try: final_output.append(bytes(obj).hex()) except AssertionError: - final_output.append(None) + final_output.append("None") pprint(final_output) if ctx.obj["id"]: pprint([id_calc(obj) for obj in objs]) @@ -64,7 +72,7 @@ def inspect_callback(objs, ctx, id_calc=None, type="Unknown"): # Utility functions -def json_and_key_strip(input): +def json_and_key_strip(input: str) -> Dict: json_dict = json.loads(input) if len(json_dict.keys()) == 1: return json_dict[list(json_dict.keys())[0]] @@ -72,8 +80,13 @@ def json_and_key_strip(input): return json_dict -def streamable_load(cls, inputs): - input_objs = [] +def streamable_load(cls: Any, inputs: Iterable[Any]) -> List[Any]: + if inputs and not isinstance(list(inputs)[0], str): + for inst in inputs: + assert isinstance(inst, cls) + return list(inputs) + + input_objs: List[Any] = [] for input in inputs: if "{" in input: input_objs.append(cls.from_json_dict(json_and_key_strip(input))) @@ -94,10 +107,10 @@ def streamable_load(cls, inputs): ) @click.argument("objects", nargs=-1, required=False) @click.pass_context -def inspect_any_cmd(ctx, objects): +def inspect_any_cmd(ctx: click.Context, objects: Tuple[str]): input_objects = [] for obj in objects: - in_obj = obj + in_obj: Any = obj # Try it as Streamable types for cls in [Coin, CoinSpend, SpendBundle, CoinRecord]: try: @@ -107,7 +120,7 @@ def inspect_any_cmd(ctx, objects): # Try it as some key stuff for cls in [G1Element, G2Element, PrivateKey]: try: - in_obj = cls.from_bytes(hexstr_to_bytes(obj)) + in_obj = cls.from_bytes(hexstr_to_bytes(obj)) # type: ignore except Exception: pass # Try it as a Program @@ -149,13 +162,18 @@ def inspect_any_cmd(ctx, objects): ) @click.option("-a", "--amount", help="The amount of the coin") @click.pass_context -def inspect_coin_cmd(ctx, coins, **kwargs): +def inspect_coin_cmd(ctx: click.Context, coins: Tuple[str], **kwargs): do_inspect_coin_cmd(ctx, coins, **kwargs) -def do_inspect_coin_cmd(ctx, coins, print_results=True, **kwargs): +def do_inspect_coin_cmd( + ctx: click.Context, + coins: Union[Tuple[str], List[Coin]], + print_results: bool = True, + **kwargs, +) -> List[Coin]: if kwargs and all([kwargs[key] for key in kwargs.keys()]): - coin_objs = [ + coin_objs: List[Coin] = [ Coin( hexstr_to_bytes(kwargs["parent_id"]), hexstr_to_bytes(kwargs["puzzle_hash"]), @@ -163,24 +181,20 @@ def do_inspect_coin_cmd(ctx, coins, print_results=True, **kwargs): ) ] elif not kwargs or not any([kwargs[key] for key in kwargs.keys()]): - coin_objs = [] try: - if type(coins[0]) == str: - coin_objs = streamable_load(Coin, coins) - else: - coin_objs = coins + coin_objs = streamable_load(Coin, coins) except Exception: print("One or more of the specified objects was not a coin") else: print("Invalid arguments specified.") - return + sys.exit(1) if print_results: inspect_callback( coin_objs, ctx, id_calc=(lambda e: e.name().hex()), type="Coin" ) - else: - return coin_objs + + return coin_objs @inspect_cmd.command( @@ -209,13 +223,18 @@ def do_inspect_coin_cmd(ctx, coins, print_results=True, **kwargs): help="The cost per byte in the puzzle and solution reveal to use when calculating cost", ) @click.pass_context -def inspect_coin_spend_cmd(ctx, spends, **kwargs): +def inspect_coin_spend_cmd(ctx: click.Context, spends: Tuple[str], **kwargs): do_inspect_coin_spend_cmd(ctx, spends, **kwargs) -def do_inspect_coin_spend_cmd(ctx, spends, print_results=True, **kwargs): - cost_flag = False - cost_per_byte = 12000 +def do_inspect_coin_spend_cmd( + ctx: click.Context, + spends: Union[Tuple[str], List[CoinSpend]], + print_results: bool = True, + **kwargs, +) -> List[CoinSpend]: + cost_flag: bool = False + cost_per_byte: int = 12000 if kwargs: cost_flag = kwargs["cost"] cost_per_byte = kwargs["cost_per_byte"] @@ -225,7 +244,7 @@ def do_inspect_coin_spend_cmd(ctx, spends, print_results=True, **kwargs): if (not kwargs["coin"]) and all( [kwargs["parent_id"], kwargs["puzzle_hash"], kwargs["amount"]] ): - coin_spend_objs = [ + coin_spend_objs: List[CoinSpend] = [ CoinSpend( Coin( hexstr_to_bytes(kwargs["parent_id"]), @@ -247,17 +266,14 @@ def do_inspect_coin_spend_cmd(ctx, spends, print_results=True, **kwargs): else: print("Invalid arguments specified.") elif not kwargs or not any([kwargs[key] for key in kwargs.keys()]): - coin_spend_objs = [] try: - if type(spends[0]) == str: - coin_spend_objs = streamable_load(CoinSpend, spends) - else: - coin_spend_objs = spends + coin_spend_objs = streamable_load(CoinSpend, spends) except Exception: print("One or more of the specified objects was not a coin spend") + sys.exit(1) else: print("Invalid arguments specified.") - return + sys.exit(1) if print_results: inspect_callback( @@ -268,16 +284,18 @@ def do_inspect_coin_spend_cmd(ctx, spends, print_results=True, **kwargs): ) if cost_flag: for coin_spend in coin_spend_objs: - program = simple_solution_generator( + program: BlockGenerator = simple_solution_generator( SpendBundle([coin_spend], G2Element()) ) - npc_result = get_name_puzzle_conditions( + npc_result: NPCResult = get_name_puzzle_conditions( program, INFINITE_COST, cost_per_byte=cost_per_byte, safe_mode=True ) - cost = calculate_cost_of_program(program, npc_result, cost_per_byte) + cost: int = calculate_cost_of_program( + program.program, npc_result, cost_per_byte + ) print(f"Cost: {cost}") - else: - return coin_spend_objs + + return coin_spend_objs @inspect_cmd.command( @@ -321,31 +339,32 @@ def do_inspect_coin_spend_cmd(ctx, spends, print_results=True, **kwargs): help="The cost per byte in the puzzle and solution reveal to use when calculating cost", ) @click.pass_context -def inspect_spend_bundle_cmd(ctx, bundles, **kwargs): +def inspect_spend_bundle_cmd(ctx: click.Context, bundles: Tuple[str], **kwargs): do_inspect_spend_bundle_cmd(ctx, bundles, **kwargs) -def do_inspect_spend_bundle_cmd(ctx, bundles, print_results=True, **kwargs): +def do_inspect_spend_bundle_cmd( + ctx: click.Context, + bundles: Union[Tuple[str], List[SpendBundle]], + print_results: bool = True, + **kwargs, +) -> List[SpendBundle]: if kwargs and (len(kwargs["spend"]) > 0): if len(kwargs["aggsig"]) > 0: - sig = AugSchemeMPL.aggregate( + sig: G2Element = AugSchemeMPL.aggregate( [G2Element(hexstr_to_bytes(sig)) for sig in kwargs["aggsig"]] ) else: sig = G2Element() - spend_bundle_objs = [ + spend_bundle_objs: List[SpendBundle] = [ SpendBundle( do_inspect_coin_spend_cmd(ctx, kwargs["spend"], print_results=False), sig, ) ] else: - spend_bundle_objs = [] try: - if type(bundles[0]) == str: - spend_bundle_objs = streamable_load(SpendBundle, bundles) - else: - spend_bundle_objs = bundles + spend_bundle_objs = streamable_load(SpendBundle, bundles) except Exception: print("One or more of the specified objects was not a spend bundle") @@ -359,15 +378,15 @@ def do_inspect_spend_bundle_cmd(ctx, bundles, print_results=True, **kwargs): if kwargs: if kwargs["cost"]: for spend_bundle in spend_bundle_objs: - program = simple_solution_generator(spend_bundle) - npc_result = get_name_puzzle_conditions( + program: BlockGenerator = simple_solution_generator(spend_bundle) + npc_result: NPCResult = get_name_puzzle_conditions( program, INFINITE_COST, cost_per_byte=kwargs["cost_per_byte"], safe_mode=True, ) - cost = calculate_cost_of_program( - program, npc_result, kwargs["cost_per_byte"] + cost: int = calculate_cost_of_program( + program.program, npc_result, kwargs["cost_per_byte"] ) print(f"Cost: {cost}") if kwargs["debug"]: @@ -380,7 +399,7 @@ def do_inspect_spend_bundle_cmd(ctx, bundles, print_results=True, **kwargs): print("") print("Public Key/Message Pairs") print("------------------------") - pkm_dict = {} + pkm_dict: Dict[str, List[bytes]] = {} for obj in spend_bundle_objs: for coin_spend in obj.coin_spends: err, conditions_dict, _ = conditions_dict_for_solution( @@ -394,8 +413,8 @@ def do_inspect_spend_bundle_cmd(ctx, bundles, print_results=True, **kwargs): from chia.util.default_root import DEFAULT_ROOT_PATH from chia.util.config import load_config - config = load_config(DEFAULT_ROOT_PATH, "config.yaml") - genesis_challenge = config["network_overrides"][ + config: Dict = load_config(DEFAULT_ROOT_PATH, "config.yaml") + genesis_challenge: str = config["network_overrides"][ "constants" ][kwargs["network"]]["GENESIS_CHALLENGE"] for pk, msg in pkm_pairs_for_conditions_dict( @@ -411,8 +430,8 @@ def do_inspect_spend_bundle_cmd(ctx, bundles, print_results=True, **kwargs): print(f"{pk}:") for msg in msgs: print(f"\t- {msg.hex()}") - else: - return spend_bundle_objs + + return spend_bundle_objs @inspect_cmd.command( @@ -454,16 +473,21 @@ def do_inspect_spend_bundle_cmd(ctx, bundles, print_results=True, **kwargs): help="The timestamp of the block in which this coin was created", ) @click.pass_context -def inspect_coin_record_cmd(ctx, records, **kwargs): +def inspect_coin_record_cmd(ctx: click.Context, records: Tuple[str], **kwargs): do_inspect_coin_record_cmd(ctx, records, **kwargs) -def do_inspect_coin_record_cmd(ctx, records, print_results=True, **kwargs): +def do_inspect_coin_record_cmd( + ctx: click.Context, + records: Union[Tuple[str], List[CoinRecord]], + print_results: bool = True, + **kwargs, +) -> List[CoinRecord]: if kwargs and all([kwargs["confirmed_block_index"], kwargs["timestamp"]]): if (not kwargs["coin"]) and all( [kwargs["parent_id"], kwargs["puzzle_hash"], kwargs["amount"]] ): - coin_record_objs = [ + coin_record_objs: List[CoinRecord] = [ CoinRecord( Coin( hexstr_to_bytes(kwargs["parent_id"]), @@ -480,7 +504,7 @@ def do_inspect_coin_record_cmd(ctx, records, print_results=True, **kwargs): elif kwargs["coin"]: coin_record_objs = [ CoinRecord( - do_inspect_coin_cmd(ctx, [kwargs["coin"]], print_results=False)[0], + do_inspect_coin_cmd(ctx, (kwargs["coin"],), print_results=False)[0], kwargs["confirmed_block_index"], kwargs["spent_block_index"], kwargs["spent"], @@ -491,17 +515,13 @@ def do_inspect_coin_record_cmd(ctx, records, print_results=True, **kwargs): else: print("Invalid arguments specified.") elif not kwargs or not any([kwargs[key] for key in kwargs.keys()]): - coin_record_objs = [] try: - if type(records[0]) == str: - coin_record_objs = streamable_load(CoinRecord, records) - else: - coin_record_objs = records + coin_record_objs = streamable_load(CoinRecord, records) except Exception: print("One or more of the specified objects was not a coin record") else: print("Invalid arguments specified.") - return + sys.exit(1) if print_results: inspect_callback( @@ -510,8 +530,8 @@ def do_inspect_coin_record_cmd(ctx, records, print_results=True, **kwargs): id_calc=(lambda e: e.coin.name().hex()), type="CoinRecord", ) - else: - return coin_record_objs + + return coin_record_objs @inspect_cmd.command( @@ -519,19 +539,21 @@ def do_inspect_coin_record_cmd(ctx, records, print_results=True, **kwargs): ) @click.argument("programs", nargs=-1, required=False) @click.pass_context -def inspect_program_cmd(ctx, programs, **kwargs): +def inspect_program_cmd(ctx: click.Context, programs: Tuple[str], **kwargs): do_inspect_program_cmd(ctx, programs, **kwargs) -def do_inspect_program_cmd(ctx, programs, print_results=True, **kwargs): - program_objs = [] +def do_inspect_program_cmd( + ctx: click.Context, + programs: Union[Tuple[str], List[Program]], + print_results: bool = True, + **kwargs, +) -> List[Program]: try: - if type(programs[0]) == str: - program_objs = [parse_program(prog) for prog in programs] - else: - program_objs = programs + program_objs: List[Program] = [parse_program(prog) for prog in programs] except Exception: print("One or more of the specified objects was not a Program") + sys.exit(1) if print_results: inspect_callback( @@ -540,8 +562,8 @@ def do_inspect_program_cmd(ctx, programs, print_results=True, **kwargs): id_calc=(lambda e: e.get_tree_hash().hex()), type="Program", ) - else: - return program_objs + + return program_objs @inspect_cmd.command( @@ -585,14 +607,15 @@ def do_inspect_program_cmd(ctx, programs, print_results=True, **kwargs): help="The hidden puzzle to use when calculating a synthetic key", ) @click.pass_context -def inspect_keys_cmd(ctx, **kwargs): +def inspect_keys_cmd(ctx: click.Context, **kwargs): do_inspect_keys_cmd(ctx, **kwargs) -def do_inspect_keys_cmd(ctx, print_results=True, **kwargs): - sk = None - pk = None - path = "m" +def do_inspect_keys_cmd(ctx: click.Context, print_results: bool = True, **kwargs): + sk: Optional[PrivateKey] = None + pk: G1Element = G1Element() + path: str = "m" + # If we're receiving this from the any command if len(kwargs) == 1: if "secret_key" in kwargs: sk = kwargs["secret_key"] @@ -627,28 +650,29 @@ def one_or_zero(value): ) pk = sk.get_g1() + list_path: List[int] = [] if kwargs["hd_path"] and (kwargs["hd_path"] != "m"): - path = [ + list_path = [ uint32(int(i)) for i in kwargs["hd_path"].split("/") if i != "m" ] elif kwargs["key_type"]: case = kwargs["key_type"] if case == "farmer": - path = [12381, 8444, 0, 0] + list_path = [12381, 8444, 0, 0] if case == "pool": - path = [12381, 8444, 1, 0] + list_path = [12381, 8444, 1, 0] if case == "wallet": - path = [12381, 8444, 2, 0] + list_path = [12381, 8444, 2, 0] if case == "local": - path = [12381, 8444, 3, 0] + list_path = [12381, 8444, 3, 0] if case == "backup": - path = [12381, 8444, 4, 0] + list_path = [12381, 8444, 4, 0] if case == "owner": - path = [12381, 8444, 5, 0] + list_path = [12381, 8444, 5, 0] if case == "auth": - path = [12381, 8444, 6, 0] - if path != "m": - sk = _derive_path(sk, path) + list_path = [12381, 8444, 6, 0] + if list_path: + sk = _derive_path(sk, list_path) pk = sk.get_g1() path = "m/" + "/".join([str(e) for e in path]) @@ -671,7 +695,7 @@ def one_or_zero(value): class OrderedParamsCommand(click.Command): - _options = [] + _options: List = [] def parse_args(self, ctx, args): # run the parser for ourselves to preserve the passed order @@ -707,13 +731,15 @@ def parse_args(self, ctx, args): ) @click.option("-sig", "--aggsig", multiple=True, help="A signature to be aggregated") @click.pass_context -def inspect_sigs_cmd(ctx, **kwargs): +def inspect_sigs_cmd(ctx: click.Context, **kwargs): do_inspect_sigs_cmd(ctx, **kwargs) -def do_inspect_sigs_cmd(ctx, print_results=True, **kwargs): +def do_inspect_sigs_cmd( + ctx: click.Context, print_results: bool = True, **kwargs +) -> G2Element: base = G2Element() - sk = None + sk: Optional[PrivateKey] = None for param, value in OrderedParamsCommand._options: if param.name == "secret_key": sk = PrivateKey.from_bytes(hexstr_to_bytes(value)) @@ -730,5 +756,5 @@ def do_inspect_sigs_cmd(ctx, print_results=True, **kwargs): if print_results: print(str(base)) - else: - return base + + return base diff --git a/cdv/cmds/cli.py b/cdv/cmds/cli.py index cb27e2a..1b57305 100644 --- a/cdv/cmds/cli.py +++ b/cdv/cmds/cli.py @@ -2,6 +2,8 @@ import pytest import os import shutil + +from typing import List from pathlib import Path from cdv import __version__ @@ -50,29 +52,27 @@ def cli(ctx: click.Context) -> None: "-d", "--discover", is_flag=True, - type=bool, help="List the tests without running them", ) @click.option( "-i", "--init", is_flag=True, - type=bool, help="Create the test directory and/or add a new test skeleton", ) def test_cmd(tests: str, discover: bool, init: str): - test_paths = Path.cwd().glob(tests) - test_paths = list(map(lambda e: str(e), test_paths)) + test_paths: List[str] = list(map(lambda e: str(e), Path.cwd().glob(tests))) if init: test_dir = Path(os.getcwd()).joinpath("tests") if not test_dir.exists(): os.mkdir("tests") + import cdv.test as testlib src_path = Path(testlib.__file__).parent.joinpath("test_skeleton.py") - dest_path = test_dir.joinpath("test_skeleton.py") + dest_path: Path = test_dir.joinpath("test_skeleton.py") shutil.copyfile(src_path, dest_path) - dest_path_init = test_dir.joinpath("__init__.py") + dest_path_init: Path = test_dir.joinpath("__init__.py") open(dest_path_init, "w") if discover: pytest.main(["--collect-only", *test_paths]) @@ -84,7 +84,7 @@ def test_cmd(tests: str, discover: bool, init: str): "hash", short_help="SHA256 hash UTF-8 strings or bytes (use 0x prefix for bytes)" ) @click.argument("data", nargs=1, required=True) -def hash_cmd(data): +def hash_cmd(data: str): if data[:2] == "0x": hash_data = bytes.fromhex(data[2:]) else: @@ -97,19 +97,18 @@ def hash_cmd(data): @click.option( "-p", "--prefix", - type=str, default="xch", show_default=True, required=False, help="The prefix to encode with", ) -def encode_cmd(puzzle_hash, prefix): +def encode_cmd(puzzle_hash: str, prefix: str): print(encode_puzzle_hash(bytes.fromhex(puzzle_hash), prefix)) @cli.command("decode", short_help="Decode a bech32m address to a puzzle hash") @click.argument("address", nargs=1, required=True) -def decode_cmd(address): +def decode_cmd(address: str): print(decode_puzzle_hash(address).hex()) diff --git a/cdv/cmds/clsp.py b/cdv/cmds/clsp.py index 2c0b5ca..c8abeaf 100644 --- a/cdv/cmds/clsp.py +++ b/cdv/cmds/clsp.py @@ -2,8 +2,11 @@ import os import shutil +from typing import Tuple, List from pathlib import Path +from chia.types.blockchain_format.program import Program + from clvm_tools.clvmc import compile_clvm from clvm_tools.binutils import disassemble, assemble @@ -27,7 +30,7 @@ def clsp_cmd(): multiple=True, help="Paths to search for include files (./include will be searched automatically)", ) -def build_cmd(files, include) -> None: +def build_cmd(files: Tuple[str], include: Tuple[str]) -> None: project_path = Path.cwd() clvm_files = [] for glob in files: @@ -39,7 +42,7 @@ def build_cmd(files, include) -> None: clvm_files.append(path) for filename in clvm_files: - hex_file_name = filename.name + ".hex" + hex_file_name: str = filename.name + ".hex" full_hex_file_name = Path(filename.parent).joinpath(hex_file_name) if not ( full_hex_file_name.exists() @@ -60,7 +63,7 @@ def build_cmd(files, include) -> None: "disassemble", short_help="Disassemble serialized clvm into human readable form." ) @click.argument("programs", nargs=-1, required=True) -def disassemble_cmd(programs): +def disassemble_cmd(programs: Tuple[str]): for program in programs: print(disassemble(parse_program(program))) @@ -76,7 +79,7 @@ def disassemble_cmd(programs): multiple=True, help="Paths to search for include files (./include will be searched automatically)", ) -def treehash_cmd(program: str, include): +def treehash_cmd(program: str, include: Tuple[str]): print(parse_program(program, include).get_tree_hash()) @@ -104,11 +107,13 @@ def treehash_cmd(program: str, include): multiple=True, help="Paths to search for include files (./include will be searched automatically)", ) -def curry_cmd(program, args, treehash, dump, include): - prog = parse_program(program, include) - curry_args = [assemble(arg) for arg in args] +def curry_cmd( + program: str, args: Tuple[str], treehash: bool, dump: bool, include: Tuple[str] +): + prog: Program = parse_program(program, include) + curry_args: List[Program] = [assemble(arg) for arg in args] - prog_final = prog.curry(*curry_args) + prog_final: Program = prog.curry(*curry_args) if treehash: print(prog_final.get_tree_hash()) elif dump: @@ -122,7 +127,7 @@ def curry_cmd(program, args, treehash, dump, include): short_help="Copy the specified .clib file to the current directory (for example sha256tree)", ) @click.argument("libraries", nargs=-1, required=True) -def retrieve_cmd(libraries): +def retrieve_cmd(libraries: Tuple[str]): import cdv.clibs as clibs for lib in libraries: diff --git a/cdv/cmds/rpc.py b/cdv/cmds/rpc.py index 3a62722..e5d086f 100644 --- a/cdv/cmds/rpc.py +++ b/cdv/cmds/rpc.py @@ -2,15 +2,22 @@ import aiohttp import asyncio +from typing import Dict, Optional, List, Tuple from pprint import pprint +from chia.consensus.block_record import BlockRecord from chia.rpc.full_node_rpc_client import FullNodeRpcClient from chia.util.default_root import DEFAULT_ROOT_PATH from chia.util.config import load_config -from chia.util.ints import uint16 +from chia.util.ints import uint16, uint64 from chia.util.misc import format_bytes from chia.util.byte_types import hexstr_to_bytes from chia.types.blockchain_format.coin import Coin +from chia.types.blockchain_format.sized_bytes import bytes32 +from chia.types.coin_spend import CoinSpend +from chia.types.coin_record import CoinRecord +from chia.types.full_block import FullBlock +from chia.types.unfinished_header_block import UnfinishedHeaderBlock from cdv.cmds.util import fake_context from cdv.cmds.chia_inspect import do_inspect_spend_bundle_cmd @@ -21,7 +28,7 @@ def rpc_cmd(): pass -async def get_client(): +async def get_client() -> Optional[FullNodeRpcClient]: try: config = load_config(DEFAULT_ROOT_PATH, "config.yaml") self_hostname = config["self_hostname"] @@ -46,8 +53,8 @@ async def get_client(): def rpc_state_cmd(): async def do_command(): try: - node_client = await get_client() - state = await node_client.get_blockchain_state() + node_client: FullNodeRpcClient = await get_client() + state: Dict = await node_client.get_blockchain_state() state["peak"] = state["peak"].to_json_dict() pprint(state) finally: @@ -61,14 +68,16 @@ async def do_command(): @click.option("-hh", "--header-hash", help="The header hash of the block to get") @click.option("-s", "--start", help="The block index to start at (included)") @click.option("-e", "--end", help="The block index to end at (excluded)") -def rpc_blocks_cmd(header_hash, start, end): +def rpc_blocks_cmd(header_hash: str, start: int, end: int): async def do_command(): try: - node_client = await get_client() + node_client: FullNodeRpcClient = await get_client() if header_hash: - blocks = [await node_client.get_block(hexstr_to_bytes(header_hash))] + blocks: List[FullBlock] = [ + await node_client.get_block(hexstr_to_bytes(header_hash)) + ] elif start and end: - blocks = await node_client.get_all_block(start, end) + blocks: List[FullBlock] = await node_client.get_all_block(start, end) else: print("Invalid arguments specified") return @@ -88,20 +97,26 @@ async def do_command(): @click.option("-i", "--height", help="The height of the block to get") @click.option("-s", "--start", help="The block index to start at (included)") @click.option("-e", "--end", help="The block index to end at (excluded)") -def rpc_blockrecords_cmd(header_hash, height, start, end): +def rpc_blockrecords_cmd(header_hash: str, height: int, start: int, end: int): async def do_command(): try: - node_client = await get_client() + node_client: FullNodeRpcClient = await get_client() if header_hash: - block_record = await node_client.get_block_record( + block_record: BlockRecord = await node_client.get_block_record( hexstr_to_bytes(header_hash) ) - block_records = block_record.to_json_dict() if block_record else [] + block_records: List = ( + block_record.to_json_dict() if block_record else [] + ) elif height: - block_record = await node_client.get_block_record_by_height(height) - block_records = block_record.to_json_dict() if block_record else [] + block_record: BlockRecord = ( + await node_client.get_block_record_by_height(height) + ) + block_records: List = ( + block_record.to_json_dict() if block_record else [] + ) elif start and end: - block_records = await node_client.get_block_records(start, end) + block_records: List = await node_client.get_block_records(start, end) else: print("Invalid arguments specified") pprint(block_records) @@ -119,8 +134,10 @@ async def do_command(): def rpc_unfinished_cmd(): async def do_command(): try: - node_client = await get_client() - header_blocks = await node_client.get_unfinished_block_headers() + node_client: FullNodeRpcClient = await get_client() + header_blocks: List[ + UnfinishedHeaderBlock + ] = await node_client.get_unfinished_block_headers() pprint([block.to_json_dict() for block in header_blocks]) finally: node_client.close() @@ -137,39 +154,41 @@ async def do_command(): @click.option("-new", "--newer", help="The header hash of the newer block") @click.option("-s", "--start", help="The height of the block to start at") @click.option("-e", "--end", help="The height of the block to end at") -def rpc_space_cmd(older, newer, start, end): +def rpc_space_cmd(older: str, newer: str, start: int, end: int): async def do_command(): try: - node_client = await get_client() + node_client: FullNodeRpcClient = await get_client() if (older and start) or (newer and end): pprint("Invalid arguments specified.") else: if start: - start_hash = ( + start_hash: bytes32 = ( await node_client.get_block_record_by_height(start) ).header_hash elif older: - start_hash = hexstr_to_bytes(older) + start_hash: bytes32 = hexstr_to_bytes(older) else: - start_hash = ( + start_hash: bytes32 = ( await node_client.get_block_record_by_height(0) ).header_hash if end: - end_hash = ( + end_hash: bytes32 = ( await node_client.get_block_record_by_height(end) ).header_hash elif newer: - end_hash = hexstr_to_bytes(newer) + end_hash: bytes32 = hexstr_to_bytes(newer) else: - end_hash = ( + end_hash: bytes32 = ( await node_client.get_block_record_by_height( (await node_client.get_blockchain_state())["peak"].height ) ).header_hash - netspace = await node_client.get_network_space(start_hash, end_hash) + netspace: Optional[uint64] = await node_client.get_network_space( + start_hash, end_hash + ) if netspace: pprint(format_bytes(netspace)) else: @@ -187,15 +206,15 @@ async def do_command(): short_help="Gets the coins added and removed for a specific header hash (get_additions_and_removals)", ) @click.argument("headerhash", nargs=1, required=True) -def rpc_addrem_cmd(headerhash): +def rpc_addrem_cmd(headerhash: str): async def do_command(): try: - node_client = await get_client() + node_client: FullNodeRpcClient = await get_client() additions, removals = await node_client.get_additions_and_removals( bytes.fromhex(headerhash) ) - additions = [rec.to_json_dict() for rec in additions] - removals = [rec.to_json_dict() for rec in removals] + additions: List[Dict] = [rec.to_json_dict() for rec in additions] + removals: List[Dict] = [rec.to_json_dict() for rec in removals] pprint({"additions": additions, "removals": removals}) finally: node_client.close() @@ -218,11 +237,11 @@ async def do_command(): type=int, help="The block height in which the coin was spent", ) -def rpc_puzsol_cmd(coinid, block_height): +def rpc_puzsol_cmd(coinid: str, block_height: int): async def do_command(): try: - node_client = await get_client() - coin_spend = await node_client.get_puzzle_and_solution( + node_client: FullNodeRpcClient = await get_client() + coin_spend: Optional[CoinSpend] = await node_client.get_puzzle_and_solution( bytes.fromhex(coinid), block_height ) pprint(coin_spend) @@ -235,15 +254,15 @@ async def do_command(): @rpc_cmd.command("pushtx", short_help="Pushes a spend bundle to the network (push_tx)") @click.argument("spendbundles", nargs=-1, required=True) -def rpc_pushtx_cmd(spendbundles): +def rpc_pushtx_cmd(spendbundles: Tuple[str]): async def do_command(): try: - node_client = await get_client() + node_client: FullNodeRpcClient = await get_client() for bundle in do_inspect_spend_bundle_cmd( fake_context(), spendbundles, print_results=False ): try: - result = await node_client.push_tx(bundle) + result: Dict = await node_client.push_tx(bundle) pprint(result) except ValueError as e: pprint(str(e)) @@ -266,17 +285,17 @@ async def do_command(): @click.option( "--ids-only", is_flag=True, help="Only show the IDs of the retrieved spend bundles" ) -def rpc_mempool_cmd(transaction_id, ids_only): +def rpc_mempool_cmd(transaction_id: str, ids_only: bool): async def do_command(): try: - node_client = await get_client() + node_client: FullNodeRpcClient = await get_client() if transaction_id: items = {} items[transaction_id] = await node_client.get_mempool_item_by_tx_id( hexstr_to_bytes(transaction_id) ) else: - b_items = await node_client.get_all_mempool_items() + b_items: Dict = await node_client.get_all_mempool_items() items = {} for key in b_items.keys(): items[key.hex()] = b_items[key] @@ -312,25 +331,22 @@ async def do_command(): ) @click.option("-s", "--start", type=int, help="The block index to start at (included)") @click.option("-e", "--end", type=int, help="The block index to end at (excluded)") -def rpc_coinrecords_cmd(values, by, as_name_dict, **kwargs): - async def do_command(_kwargs): +def rpc_coinrecords_cmd(values: Tuple[str], by: str, as_name_dict: bool, **kwargs): + async def do_command(): try: - node_client = await get_client() - clean_values = map( - lambda value: value[2:] if value[:2] == "0x" else value, values - ) - clean_values = [bytes.fromhex(value) for value in clean_values] + node_client: FullNodeRpcClient = await get_client() + clean_values: bytes32 = map(lambda hex: hexstr_to_bytes(hex), values) if by in ["name", "id"]: - coin_records = [ + coin_records: List[CoinRecord] = [ await node_client.get_coin_record_by_name(value) for value in clean_values ] if not kwargs["include_spent_coins"]: - coin_records = list( + coin_records: List[CoinRecord] = list( filter(lambda record: record.spent is False, coin_records) ) if kwargs["start_height"] is not None: - coin_records = list( + coin_records: List[CoinRecord] = list( filter( lambda record: record.confirmed_block_index >= kwargs["start_height"], @@ -338,7 +354,7 @@ async def do_command(_kwargs): ) ) if kwargs["end_height"] is not None: - coin_records = list( + coin_records: List[CoinRecord] = list( filter( lambda record: record.confirmed_block_index < kwargs["end_height"], @@ -346,8 +362,10 @@ async def do_command(_kwargs): ) ) elif by in ["puzhash", "puzzle_hash", "puzzlehash"]: - coin_records = await node_client.get_coin_records_by_puzzle_hashes( - clean_values, **_kwargs + coin_records: List[ + CoinRecord + ] = await node_client.get_coin_records_by_puzzle_hashes( + clean_values, **kwargs ) elif by in [ "parent_id", @@ -357,11 +375,13 @@ async def do_command(_kwargs): "parentinfo", "parent", ]: - coin_records = await node_client.get_coin_records_by_parent_ids( - clean_values, **_kwargs + coin_records: List[ + CoinRecord + ] = await node_client.get_coin_records_by_parent_ids( + clean_values, **kwargs ) - coin_records = [rec.to_json_dict() for rec in coin_records] + coin_records: List[Dict] = [rec.to_json_dict() for rec in coin_records] if as_name_dict: cr_dict = {} @@ -377,4 +397,4 @@ async def do_command(_kwargs): kwargs["include_spent_coins"] = not kwargs.pop("only_unspent") kwargs["start_height"] = kwargs.pop("start") kwargs["end_height"] = kwargs.pop("end") - asyncio.get_event_loop().run_until_complete(do_command(kwargs)) + asyncio.get_event_loop().run_until_complete(do_command()) diff --git a/cdv/cmds/util.py b/cdv/cmds/util.py index 0d7ffb7..4abf616 100644 --- a/cdv/cmds/util.py +++ b/cdv/cmds/util.py @@ -1,5 +1,7 @@ import re +from typing import Dict, Iterable, List, Union + from chia.types.blockchain_format.program import Program from chia.util.byte_types import hexstr_to_bytes @@ -7,13 +9,13 @@ from clvm_tools.binutils import assemble -def fake_context(): +def fake_context() -> Dict: ctx = {} ctx["obj"] = {"json": True} return ctx -def append_include(search_paths): +def append_include(search_paths: Iterable[str]) -> List[str]: if search_paths: search_list = list(search_paths) search_list.append("./include") @@ -22,22 +24,25 @@ def append_include(search_paths): return ["./include"] -def parse_program(program: str, include=[]): - if "(" in program: - prog = Program.to(assemble(program)) - elif "." not in program: - prog = Program.from_bytes(hexstr_to_bytes(program)) +def parse_program(program: Union[str, Program], include: Iterable = []) -> Program: + if isinstance(program, Program): + return program else: - with open(program, "r") as file: - filestring = file.read() - if "(" in filestring: - # TODO: This should probably be more robust - if re.compile(r"\(mod\s").search(filestring): - prog = Program.to( - compile_clvm_text(filestring, append_include(include)) - ) + if "(" in program: + prog = Program.to(assemble(program)) + elif "." not in program: + prog = Program.from_bytes(hexstr_to_bytes(program)) + else: + with open(program, "r") as file: + filestring: str = file.read() + if "(" in filestring: + # TODO: This should probably be more robust + if re.compile(r"\(mod\s").search(filestring): + prog = Program.to( + compile_clvm_text(filestring, append_include(include)) + ) + else: + prog = Program.to(assemble(filestring)) else: - prog = Program.to(assemble(filestring)) - else: - prog = Program.from_bytes(hexstr_to_bytes(filestring)) - return prog + prog = Program.from_bytes(hexstr_to_bytes(filestring)) + return prog diff --git a/cdv/examples/drivers/__init__.py b/cdv/examples/drivers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cdv/examples/drivers/piggybank_drivers.py b/cdv/examples/drivers/piggybank_drivers.py index cda8e1d..f2ebaeb 100644 --- a/cdv/examples/drivers/piggybank_drivers.py +++ b/cdv/examples/drivers/piggybank_drivers.py @@ -1,3 +1,5 @@ +from typing import List + from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.blockchain_format.program import Program @@ -9,23 +11,23 @@ from cdv.util.load_clvm import load_clvm -PIGGYBANK_MOD = load_clvm("piggybank.clsp", "cdv.examples.clsp") +PIGGYBANK_MOD: Program = load_clvm("piggybank.clsp", "cdv.examples.clsp") # Create a piggybank -def create_piggybank_puzzle(amount: uint64, cash_out_puzhash: bytes32): +def create_piggybank_puzzle(amount: uint64, cash_out_puzhash: bytes32) -> Program: return PIGGYBANK_MOD.curry(amount, cash_out_puzhash) # Generate a solution to contribute to a piggybank -def solution_for_piggybank(pb_coin: Coin, contrib_amount: uint64): +def solution_for_piggybank(pb_coin: Coin, contrib_amount: uint64) -> Program: return Program.to( [pb_coin.puzzle_hash, pb_coin.amount, (pb_coin.amount + contrib_amount)] ) # Return the condition to assert the announcement -def piggybank_announcement_assertion(pb_coin: Coin, contrib_amount: uint64): +def piggybank_announcement_assertion(pb_coin: Coin, contrib_amount: uint64) -> List: return [ ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, std_hash(pb_coin.name() + int_to_bytes(pb_coin.amount + contrib_amount)), diff --git a/cdv/examples/tests/test_piggybank.py b/cdv/examples/tests/test_piggybank.py index becb311..419db4d 100644 --- a/cdv/examples/tests/test_piggybank.py +++ b/cdv/examples/tests/test_piggybank.py @@ -1,13 +1,19 @@ import pytest +from typing import Dict, List, Optional + +from chia.types.blockchain_format.coin import Coin from chia.types.spend_bundle import SpendBundle from chia.types.condition_opcodes import ConditionOpcode +from chia.util.ints import uint64 from cdv.examples.drivers.piggybank_drivers import ( create_piggybank_puzzle, solution_for_piggybank, piggybank_announcement_assertion, ) + +from cdv.test import CoinWrapper from cdv.test import setup as setup_test @@ -17,25 +23,33 @@ async def setup(self): network, alice, bob = await setup_test() yield network, alice, bob - async def make_and_spend_piggybank(self, network, alice, bob, CONTRIBUTION_AMOUNT): + async def make_and_spend_piggybank( + self, network, alice, bob, CONTRIBUTION_AMOUNT + ) -> Dict[str, List[Coin]]: # Get our alice wallet some money await network.farm_block(farmer=alice) # This will use one mojo to create our piggybank on the blockchain. - piggybank_coin = await alice.launch_smart_coin( - create_piggybank_puzzle(1000000000000, bob.puzzle_hash) + piggybank_coin: Optional[CoinWrapper] = await alice.launch_smart_coin( + create_piggybank_puzzle(uint64(1000000000000), bob.puzzle_hash) ) # This retrieves us a coin that is at least 500 mojos. - contribution_coin = await alice.choose_coin(CONTRIBUTION_AMOUNT) + contribution_coin: Optional[CoinWrapper] = await alice.choose_coin( + CONTRIBUTION_AMOUNT + ) + + # Make sure everything succeeded + if not piggybank_coin or not contribution_coin: + raise ValueError("Something went wrong launching/choosing a coin") # This is the spend of the piggy bank coin. We use the driver code to create the solution. - piggybank_spend = await alice.spend_coin( + piggybank_spend: SpendBundle = await alice.spend_coin( piggybank_coin, pushtx=False, args=solution_for_piggybank(piggybank_coin.as_coin(), CONTRIBUTION_AMOUNT), ) # This is the spend of a standard coin. We simply spend to ourselves but minus the CONTRIBUTION_AMOUNT. - contribution_spend = await alice.spend_coin( + contribution_spend: SpendBundle = await alice.spend_coin( contribution_coin, pushtx=False, amt=(contribution_coin.amount - CONTRIBUTION_AMOUNT), @@ -61,11 +75,13 @@ async def make_and_spend_piggybank(self, network, alice, bob, CONTRIBUTION_AMOUN async def test_piggybank_contribution(self, setup): network, alice, bob = setup try: - result = await self.make_and_spend_piggybank(network, alice, bob, 500) + result: Dict[str, List[Coin]] = await self.make_and_spend_piggybank( + network, alice, bob, 500 + ) assert "error" not in result - filtered_result = list( + filtered_result: List[Coin] = list( filter( lambda addition: (addition.amount == 501) and ( @@ -85,13 +101,13 @@ async def test_piggybank_contribution(self, setup): async def test_piggybank_completion(self, setup): network, alice, bob = setup try: - result = await self.make_and_spend_piggybank( + result: Dict[str, List[Coin]] = await self.make_and_spend_piggybank( network, alice, bob, 1000000000000 ) assert "error" not in result - filtered_result = list( + filtered_result: List[Coin] = list( filter( lambda addition: (addition.amount == 0) and ( @@ -105,7 +121,7 @@ async def test_piggybank_completion(self, setup): ) assert len(filtered_result) == 1 - filtered_result = list( + filtered_result: List[Coin] = list( filter( lambda addition: (addition.amount == 1000000000001) and (addition.puzzle_hash == bob.puzzle_hash), @@ -120,7 +136,9 @@ async def test_piggybank_completion(self, setup): async def test_piggybank_stealing(self, setup): network, alice, bob = setup try: - result = await self.make_and_spend_piggybank(network, alice, bob, -100) + result: Dict[str, List[Coin]] = await self.make_and_spend_piggybank( + network, alice, bob, -100 + ) assert "error" in result assert "GENERATOR_RUNTIME_ERROR" in result["error"] finally: diff --git a/cdv/test/__init__.py b/cdv/test/__init__.py index d6d6830..3431aa2 100644 --- a/cdv/test/__init__.py +++ b/cdv/test/__init__.py @@ -1,13 +1,15 @@ import datetime import pytimeparse -from typing import Dict -from blspy import AugSchemeMPL, G1Element, G2Element + +from typing import Dict, List, Tuple, Optional, Union +from blspy import AugSchemeMPL, G1Element, G2Element, PrivateKey from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.program import Program from chia.types.spend_bundle import SpendBundle from chia.types.coin_spend import CoinSpend +from chia.types.coin_record import CoinRecord from chia.util.ints import uint64 from chia.util.condition_tools import ConditionOpcode from chia.util.hash import std_hash @@ -28,7 +30,7 @@ class SpendResult: - def __init__(self, result): + def __init__(self, result: Dict): """Constructor for internal use. error - a string describing the error or None @@ -37,13 +39,13 @@ def __init__(self, result): """ self.result = result if "error" in result: - self.error = result["error"] - self.outputs = [] + self.error: Optional[str] = result["error"] + self.outputs: List[Coin] = [] else: self.error = None self.outputs = result["additions"] - def find_standard_coins(self, puzzle_hash): + def find_standard_coins(self, puzzle_hash: bytes32) -> List[Coin]: """Given a Wallet's puzzle_hash, find standard coins usable by it. These coins are recognized as changing the Wallet's chia balance and are @@ -69,11 +71,11 @@ def puzzle_hash(self) -> bytes32: """Return this coin's puzzle hash""" return self.puzzle().get_tree_hash() - def smart_coin(self): + def smart_coin(self) -> "SmartCoinWrapper": """Return a smart coin object wrapping this coin's program""" - return ContractWrapper(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, self.source) + return SmartCoinWrapper(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, self.source) - def as_coin(self): + def as_coin(self) -> Coin: return Coin( self.parent_coin_info, self.puzzle_hash, @@ -81,7 +83,7 @@ def as_coin(self): ) @classmethod - def from_coin(cls, coin: Coin, puzzle: Program): + def from_coin(cls, coin: Coin, puzzle: Program) -> "CoinWrapper": return cls( coin.parent_coin_info, coin.puzzle_hash, @@ -89,7 +91,7 @@ def from_coin(cls, coin: Coin, puzzle: Program): puzzle, ) - def create_standard_spend(self, priv, conditions): + def create_standard_spend(self, priv: PrivateKey, conditions: List[List]): delegated_puzzle_solution = Program.to((1, conditions)) solution = Program.to([[], delegated_puzzle_solution, []]) @@ -100,7 +102,7 @@ def create_standard_spend(self, priv, conditions): ) # Create a signature for each of these. We'll aggregate them at the end. - signature = AugSchemeMPL.sign( + signature: G2Element = AugSchemeMPL.sign( calculate_synthetic_secret_key(priv, DEFAULT_HIDDEN_PUZZLE_HASH), ( delegated_puzzle_solution.get_tree_hash() @@ -117,27 +119,27 @@ def create_standard_spend(self, priv, conditions): # They enable a user to "spend money" and "take actions on the network" # that have monetary value. # -# - Contract coins which either lock value or embody information and +# - Smart coins which either lock value or embody information and # services. These also contain a chia balance but are used for purposes # other than a fungible, liquid, spendable resource. They should not show # up in a "wallet" in the same way. We should use them by locking value # into wallet coins. We should ensure that value contained in a smart coin # coin is never destroyed. -class ContractWrapper: - def __init__(self, genesis_challenge, source): +class SmartCoinWrapper: + def __init__(self, genesis_challenge: bytes32, source: Program): """A wrapper for a smart coin carrying useful methods for interacting with chia.""" self.genesis_challenge = genesis_challenge self.source = source - def puzzle(self): + def puzzle(self) -> Program: """Give this smart coin's program""" return self.source - def puzzle_hash(self): + def puzzle_hash(self) -> bytes32: """Give this smart coin's puzzle hash""" return self.source.get_tree_hash() - def custom_coin(self, parent: Coin, amt: uint64): + def custom_coin(self, parent: Coin, amt: uint64) -> CoinWrapper: """Given a parent and an amount, create the Coin object representing this smart coin as it would exist post launch""" return CoinWrapper(parent.name(), self.puzzle_hash(), amt, self.source) @@ -148,15 +150,15 @@ def custom_coin(self, parent: Coin, amt: uint64): # Result is the smallest set of coins whose sum of amounts is greater # than target_amount. class CoinPairSearch: - def __init__(self, target_amount): + def __init__(self, target_amount: uint64): self.target = target_amount - self.total = 0 - self.max_coins = [] + self.total: uint64 = uint64(0) + self.max_coins: List[Coin] = [] - def get_result(self): + def get_result(self) -> Tuple[List[Coin], uint64]: return self.max_coins, self.total - def insort(self, coin, s, e): + def insort(self, coin: Coin): for i in range(len(self.max_coins)): if self.max_coins[i].amount < coin.amount: self.max_coins.insert(i, coin) @@ -164,14 +166,14 @@ def insort(self, coin, s, e): else: self.max_coins.append(coin) - def process_coin_for_combine_search(self, coin): + def process_coin_for_combine_search(self, coin: Coin): if self.target == 0: breakpoint() - self.total += coin.amount + self.total = uint64(self.total + coin.amount) if len(self.max_coins) == 0: self.max_coins.append(coin) else: - self.insort(coin, 0, len(self.max_coins) - 1) + self.insort(coin) while ( (len(self.max_coins) > 0) and (self.total - self.max_coins[-1].amount >= self.target) @@ -180,7 +182,7 @@ def process_coin_for_combine_search(self, coin): or (len(self.max_coins) > 1) ) ): - self.total -= self.max_coins[-1].amount + self.total = uint64(self.total - self.max_coins[-1].amount) self.max_coins = self.max_coins[:-1] @@ -189,7 +191,7 @@ def process_coin_for_combine_search(self, coin): # chia that is released by smart coins, if the smart coins interact # meaningfully with them, as many likely will. class Wallet: - def __init__(self, parent, name, pk, priv): + def __init__(self, parent: "Network", name: str, pk: G1Element, priv: PrivateKey): """Internal use constructor, use Network::make_wallet Fields: @@ -206,27 +208,34 @@ def __init__(self, parent, name, pk, priv): self.name = name self.pk_ = pk self.sk_ = priv - self.usable_coins = {} - self.puzzle = puzzle_for_pk(self.pk()) - self.puzzle_hash = self.puzzle.get_tree_hash() + self.usable_coins: Dict[bytes32, Coin] = {} + self.puzzle: Program = puzzle_for_pk(self.pk()) + self.puzzle_hash: bytes32 = self.puzzle.get_tree_hash() - synth_sk = calculate_synthetic_secret_key(self.sk_, DEFAULT_HIDDEN_PUZZLE_HASH) - self.pk_to_sk_dict = {str(self.pk_): self.sk_, str(synth_sk.get_g1()): synth_sk} + synth_sk: PrivateKey = calculate_synthetic_secret_key( + self.sk_, DEFAULT_HIDDEN_PUZZLE_HASH + ) + self.pk_to_sk_dict: Dict[str, PrivateKey] = { + str(self.pk_): self.sk_, + str(synth_sk.get_g1()): synth_sk, + } - def __repr__(self): + def __repr__(self) -> str: return ( f"" ) # Make this coin available to the user it goes with. - def add_coin(self, coin): + def add_coin(self, coin: Coin): self.usable_coins[coin.name()] = coin - def pk_to_sk(self, pk: G1Element): + def pk_to_sk(self, pk: G1Element) -> PrivateKey: assert str(pk) in self.pk_to_sk_dict return self.pk_to_sk_dict[str(pk)] - def compute_combine_action(self, amt, actions, usable_coins): + def compute_combine_action( + self, amt: uint64, actions: List, usable_coins: Dict[bytes32, Coin] + ) -> Optional[List[Coin]]: # No one coin is enough, try to find a best fit pair, otherwise combine the two # maximum coins. searcher = CoinPairSearch(amt) @@ -302,30 +311,30 @@ def compute_combine_action(self, amt, actions, usable_coins): # signature = AugSchemeMPL.aggregate(signatures) # spend_bundle = SpendBundle(coin_solutions, signature) # - async def combine_coins(self, coins): + async def combine_coins(self, coins: List[CoinWrapper]) -> Optional[SpendResult]: # Overall structure: # Create len-1 spends that just assert that the final coin is created with full value. # Create 1 spend for the final coin that asserts the other spends occurred and # Creates the new coin. - beginning_balance = self.balance() - beginning_coins = len(self.usable_coins) + beginning_balance: uint64 = self.balance() + beginning_coins: int = len(self.usable_coins) # We need the final coin to know what the announced coin name will be. final_coin = CoinWrapper( coins[-1].name(), self.puzzle_hash, - sum(map(lambda x: x.amount, coins)), + uint64(sum(map(lambda x: x.amount, coins))), self.puzzle, ) - destroyed_coin_solutions = [] + destroyed_coin_solutions: List[CoinSpend] = [] # Each coin wants agg_sig_me so we aggregate them at the end. - signatures = [] + signatures: List[G2Element] = [] for c in coins[:-1]: - announce_conditions = [ + announce_conditions: List[List] = [ # Each coin expects the final coin creation announcement [ ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, @@ -339,7 +348,7 @@ async def combine_coins(self, coins): destroyed_coin_solutions.append(coin_solution) signatures.append(signature) - final_coin_creation = [ + final_coin_creation: List[List] = [ [ConditionOpcode.CREATE_COIN_ANNOUNCEMENT, final_coin.name()], [ConditionOpcode.CREATE_COIN, self.puzzle_hash, final_coin.amount], ] @@ -353,7 +362,9 @@ async def combine_coins(self, coins): signature = AugSchemeMPL.aggregate(signatures) spend_bundle = SpendBundle(destroyed_coin_solutions, signature) - pushed = await self.parent.push_tx(spend_bundle) + pushed: Dict[str, Union[str, List[Coin]]] = await self.parent.push_tx( + spend_bundle + ) # We should have the same amount of money. assert beginning_balance == self.balance() @@ -364,21 +375,29 @@ async def combine_coins(self, coins): # Find a coin containing amt we can use as a parent. # Synthesize a coin with sufficient funds if possible. - async def choose_coin(self, amt) -> CoinWrapper: + async def choose_coin(self, amt) -> Optional[CoinWrapper]: """Given an amount requirement, find a coin that contains at least that much chia""" - start_balance = self.balance() - coins_to_spend = self.compute_combine_action(amt, [], dict(self.usable_coins)) + start_balance: uint64 = self.balance() + coins_to_spend: Optional[List[Coin]] = self.compute_combine_action( + amt, [], dict(self.usable_coins) + ) # Couldn't find a working combination. if coins_to_spend is None: return None if len(coins_to_spend) == 1: - return coins_to_spend[0] + only_coin: Coin = coins_to_spend[0] + return CoinWrapper( + only_coin.parent_coin_info, + only_coin.puzzle_hash, + only_coin.amount, + self.puzzle, + ) # We receive a timeline of actions to take (indicating that we have a plan) # Do the first action and start over. - result = await self.combine_coins( + result: Optional[SpendResult] = await self.combine_coins( list( map( lambda x: CoinWrapper( @@ -400,20 +419,22 @@ async def choose_coin(self, amt) -> CoinWrapper: # - allow use of more than one coin to launch smart coin # - ensure input chia = output chia. it'd be dumb to just allow somebody # to lose their chia without telling them. - async def launch_smart_coin(self, source, **kwargs) -> CoinWrapper: + async def launch_smart_coin( + self, source: Program, **kwargs + ) -> Optional[CoinWrapper]: """Create a new smart coin based on a parent coin and return the smart coin's living coin to the user or None if the spend failed.""" - amt = 1 + amt = uint64(1) if "amt" in kwargs: amt = kwargs["amt"] - found_coin = await self.choose_coin(amt) + found_coin: Optional[CoinWrapper] = await self.choose_coin(amt) if found_coin is None: raise ValueError(f"could not find available coin containing {amt} mojo") # Create a puzzle based on the incoming smart coin - cw = ContractWrapper(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, source) - condition_args = [ + cw = SmartCoinWrapper(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, source) + condition_args: List[List] = [ [ConditionOpcode.CREATE_COIN, cw.puzzle_hash(), amt], ] if amt < found_coin.amount: @@ -425,7 +446,7 @@ async def launch_smart_coin(self, source, **kwargs) -> CoinWrapper: solution = Program.to([[], delegated_puzzle_solution, []]) # Sign the (delegated_puzzle_hash + coin_name) with synthetic secret key - signature = AugSchemeMPL.sign( + signature: G2Element = AugSchemeMPL.sign( calculate_synthetic_secret_key(self.sk_, DEFAULT_HIDDEN_PUZZLE_HASH), ( delegated_puzzle_solution.get_tree_hash() @@ -444,14 +465,16 @@ async def launch_smart_coin(self, source, **kwargs) -> CoinWrapper: ], signature, ) - pushed = await self.parent.push_tx(spend_bundle) + pushed: Dict[str, Union[str, List[Coin]]] = await self.parent.push_tx( + spend_bundle + ) if "error" not in pushed: return cw.custom_coin(found_coin, amt) else: return None # Give chia - async def give_chia(self, target, amt): + async def give_chia(self, target: "Wallet", amt: uint64) -> Optional[CoinWrapper]: return await self.launch_smart_coin(target.puzzle, amt=amt) # Called each cycle before coins are re-established from the simulator. @@ -459,43 +482,47 @@ def _clear_coins(self): self.usable_coins = {} # Public key of wallet - def pk(self): + def pk(self) -> G1Element: """Return actor's public key""" return self.pk_ # Balance of wallet - def balance(self): + def balance(self) -> uint64: """Return the actor's balance in standard coins as we understand it""" - return sum(map(lambda x: x.amount, self.usable_coins.values())) + return uint64(sum(map(lambda x: x.amount, self.usable_coins.values()))) # Spend a coin, probably a smart coin. # Allows the user to specify the arguments for the puzzle solution. # Automatically takes care of signing, etc. # Result is an object representing the actions taken when the block # with this transaction was farmed. - async def spend_coin(self, coin: CoinWrapper, pushtx=True, **kwargs): + async def spend_coin( + self, coin: CoinWrapper, pushtx: bool = True, **kwargs + ) -> Union[SpendResult, SpendBundle]: """Given a coin object, invoke it on the blockchain, either as a standard coin if no arguments are given or with custom arguments in args=""" - amt = 1 + amt = uint64(1) if "amt" in kwargs: amt = kwargs["amt"] - delegated_puzzle_solution = None + delegated_puzzle_solution: Optional[Program] = None if "args" not in kwargs: - target_puzzle_hash = self.puzzle_hash + target_puzzle_hash: bytes32 = self.puzzle_hash # Allow the user to 'give this much chia' to another user. if "to" in kwargs: target_puzzle_hash = kwargs["to"].puzzle_hash # Automatic arguments from the user's intention. if "custom_conditions" not in kwargs: - solution_list = [[ConditionOpcode.CREATE_COIN, target_puzzle_hash, amt]] + solution_list: List[List] = [ + [ConditionOpcode.CREATE_COIN, target_puzzle_hash, amt] + ] else: solution_list = kwargs["custom_conditions"] if "remain" in kwargs: - remainer = kwargs["remain"] - remain_amt = coin.amount - amt - if isinstance(remainer, ContractWrapper): + remainer: Union[SmartCoinWrapper, Wallet] = kwargs["remain"] + remain_amt = uint64(coin.amount - amt) + if isinstance(remainer, SmartCoinWrapper): solution_list.append( [ ConditionOpcode.CREATE_COIN, @@ -527,7 +554,7 @@ async def spend_coin(self, coin: CoinWrapper, pushtx=True, **kwargs): # the signing for non-standard coins. I don't fully understand the difference but # this definitely does the right thing. try: - spend_bundle = await sign_coin_spends( + spend_bundle: SpendBundle = await sign_coin_spends( [solution_for_coin], self.pk_to_sk, DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA, @@ -540,7 +567,9 @@ async def spend_coin(self, coin: CoinWrapper, pushtx=True, **kwargs): ) if pushtx: - pushed = await self.parent.push_tx(spend_bundle) + pushed: Dict[str, Union[str, List[Coin]]] = await self.parent.push_tx( + spend_bundle + ) return SpendResult(pushed) else: return spend_bundle @@ -551,13 +580,14 @@ class Network: """An object that owns a simulation, responsible for managing Wallet actors, time and initialization.""" - time: uint64 + time: datetime.timedelta sim: SpendSim + sim_client: SimClient wallets: Dict[str, Wallet] nobody: Wallet @classmethod - async def create(cls): + async def create(cls) -> "Network": self = cls() self.time = datetime.timedelta( days=18750, seconds=61201 @@ -573,26 +603,28 @@ async def close(self): await self.sim.close() # Have the system farm one block with a specific beneficiary (nobody if not specified). - async def farm_block(self, **kwargs): + async def farm_block(self, **kwargs) -> Tuple[List[Coin], List[Coin]]: """Given a farmer, farm a block with that actor as the beneficiary of the farm reward. Used for causing chia balance to exist so the system can do things. """ - farmer = self.nobody + farmer: Wallet = self.nobody if "farmer" in kwargs: farmer = kwargs["farmer"] farm_duration = datetime.timedelta(block_time) - farmed = await self.sim.farm_block(farmer.puzzle_hash) + farmed: Tuple[List[Coin], List[Coin]] = await self.sim.farm_block( + farmer.puzzle_hash + ) for k, w in self.wallets.items(): w._clear_coins() for kw, w in self.wallets.items(): - coin_records = await self.sim_client.get_coin_records_by_puzzle_hash( - w.puzzle_hash - ) + coin_records: List[ + CoinRecord + ] = await self.sim_client.get_coin_records_by_puzzle_hash(w.puzzle_hash) for coin_record in coin_records: if coin_record.spent is False: w.add_coin(CoinWrapper.from_coin(coin_record.coin, w.puzzle)) @@ -600,16 +632,16 @@ async def farm_block(self, **kwargs): self.time += farm_duration return farmed - def _alloc_key(self): - key_idx = len(self.wallets) - pk = public_key_for_index(key_idx) - priv = private_key_for_index(key_idx) + def _alloc_key(self) -> Tuple[G1Element, PrivateKey]: + key_idx: int = len(self.wallets) + pk: G1Element = public_key_for_index(key_idx) + priv: PrivateKey = private_key_for_index(key_idx) return pk, priv # Allow the user to create a wallet identity to whom standard coins may be targeted. # This results in the creation of a wallet that tracks balance and standard coins. # Public and private key from here are used in signing. - def make_wallet(self, name): + def make_wallet(self, name: str) -> Wallet: """Create a wallet for an actor. This causes the actor's chia balance in standard coin to be tracked during the simulation. Wallets have some domain specific methods that behave in similar ways to other blockchains.""" @@ -619,7 +651,7 @@ def make_wallet(self, name): return w # Skip real time by farming blocks until the target duration is achieved. - async def skip_time(self, target_duration, **kwargs): + async def skip_time(self, target_duration: str, **kwargs): """Skip a duration of simulated time, causing blocks to be farmed. If a farmer is specified, they win each block""" target_time = self.time + datetime.timedelta( @@ -627,17 +659,17 @@ async def skip_time(self, target_duration, **kwargs): ) while target_time > self.get_timestamp(): await self.farm_block(**kwargs) - self.sim.pass_time(20) + self.sim.pass_time(uint64(20)) # Or possibly aggregate farm_block results. return None - def get_timestamp(self): + def get_timestamp(self) -> datetime.timedelta: """Return the current simualtion time in seconds.""" return datetime.timedelta(seconds=self.sim.timestamp) # Given a spend bundle, farm a block and analyze the result. - async def push_tx(self, bundle): + async def push_tx(self, bundle: SpendBundle) -> Dict[str, Union[str, List[Coin]]]: """Given a spend bundle, try to farm a block containing it. If the spend bundle didn't validate, then a result containing an 'error' key is returned. The reward for the block goes to Network::nobody""" @@ -654,8 +686,8 @@ async def push_tx(self, bundle): } -async def setup(): - network = await Network.create() - alice = network.make_wallet("alice") - bob = network.make_wallet("bob") +async def setup() -> Tuple[Network, Wallet, Wallet]: + network: Network = await Network.create() + alice: Wallet = network.make_wallet("alice") + bob: Wallet = network.make_wallet("bob") return network, alice, bob diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..8a8fdcb --- /dev/null +++ b/mypy.ini @@ -0,0 +1,5 @@ +[mypy] +ignore_missing_imports = True + +[mypy - lib] +ignore_errors = True diff --git a/tests/cmds/test_cdv.py b/tests/cmds/test_cdv.py index a6e25ef..0a15edb 100644 --- a/tests/cmds/test_cdv.py +++ b/tests/cmds/test_cdv.py @@ -1,6 +1,6 @@ from pathlib import Path -from click.testing import CliRunner +from click.testing import CliRunner, Result from cdv.cmds.cli import cli @@ -8,10 +8,12 @@ class TestCdvCommands: def test_encode_decode(self): runner = CliRunner() - puzhash = "3d4237d9383a7b6e60d1bfe551139ec2d6e5468205bf179ed381e66bed7b9788" + puzhash: str = ( + "3d4237d9383a7b6e60d1bfe551139ec2d6e5468205bf179ed381e66bed7b9788" + ) - result = runner.invoke(cli, ["encode", puzhash]) - address = "xch184pr0kfc8fakucx3hlj4zyu7cttw235zqkl308kns8nxhmtmj7yqxsnauc" + result: Result = runner.invoke(cli, ["encode", puzhash]) + address: str = "xch184pr0kfc8fakucx3hlj4zyu7cttw235zqkl308kns8nxhmtmj7yqxsnauc" assert result.exit_code == 0 assert address in result.output result = runner.invoke(cli, ["decode", address]) @@ -19,7 +21,9 @@ def test_encode_decode(self): assert puzhash in result.output result = runner.invoke(cli, ["encode", puzhash, "--prefix", "txch"]) - test_address = "txch184pr0kfc8fakucx3hlj4zyu7cttw235zqkl308kns8nxhmtmj7yqth5tat" + test_address: str = ( + "txch184pr0kfc8fakucx3hlj4zyu7cttw235zqkl308kns8nxhmtmj7yqth5tat" + ) assert result.exit_code == 0 assert test_address in result.output result = runner.invoke(cli, ["decode", test_address]) @@ -28,10 +32,10 @@ def test_encode_decode(self): def test_hash(self): runner = CliRunner() - str_msg = "chia" - b_msg = "0xcafef00d" + str_msg: str = "chia" + b_msg: str = "0xcafef00d" - result = runner.invoke(cli, ["hash", str_msg]) + result: Result = runner.invoke(cli, ["hash", str_msg]) assert result.exit_code == 0 assert ( "3d4237d9383a7b6e60d1bfe551139ec2d6e5468205bf179ed381e66bed7b9788" @@ -47,7 +51,7 @@ def test_hash(self): def test_test(self): runner = CliRunner() with runner.isolated_filesystem(): - result = runner.invoke(cli, ["test", "--init"]) + result: Result = runner.invoke(cli, ["test", "--init"]) assert result.exit_code == 0 assert ( Path("./tests").exists() and Path("./tests/test_skeleton.py").exists() diff --git a/tests/cmds/test_clsp.py b/tests/cmds/test_clsp.py index 9896968..94cc9af 100644 --- a/tests/cmds/test_clsp.py +++ b/tests/cmds/test_clsp.py @@ -1,8 +1,9 @@ import os import shutil +from typing import IO, List from pathlib import Path -from click.testing import CliRunner +from click.testing import CliRunner, Result from chia.types.blockchain_format.program import Program @@ -11,16 +12,16 @@ from cdv.cmds.cli import cli -class TestCdvCommands: - program = "(q . 1)" - serialized = "ff0101" - mod = "(mod () (include condition_codes.clib) CREATE_COIN)" +class TestClspCommands: + program: str = "(q . 1)" + serialized: str = "ff0101" + mod: str = "(mod () (include condition_codes.clib) CREATE_COIN)" # This comes before build because build is going to use retrieve def test_retrieve(self): runner = CliRunner() with runner.isolated_filesystem(): - result = runner.invoke(cli, ["clsp", "retrieve", "condition_codes"]) + result: Result = runner.invoke(cli, ["clsp", "retrieve", "condition_codes"]) assert result.exit_code == 0 assert Path("./include/condition_codes.clib").exists() @@ -32,10 +33,10 @@ def test_build(self): runner = CliRunner() with runner.isolated_filesystem(): # Test building CLVM - program_file = open("program.clvm", "w") + program_file: IO = open("program.clvm", "w") program_file.write(self.program) program_file.close() - result = runner.invoke(cli, ["clsp", "build", "."]) + result: Result = runner.invoke(cli, ["clsp", "build", "."]) assert result.exit_code == 0 assert Path("./program.clvm.hex").exists() assert open("program.clvm.hex", "r").read() == "01" @@ -44,7 +45,7 @@ def test_build(self): runner.invoke(cli, ["clsp", "retrieve", "condition_codes"]) # Test building Chialisp (automatic include search) - mod_file = open("mod.clsp", "w") + mod_file: IO = open("mod.clsp", "w") mod_file.write(self.mod) mod_file.close() result = runner.invoke(cli, ["clsp", "build", "./mod.clsp"]) @@ -64,15 +65,15 @@ def test_build(self): assert open("mod.clsp.hex", "r").read() == "ff0133" def test_curry(self): - integer = 1 + integer: int = 1 hexadecimal = bytes.fromhex("aabbccddeeff") - string = "hello" + string: str = "hello" program = Program.to([2, 2, 3]) mod = Program.from_bytes(bytes.fromhex(self.serialized)) - curried_mod = mod.curry(integer, hexadecimal, string, program) + curried_mod: Program = mod.curry(integer, hexadecimal, string, program) runner = CliRunner() - cmd = [ + cmd: List[str] = [ "clsp", "curry", str(mod), @@ -86,7 +87,7 @@ def test_curry(self): disassemble(program), ] - result = runner.invoke(cli, cmd) + result: Result = runner.invoke(cli, cmd) assert result.exit_code == 0 assert disassemble(curried_mod) in result.output @@ -104,7 +105,7 @@ def test_curry(self): def test_disassemble(self): runner = CliRunner() # Test the program passed in as a hex string - result = runner.invoke(cli, ["clsp", "disassemble", self.serialized]) + result: Result = runner.invoke(cli, ["clsp", "disassemble", self.serialized]) assert result.exit_code == 0 assert self.program in result.output # Test the program passed in as a hex file @@ -119,19 +120,19 @@ def test_disassemble(self): def test_treehash(self): # Test a program passed as a string runner = CliRunner() - program = "(a 2 3)" - program_as_mod = "(mod (arg . arg2) (a arg arg2))" - program_hash = ( + program: str = "(a 2 3)" + program_as_mod: str = "(mod (arg . arg2) (a arg arg2))" + program_hash: str = ( "530d1b3283c802be3a7bdb34b788c1898475ed76c89ecb2224e4b4f40c32d1a4" ) - result = runner.invoke(cli, ["clsp", "treehash", program]) + result: Result = runner.invoke(cli, ["clsp", "treehash", program]) assert result.exit_code == 0 assert program_hash in result.output # Test a program passed in as a CLVM file - filename = "program.clvm" + filename: str = "program.clvm" with runner.isolated_filesystem(): - program_file = open(filename, "w") + program_file: IO = open(filename, "w") program_file.write(program) program_file.close() result = runner.invoke(cli, ["clsp", "treehash", filename]) @@ -140,7 +141,7 @@ def test_treehash(self): # Test a program passed in as a Chialisp file with runner.isolated_filesystem(): - program_file = open(filename, "w") + program_file: IO = open(filename, "w") program_file.write(program_as_mod) program_file.close() result = runner.invoke(cli, ["clsp", "treehash", filename]) diff --git a/tests/cmds/test_inspect.py b/tests/cmds/test_inspect.py index 383f59e..134f34e 100644 --- a/tests/cmds/test_inspect.py +++ b/tests/cmds/test_inspect.py @@ -2,7 +2,8 @@ from pathlib import Path -from click.testing import CliRunner +from typing import Dict, List +from click.testing import CliRunner, Result from cdv.cmds.cli import cli @@ -14,7 +15,7 @@ def test_any(self): runner = CliRunner() # Try to inspect a program - result = runner.invoke(cli, ["inspect", "any", "ff0101"]) + result: Result = runner.invoke(cli, ["inspect", "any", "ff0101"]) assert result.exit_code == 0 assert "guess" not in result.output @@ -64,8 +65,8 @@ def test_any(self): metadata_path = Path(__file__).parent.joinpath( f"object_files/{class_type}s/{class_type}_metadata.json" ) - valid_json = json.loads(open(valid_json_path, "r").read()) - metadata_json = json.loads(open(metadata_path, "r").read()) + valid_json: Dict = json.loads(open(valid_json_path, "r").read()) + metadata_json: Dict = json.loads(open(metadata_path, "r").read()) # Try to load the invalid and make sure it fails result = runner.invoke(cli, ["inspect", "any", str(invalid_json_path)]) @@ -95,7 +96,7 @@ def test_any(self): assert "'coin':" in result.output # From a string - valid_hex = open(valid_hex_path, "r").read() + valid_hex: str = open(valid_hex_path, "r").read() result = runner.invoke(cli, ["inspect", "--json", "any", valid_hex]) assert result.exit_code == 0 assert "'coin':" in result.output @@ -122,13 +123,13 @@ def test_any(self): assert metadata_json["type"] in result.output def test_coins(self): - pid = "0x0000000000000000000000000000000000000000000000000000000000000000" - ph = "0000000000000000000000000000000000000000000000000000000000000000" - amount = "0" - id = "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b" + pid: str = "0x0000000000000000000000000000000000000000000000000000000000000000" + ph: str = "0000000000000000000000000000000000000000000000000000000000000000" + amount: str = "0" + id: str = "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b" runner = CliRunner() - result = runner.invoke( + result: Result = runner.invoke( cli, ["inspect", "--id", "coins", "-pid", pid, "-ph", ph, "-a", amount] ) assert result.exit_code == 0 @@ -136,20 +137,20 @@ def test_coins(self): def test_spends(self): coin_path = Path(__file__).parent.joinpath("object_files/coins/coin.json") - pid = "0x0000000000000000000000000000000000000000000000000000000000000000" - ph = "0000000000000000000000000000000000000000000000000000000000000000" - amount = "0" - id = "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b" - puzzle_reveal = "ff0101" - solution = "80" - cost = "63600" - cost_modifier = "1" - modified_cost = "53" + pid: str = "0x0000000000000000000000000000000000000000000000000000000000000000" + ph: str = "0000000000000000000000000000000000000000000000000000000000000000" + amount: str = "0" + id: str = "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b" + puzzle_reveal: str = "ff0101" + solution: str = "80" + cost: str = "588000" + cost_modifier: str = "1" + modified_cost: str = "49" runner = CliRunner() # Specify the coin file - result = runner.invoke( + result: Result = runner.invoke( cli, [ "inspect", @@ -167,7 +168,7 @@ def test_spends(self): assert id in result.output # Specify all of the arguments - base_command = [ + base_command: List[str] = [ "inspect", "--id", "spends", @@ -203,21 +204,25 @@ def test_spends(self): def test_spendbundles(self): spend_path = Path(__file__).parent.joinpath("object_files/spends/spend.json") - pubkey = "80df54b2a616f5c79baaed254134ae5dfc6e24e2d8e1165b251601ceb67b1886db50aacf946eb20f00adc303e7534dd0" - signable_data = "24f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4bccd5bb71183532bff220ba46c268991a3ff07eb358e8255a65c30a2dce0e5fbb" # noqa - agg_sig = "b83fe374efbc5776735df7cbfb7e27ede5079b41cd282091450e4de21c4b772e254ce906508834b0c2dcd3d58c47a96914c782f0baf8eaff7ece3b070d2035cd878f744deadcd6c6625c1d0a1b418437ee3f25c2df08ffe08bdfe06b8a83b514" # noqa - id_no_sig = "3ac222f0e8f19afcad367b3068273801ca21fe515311dae8d399a5baad9c3c73" - id_with_sig = "bd9acfbb344c006cf520f1265a9b611a20cd478f234f51cd31a978b2d3ad9bbb" - network_modifier = "testnet7" - modified_signable_data = "24f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b117816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015af" # noqa - cost = "4868283" - cost_modifier = "0" - modified_cost = "2408283" + pubkey: str = "80df54b2a616f5c79baaed254134ae5dfc6e24e2d8e1165b251601ceb67b1886db50aacf946eb20f00adc303e7534dd0" + signable_data: str = "24f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4bccd5bb71183532bff220ba46c268991a3ff07eb358e8255a65c30a2dce0e5fbb" # noqa + agg_sig: str = "b83fe374efbc5776735df7cbfb7e27ede5079b41cd282091450e4de21c4b772e254ce906508834b0c2dcd3d58c47a96914c782f0baf8eaff7ece3b070d2035cd878f744deadcd6c6625c1d0a1b418437ee3f25c2df08ffe08bdfe06b8a83b514" # noqa + id_no_sig: str = ( + "3ac222f0e8f19afcad367b3068273801ca21fe515311dae8d399a5baad9c3c73" + ) + id_with_sig: str = ( + "bd9acfbb344c006cf520f1265a9b611a20cd478f234f51cd31a978b2d3ad9bbb" + ) + network_modifier: str = "testnet7" + modified_signable_data: str = "24f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b117816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015af" # noqa + cost: str = "4820283" + cost_modifier: str = "0" + modified_cost: str = "2408283" runner = CliRunner() # Build with only the spends - result = runner.invoke( + result: Result = runner.invoke( cli, [ "inspect", @@ -233,7 +238,7 @@ def test_spendbundles(self): assert id_no_sig in result.output # Build with the aggsig as well - base_command = [ + base_command: List[str] = [ "inspect", "--id", "spendbundles", @@ -299,15 +304,15 @@ def test_spendbundles(self): def test_coinrecords(self): coin_path = Path(__file__).parent.joinpath("object_files/coins/coin.json") - pid = "0x0000000000000000000000000000000000000000000000000000000000000000" - ph = "0000000000000000000000000000000000000000000000000000000000000000" - amount = "0" - id = "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b" - coinbase = "False" - confirmed_block_index = "500" - spent = "True" - spent_block_index = "501" - timestamp = "909469800" + pid: str = "0x0000000000000000000000000000000000000000000000000000000000000000" + ph: str = "0000000000000000000000000000000000000000000000000000000000000000" + amount: str = "0" + id: str = "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b" + coinbase: str = "False" + confirmed_block_index: str = "500" + spent: str = "True" + spent_block_index: str = "501" + timestamp: str = "909469800" runner = CliRunner() @@ -315,7 +320,9 @@ def test_coinrecords(self): record_path = Path(__file__).parent.joinpath( "object_files/coinrecords/coinrecord.json" ) - result = runner.invoke(cli, ["inspect", "coinrecords", str(record_path)]) + result: Result = runner.invoke( + cli, ["inspect", "coinrecords", str(record_path)] + ) assert result.exit_code == 0 assert "'coin'" in result.output @@ -372,39 +379,43 @@ def test_coinrecords(self): assert id in result.output def test_programs(self): - program = "ff0101" - id = "69ae360134b1fae04326e5546f25dc794a19192a1f22a44a46d038e7f0d1ecbb" + program: str = "ff0101" + id: str = "69ae360134b1fae04326e5546f25dc794a19192a1f22a44a46d038e7f0d1ecbb" runner = CliRunner() - result = runner.invoke(cli, ["inspect", "--id", "programs", program]) + result: Result = runner.invoke(cli, ["inspect", "--id", "programs", program]) assert result.exit_code == 0 assert id in result.output def test_keys(self): - mnemonic = "chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia" # noqa - passphrase = "chia" - sk = "68cbc26a245903f3d20a405c0673a9f32b2382174abeeabadb7ba1478b162326" - pk = "b7531990662d3fbff22d073a08123ddeae70e0a118cecebf8f207b373da5a90aaefcfed2d9cab8fbe711d6b4f5c72e89" - hd_modifier = "m/12381/8444/0/0" - type_modifier = "farmer" # Should be same as above HD Path - farmer_sk = "5729513405cb68999711618aa02e317335cecdea63d666886bbb39c0fc487dae" - synthetic_sk = ( + mnemonic: str = "chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia chia" # noqa + passphrase: str = "chia" + sk: str = "68cbc26a245903f3d20a405c0673a9f32b2382174abeeabadb7ba1478b162326" + pk: str = "b7531990662d3fbff22d073a08123ddeae70e0a118cecebf8f207b373da5a90aaefcfed2d9cab8fbe711d6b4f5c72e89" + hd_modifier: str = "m/12381/8444/0/0" + type_modifier: str = "farmer" # Should be same as above HD Path + farmer_sk: str = ( + "5729513405cb68999711618aa02e317335cecdea63d666886bbb39c0fc487dae" + ) + synthetic_sk: str = ( "6124dbc2580cbdc2f3c716572950434ecdb42f952ec2e947bd393643c13e8ec2" ) - ph_modifier = "69ae360134b1fae04326e5546f25dc794a19192a1f22a44a46d038e7f0d1ecbb" - modified_synthetic_sk = ( + ph_modifier: str = ( + "69ae360134b1fae04326e5546f25dc794a19192a1f22a44a46d038e7f0d1ecbb" + ) + modified_synthetic_sk: str = ( "66dff9a8d49d90029e5fb42378562d459e375965150bc72c3a7ea2c523ab49f5" ) runner = CliRunner() # Build the key from secret key - result = runner.invoke(cli, ["inspect", "keys", "-sk", sk]) + result: Result = runner.invoke(cli, ["inspect", "keys", "-sk", sk]) assert result.exit_code == 0 assert sk in result.output assert pk in result.output - key_output = result.output + key_output: str = result.output # Build the key from mnemonic result = runner.invoke( @@ -447,16 +458,16 @@ def test_keys(self): assert modified_synthetic_sk in result.output def test_signatures(self): - secret_key_1 = ( + secret_key_1: str = ( "70432627e84c13c1a6e6007bf6d9a7a0342018fdef7fc911757aad5a6929d20a" ) - secret_key_2 = ( + secret_key_2: str = ( "0f01f7f68935f8594548bca3892fec419c6b2aa7cff54c3353a2e9b1011f09c7" ) - text_message = "cafe food" - bytes_message = "0xcafef00d" - extra_signature = "b5d4e653ec9a737d19abe9af7050d37b0f464f9570ec66a8457fbdabdceb50a77c6610eb442ed1e4ace39d9ecc6d40560de239c1c8f7a115e052438385d594be7394df9287cf30c3254d39f0ae21daefc38d3d07ba3e373628bf8ed73f074a80" # noqa - final_signature = "b7a6ab2c825068eb40298acab665f95c13779e828d900b8056215b54e47d8b8314e8b61fbb9c98a23ef8a134155a35b109ba284bd5f1f90f96e0d41427132b3ca6a83faae0806daa632ee6b1602a0b4bad92f2743fdeb452822f0599dfa147c0" # noqa + text_message: str = "cafe food" + bytes_message: str = "0xcafef00d" + extra_signature: str = "b5d4e653ec9a737d19abe9af7050d37b0f464f9570ec66a8457fbdabdceb50a77c6610eb442ed1e4ace39d9ecc6d40560de239c1c8f7a115e052438385d594be7394df9287cf30c3254d39f0ae21daefc38d3d07ba3e373628bf8ed73f074a80" # noqa + final_signature: str = "b7a6ab2c825068eb40298acab665f95c13779e828d900b8056215b54e47d8b8314e8b61fbb9c98a23ef8a134155a35b109ba284bd5f1f90f96e0d41427132b3ca6a83faae0806daa632ee6b1602a0b4bad92f2743fdeb452822f0599dfa147c0" # noqa runner = CliRunner() From 23e21c46b3db97c96d0fc8fa947f14dfa2ca0df8 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Thu, 12 Aug 2021 08:59:05 -0700 Subject: [PATCH 24/31] Added integreation for github CI --- .github/linters/.flake8 | 4 + .github/linters/.isort.cfg | 20 + .github/linters/.markdown-lint.yml | 38 ++ .github/linters/.python-black | 2 + .github/linters/.python-lint | 564 +++++++++++++++++++++++++++++ .github/workflows/super-linter.yml | 80 ++++ 6 files changed, 708 insertions(+) create mode 100644 .github/linters/.flake8 create mode 100644 .github/linters/.isort.cfg create mode 100644 .github/linters/.markdown-lint.yml create mode 100644 .github/linters/.python-black create mode 100644 .github/linters/.python-lint create mode 100644 .github/workflows/super-linter.yml diff --git a/.github/linters/.flake8 b/.github/linters/.flake8 new file mode 100644 index 0000000..ea38e9c --- /dev/null +++ b/.github/linters/.flake8 @@ -0,0 +1,4 @@ +[flake8] +max-line-length = 120 +exclude = ./typings/**/* +ignore = E203,W503 diff --git a/.github/linters/.isort.cfg b/.github/linters/.isort.cfg new file mode 100644 index 0000000..d21b9f2 --- /dev/null +++ b/.github/linters/.isort.cfg @@ -0,0 +1,20 @@ +[settings] +profile= + +; vertical hanging indent mode also used in black configuration +multi_line_output = 3 + +; necessary because black expect the trailing comma +include_trailing_comma = true + +; black compatibility +force_grid_wrap = 0 + +; black compatibility +use_parentheses = True + +; black compatibility +ensure_newline_before_comments = True + +; we chose 120 as line length +line_length = 120 diff --git a/.github/linters/.markdown-lint.yml b/.github/linters/.markdown-lint.yml new file mode 100644 index 0000000..54a2067 --- /dev/null +++ b/.github/linters/.markdown-lint.yml @@ -0,0 +1,38 @@ +--- +########################### +########################### +## Markdown Linter rules ## +########################### +########################### + +# Linter rules doc: +# - https://github.com/DavidAnson/markdownlint +# +# Note: +# To comment out a single error: +# +# any violations you want +# +# + +############### +# Rules by id # +############### +MD004: false # Unordered list style +MD007: + indent: 2 # Unordered list indentation +MD013: + line_length: 808 # Line length +MD024: + allow_different_nesting: true +MD026: + punctuation: ".,;:!。,;:" # List of not allowed +MD029: false # Ordered list item prefix +MD033: false # Allow inline HTML +MD036: false # Emphasis used instead of a heading +MD041: false # Allow file to start without h1 + +################# +# Rules by tags # +################# +blank_lines: false # Error on blank lines diff --git a/.github/linters/.python-black b/.github/linters/.python-black new file mode 100644 index 0000000..aa709bf --- /dev/null +++ b/.github/linters/.python-black @@ -0,0 +1,2 @@ +[tool.black] +line_length = 120 diff --git a/.github/linters/.python-lint b/.github/linters/.python-lint new file mode 100644 index 0000000..0e6ea29 --- /dev/null +++ b/.github/linters/.python-lint @@ -0,0 +1,564 @@ +[MASTER] +errors-only= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. +jobs=1 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + locally-enabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + import-error + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio).You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +# Activate the evaluation score. +score=no + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=optparse.Values,sys.exit + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins=cls + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules=blspy, + chiabip158, + chiapos, + chiavdf, + cryptography, + aiohttp, + websockets, + keyring, + keyrings.cryptfile, + bitstring, + clvm_tools, + setproctitle, + clvm, + colorlog, + concurrent_log_handler, + aiosqlite, + sortedcontainers, + aiter, + miniupnpc, + pytest, + setuptools_scm + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + + +[BASIC] + +# Naming style matching correct argument names +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style +#argument-rgx= + +# Naming style matching correct attribute names +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Naming style matching correct class attribute names +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style +#class-attribute-rgx= + +# Naming style matching correct class names +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming-style +#class-rgx= + +# Naming style matching correct constant names +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma +good-names=i, + j, + k, + ex, + Run, + _ + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# Naming style matching correct inline iteration names +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style +#inlinevar-rgx= + +# Naming style matching correct method names +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style +#method-rgx= + +# Naming style matching correct module names +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style +#variable-rgx= + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma, + dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in a if statement +max-bool-expr=5 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of statements in function / method body +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub, + TERMIOS, + Bastion, + rexec + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/.github/workflows/super-linter.yml b/.github/workflows/super-linter.yml new file mode 100644 index 0000000..9133b8e --- /dev/null +++ b/.github/workflows/super-linter.yml @@ -0,0 +1,80 @@ +--- +########################### +########################### +## Linter GitHub Actions ## +########################### +########################### +name: GithHub Super Linter + +# +# Documentation: +# https://github.com/github/super-linter +# https://help.github.com/en/articles/workflow-syntax-for-github-actions +# + +############################# +# Start the job on all push # +############################# +on: + push: + branches-ignore: + - 'main' + pull_request: + branches-ignore: + - '' + +############### +# Set the Job # +############### +jobs: + build: + # Name the Job + name: Lint Code Base + # Set the agent to run on + runs-on: ubuntu-latest + timeout-minutes: 60 + + ################## + # Load all steps # + ################## + steps: + ########################## + # Checkout the code base # + ########################## + - name: Checkout Code + uses: actions/checkout@v2 + + ################################ + # Run Linter against code base # + ################################ + - name: Lint Code Base + uses: github/super-linter@v4.5.1 +# uses: docker://github/super-linter:v3.10.2 + env: + VALIDATE_ALL_CODEBASE: true + DEFAULT_BRANCH: main + LINTER_RULES_PATH: .github/linters + VALIDATE_BASH: true + VALIDATE_CSS: true + VALIDATE_DOCKER: true + VALIDATE_GO: true + VALIDATE_HTML: true + VALIDATE_JAVASCRIPT_ES: true + VALIDATE_JSON: true + VALIDATE_MD: true + VALIDATE_POWERSHELL: true + VALIDATE_PYTHON: true + VALIDATE_PYTHON_PYLINT: true + VALIDATE_PYTHON_FLAKE8: true + VALIDATE_PYTHON_BLACK: true +# VALIDATE_PYTHON_ISORT: true + VALIDATE_SHELL_SHFMT: true + VALIDATE_TYPESCRIPT_ES: true + VALIDATE_YAML: true + DISABLE_ERRORS: false + PYTHONPATH: ${{ github.workspace }}:$PYTHONPATH + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + FILTER_REGEX_EXCLUDE: .*github/ISSUE_TEMPLATE/config.yml +# ACTIONS_RUNNER_DEBUG: true + +... \ No newline at end of file From c81126d562fba0686ac630e3b4264841d5849ab6 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Thu, 12 Aug 2021 09:17:23 -0700 Subject: [PATCH 25/31] Lint fix --- cdv/cmds/chia_inspect.py | 110 ++++++---------------- cdv/cmds/cli.py | 4 +- cdv/cmds/clsp.py | 29 ++---- cdv/cmds/rpc.py | 89 +++++------------ cdv/cmds/util.py | 4 +- cdv/examples/drivers/piggybank_drivers.py | 4 +- cdv/examples/tests/test_piggybank.py | 37 ++------ cdv/test/__init__.py | 87 +++++------------ cdv/util/load_clvm.py | 20 +--- pyproject.toml | 10 +- setup.py | 10 +- tests/cmds/test_cdv.py | 22 +---- tests/cmds/test_clsp.py | 8 +- tests/cmds/test_inspect.py | 97 +++++-------------- 14 files changed, 139 insertions(+), 392 deletions(-) diff --git a/cdv/cmds/chia_inspect.py b/cdv/cmds/chia_inspect.py index 34df46c..e35720d 100644 --- a/cdv/cmds/chia_inspect.py +++ b/cdv/cmds/chia_inspect.py @@ -102,9 +102,7 @@ def streamable_load(cls: Any, inputs: Iterable[Any]) -> List[Any]: return input_objs -@inspect_cmd.command( - "any", short_help="Attempt to guess the type of the object before inspecting it" -) +@inspect_cmd.command("any", short_help="Attempt to guess the type of the object before inspecting it") @click.argument("objects", nargs=-1, required=False) @click.pass_context def inspect_any_cmd(ctx: click.Context, objects: Tuple[str]): @@ -152,14 +150,10 @@ def inspect_any_cmd(ctx: click.Context, objects: Tuple[str]): print("That's a BLS aggregated signature") -@inspect_cmd.command( - "coins", short_help="Various methods for examining and calculating coin objects" -) +@inspect_cmd.command("coins", short_help="Various methods for examining and calculating coin objects") @click.argument("coins", nargs=-1, required=False) @click.option("-pid", "--parent-id", help="The parent coin's ID") -@click.option( - "-ph", "--puzzle-hash", help="The tree hash of the CLVM puzzle that locks this coin" -) +@click.option("-ph", "--puzzle-hash", help="The tree hash of the CLVM puzzle that locks this coin") @click.option("-a", "--amount", help="The amount of the coin") @click.pass_context def inspect_coin_cmd(ctx: click.Context, coins: Tuple[str], **kwargs): @@ -190,9 +184,7 @@ def do_inspect_coin_cmd( sys.exit(1) if print_results: - inspect_callback( - coin_objs, ctx, id_calc=(lambda e: e.name().hex()), type="Coin" - ) + inspect_callback(coin_objs, ctx, id_calc=(lambda e: e.name().hex()), type="Coin") return coin_objs @@ -210,9 +202,7 @@ def do_inspect_coin_cmd( help="The tree hash of the CLVM puzzle that locks the coin being spent", ) @click.option("-a", "--amount", help="The amount of the coin being spent") -@click.option( - "-pr", "--puzzle-reveal", help="The program that is hashed into this coin" -) +@click.option("-pr", "--puzzle-reveal", help="The program that is hashed into this coin") @click.option("-s", "--solution", help="The attempted solution to the puzzle") @click.option("-ec", "--cost", is_flag=True, help="Print the CLVM cost of the spend") @click.option( @@ -241,9 +231,7 @@ def do_inspect_coin_spend_cmd( del kwargs["cost"] del kwargs["cost_per_byte"] if kwargs and all([kwargs["puzzle_reveal"], kwargs["solution"]]): - if (not kwargs["coin"]) and all( - [kwargs["parent_id"], kwargs["puzzle_hash"], kwargs["amount"]] - ): + if (not kwargs["coin"]) and all([kwargs["parent_id"], kwargs["puzzle_hash"], kwargs["amount"]]): coin_spend_objs: List[CoinSpend] = [ CoinSpend( Coin( @@ -284,15 +272,11 @@ def do_inspect_coin_spend_cmd( ) if cost_flag: for coin_spend in coin_spend_objs: - program: BlockGenerator = simple_solution_generator( - SpendBundle([coin_spend], G2Element()) - ) + program: BlockGenerator = simple_solution_generator(SpendBundle([coin_spend], G2Element())) npc_result: NPCResult = get_name_puzzle_conditions( program, INFINITE_COST, cost_per_byte=cost_per_byte, safe_mode=True ) - cost: int = calculate_cost_of_program( - program.program, npc_result, cost_per_byte - ) + cost: int = calculate_cost_of_program(program.program, npc_result, cost_per_byte) print(f"Cost: {cost}") return coin_spend_objs @@ -303,18 +287,14 @@ def do_inspect_coin_spend_cmd( short_help="Various methods for examining and calculating SpendBundle objects", ) @click.argument("bundles", nargs=-1, required=False) -@click.option( - "-s", "--spend", multiple=True, help="A coin spend object to add to the bundle" -) +@click.option("-s", "--spend", multiple=True, help="A coin spend object to add to the bundle") @click.option( "-as", "--aggsig", multiple=True, help="A BLS signature to aggregate into the bundle (can be used more than once)", ) -@click.option( - "-db", "--debug", is_flag=True, help="Show debugging information about the bundles" -) +@click.option("-db", "--debug", is_flag=True, help="Show debugging information about the bundles") @click.option( "-sd", "--signable_data", @@ -328,9 +308,7 @@ def do_inspect_coin_spend_cmd( show_default=True, help="The network this spend bundle will be pushed to (for AGG_SIG_ME)", ) -@click.option( - "-ec", "--cost", is_flag=True, help="Print the CLVM cost of the entire bundle" -) +@click.option("-ec", "--cost", is_flag=True, help="Print the CLVM cost of the entire bundle") @click.option( "-bc", "--cost-per-byte", @@ -351,9 +329,7 @@ def do_inspect_spend_bundle_cmd( ) -> List[SpendBundle]: if kwargs and (len(kwargs["spend"]) > 0): if len(kwargs["aggsig"]) > 0: - sig: G2Element = AugSchemeMPL.aggregate( - [G2Element(hexstr_to_bytes(sig)) for sig in kwargs["aggsig"]] - ) + sig: G2Element = AugSchemeMPL.aggregate([G2Element(hexstr_to_bytes(sig)) for sig in kwargs["aggsig"]]) else: sig = G2Element() spend_bundle_objs: List[SpendBundle] = [ @@ -385,9 +361,7 @@ def do_inspect_spend_bundle_cmd( cost_per_byte=kwargs["cost_per_byte"], safe_mode=True, ) - cost: int = calculate_cost_of_program( - program.program, npc_result, kwargs["cost_per_byte"] - ) + cost: int = calculate_cost_of_program(program.program, npc_result, kwargs["cost_per_byte"]) print(f"Cost: {cost}") if kwargs["debug"]: print("") @@ -406,17 +380,15 @@ def do_inspect_spend_bundle_cmd( coin_spend.puzzle_reveal, coin_spend.solution, INFINITE_COST ) if err or conditions_dict is None: - print( - f"Generating conditions failed, con:{conditions_dict}, error: {err}" - ) + print(f"Generating conditions failed, con:{conditions_dict}, error: {err}") else: from chia.util.default_root import DEFAULT_ROOT_PATH from chia.util.config import load_config config: Dict = load_config(DEFAULT_ROOT_PATH, "config.yaml") - genesis_challenge: str = config["network_overrides"][ - "constants" - ][kwargs["network"]]["GENESIS_CHALLENGE"] + genesis_challenge: str = config["network_overrides"]["constants"][kwargs["network"]][ + "GENESIS_CHALLENGE" + ] for pk, msg in pkm_pairs_for_conditions_dict( conditions_dict, coin_spend.coin.name(), @@ -484,9 +456,7 @@ def do_inspect_coin_record_cmd( **kwargs, ) -> List[CoinRecord]: if kwargs and all([kwargs["confirmed_block_index"], kwargs["timestamp"]]): - if (not kwargs["coin"]) and all( - [kwargs["parent_id"], kwargs["puzzle_hash"], kwargs["amount"]] - ): + if (not kwargs["coin"]) and all([kwargs["parent_id"], kwargs["puzzle_hash"], kwargs["amount"]]): coin_record_objs: List[CoinRecord] = [ CoinRecord( Coin( @@ -534,9 +504,7 @@ def do_inspect_coin_record_cmd( return coin_record_objs -@inspect_cmd.command( - "programs", short_help="Various methods for examining CLVM Program objects" -) +@inspect_cmd.command("programs", short_help="Various methods for examining CLVM Program objects") @click.argument("programs", nargs=-1, required=False) @click.pass_context def inspect_program_cmd(ctx: click.Context, programs: Tuple[str], **kwargs): @@ -566,16 +534,10 @@ def do_inspect_program_cmd( return program_objs -@inspect_cmd.command( - "keys", short_help="Various methods for examining and generating BLS Keys" -) +@inspect_cmd.command("keys", short_help="Various methods for examining and generating BLS Keys") @click.option("-pk", "--public-key", help="A BLS public key") -@click.option( - "-sk", "--secret-key", help="The secret key from which to derive the public key" -) -@click.option( - "-m", "--mnemonic", help="A 24 word mnemonic from which to derive the secret key" -) +@click.option("-sk", "--secret-key", help="The secret key from which to derive the public key") +@click.option("-m", "--mnemonic", help="A 24 word mnemonic from which to derive the secret key") @click.option( "-pw", "--passphrase", @@ -584,9 +546,7 @@ def do_inspect_program_cmd( help="A passphrase to use when deriving a secret key from mnemonic", ) @click.option("-r", "--random", is_flag=True, help="Generate a random set of keys") -@click.option( - "-hd", "--hd-path", help="Enter the HD path in the form 'm/12381/8444/n/n'" -) +@click.option("-hd", "--hd-path", help="Enter the HD path in the form 'm/12381/8444/n/n'") @click.option( "-t", "--key-type", @@ -645,16 +605,12 @@ def one_or_zero(value): sk = AugSchemeMPL.key_gen(seed) pk = sk.get_g1() elif kwargs["random"]: - sk = AugSchemeMPL.key_gen( - mnemonic_to_seed(bytes_to_mnemonic(token_bytes(32)), "") - ) + sk = AugSchemeMPL.key_gen(mnemonic_to_seed(bytes_to_mnemonic(token_bytes(32)), "")) pk = sk.get_g1() list_path: List[int] = [] if kwargs["hd_path"] and (kwargs["hd_path"] != "m"): - list_path = [ - uint32(int(i)) for i in kwargs["hd_path"].split("/") if i != "m" - ] + list_path = [uint32(int(i)) for i in kwargs["hd_path"].split("/") if i != "m"] elif kwargs["key_type"]: case = kwargs["key_type"] if case == "farmer": @@ -678,12 +634,8 @@ def one_or_zero(value): if kwargs["synthetic"]: if sk: - sk = calculate_synthetic_secret_key( - sk, hexstr_to_bytes(kwargs["hidden_puzhash"]) - ) - pk = calculate_synthetic_public_key( - pk, hexstr_to_bytes(kwargs["hidden_puzhash"]) - ) + sk = calculate_synthetic_secret_key(sk, hexstr_to_bytes(kwargs["hidden_puzhash"])) + pk = calculate_synthetic_public_key(pk, hexstr_to_bytes(kwargs["hidden_puzhash"])) else: print("Invalid arguments specified.") @@ -714,9 +666,7 @@ def parse_args(self, ctx, args): cls=OrderedParamsCommand, short_help="Various methods for examining and creating BLS aggregated signatures", ) -@click.option( - "-sk", "--secret-key", multiple=True, help="A secret key to sign a message with" -) +@click.option("-sk", "--secret-key", multiple=True, help="A secret key to sign a message with") @click.option( "-t", "--utf-8", @@ -735,9 +685,7 @@ def inspect_sigs_cmd(ctx: click.Context, **kwargs): do_inspect_sigs_cmd(ctx, **kwargs) -def do_inspect_sigs_cmd( - ctx: click.Context, print_results: bool = True, **kwargs -) -> G2Element: +def do_inspect_sigs_cmd(ctx: click.Context, print_results: bool = True, **kwargs) -> G2Element: base = G2Element() sk: Optional[PrivateKey] = None for param, value in OrderedParamsCommand._options: diff --git a/cdv/cmds/cli.py b/cdv/cmds/cli.py index 1b57305..3c7cf8f 100644 --- a/cdv/cmds/cli.py +++ b/cdv/cmds/cli.py @@ -80,9 +80,7 @@ def test_cmd(tests: str, discover: bool, init: str): pytest.main([*test_paths]) -@cli.command( - "hash", short_help="SHA256 hash UTF-8 strings or bytes (use 0x prefix for bytes)" -) +@cli.command("hash", short_help="SHA256 hash UTF-8 strings or bytes (use 0x prefix for bytes)") @click.argument("data", nargs=1, required=True) def hash_cmd(data: str): if data[:2] == "0x": diff --git a/cdv/cmds/clsp.py b/cdv/cmds/clsp.py index c8abeaf..3338ed8 100644 --- a/cdv/cmds/clsp.py +++ b/cdv/cmds/clsp.py @@ -44,33 +44,24 @@ def build_cmd(files: Tuple[str], include: Tuple[str]) -> None: for filename in clvm_files: hex_file_name: str = filename.name + ".hex" full_hex_file_name = Path(filename.parent).joinpath(hex_file_name) - if not ( - full_hex_file_name.exists() - and full_hex_file_name.stat().st_mtime > filename.stat().st_mtime - ): + if not (full_hex_file_name.exists() and full_hex_file_name.stat().st_mtime > filename.stat().st_mtime): outfile = str(filename) + ".hex" try: print("Beginning compilation of " + filename.name + "...") - compile_clvm( - str(filename), outfile, search_paths=append_include(include) - ) + compile_clvm(str(filename), outfile, search_paths=append_include(include)) print("...Compilation finished") except Exception as e: print("Couldn't build " + filename.name + ": " + str(e)) -@clsp_cmd.command( - "disassemble", short_help="Disassemble serialized clvm into human readable form." -) +@clsp_cmd.command("disassemble", short_help="Disassemble serialized clvm into human readable form.") @click.argument("programs", nargs=-1, required=True) def disassemble_cmd(programs: Tuple[str]): for program in programs: print(disassemble(parse_program(program))) -@clsp_cmd.command( - "treehash", short_help="Return the tree hash of a clvm file or string" -) +@clsp_cmd.command("treehash", short_help="Return the tree hash of a clvm file or string") @click.argument("program", nargs=1, required=True) @click.option( "-i", @@ -91,9 +82,7 @@ def treehash_cmd(program: str, include: Tuple[str]): multiple=True, help="An argument to be curried in (i.e. -a 0xdeadbeef -a '(a 2 3)')", ) -@click.option( - "-H", "--treehash", is_flag=True, help="Output the tree hash of the curried puzzle" -) +@click.option("-H", "--treehash", is_flag=True, help="Output the tree hash of the curried puzzle") @click.option( "-x", "--dump", @@ -107,9 +96,7 @@ def treehash_cmd(program: str, include: Tuple[str]): multiple=True, help="Paths to search for include files (./include will be searched automatically)", ) -def curry_cmd( - program: str, args: Tuple[str], treehash: bool, dump: bool, include: Tuple[str] -): +def curry_cmd(program: str, args: Tuple[str], treehash: bool, dump: bool, include: Tuple[str]): prog: Program = parse_program(program, include) curry_args: List[Program] = [assemble(arg) for arg in args] @@ -140,6 +127,4 @@ def retrieve_cmd(libraries: Tuple[str]): if src_path.exists(): shutil.copyfile(src_path, include_path.joinpath(f"{lib}.clib")) else: - print( - f"Could not find {lib}.clib. You can always create it in ./include yourself." - ) + print(f"Could not find {lib}.clib. You can always create it in ./include yourself.") diff --git a/cdv/cmds/rpc.py b/cdv/cmds/rpc.py index e5d086f..062ed22 100644 --- a/cdv/cmds/rpc.py +++ b/cdv/cmds/rpc.py @@ -39,17 +39,13 @@ async def get_client() -> Optional[FullNodeRpcClient]: return full_node_client except Exception as e: if isinstance(e, aiohttp.ClientConnectorError): - pprint( - f"Connection error. Check if full node is running at {full_node_rpc_port}" - ) + pprint(f"Connection error. Check if full node is running at {full_node_rpc_port}") else: pprint(f"Exception from 'harvester' {e}") return None -@rpc_cmd.command( - "state", short_help="Gets the status of the blockchain (get_blockchain_state)" -) +@rpc_cmd.command("state", short_help="Gets the status of the blockchain (get_blockchain_state)") def rpc_state_cmd(): async def do_command(): try: @@ -73,9 +69,7 @@ async def do_command(): try: node_client: FullNodeRpcClient = await get_client() if header_hash: - blocks: List[FullBlock] = [ - await node_client.get_block(hexstr_to_bytes(header_hash)) - ] + blocks: List[FullBlock] = [await node_client.get_block(hexstr_to_bytes(header_hash))] elif start and end: blocks: List[FullBlock] = await node_client.get_all_block(start, end) else: @@ -102,19 +96,11 @@ async def do_command(): try: node_client: FullNodeRpcClient = await get_client() if header_hash: - block_record: BlockRecord = await node_client.get_block_record( - hexstr_to_bytes(header_hash) - ) - block_records: List = ( - block_record.to_json_dict() if block_record else [] - ) + block_record: BlockRecord = await node_client.get_block_record(hexstr_to_bytes(header_hash)) + block_records: List = block_record.to_json_dict() if block_record else [] elif height: - block_record: BlockRecord = ( - await node_client.get_block_record_by_height(height) - ) - block_records: List = ( - block_record.to_json_dict() if block_record else [] - ) + block_record: BlockRecord = await node_client.get_block_record_by_height(height) + block_records: List = block_record.to_json_dict() if block_record else [] elif start and end: block_records: List = await node_client.get_block_records(start, end) else: @@ -135,9 +121,7 @@ def rpc_unfinished_cmd(): async def do_command(): try: node_client: FullNodeRpcClient = await get_client() - header_blocks: List[ - UnfinishedHeaderBlock - ] = await node_client.get_unfinished_block_headers() + header_blocks: List[UnfinishedHeaderBlock] = await node_client.get_unfinished_block_headers() pprint([block.to_json_dict() for block in header_blocks]) finally: node_client.close() @@ -163,20 +147,14 @@ async def do_command(): pprint("Invalid arguments specified.") else: if start: - start_hash: bytes32 = ( - await node_client.get_block_record_by_height(start) - ).header_hash + start_hash: bytes32 = (await node_client.get_block_record_by_height(start)).header_hash elif older: start_hash: bytes32 = hexstr_to_bytes(older) else: - start_hash: bytes32 = ( - await node_client.get_block_record_by_height(0) - ).header_hash + start_hash: bytes32 = (await node_client.get_block_record_by_height(0)).header_hash if end: - end_hash: bytes32 = ( - await node_client.get_block_record_by_height(end) - ).header_hash + end_hash: bytes32 = (await node_client.get_block_record_by_height(end)).header_hash elif newer: end_hash: bytes32 = hexstr_to_bytes(newer) else: @@ -186,9 +164,7 @@ async def do_command(): ) ).header_hash - netspace: Optional[uint64] = await node_client.get_network_space( - start_hash, end_hash - ) + netspace: Optional[uint64] = await node_client.get_network_space(start_hash, end_hash) if netspace: pprint(format_bytes(netspace)) else: @@ -210,9 +186,7 @@ def rpc_addrem_cmd(headerhash: str): async def do_command(): try: node_client: FullNodeRpcClient = await get_client() - additions, removals = await node_client.get_additions_and_removals( - bytes.fromhex(headerhash) - ) + additions, removals = await node_client.get_additions_and_removals(bytes.fromhex(headerhash)) additions: List[Dict] = [rec.to_json_dict() for rec in additions] removals: List[Dict] = [rec.to_json_dict() for rec in removals] pprint({"additions": additions, "removals": removals}) @@ -227,9 +201,7 @@ async def do_command(): "blockspends", short_help="Gets the puzzle and solution for a coin spent at the specified block height (get_puzzle_and_solution)", ) -@click.option( - "-id", "--coinid", required=True, help="The id of the coin that was spent" -) +@click.option("-id", "--coinid", required=True, help="The id of the coin that was spent") @click.option( "-h", "--block-height", @@ -258,9 +230,7 @@ def rpc_pushtx_cmd(spendbundles: Tuple[str]): async def do_command(): try: node_client: FullNodeRpcClient = await get_client() - for bundle in do_inspect_spend_bundle_cmd( - fake_context(), spendbundles, print_results=False - ): + for bundle in do_inspect_spend_bundle_cmd(fake_context(), spendbundles, print_results=False): try: result: Dict = await node_client.push_tx(bundle) pprint(result) @@ -282,18 +252,14 @@ async def do_command(): "--transaction-id", help="The ID of a spend bundle that is sitting in the mempool", ) -@click.option( - "--ids-only", is_flag=True, help="Only show the IDs of the retrieved spend bundles" -) +@click.option("--ids-only", is_flag=True, help="Only show the IDs of the retrieved spend bundles") def rpc_mempool_cmd(transaction_id: str, ids_only: bool): async def do_command(): try: node_client: FullNodeRpcClient = await get_client() if transaction_id: items = {} - items[transaction_id] = await node_client.get_mempool_item_by_tx_id( - hexstr_to_bytes(transaction_id) - ) + items[transaction_id] = await node_client.get_mempool_item_by_tx_id(hexstr_to_bytes(transaction_id)) else: b_items: Dict = await node_client.get_all_mempool_items() items = {} @@ -338,33 +304,26 @@ async def do_command(): clean_values: bytes32 = map(lambda hex: hexstr_to_bytes(hex), values) if by in ["name", "id"]: coin_records: List[CoinRecord] = [ - await node_client.get_coin_record_by_name(value) - for value in clean_values + await node_client.get_coin_record_by_name(value) for value in clean_values ] if not kwargs["include_spent_coins"]: - coin_records: List[CoinRecord] = list( - filter(lambda record: record.spent is False, coin_records) - ) + coin_records: List[CoinRecord] = list(filter(lambda record: record.spent is False, coin_records)) if kwargs["start_height"] is not None: coin_records: List[CoinRecord] = list( filter( - lambda record: record.confirmed_block_index - >= kwargs["start_height"], + lambda record: record.confirmed_block_index >= kwargs["start_height"], coin_records, ) ) if kwargs["end_height"] is not None: coin_records: List[CoinRecord] = list( filter( - lambda record: record.confirmed_block_index - < kwargs["end_height"], + lambda record: record.confirmed_block_index < kwargs["end_height"], coin_records, ) ) elif by in ["puzhash", "puzzle_hash", "puzzlehash"]: - coin_records: List[ - CoinRecord - ] = await node_client.get_coin_records_by_puzzle_hashes( + coin_records: List[CoinRecord] = await node_client.get_coin_records_by_puzzle_hashes( clean_values, **kwargs ) elif by in [ @@ -375,9 +334,7 @@ async def do_command(): "parentinfo", "parent", ]: - coin_records: List[ - CoinRecord - ] = await node_client.get_coin_records_by_parent_ids( + coin_records: List[CoinRecord] = await node_client.get_coin_records_by_parent_ids( clean_values, **kwargs ) diff --git a/cdv/cmds/util.py b/cdv/cmds/util.py index 4abf616..46f0daa 100644 --- a/cdv/cmds/util.py +++ b/cdv/cmds/util.py @@ -38,9 +38,7 @@ def parse_program(program: Union[str, Program], include: Iterable = []) -> Progr if "(" in filestring: # TODO: This should probably be more robust if re.compile(r"\(mod\s").search(filestring): - prog = Program.to( - compile_clvm_text(filestring, append_include(include)) - ) + prog = Program.to(compile_clvm_text(filestring, append_include(include))) else: prog = Program.to(assemble(filestring)) else: diff --git a/cdv/examples/drivers/piggybank_drivers.py b/cdv/examples/drivers/piggybank_drivers.py index f2ebaeb..e45b5cb 100644 --- a/cdv/examples/drivers/piggybank_drivers.py +++ b/cdv/examples/drivers/piggybank_drivers.py @@ -21,9 +21,7 @@ def create_piggybank_puzzle(amount: uint64, cash_out_puzhash: bytes32) -> Progra # Generate a solution to contribute to a piggybank def solution_for_piggybank(pb_coin: Coin, contrib_amount: uint64) -> Program: - return Program.to( - [pb_coin.puzzle_hash, pb_coin.amount, (pb_coin.amount + contrib_amount)] - ) + return Program.to([pb_coin.puzzle_hash, pb_coin.amount, (pb_coin.amount + contrib_amount)]) # Return the condition to assert the announcement diff --git a/cdv/examples/tests/test_piggybank.py b/cdv/examples/tests/test_piggybank.py index 419db4d..fc8b316 100644 --- a/cdv/examples/tests/test_piggybank.py +++ b/cdv/examples/tests/test_piggybank.py @@ -23,9 +23,7 @@ async def setup(self): network, alice, bob = await setup_test() yield network, alice, bob - async def make_and_spend_piggybank( - self, network, alice, bob, CONTRIBUTION_AMOUNT - ) -> Dict[str, List[Coin]]: + async def make_and_spend_piggybank(self, network, alice, bob, CONTRIBUTION_AMOUNT) -> Dict[str, List[Coin]]: # Get our alice wallet some money await network.farm_block(farmer=alice) @@ -34,9 +32,7 @@ async def make_and_spend_piggybank( create_piggybank_puzzle(uint64(1000000000000), bob.puzzle_hash) ) # This retrieves us a coin that is at least 500 mojos. - contribution_coin: Optional[CoinWrapper] = await alice.choose_coin( - CONTRIBUTION_AMOUNT - ) + contribution_coin: Optional[CoinWrapper] = await alice.choose_coin(CONTRIBUTION_AMOUNT) # Make sure everything succeeded if not piggybank_coin or not contribution_coin: @@ -59,9 +55,7 @@ async def make_and_spend_piggybank( contribution_coin.puzzle_hash, (contribution_coin.amount - CONTRIBUTION_AMOUNT), ], - piggybank_announcement_assertion( - piggybank_coin.as_coin(), CONTRIBUTION_AMOUNT - ), + piggybank_announcement_assertion(piggybank_coin.as_coin(), CONTRIBUTION_AMOUNT), ], ) @@ -75,9 +69,7 @@ async def make_and_spend_piggybank( async def test_piggybank_contribution(self, setup): network, alice, bob = setup try: - result: Dict[str, List[Coin]] = await self.make_and_spend_piggybank( - network, alice, bob, 500 - ) + result: Dict[str, List[Coin]] = await self.make_and_spend_piggybank(network, alice, bob, 500) assert "error" not in result @@ -85,10 +77,7 @@ async def test_piggybank_contribution(self, setup): filter( lambda addition: (addition.amount == 501) and ( - addition.puzzle_hash - == create_piggybank_puzzle( - 1000000000000, bob.puzzle_hash - ).get_tree_hash() + addition.puzzle_hash == create_piggybank_puzzle(1000000000000, bob.puzzle_hash).get_tree_hash() ), result["additions"], ) @@ -101,9 +90,7 @@ async def test_piggybank_contribution(self, setup): async def test_piggybank_completion(self, setup): network, alice, bob = setup try: - result: Dict[str, List[Coin]] = await self.make_and_spend_piggybank( - network, alice, bob, 1000000000000 - ) + result: Dict[str, List[Coin]] = await self.make_and_spend_piggybank(network, alice, bob, 1000000000000) assert "error" not in result @@ -111,10 +98,7 @@ async def test_piggybank_completion(self, setup): filter( lambda addition: (addition.amount == 0) and ( - addition.puzzle_hash - == create_piggybank_puzzle( - 1000000000000, bob.puzzle_hash - ).get_tree_hash() + addition.puzzle_hash == create_piggybank_puzzle(1000000000000, bob.puzzle_hash).get_tree_hash() ), result["additions"], ) @@ -123,8 +107,7 @@ async def test_piggybank_completion(self, setup): filtered_result: List[Coin] = list( filter( - lambda addition: (addition.amount == 1000000000001) - and (addition.puzzle_hash == bob.puzzle_hash), + lambda addition: (addition.amount == 1000000000001) and (addition.puzzle_hash == bob.puzzle_hash), result["additions"], ) ) @@ -136,9 +119,7 @@ async def test_piggybank_completion(self, setup): async def test_piggybank_stealing(self, setup): network, alice, bob = setup try: - result: Dict[str, List[Coin]] = await self.make_and_spend_piggybank( - network, alice, bob, -100 - ) + result: Dict[str, List[Coin]] = await self.make_and_spend_piggybank(network, alice, bob, -100) assert "error" in result assert "GENERATOR_RUNTIME_ERROR" in result["error"] finally: diff --git a/cdv/test/__init__.py b/cdv/test/__init__.py index 3431aa2..b968a7e 100644 --- a/cdv/test/__init__.py +++ b/cdv/test/__init__.py @@ -56,9 +56,7 @@ def find_standard_coins(self, puzzle_hash: bytes32) -> List[Coin]: class CoinWrapper(Coin): """A class that provides some useful methods on coins.""" - def __init__( - self, parent: Coin, puzzle_hash: bytes32, amt: uint64, source: Program - ): + def __init__(self, parent: Coin, puzzle_hash: bytes32, amt: uint64, source: Program): """Given parent, puzzle_hash and amount, give an object representing the coin""" super().__init__(parent, puzzle_hash, amt) self.source = source @@ -104,11 +102,7 @@ def create_standard_spend(self, priv: PrivateKey, conditions: List[List]): # Create a signature for each of these. We'll aggregate them at the end. signature: G2Element = AugSchemeMPL.sign( calculate_synthetic_secret_key(priv, DEFAULT_HIDDEN_PUZZLE_HASH), - ( - delegated_puzzle_solution.get_tree_hash() - + self.name() - + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA - ), + (delegated_puzzle_solution.get_tree_hash() + self.name() + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA), ) return coin_solution_object, signature @@ -177,10 +171,7 @@ def process_coin_for_combine_search(self, coin: Coin): while ( (len(self.max_coins) > 0) and (self.total - self.max_coins[-1].amount >= self.target) - and ( - (self.total - self.max_coins[-1].amount > 0) - or (len(self.max_coins) > 1) - ) + and ((self.total - self.max_coins[-1].amount > 0) or (len(self.max_coins) > 1)) ): self.total = uint64(self.total - self.max_coins[-1].amount) self.max_coins = self.max_coins[:-1] @@ -212,18 +203,14 @@ def __init__(self, parent: "Network", name: str, pk: G1Element, priv: PrivateKey self.puzzle: Program = puzzle_for_pk(self.pk()) self.puzzle_hash: bytes32 = self.puzzle.get_tree_hash() - synth_sk: PrivateKey = calculate_synthetic_secret_key( - self.sk_, DEFAULT_HIDDEN_PUZZLE_HASH - ) + synth_sk: PrivateKey = calculate_synthetic_secret_key(self.sk_, DEFAULT_HIDDEN_PUZZLE_HASH) self.pk_to_sk_dict: Dict[str, PrivateKey] = { str(self.pk_): self.sk_, str(synth_sk.get_g1()): synth_sk, } def __repr__(self) -> str: - return ( - f"" - ) + return f"" # Make this coin available to the user it goes with. def add_coin(self, coin: Coin): @@ -342,9 +329,7 @@ async def combine_coins(self, coins: List[CoinWrapper]) -> Optional[SpendResult] ] ] - coin_solution, signature = c.create_standard_spend( - self.sk_, announce_conditions - ) + coin_solution, signature = c.create_standard_spend(self.sk_, announce_conditions) destroyed_coin_solutions.append(coin_solution) signatures.append(signature) @@ -353,18 +338,14 @@ async def combine_coins(self, coins: List[CoinWrapper]) -> Optional[SpendResult] [ConditionOpcode.CREATE_COIN, self.puzzle_hash, final_coin.amount], ] - coin_solution, signature = coins[-1].create_standard_spend( - self.sk_, final_coin_creation - ) + coin_solution, signature = coins[-1].create_standard_spend(self.sk_, final_coin_creation) destroyed_coin_solutions.append(coin_solution) signatures.append(signature) signature = AugSchemeMPL.aggregate(signatures) spend_bundle = SpendBundle(destroyed_coin_solutions, signature) - pushed: Dict[str, Union[str, List[Coin]]] = await self.parent.push_tx( - spend_bundle - ) + pushed: Dict[str, Union[str, List[Coin]]] = await self.parent.push_tx(spend_bundle) # We should have the same amount of money. assert beginning_balance == self.balance() @@ -378,9 +359,7 @@ async def combine_coins(self, coins: List[CoinWrapper]) -> Optional[SpendResult] async def choose_coin(self, amt) -> Optional[CoinWrapper]: """Given an amount requirement, find a coin that contains at least that much chia""" start_balance: uint64 = self.balance() - coins_to_spend: Optional[List[Coin]] = self.compute_combine_action( - amt, [], dict(self.usable_coins) - ) + coins_to_spend: Optional[List[Coin]] = self.compute_combine_action(amt, [], dict(self.usable_coins)) # Couldn't find a working combination. if coins_to_spend is None: @@ -400,9 +379,7 @@ async def choose_coin(self, amt) -> Optional[CoinWrapper]: result: Optional[SpendResult] = await self.combine_coins( list( map( - lambda x: CoinWrapper( - x.parent_coin_info, x.puzzle_hash, x.amount, self.puzzle - ), + lambda x: CoinWrapper(x.parent_coin_info, x.puzzle_hash, x.amount, self.puzzle), coins_to_spend, ) ) @@ -419,9 +396,7 @@ async def choose_coin(self, amt) -> Optional[CoinWrapper]: # - allow use of more than one coin to launch smart coin # - ensure input chia = output chia. it'd be dumb to just allow somebody # to lose their chia without telling them. - async def launch_smart_coin( - self, source: Program, **kwargs - ) -> Optional[CoinWrapper]: + async def launch_smart_coin(self, source: Program, **kwargs) -> Optional[CoinWrapper]: """Create a new smart coin based on a parent coin and return the smart coin's living coin to the user or None if the spend failed.""" amt = uint64(1) @@ -438,9 +413,7 @@ async def launch_smart_coin( [ConditionOpcode.CREATE_COIN, cw.puzzle_hash(), amt], ] if amt < found_coin.amount: - condition_args.append( - [ConditionOpcode.CREATE_COIN, self.puzzle_hash, found_coin.amount - amt] - ) + condition_args.append([ConditionOpcode.CREATE_COIN, self.puzzle_hash, found_coin.amount - amt]) delegated_puzzle_solution = Program.to((1, condition_args)) solution = Program.to([[], delegated_puzzle_solution, []]) @@ -465,9 +438,7 @@ async def launch_smart_coin( ], signature, ) - pushed: Dict[str, Union[str, List[Coin]]] = await self.parent.push_tx( - spend_bundle - ) + pushed: Dict[str, Union[str, List[Coin]]] = await self.parent.push_tx(spend_bundle) if "error" not in pushed: return cw.custom_coin(found_coin, amt) else: @@ -496,9 +467,7 @@ def balance(self) -> uint64: # Automatically takes care of signing, etc. # Result is an object representing the actions taken when the block # with this transaction was farmed. - async def spend_coin( - self, coin: CoinWrapper, pushtx: bool = True, **kwargs - ) -> Union[SpendResult, SpendBundle]: + async def spend_coin(self, coin: CoinWrapper, pushtx: bool = True, **kwargs) -> Union[SpendResult, SpendBundle]: """Given a coin object, invoke it on the blockchain, either as a standard coin if no arguments are given or with custom arguments in args=""" amt = uint64(1) @@ -514,9 +483,7 @@ async def spend_coin( # Automatic arguments from the user's intention. if "custom_conditions" not in kwargs: - solution_list: List[List] = [ - [ConditionOpcode.CREATE_COIN, target_puzzle_hash, amt] - ] + solution_list: List[List] = [[ConditionOpcode.CREATE_COIN, target_puzzle_hash, amt]] else: solution_list = kwargs["custom_conditions"] if "remain" in kwargs: @@ -531,9 +498,7 @@ async def spend_coin( ] ) elif isinstance(remainer, Wallet): - solution_list.append( - [ConditionOpcode.CREATE_COIN, remainer.puzzle_hash, remain_amt] - ) + solution_list.append([ConditionOpcode.CREATE_COIN, remainer.puzzle_hash, remain_amt]) else: raise ValueError("remainer is not a wallet or a smart coin") @@ -567,9 +532,7 @@ async def spend_coin( ) if pushtx: - pushed: Dict[str, Union[str, List[Coin]]] = await self.parent.push_tx( - spend_bundle - ) + pushed: Dict[str, Union[str, List[Coin]]] = await self.parent.push_tx(spend_bundle) return SpendResult(pushed) else: return spend_bundle @@ -589,9 +552,7 @@ class Network: @classmethod async def create(cls) -> "Network": self = cls() - self.time = datetime.timedelta( - days=18750, seconds=61201 - ) # Past the initial transaction freeze + self.time = datetime.timedelta(days=18750, seconds=61201) # Past the initial transaction freeze self.sim = await SpendSim.create() self.sim_client = SimClient(self.sim) self.wallets = {} @@ -614,17 +575,13 @@ async def farm_block(self, **kwargs) -> Tuple[List[Coin], List[Coin]]: farmer = kwargs["farmer"] farm_duration = datetime.timedelta(block_time) - farmed: Tuple[List[Coin], List[Coin]] = await self.sim.farm_block( - farmer.puzzle_hash - ) + farmed: Tuple[List[Coin], List[Coin]] = await self.sim.farm_block(farmer.puzzle_hash) for k, w in self.wallets.items(): w._clear_coins() for kw, w in self.wallets.items(): - coin_records: List[ - CoinRecord - ] = await self.sim_client.get_coin_records_by_puzzle_hash(w.puzzle_hash) + coin_records: List[CoinRecord] = await self.sim_client.get_coin_records_by_puzzle_hash(w.puzzle_hash) for coin_record in coin_records: if coin_record.spent is False: w.add_coin(CoinWrapper.from_coin(coin_record.coin, w.puzzle)) @@ -654,9 +611,7 @@ def make_wallet(self, name: str) -> Wallet: async def skip_time(self, target_duration: str, **kwargs): """Skip a duration of simulated time, causing blocks to be farmed. If a farmer is specified, they win each block""" - target_time = self.time + datetime.timedelta( - pytimeparse.parse(target_duration) / duration_div - ) + target_time = self.time + datetime.timedelta(pytimeparse.parse(target_duration) / duration_div) while target_time > self.get_timestamp(): await self.farm_block(**kwargs) self.sim.pass_time(uint64(20)) diff --git a/cdv/util/load_clvm.py b/cdv/util/load_clvm.py index 1eca87b..9af2b98 100644 --- a/cdv/util/load_clvm.py +++ b/cdv/util/load_clvm.py @@ -6,9 +6,7 @@ from chia.types.blockchain_format.program import Program, SerializedProgram -def load_serialized_clvm( - clvm_filename, package_or_requirement=__name__ -) -> SerializedProgram: +def load_serialized_clvm(clvm_filename, package_or_requirement=__name__) -> SerializedProgram: """ This function takes a .clvm file in the given package and compiles it to a .clvm.hex file if the .hex file is missing or older than the .clvm file, then @@ -22,9 +20,7 @@ def load_serialized_clvm( try: if pkg_resources.resource_exists(package_or_requirement, clvm_filename): - full_path = pathlib.Path( - pkg_resources.resource_filename(package_or_requirement, clvm_filename) - ) + full_path = pathlib.Path(pkg_resources.resource_filename(package_or_requirement, clvm_filename)) output = full_path.parent / hex_filename compile_clvm( full_path, @@ -36,18 +32,10 @@ def load_serialized_clvm( # so we just fall through to loading the hex clvm pass - clvm_hex = pkg_resources.resource_string( - package_or_requirement, hex_filename - ).decode("utf8") + clvm_hex = pkg_resources.resource_string(package_or_requirement, hex_filename).decode("utf8") clvm_blob = bytes.fromhex(clvm_hex) return SerializedProgram.from_bytes(clvm_blob) def load_clvm(clvm_filename, package_or_requirement=__name__) -> Program: - return Program.from_bytes( - bytes( - load_serialized_clvm( - clvm_filename, package_or_requirement=package_or_requirement - ) - ) - ) + return Program.from_bytes(bytes(load_serialized_clvm(clvm_filename, package_or_requirement=package_or_requirement))) diff --git a/pyproject.toml b/pyproject.toml index b5a3c46..aa29f9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,4 +3,12 @@ requires = [ "setuptools>=42", "wheel" ] -build-backend = "setuptools.build_meta" \ No newline at end of file +build-backend = "setuptools.build_meta" + +[tool.black] +line-length = 120 + +[flake8] +max-line-length = 120 +exclude = "./typings/**/*" +ignore = "E203,W503" \ No newline at end of file diff --git a/setup.py b/setup.py index 2814cb4..96ae10f 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,6 @@ #!/usr/bin/env python from setuptools import setup, find_packages -from glob import glob -from pathlib import Path with open("README.md", "rt") as fh: long_description = fh.read() @@ -26,9 +24,7 @@ packages=find_packages(exclude=("tests",)), author="Quexington", entry_points={ - "console_scripts": [ - "cdv = cdv.cmds.cli:main" - ], + "console_scripts": ["cdv = cdv.cmds.cli:main"], }, package_data={ "": ["*.clvm", "*.clvm.hex", "*.clib", "*.clsp", "*.clsp.hex"], @@ -48,7 +44,9 @@ "License :: OSI Approved :: Apache Software License", "Topic :: Security :: Cryptography", ], - extras_require=dict(dev=dev_dependencies,), + extras_require=dict( + dev=dev_dependencies, + ), project_urls={ "Bug Reports": "https://github.com/Chia-Network/chia-dev-tools", "Source": "https://github.com/Chia-Network/chia-dev-tools", diff --git a/tests/cmds/test_cdv.py b/tests/cmds/test_cdv.py index 0a15edb..b4ccb2d 100644 --- a/tests/cmds/test_cdv.py +++ b/tests/cmds/test_cdv.py @@ -8,9 +8,7 @@ class TestCdvCommands: def test_encode_decode(self): runner = CliRunner() - puzhash: str = ( - "3d4237d9383a7b6e60d1bfe551139ec2d6e5468205bf179ed381e66bed7b9788" - ) + puzhash: str = "3d4237d9383a7b6e60d1bfe551139ec2d6e5468205bf179ed381e66bed7b9788" result: Result = runner.invoke(cli, ["encode", puzhash]) address: str = "xch184pr0kfc8fakucx3hlj4zyu7cttw235zqkl308kns8nxhmtmj7yqxsnauc" @@ -21,9 +19,7 @@ def test_encode_decode(self): assert puzhash in result.output result = runner.invoke(cli, ["encode", puzhash, "--prefix", "txch"]) - test_address: str = ( - "txch184pr0kfc8fakucx3hlj4zyu7cttw235zqkl308kns8nxhmtmj7yqth5tat" - ) + test_address: str = "txch184pr0kfc8fakucx3hlj4zyu7cttw235zqkl308kns8nxhmtmj7yqth5tat" assert result.exit_code == 0 assert test_address in result.output result = runner.invoke(cli, ["decode", test_address]) @@ -37,25 +33,17 @@ def test_hash(self): result: Result = runner.invoke(cli, ["hash", str_msg]) assert result.exit_code == 0 - assert ( - "3d4237d9383a7b6e60d1bfe551139ec2d6e5468205bf179ed381e66bed7b9788" - in result.output - ) + assert "3d4237d9383a7b6e60d1bfe551139ec2d6e5468205bf179ed381e66bed7b9788" in result.output result = runner.invoke(cli, ["hash", b_msg]) assert result.exit_code == 0 - assert ( - "8f6e594e007ca1a1676ef64469c58f7ece8cddc9deae0faf66fbce2466519ebd" - in result.output - ) + assert "8f6e594e007ca1a1676ef64469c58f7ece8cddc9deae0faf66fbce2466519ebd" in result.output def test_test(self): runner = CliRunner() with runner.isolated_filesystem(): result: Result = runner.invoke(cli, ["test", "--init"]) assert result.exit_code == 0 - assert ( - Path("./tests").exists() and Path("./tests/test_skeleton.py").exists() - ) + assert Path("./tests").exists() and Path("./tests/test_skeleton.py").exists() result = runner.invoke(cli, ["test", "--discover"]) assert result.exit_code == 0 diff --git a/tests/cmds/test_clsp.py b/tests/cmds/test_clsp.py index 94cc9af..ebe5ca3 100644 --- a/tests/cmds/test_clsp.py +++ b/tests/cmds/test_clsp.py @@ -57,9 +57,7 @@ def test_build(self): os.remove(Path("./mod.clsp.hex")) shutil.copytree("./include", "./include_test") shutil.rmtree("./include") - result = runner.invoke( - cli, ["clsp", "build", ".", "--include", "./include_test"] - ) + result = runner.invoke(cli, ["clsp", "build", ".", "--include", "./include_test"]) assert result.exit_code == 0 assert Path("./mod.clsp.hex").exists() assert open("mod.clsp.hex", "r").read() == "ff0133" @@ -122,9 +120,7 @@ def test_treehash(self): runner = CliRunner() program: str = "(a 2 3)" program_as_mod: str = "(mod (arg . arg2) (a arg arg2))" - program_hash: str = ( - "530d1b3283c802be3a7bdb34b788c1898475ed76c89ecb2224e4b4f40c32d1a4" - ) + program_hash: str = "530d1b3283c802be3a7bdb34b788c1898475ed76c89ecb2224e4b4f40c32d1a4" result: Result = runner.invoke(cli, ["clsp", "treehash", program]) assert result.exit_code == 0 assert program_hash in result.output diff --git a/tests/cmds/test_inspect.py b/tests/cmds/test_inspect.py index 134f34e..7af471e 100644 --- a/tests/cmds/test_inspect.py +++ b/tests/cmds/test_inspect.py @@ -56,15 +56,9 @@ def test_any(self): assert "guess" not in result.output for class_type in ["coinrecord", "coin", "spendbundle", "spend"]: - valid_json_path = Path(__file__).parent.joinpath( - f"object_files/{class_type}s/{class_type}.json" - ) - invalid_json_path = Path(__file__).parent.joinpath( - f"object_files/{class_type}s/{class_type}_invalid.json" - ) - metadata_path = Path(__file__).parent.joinpath( - f"object_files/{class_type}s/{class_type}_metadata.json" - ) + valid_json_path = Path(__file__).parent.joinpath(f"object_files/{class_type}s/{class_type}.json") + invalid_json_path = Path(__file__).parent.joinpath(f"object_files/{class_type}s/{class_type}_invalid.json") + metadata_path = Path(__file__).parent.joinpath(f"object_files/{class_type}s/{class_type}_metadata.json") valid_json: Dict = json.loads(open(valid_json_path, "r").read()) metadata_json: Dict = json.loads(open(metadata_path, "r").read()) @@ -84,14 +78,10 @@ def test_any(self): # Try to load bytes if class_type != "coin": - valid_hex_path = Path(__file__).parent.joinpath( - f"object_files/{class_type}s/{class_type}.hex" - ) + valid_hex_path = Path(__file__).parent.joinpath(f"object_files/{class_type}s/{class_type}.hex") # From a file - result = runner.invoke( - cli, ["inspect", "--json", "any", str(valid_hex_path)] - ) + result = runner.invoke(cli, ["inspect", "--json", "any", str(valid_hex_path)]) assert result.exit_code == 0 assert "'coin':" in result.output @@ -102,23 +92,17 @@ def test_any(self): assert "'coin':" in result.output # Make sure the ID calculation is correct - result = runner.invoke( - cli, ["inspect", "--id", "any", str(valid_json_path)] - ) + result = runner.invoke(cli, ["inspect", "--id", "any", str(valid_json_path)]) assert result.exit_code == 0 assert metadata_json["id"] in result.output # Make sure the bytes encoding is correct - result = runner.invoke( - cli, ["inspect", "--bytes", "any", str(valid_json_path)] - ) + result = runner.invoke(cli, ["inspect", "--bytes", "any", str(valid_json_path)]) assert result.exit_code == 0 assert metadata_json["bytes"] in result.output # Make sure the type guessing is correct - result = runner.invoke( - cli, ["inspect", "--type", "any", str(valid_json_path)] - ) + result = runner.invoke(cli, ["inspect", "--type", "any", str(valid_json_path)]) assert result.exit_code == 0 assert metadata_json["type"] in result.output @@ -129,9 +113,7 @@ def test_coins(self): id: str = "f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b" runner = CliRunner() - result: Result = runner.invoke( - cli, ["inspect", "--id", "coins", "-pid", pid, "-ph", ph, "-a", amount] - ) + result: Result = runner.invoke(cli, ["inspect", "--id", "coins", "-pid", pid, "-ph", ph, "-a", amount]) assert result.exit_code == 0 assert id in result.output @@ -207,12 +189,8 @@ def test_spendbundles(self): pubkey: str = "80df54b2a616f5c79baaed254134ae5dfc6e24e2d8e1165b251601ceb67b1886db50aacf946eb20f00adc303e7534dd0" signable_data: str = "24f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4bccd5bb71183532bff220ba46c268991a3ff07eb358e8255a65c30a2dce0e5fbb" # noqa agg_sig: str = "b83fe374efbc5776735df7cbfb7e27ede5079b41cd282091450e4de21c4b772e254ce906508834b0c2dcd3d58c47a96914c782f0baf8eaff7ece3b070d2035cd878f744deadcd6c6625c1d0a1b418437ee3f25c2df08ffe08bdfe06b8a83b514" # noqa - id_no_sig: str = ( - "3ac222f0e8f19afcad367b3068273801ca21fe515311dae8d399a5baad9c3c73" - ) - id_with_sig: str = ( - "bd9acfbb344c006cf520f1265a9b611a20cd478f234f51cd31a978b2d3ad9bbb" - ) + id_no_sig: str = "3ac222f0e8f19afcad367b3068273801ca21fe515311dae8d399a5baad9c3c73" + id_with_sig: str = "bd9acfbb344c006cf520f1265a9b611a20cd478f234f51cd31a978b2d3ad9bbb" network_modifier: str = "testnet7" modified_signable_data: str = "24f5a5fd42d16a20302798ef6ed309979b43003d2320d9f0e8ea9831a92759fb4b117816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015af" # noqa cost: str = "4820283" @@ -290,17 +268,8 @@ def test_spendbundles(self): from cdv.cmds.chia_inspect import do_inspect_spend_bundle_cmd from cdv.cmds.util import fake_context - bundle_path = Path(__file__).parent.joinpath( - "object_files/spendbundles/spendbundle.json" - ) - assert ( - len( - do_inspect_spend_bundle_cmd( - fake_context(), [str(bundle_path)], print_results=False - ) - ) - > 0 - ) + bundle_path = Path(__file__).parent.joinpath("object_files/spendbundles/spendbundle.json") + assert len(do_inspect_spend_bundle_cmd(fake_context(), [str(bundle_path)], print_results=False)) > 0 def test_coinrecords(self): coin_path = Path(__file__).parent.joinpath("object_files/coins/coin.json") @@ -317,12 +286,8 @@ def test_coinrecords(self): runner = CliRunner() # Try to load it from a file - record_path = Path(__file__).parent.joinpath( - "object_files/coinrecords/coinrecord.json" - ) - result: Result = runner.invoke( - cli, ["inspect", "coinrecords", str(record_path)] - ) + record_path = Path(__file__).parent.joinpath("object_files/coinrecords/coinrecord.json") + result: Result = runner.invoke(cli, ["inspect", "coinrecords", str(record_path)]) assert result.exit_code == 0 assert "'coin'" in result.output @@ -395,18 +360,10 @@ def test_keys(self): pk: str = "b7531990662d3fbff22d073a08123ddeae70e0a118cecebf8f207b373da5a90aaefcfed2d9cab8fbe711d6b4f5c72e89" hd_modifier: str = "m/12381/8444/0/0" type_modifier: str = "farmer" # Should be same as above HD Path - farmer_sk: str = ( - "5729513405cb68999711618aa02e317335cecdea63d666886bbb39c0fc487dae" - ) - synthetic_sk: str = ( - "6124dbc2580cbdc2f3c716572950434ecdb42f952ec2e947bd393643c13e8ec2" - ) - ph_modifier: str = ( - "69ae360134b1fae04326e5546f25dc794a19192a1f22a44a46d038e7f0d1ecbb" - ) - modified_synthetic_sk: str = ( - "66dff9a8d49d90029e5fb42378562d459e375965150bc72c3a7ea2c523ab49f5" - ) + farmer_sk: str = "5729513405cb68999711618aa02e317335cecdea63d666886bbb39c0fc487dae" + synthetic_sk: str = "6124dbc2580cbdc2f3c716572950434ecdb42f952ec2e947bd393643c13e8ec2" + ph_modifier: str = "69ae360134b1fae04326e5546f25dc794a19192a1f22a44a46d038e7f0d1ecbb" + modified_synthetic_sk: str = "66dff9a8d49d90029e5fb42378562d459e375965150bc72c3a7ea2c523ab49f5" runner = CliRunner() @@ -418,9 +375,7 @@ def test_keys(self): key_output: str = result.output # Build the key from mnemonic - result = runner.invoke( - cli, ["inspect", "keys", "-m", mnemonic, "-pw", passphrase] - ) + result = runner.invoke(cli, ["inspect", "keys", "-m", mnemonic, "-pw", passphrase]) assert result.exit_code == 0 assert result.output == key_output @@ -451,19 +406,13 @@ def test_keys(self): assert synthetic_sk in result.output # Check that using a non default puzzle hash is working - result = runner.invoke( - cli, ["inspect", "keys", "-sk", sk, "-sy", "-ph", ph_modifier] - ) + result = runner.invoke(cli, ["inspect", "keys", "-sk", sk, "-sy", "-ph", ph_modifier]) assert result.exit_code == 0 assert modified_synthetic_sk in result.output def test_signatures(self): - secret_key_1: str = ( - "70432627e84c13c1a6e6007bf6d9a7a0342018fdef7fc911757aad5a6929d20a" - ) - secret_key_2: str = ( - "0f01f7f68935f8594548bca3892fec419c6b2aa7cff54c3353a2e9b1011f09c7" - ) + secret_key_1: str = "70432627e84c13c1a6e6007bf6d9a7a0342018fdef7fc911757aad5a6929d20a" + secret_key_2: str = "0f01f7f68935f8594548bca3892fec419c6b2aa7cff54c3353a2e9b1011f09c7" text_message: str = "cafe food" bytes_message: str = "0xcafef00d" extra_signature: str = "b5d4e653ec9a737d19abe9af7050d37b0f464f9570ec66a8457fbdabdceb50a77c6610eb442ed1e4ace39d9ecc6d40560de239c1c8f7a115e052438385d594be7394df9287cf30c3254d39f0ae21daefc38d3d07ba3e373628bf8ed73f074a80" # noqa From dd696d55b700123cf5e9f2fb5d17846266cedf60 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Thu, 12 Aug 2021 14:35:19 -0700 Subject: [PATCH 26/31] Minor RPC fixes and commenting --- cdv/cmds/chia_inspect.py | 51 ++++++++++++++++++++++++---- cdv/cmds/cli.py | 2 ++ cdv/cmds/clsp.py | 3 +- cdv/cmds/rpc.py | 40 ++++++++++++++++------ cdv/cmds/util.py | 17 ++++++---- cdv/examples/clsp/piggybank.clsp | 7 ++++ cdv/examples/tests/test_piggybank.py | 7 +++- tests/cmds/test_cdv.py | 2 ++ tests/cmds/test_clsp.py | 1 + 9 files changed, 104 insertions(+), 26 deletions(-) diff --git a/cdv/cmds/chia_inspect.py b/cdv/cmds/chia_inspect.py index e35720d..a24574a 100644 --- a/cdv/cmds/chia_inspect.py +++ b/cdv/cmds/chia_inspect.py @@ -33,6 +33,11 @@ from cdv.cmds.util import parse_program +""" +This group of commands is for guessing the types of objects when you don't know what they are, +but also for building them from scratch and for modifying them once you have them completely loaded/built. +""" + @click.group("inspect", short_help="Inspect various data structures") @click.option("-j", "--json", is_flag=True, help="Output the result as JSON") @@ -46,12 +51,15 @@ def inspect_cmd(ctx: click.Context, **kwargs): ctx.obj[key] = value +# Every inspect command except the key related ones will call this when they're done +# The function allows the flags on "inspect" apply AFTER the objects have been successfully loaded def inspect_callback( objs: List[Any], ctx: click.Context, id_calc: Callable = (lambda: None), type: str = "Unknown", ): + # By default we return JSON if (not any([value for key, value in ctx.obj.items()])) or ctx.obj["json"]: if getattr(objs[0], "to_json_dict", None): pprint([obj.to_json_dict() for obj in objs]) @@ -63,7 +71,7 @@ def inspect_callback( try: final_output.append(bytes(obj).hex()) except AssertionError: - final_output.append("None") + final_output.append("None") # This is for coins since coins overload the __bytes__ method pprint(final_output) if ctx.obj["id"]: pprint([id_calc(obj) for obj in objs]) @@ -72,6 +80,8 @@ def inspect_callback( # Utility functions + +# If there's only one key, return the data on that key instead (for things like {'spend_bundle': {...}}) def json_and_key_strip(input: str) -> Dict: json_dict = json.loads(input) if len(json_dict.keys()) == 1: @@ -80,7 +90,9 @@ def json_and_key_strip(input: str) -> Dict: return json_dict +# Streamable objects can be in either bytes or JSON and we'll take them via CLI or file def streamable_load(cls: Any, inputs: Iterable[Any]) -> List[Any]: + # If we're receiving a group of objects rather than strings to parse, we're going to return them back as a list if inputs and not isinstance(list(inputs)[0], str): for inst in inputs: assert isinstance(inst, cls) @@ -88,20 +100,21 @@ def streamable_load(cls: Any, inputs: Iterable[Any]) -> List[Any]: input_objs: List[Any] = [] for input in inputs: - if "{" in input: + if "{" in input: # If it's a JSON string input_objs.append(cls.from_json_dict(json_and_key_strip(input))) - elif "." in input: + elif "." in input: # If it's a filename file_string = open(input, "r").read() - if "{" in file_string: + if "{" in file_string: # If it's a JSON file input_objs.append(cls.from_json_dict(json_and_key_strip(file_string))) - else: + else: # If it's bytes in a file input_objs.append(cls.from_bytes(hexstr_to_bytes(file_string))) - else: + else: # If it's a byte string input_objs.append(cls.from_bytes(hexstr_to_bytes(input))) return input_objs +# Theoretically, every type of data should have it's command called if it's passed through this function @inspect_cmd.command("any", short_help="Attempt to guess the type of the object before inspecting it") @click.argument("objects", nargs=-1, required=False) @click.pass_context @@ -147,7 +160,14 @@ def inspect_any_cmd(ctx: click.Context, objects: Tuple[str]): elif type(obj) == PrivateKey: do_inspect_keys_cmd(ctx, secret_key=obj) elif type(obj) == G2Element: - print("That's a BLS aggregated signature") + print("That's a BLS aggregated signature") # This is more helpful than just printing it back to them + + +""" +Most of the following commands are designed to also be available as importable functions and usable by the "any" cmd. +This is why there's the cmd/do_cmd pattern in all of them. +The objects they are inspecting can be a list of strings (from the cmd line), or a list of the object being inspected. +""" @inspect_cmd.command("coins", short_help="Various methods for examining and calculating coin objects") @@ -166,6 +186,7 @@ def do_inspect_coin_cmd( print_results: bool = True, **kwargs, ) -> List[Coin]: + # If this is built from the command line and all relevant arguments are there if kwargs and all([kwargs[key] for key in kwargs.keys()]): coin_objs: List[Coin] = [ Coin( @@ -226,11 +247,14 @@ def do_inspect_coin_spend_cmd( cost_flag: bool = False cost_per_byte: int = 12000 if kwargs: + # These args don't really fit with the logic below so we're going to store and delete them cost_flag = kwargs["cost"] cost_per_byte = kwargs["cost_per_byte"] del kwargs["cost"] del kwargs["cost_per_byte"] + # If this is being built from the command line and the two required args are there if kwargs and all([kwargs["puzzle_reveal"], kwargs["solution"]]): + # If they specified the coin components if (not kwargs["coin"]) and all([kwargs["parent_id"], kwargs["puzzle_hash"], kwargs["amount"]]): coin_spend_objs: List[CoinSpend] = [ CoinSpend( @@ -243,6 +267,7 @@ def do_inspect_coin_spend_cmd( parse_program(kwargs["solution"]), ) ] + # If they specifed a coin object to parse elif kwargs["coin"]: coin_spend_objs = [ CoinSpend( @@ -270,6 +295,7 @@ def do_inspect_coin_spend_cmd( id_calc=(lambda e: e.coin.name().hex()), type="CoinSpend", ) + # We're going to print some extra stuff if they wanted to see the cost if cost_flag: for coin_spend in coin_spend_objs: program: BlockGenerator = simple_solution_generator(SpendBundle([coin_spend], G2Element())) @@ -327,6 +353,7 @@ def do_inspect_spend_bundle_cmd( print_results: bool = True, **kwargs, ) -> List[SpendBundle]: + # If this is from the command line and they've specified at lease one spend to parse if kwargs and (len(kwargs["spend"]) > 0): if len(kwargs["aggsig"]) > 0: sig: G2Element = AugSchemeMPL.aggregate([G2Element(hexstr_to_bytes(sig)) for sig in kwargs["aggsig"]]) @@ -351,6 +378,7 @@ def do_inspect_spend_bundle_cmd( id_calc=(lambda e: e.name().hex()), type="SpendBundle", ) + # We're going to print some extra stuff if they've asked for it. if kwargs: if kwargs["cost"]: for spend_bundle in spend_bundle_objs: @@ -398,6 +426,7 @@ def do_inspect_spend_bundle_cmd( pkm_dict[str(pk)].append(msg) else: pkm_dict[str(pk)] = [msg] + # This very deliberately prints identical messages multiple times for pk, msgs in pkm_dict.items(): print(f"{pk}:") for msg in msgs: @@ -455,7 +484,9 @@ def do_inspect_coin_record_cmd( print_results: bool = True, **kwargs, ) -> List[CoinRecord]: + # If we're building this from the command line and we have the two arguements we forsure need if kwargs and all([kwargs["confirmed_block_index"], kwargs["timestamp"]]): + # If they've specified the components of a coin if (not kwargs["coin"]) and all([kwargs["parent_id"], kwargs["puzzle_hash"], kwargs["amount"]]): coin_record_objs: List[CoinRecord] = [ CoinRecord( @@ -471,6 +502,7 @@ def do_inspect_coin_record_cmd( kwargs["timestamp"], ) ] + # If they've specified a coin to parse elif kwargs["coin"]: coin_record_objs = [ CoinRecord( @@ -590,6 +622,7 @@ def do_inspect_keys_cmd(ctx: click.Context, print_results: bool = True, **kwargs kwargs["random"], ] + # This a construct to ensure there is exactly one of these conditions set def one_or_zero(value): return 1 if value else 0 @@ -646,6 +679,7 @@ def one_or_zero(value): print(f"HD Path: {path}") +# This class is necessary for being able to handle parameters in order, rather than grouped by name class OrderedParamsCommand(click.Command): _options: List = [] @@ -685,6 +719,9 @@ def inspect_sigs_cmd(ctx: click.Context, **kwargs): do_inspect_sigs_cmd(ctx, **kwargs) +# This command sort of works like a script: +# Whenever you use a parameter, it changes some state, +# at the end it returns the result of running those parameters in that order. def do_inspect_sigs_cmd(ctx: click.Context, print_results: bool = True, **kwargs) -> G2Element: base = G2Element() sk: Optional[PrivateKey] = None diff --git a/cdv/cmds/cli.py b/cdv/cmds/cli.py index 3c7cf8f..7d144ac 100644 --- a/cdv/cmds/cli.py +++ b/cdv/cmds/cli.py @@ -69,6 +69,8 @@ def test_cmd(tests: str, discover: bool, init: str): import cdv.test as testlib + # It finds these files relative to its position in the venv + # If the cdv/test/__init__.py file or any of the relvant files move, this will break src_path = Path(testlib.__file__).parent.joinpath("test_skeleton.py") dest_path: Path = test_dir.joinpath("test_skeleton.py") shutil.copyfile(src_path, dest_path) diff --git a/cdv/cmds/clsp.py b/cdv/cmds/clsp.py index 3338ed8..7087444 100644 --- a/cdv/cmds/clsp.py +++ b/cdv/cmds/clsp.py @@ -44,6 +44,7 @@ def build_cmd(files: Tuple[str], include: Tuple[str]) -> None: for filename in clvm_files: hex_file_name: str = filename.name + ".hex" full_hex_file_name = Path(filename.parent).joinpath(hex_file_name) + # We only rebuild the file if the .hex is older if not (full_hex_file_name.exists() and full_hex_file_name.stat().st_mtime > filename.stat().st_mtime): outfile = str(filename) + ".hex" try: @@ -118,7 +119,7 @@ def retrieve_cmd(libraries: Tuple[str]): import cdv.clibs as clibs for lib in libraries: - if lib[-5:] == ".clib": + if lib[-5:] == ".clib": # We'll take it with or without the extension lib = lib[:-5] src_path = Path(clibs.__file__).parent.joinpath(f"{lib}.clib") include_path = Path(os.getcwd()).joinpath("include") diff --git a/cdv/cmds/rpc.py b/cdv/cmds/rpc.py index 062ed22..fceab6d 100644 --- a/cdv/cmds/rpc.py +++ b/cdv/cmds/rpc.py @@ -23,11 +23,18 @@ from cdv.cmds.chia_inspect import do_inspect_spend_bundle_cmd +""" +These functions are untested because it is relatively basic code that would be very complex to test. +Please be careful when making changes. +""" + + @click.group("rpc", short_help="Make RPC requests to a Chia full node") def rpc_cmd(): pass +# Loading the client requires the standard chia root directory configuration that all of the chia commands rely on async def get_client() -> Optional[FullNodeRpcClient]: try: config = load_config(DEFAULT_ROOT_PATH, "config.yaml") @@ -88,7 +95,7 @@ async def do_command(): short_help="Gets block records between two indexes (get_block_record(s), get_block_record_by_height)", ) @click.option("-hh", "--header-hash", help="The header hash of the block to get") -@click.option("-i", "--height", help="The height of the block to get") +@click.option("-i", "--height", help="The height of the block to get") # This option is not in the standard RPC API @click.option("-s", "--start", help="The block index to start at (included)") @click.option("-e", "--end", help="The block index to end at (excluded)") def rpc_blockrecords_cmd(header_hash: str, height: int, start: int, end: int): @@ -113,6 +120,7 @@ async def do_command(): asyncio.get_event_loop().run_until_complete(do_command()) +# This maybe shouldn't exist, I have yet to see it return anything but an empty list @rpc_cmd.command( "unfinished", short_help="Returns the current unfinished header blocks (get_unfinished_block_headers)", @@ -130,14 +138,15 @@ async def do_command(): asyncio.get_event_loop().run_until_complete(do_command()) +# Running this command plain should return the current total netspace estimation @rpc_cmd.command( "space", short_help="Gets the netspace of the network between two blocks (get_network_space)", ) -@click.option("-old", "--older", help="The header hash of the older block") -@click.option("-new", "--newer", help="The header hash of the newer block") -@click.option("-s", "--start", help="The height of the block to start at") -@click.option("-e", "--end", help="The height of the block to end at") +@click.option("-old", "--older", help="The header hash of the older block") # Default block 0 +@click.option("-new", "--newer", help="The header hash of the newer block") # Default block 0 +@click.option("-s", "--start", help="The height of the block to start at") # Default latest block +@click.option("-e", "--end", help="The height of the block to end at") # Default latest block def rpc_space_cmd(older: str, newer: str, start: int, end: int): async def do_command(): try: @@ -145,6 +154,10 @@ async def do_command(): if (older and start) or (newer and end): pprint("Invalid arguments specified.") + return + elif not any([older, start, newer, end]): + pprint(format_bytes((await node_client.get_blockchain_state())["space"])) + return else: if start: start_hash: bytes32 = (await node_client.get_block_record_by_height(start)).header_hash @@ -186,7 +199,7 @@ def rpc_addrem_cmd(headerhash: str): async def do_command(): try: node_client: FullNodeRpcClient = await get_client() - additions, removals = await node_client.get_additions_and_removals(bytes.fromhex(headerhash)) + additions, removals = await node_client.get_additions_and_removals(hexstr_to_bytes(headerhash)) additions: List[Dict] = [rec.to_json_dict() for rec in additions] removals: List[Dict] = [rec.to_json_dict() for rec in removals] pprint({"additions": additions, "removals": removals}) @@ -216,7 +229,7 @@ async def do_command(): coin_spend: Optional[CoinSpend] = await node_client.get_puzzle_and_solution( bytes.fromhex(coinid), block_height ) - pprint(coin_spend) + pprint(coin_spend.to_json_dict()) finally: node_client.close() await node_client.await_closed() @@ -230,6 +243,7 @@ def rpc_pushtx_cmd(spendbundles: Tuple[str]): async def do_command(): try: node_client: FullNodeRpcClient = await get_client() + # It loads the spend bundle using cdv inspect for bundle in do_inspect_spend_bundle_cmd(fake_context(), spendbundles, print_results=False): try: result: Dict = await node_client.push_tx(bundle) @@ -303,6 +317,7 @@ async def do_command(): node_client: FullNodeRpcClient = await get_client() clean_values: bytes32 = map(lambda hex: hexstr_to_bytes(hex), values) if by in ["name", "id"]: + # TODO: When a by-multiple-names rpc exits, use it instead coin_records: List[CoinRecord] = [ await node_client.get_coin_record_by_name(value) for value in clean_values ] @@ -333,24 +348,29 @@ async def do_command(): "parentid", "parentinfo", "parent", + "pid", ]: coin_records: List[CoinRecord] = await node_client.get_coin_records_by_parent_ids( clean_values, **kwargs ) + else: + print(f"Unaware of property {by}.") + return - coin_records: List[Dict] = [rec.to_json_dict() for rec in coin_records] + coin_record_dicts: List[Dict] = [rec.to_json_dict() for rec in coin_records] if as_name_dict: cr_dict = {} - for record in coin_records: + for record in coin_record_dicts: cr_dict[Coin.from_json_dict(record["coin"]).name().hex()] = record pprint(cr_dict) else: - pprint(coin_records) + pprint(coin_record_dicts) finally: node_client.close() await node_client.await_closed() + # Have to rename the kwargs as they will be directly passed to the RPC client kwargs["include_spent_coins"] = not kwargs.pop("only_unspent") kwargs["start_height"] = kwargs.pop("start") kwargs["end_height"] = kwargs.pop("end") diff --git a/cdv/cmds/util.py b/cdv/cmds/util.py index 46f0daa..616bc8c 100644 --- a/cdv/cmds/util.py +++ b/cdv/cmds/util.py @@ -9,12 +9,14 @@ from clvm_tools.binutils import assemble +# This is do trick inspect commands into thinking they're commands def fake_context() -> Dict: ctx = {} ctx["obj"] = {"json": True} return ctx +# The clvm loaders in this library automatically search for includable files in the directory './include' def append_include(search_paths: Iterable[str]) -> List[str]: if search_paths: search_list = list(search_paths) @@ -24,23 +26,24 @@ def append_include(search_paths: Iterable[str]) -> List[str]: return ["./include"] +# This is used in many places to go from CLI string -> Program object def parse_program(program: Union[str, Program], include: Iterable = []) -> Program: if isinstance(program, Program): return program else: - if "(" in program: + if "(" in program: # If it's raw clvm prog = Program.to(assemble(program)) - elif "." not in program: + elif "." not in program: # If it's a byte string prog = Program.from_bytes(hexstr_to_bytes(program)) - else: + else: # If it's a file with open(program, "r") as file: filestring: str = file.read() - if "(" in filestring: + if "(" in filestring: # If it's not compiled # TODO: This should probably be more robust - if re.compile(r"\(mod\s").search(filestring): + if re.compile(r"\(mod\s").search(filestring): # If it's Chialisp prog = Program.to(compile_clvm_text(filestring, append_include(include))) - else: + else: # If it's CLVM prog = Program.to(assemble(filestring)) - else: + else: # If it's serialized CLVM prog = Program.from_bytes(hexstr_to_bytes(filestring)) return prog diff --git a/cdv/examples/clsp/piggybank.clsp b/cdv/examples/clsp/piggybank.clsp index 2a07a63..1e104c6 100644 --- a/cdv/examples/clsp/piggybank.clsp +++ b/cdv/examples/clsp/piggybank.clsp @@ -1,3 +1,10 @@ +; Note that this puzzle is "insecure" as it stands +; The person who owns the CASH_OUT_PUZHASH can take out a flash loan to: +; - fill the piggybank +; - receive their funds +; - return the flash loan +; +; This effectively lets someone access the money whenever they want if they have access to a flash loan (mod ( TARGET_AMOUNT CASH_OUT_PUZHASH diff --git a/cdv/examples/tests/test_piggybank.py b/cdv/examples/tests/test_piggybank.py index fc8b316..a4ac2a7 100644 --- a/cdv/examples/tests/test_piggybank.py +++ b/cdv/examples/tests/test_piggybank.py @@ -73,6 +73,7 @@ async def test_piggybank_contribution(self, setup): assert "error" not in result + # Make sure there is exactly one piggybank with the new amount filtered_result: List[Coin] = list( filter( lambda addition: (addition.amount == 501) @@ -94,6 +95,7 @@ async def test_piggybank_completion(self, setup): assert "error" not in result + # Make sure there is exactly one piggybank with value 0 filtered_result: List[Coin] = list( filter( lambda addition: (addition.amount == 0) @@ -105,6 +107,7 @@ async def test_piggybank_completion(self, setup): ) assert len(filtered_result) == 1 + # Make sure there is exactly one coin that has been cashed out to bob filtered_result: List[Coin] = list( filter( lambda addition: (addition.amount == 1000000000001) and (addition.puzzle_hash == bob.puzzle_hash), @@ -121,6 +124,8 @@ async def test_piggybank_stealing(self, setup): try: result: Dict[str, List[Coin]] = await self.make_and_spend_piggybank(network, alice, bob, -100) assert "error" in result - assert "GENERATOR_RUNTIME_ERROR" in result["error"] + assert ( + "GENERATOR_RUNTIME_ERROR" in result["error"] + ) # This fails during puzzle execution, not in driver code finally: await network.close() diff --git a/tests/cmds/test_cdv.py b/tests/cmds/test_cdv.py index b4ccb2d..8d0e1e7 100644 --- a/tests/cmds/test_cdv.py +++ b/tests/cmds/test_cdv.py @@ -10,6 +10,7 @@ def test_encode_decode(self): runner = CliRunner() puzhash: str = "3d4237d9383a7b6e60d1bfe551139ec2d6e5468205bf179ed381e66bed7b9788" + # Without prefix result: Result = runner.invoke(cli, ["encode", puzhash]) address: str = "xch184pr0kfc8fakucx3hlj4zyu7cttw235zqkl308kns8nxhmtmj7yqxsnauc" assert result.exit_code == 0 @@ -18,6 +19,7 @@ def test_encode_decode(self): assert result.exit_code == 0 assert puzhash in result.output + # With prefix result = runner.invoke(cli, ["encode", puzhash, "--prefix", "txch"]) test_address: str = "txch184pr0kfc8fakucx3hlj4zyu7cttw235zqkl308kns8nxhmtmj7yqth5tat" assert result.exit_code == 0 diff --git a/tests/cmds/test_clsp.py b/tests/cmds/test_clsp.py index ebe5ca3..4e98377 100644 --- a/tests/cmds/test_clsp.py +++ b/tests/cmds/test_clsp.py @@ -71,6 +71,7 @@ def test_curry(self): curried_mod: Program = mod.curry(integer, hexadecimal, string, program) runner = CliRunner() + # Curry one of each kind of argument cmd: List[str] = [ "clsp", "curry", From 8e3ddf5be7922c9e725d2601b35a002e605991a0 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Fri, 13 Aug 2021 07:38:26 -0700 Subject: [PATCH 27/31] Added the examples folder to the test suite --- .github/workflows/run-test-suite.yml | 2 +- .gitignore | 1 + cdv/examples/drivers/piggybank_drivers.py | 5 ++++- cdv/examples/tests/test_piggybank.py | 1 + cdv/test/test_skeleton.py | 1 + cdv/util/load_clvm.py | 14 ++++++++++---- 6 files changed, 18 insertions(+), 6 deletions(-) diff --git a/.github/workflows/run-test-suite.yml b/.github/workflows/run-test-suite.yml index 833d80e..10e860f 100644 --- a/.github/workflows/run-test-suite.yml +++ b/.github/workflows/run-test-suite.yml @@ -43,4 +43,4 @@ jobs: . ./venv/bin/activate pip install . ./venv/bin/chia init - ./venv/bin/py.test tests/ -s -v --durations 0 \ No newline at end of file + ./venv/bin/py.test tests/ cdv/examples/tests -s -v --durations 0 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3d9a756..194e09f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /chia_dev_tools.egg-info /venv /dist +/include /.eggs __pycache__/ main.sym diff --git a/cdv/examples/drivers/piggybank_drivers.py b/cdv/examples/drivers/piggybank_drivers.py index e45b5cb..ef42c42 100644 --- a/cdv/examples/drivers/piggybank_drivers.py +++ b/cdv/examples/drivers/piggybank_drivers.py @@ -1,3 +1,4 @@ +from pathlib import Path from typing import List from chia.types.blockchain_format.coin import Coin @@ -9,9 +10,11 @@ from clvm.casts import int_to_bytes +import cdv.clibs as std_lib from cdv.util.load_clvm import load_clvm -PIGGYBANK_MOD: Program = load_clvm("piggybank.clsp", "cdv.examples.clsp") +clibs_path: Path = Path(std_lib.__file__).parent +PIGGYBANK_MOD: Program = load_clvm("piggybank.clsp", "cdv.examples.clsp", search_paths=[clibs_path]) # Create a piggybank diff --git a/cdv/examples/tests/test_piggybank.py b/cdv/examples/tests/test_piggybank.py index a4ac2a7..e9e86ef 100644 --- a/cdv/examples/tests/test_piggybank.py +++ b/cdv/examples/tests/test_piggybank.py @@ -21,6 +21,7 @@ class TestStandardTransaction: @pytest.fixture(scope="function") async def setup(self): network, alice, bob = await setup_test() + await network.farm_block() yield network, alice, bob async def make_and_spend_piggybank(self, network, alice, bob, CONTRIBUTION_AMOUNT) -> Dict[str, List[Coin]]: diff --git a/cdv/test/test_skeleton.py b/cdv/test/test_skeleton.py index 7595163..b1b37e2 100644 --- a/cdv/test/test_skeleton.py +++ b/cdv/test/test_skeleton.py @@ -7,6 +7,7 @@ class TestSomething: @pytest.fixture(scope="function") async def setup(self): network, alice, bob = await setup_test() + await network.farm_block() yield network, alice, bob @pytest.mark.asyncio diff --git a/cdv/util/load_clvm.py b/cdv/util/load_clvm.py index 9af2b98..b9a9fcd 100644 --- a/cdv/util/load_clvm.py +++ b/cdv/util/load_clvm.py @@ -6,7 +6,7 @@ from chia.types.blockchain_format.program import Program, SerializedProgram -def load_serialized_clvm(clvm_filename, package_or_requirement=__name__) -> SerializedProgram: +def load_serialized_clvm(clvm_filename, package_or_requirement=__name__, search_paths=[]) -> SerializedProgram: """ This function takes a .clvm file in the given package and compiles it to a .clvm.hex file if the .hex file is missing or older than the .clvm file, then @@ -25,7 +25,7 @@ def load_serialized_clvm(clvm_filename, package_or_requirement=__name__) -> Seri compile_clvm( full_path, output, - search_paths=[full_path.parent, pathlib.Path.cwd().joinpath("include")], + search_paths=[full_path.parent, pathlib.Path.cwd().joinpath("include"), *search_paths], ) except NotImplementedError: # pyinstaller doesn't support `pkg_resources.resource_exists` @@ -37,5 +37,11 @@ def load_serialized_clvm(clvm_filename, package_or_requirement=__name__) -> Seri return SerializedProgram.from_bytes(clvm_blob) -def load_clvm(clvm_filename, package_or_requirement=__name__) -> Program: - return Program.from_bytes(bytes(load_serialized_clvm(clvm_filename, package_or_requirement=package_or_requirement))) +def load_clvm(clvm_filename, package_or_requirement=__name__, search_paths=[]) -> Program: + return Program.from_bytes( + bytes( + load_serialized_clvm( + clvm_filename, package_or_requirement=package_or_requirement, search_paths=search_paths + ) + ) + ) From 1dcc3e0d193e00278b713a0357eca6f2a373c365 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Fri, 13 Aug 2021 08:35:37 -0700 Subject: [PATCH 28/31] Refactor out the term 'coin_solutions' --- cdv/test/__init__.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/cdv/test/__init__.py b/cdv/test/__init__.py index b968a7e..5b08dda 100644 --- a/cdv/test/__init__.py +++ b/cdv/test/__init__.py @@ -93,7 +93,7 @@ def create_standard_spend(self, priv: PrivateKey, conditions: List[List]): delegated_puzzle_solution = Program.to((1, conditions)) solution = Program.to([[], delegated_puzzle_solution, []]) - coin_solution_object = CoinSpend( + coin_spend_object = CoinSpend( self.as_coin(), self.puzzle(), solution, @@ -105,7 +105,7 @@ def create_standard_spend(self, priv: PrivateKey, conditions: List[List]): (delegated_puzzle_solution.get_tree_hash() + self.name() + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA), ) - return coin_solution_object, signature + return coin_spend_object, signature # We have two cases for coins: @@ -161,8 +161,6 @@ def insort(self, coin: Coin): self.max_coins.append(coin) def process_coin_for_combine_search(self, coin: Coin): - if self.target == 0: - breakpoint() self.total = uint64(self.total + coin.amount) if len(self.max_coins) == 0: self.max_coins.append(coin) @@ -296,7 +294,7 @@ def compute_combine_action( # Now to spend them at once: # # signature = AugSchemeMPL.aggregate(signatures) - # spend_bundle = SpendBundle(coin_solutions, signature) + # spend_bundle = SpendBundle(coin_spends, signature) # async def combine_coins(self, coins: List[CoinWrapper]) -> Optional[SpendResult]: # Overall structure: @@ -315,7 +313,7 @@ async def combine_coins(self, coins: List[CoinWrapper]) -> Optional[SpendResult] self.puzzle, ) - destroyed_coin_solutions: List[CoinSpend] = [] + destroyed_coin_spends: List[CoinSpend] = [] # Each coin wants agg_sig_me so we aggregate them at the end. signatures: List[G2Element] = [] @@ -329,8 +327,8 @@ async def combine_coins(self, coins: List[CoinWrapper]) -> Optional[SpendResult] ] ] - coin_solution, signature = c.create_standard_spend(self.sk_, announce_conditions) - destroyed_coin_solutions.append(coin_solution) + coin_spend, signature = c.create_standard_spend(self.sk_, announce_conditions) + destroyed_coin_spends.append(coin_spend) signatures.append(signature) final_coin_creation: List[List] = [ @@ -338,12 +336,12 @@ async def combine_coins(self, coins: List[CoinWrapper]) -> Optional[SpendResult] [ConditionOpcode.CREATE_COIN, self.puzzle_hash, final_coin.amount], ] - coin_solution, signature = coins[-1].create_standard_spend(self.sk_, final_coin_creation) - destroyed_coin_solutions.append(coin_solution) + coin_spend, signature = coins[-1].create_standard_spend(self.sk_, final_coin_creation) + destroyed_coin_spends.append(coin_spend) signatures.append(signature) signature = AugSchemeMPL.aggregate(signatures) - spend_bundle = SpendBundle(destroyed_coin_solutions, signature) + spend_bundle = SpendBundle(destroyed_coin_spends, signature) pushed: Dict[str, Union[str, List[Coin]]] = await self.parent.push_tx(spend_bundle) From d47bd7e86a7c0e7394d432c01183b3914d82df30 Mon Sep 17 00:00:00 2001 From: Matt Hauff Date: Mon, 16 Aug 2021 07:35:27 -0700 Subject: [PATCH 29/31] Increment setup.py and minor README and bug fix --- README.md | 4 ++-- cdv/cmds/rpc.py | 1 + setup.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 591641b..13d1e74 100644 --- a/README.md +++ b/README.md @@ -91,8 +91,8 @@ There are also commands for interacting with the full node's RPC endpoints (in d ``` cdv rpc state cdv rpc blocks -s 0 -e 1 -cdv coinrecords --by id e16dbc782f500aa24891886779067792b3305cff8b873ae1e77273ad0b7e6c05 -cdv pushtx .\spend_bundle.json +cdv rpc coinrecords --by id e16dbc782f500aa24891886779067792b3305cff8b873ae1e77273ad0b7e6c05 +cdv rpc pushtx .\spend_bundle.json ``` Python Packages diff --git a/cdv/cmds/rpc.py b/cdv/cmds/rpc.py index fceab6d..d1bf985 100644 --- a/cdv/cmds/rpc.py +++ b/cdv/cmds/rpc.py @@ -321,6 +321,7 @@ async def do_command(): coin_records: List[CoinRecord] = [ await node_client.get_coin_record_by_name(value) for value in clean_values ] + coin_records = list(filter(lambda record: record is not None, coin_records)) if not kwargs["include_spent_coins"]: coin_records: List[CoinRecord] = list(filter(lambda record: record.spent is False, coin_records)) if kwargs["start_height"] is not None: diff --git a/setup.py b/setup.py index 96ae10f..12156e9 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ setup( name="chia_dev_tools", - version="1.0.2", + version="1.0.3", packages=find_packages(exclude=("tests",)), author="Quexington", entry_points={ From 70ebfc29649ba8600dc7be24cb4d42b24eda751d Mon Sep 17 00:00:00 2001 From: arty Date: Tue, 17 Aug 2021 07:06:59 -0700 Subject: [PATCH 30/31] Allow specification of a launcher coin so we can keep track of it --- cdv/test/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cdv/test/__init__.py b/cdv/test/__init__.py index 366e566..9bf7334 100644 --- a/cdv/test/__init__.py +++ b/cdv/test/__init__.py @@ -396,11 +396,17 @@ async def choose_coin(self,amt) -> CoinWrapper: async def launch_smart_coin(self,source,**kwargs) -> CoinWrapper: """Create a new smart coin based on a parent coin and return the smart coin's living coin to the user or None if the spend failed.""" + found_coin = None + amt = 1 if 'amt' in kwargs: amt = kwargs['amt'] - found_coin = await self.choose_coin(amt) + if 'launcher' in kwargs: + found_coin = kwargs['launcher'] + else: + found_coin = await self.choose_coin(amt) + if found_coin is None: raise ValueError(f'could not find available coin containing {amt} mojo') @@ -635,4 +641,4 @@ async def setup(): network = await Network.create() alice = network.make_wallet('alice') bob = network.make_wallet('bob') - return network, alice, bob \ No newline at end of file + return network, alice, bob From b21790351868d80f79b5efcd7cd729bd8e980094 Mon Sep 17 00:00:00 2001 From: arty Date: Tue, 17 Aug 2021 08:23:40 -0700 Subject: [PATCH 31/31] Remove redundant assignment from merge --- cdv/test/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cdv/test/__init__.py b/cdv/test/__init__.py index e67b65f..06cc560 100644 --- a/cdv/test/__init__.py +++ b/cdv/test/__init__.py @@ -400,7 +400,6 @@ async def launch_smart_coin(self, source: Program, **kwargs) -> Optional[CoinWra amt = uint64(1) found_coin: Optional[CoinWrapper] = None - amt = 1 if 'amt' in kwargs: amt = kwargs['amt']