├── __init__.py ├── tests ├── __init__.py ├── util │ ├── __init__.py │ ├── config.py │ ├── protocol_messages_bytes-v1.0 │ ├── rpc.py │ ├── db_connection.py │ ├── network.py │ ├── generator_tools_testing.py │ ├── key_tool.py │ ├── alert_server.py │ ├── misc.py │ ├── blockchain.py │ ├── build_network_protocol_files.py │ ├── benchmark_cost.py │ ├── keyring.py │ └── bip39_test_vectors.json ├── pytest.ini ├── testconfig.py ├── check_pytest_monitor_output.py ├── chia-start-sim ├── time_out_assert.py ├── conftest.py ├── connection_utils.py ├── build-workflows.py ├── wallet_tools.py ├── setup_nodes.py └── test_manager.py ├── conftest.py ├── screenshot.png ├── art ├── bird3.txt ├── bird1.txt ├── bird6.txt ├── bird5.txt ├── bird2.txt └── bird4.txt ├── pyproject.toml ├── mypy.ini ├── setup.py ├── include ├── sha256tree.clib ├── condition_codes.clib ├── cat_truths.clib ├── singleton_truths.clib └── curry_and_treehash.clib ├── clsp ├── p2_creator_nft.clsp.hex ├── p2_creator_nft.clsp ├── nft_launcher.clsp.hex ├── nft_launcher.clsp ├── creator_nft.clsp.hex └── creator_nft.clsp ├── .pre-commit-config.yaml ├── .gitignore ├── README.md ├── nft.py ├── driver.py ├── nft_wallet.py ├── nft_manager.py └── sim.py /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/util/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/util/config.py: -------------------------------------------------------------------------------- 1 | job_timeout = 60 2 | -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | sys.path.append("./") 4 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geoffwalmsley/CreatorNFT/HEAD/screenshot.png -------------------------------------------------------------------------------- /art/bird3.txt: -------------------------------------------------------------------------------- 1 | MM 2 | >' \___/| 3 | \_ _/ 4 | ][ 5 | 6 | Crowing Chicken 7 | -------------------------------------------------------------------------------- /art/bird1.txt: -------------------------------------------------------------------------------- 1 | MM 2 | <' \___/| 3 | \_ _/ 4 | ][ 5 | 6 | Ordinary Chicken 7 | -------------------------------------------------------------------------------- /art/bird6.txt: -------------------------------------------------------------------------------- 1 | MM 2 | o>' \___/| 3 | O \_ _/ 4 | () ][ 5 | 6 | Puking Chicken 7 | -------------------------------------------------------------------------------- /art/bird5.txt: -------------------------------------------------------------------------------- 1 | MM 2 | <' \_____/| 3 | \_ _ / 4 | ][ ][ 5 | 6 | Four-Legged 7 | Chicken 8 | -------------------------------------------------------------------------------- /art/bird2.txt: -------------------------------------------------------------------------------- 1 | MM 2 | <' \___/| 3 | \_ _/ O 4 | ][ 5 | 6 | Chicken Laying 7 | an Egg 8 | -------------------------------------------------------------------------------- /art/bird4.txt: -------------------------------------------------------------------------------- 1 | _--_ 2 | / \ 3 | | | 4 | \____/ 5 | 6 | Chicken in an 7 | Early State 8 | -------------------------------------------------------------------------------- /tests/util/protocol_messages_bytes-v1.0: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geoffwalmsley/CreatorNFT/HEAD/tests/util/protocol_messages_bytes-v1.0 -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.black] 6 | line-length = 120 7 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | files = *.py 3 | ignore_missing_imports = True 4 | show_error_codes = True 5 | warn_unused_ignores = True 6 | 7 | [mypy - lib] 8 | ignore_errors = True 9 | -------------------------------------------------------------------------------- /tests/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | ; logging options 3 | log_cli = 1 4 | log_level = WARNING 5 | log_format = %(asctime)s %(name)s: %(levelname)s %(message)s 6 | filterwarnings = 7 | ignore::DeprecationWarning 8 | ignore::UserWarning 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name="CreatorNFT", 5 | version="0.1", 6 | py_modules=["nft"], 7 | install_requires=["Click", "pytimeparse"], 8 | entry_points={"console_scripts": ["nft = nft:main"]}, 9 | ) 10 | -------------------------------------------------------------------------------- /tests/testconfig.py: -------------------------------------------------------------------------------- 1 | # Github actions template config. 2 | oses = ["ubuntu", "macos"] 3 | 4 | # Defaults are conservative. 5 | parallel = False 6 | checkout_blocks_and_plots = True 7 | install_timelord = True 8 | job_timeout = 30 9 | custom_vars = ["CHECK_RESOURCE_USAGE"] 10 | -------------------------------------------------------------------------------- /include/sha256tree.clib: -------------------------------------------------------------------------------- 1 | ( 2 | ;; hash a tree 3 | ;; This is used to calculate a puzzle hash given a puzzle program. 4 | (defun sha256tree 5 | (TREE) 6 | (if (l TREE) 7 | (sha256 2 (sha256tree (f TREE)) (sha256tree (r TREE))) 8 | (sha256 1 TREE) 9 | ) 10 | ) 11 | ) -------------------------------------------------------------------------------- /tests/util/rpc.py: -------------------------------------------------------------------------------- 1 | async def validate_get_routes(client, api): 2 | routes_client = (await client.fetch("get_routes", {}))["routes"] 3 | assert len(routes_client) > 0 4 | routes_api = list(api.get_routes().keys()) 5 | routes_server = [ 6 | "/get_connections", 7 | "/open_connection", 8 | "/close_connection", 9 | "/stop_node", 10 | "/get_routes", 11 | ] 12 | assert len(routes_api) > 0 13 | for route in routes_api + routes_server: 14 | assert route in routes_client 15 | -------------------------------------------------------------------------------- /tests/util/db_connection.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from chia.util.db_wrapper import DBWrapper 3 | import tempfile 4 | import aiosqlite 5 | 6 | 7 | class DBConnection: 8 | def __init__(self, db_version): 9 | self.db_version = db_version 10 | 11 | async def __aenter__(self) -> DBWrapper: 12 | self.db_path = Path(tempfile.NamedTemporaryFile().name) 13 | if self.db_path.exists(): 14 | self.db_path.unlink() 15 | self.connection = await aiosqlite.connect(self.db_path) 16 | return DBWrapper(self.connection, False, self.db_version) 17 | 18 | async def __aexit__(self, exc_t, exc_v, exc_tb): 19 | await self.connection.close() 20 | self.db_path.unlink() 21 | -------------------------------------------------------------------------------- /tests/check_pytest_monitor_output.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | 4 | ret = 0 5 | 6 | # example input line 7 | # test_non_tx_aggregate_limits 0.997759588095738 1.45325589179993 554.45703125 8 | for ln in sys.stdin: 9 | line = ln.strip().split() 10 | 11 | print(f"{float(line[1]) * 100.0: 8.1f}% CPU {float(line[2]):7.1f}s {float(line[3]): 8.2f} MB RAM {line[0]}") 12 | limit = 800 13 | 14 | # until this can be optimized, use higher limits 15 | if "test_duplicate_coin_announces" in line[0]: 16 | limit = 2200 17 | elif ( 18 | "test_duplicate_large_integer_substr" in line[0] 19 | or "test_duplicate_reserve_fee" in line[0] 20 | or "test_duplicate_large_integer_negative" in line[0] 21 | or "test_duplicate_large_integer" in line[0] 22 | ): 23 | limit = 1100 24 | 25 | if float(line[3]) > limit: 26 | print(" ERROR: ^^ exceeded RAM limit ^^ \n") 27 | ret += 1 28 | 29 | if ret > 0: 30 | print("some tests used too much RAM") 31 | 32 | sys.exit(ret) 33 | -------------------------------------------------------------------------------- /clsp/p2_creator_nft.clsp.hex: -------------------------------------------------------------------------------- 1 | ff02ffff01ff02ffff03ff5fffff01ff02ff36ffff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfff808080808080808080ffff01ff088080ff0180ffff04ffff01ffffff46ff3f02ff3cff0401ffff01ff02ff02ffff03ff05ffff01ff02ff3affff04ff02ffff04ff0dffff04ffff0bff2affff0bff3cff2c80ffff0bff2affff0bff2affff0bff3cff1280ff0980ffff0bff2aff0bffff0bff3cff8080808080ff8080808080ffff010b80ff0180ffffff02ff2effff04ff02ffff04ff05ffff04ff2fffff04ffff02ff3effff04ff02ffff04ffff04ff05ffff04ff0bff178080ff80808080ff808080808080ff04ffff04ff28ffff04ffff0bffff02ff26ffff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff2fff80808080808080ff5f80ff808080ffff04ffff04ff14ffff04ffff02ff3effff04ff02ffff04ff81bfff80808080ff808080ffff04ffff04ff10ffff04ff5fff808080ff80808080ffff0bff2affff0bff3cff3880ffff0bff2affff0bff2affff0bff3cff1280ff0580ffff0bff2affff02ff3affff04ff02ffff04ff07ffff04ffff0bff3cff3c80ff8080808080ffff0bff3cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 -------------------------------------------------------------------------------- /tests/util/network.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from chia.util.network import get_host_addr 3 | 4 | 5 | class TestNetwork: 6 | @pytest.mark.asyncio 7 | async def test_get_host_addr4(self): 8 | # Run these tests forcing IPv4 resolution 9 | prefer_ipv6 = False 10 | assert get_host_addr("127.0.0.1", prefer_ipv6) == "127.0.0.1" 11 | assert get_host_addr("10.11.12.13", prefer_ipv6) == "10.11.12.13" 12 | assert get_host_addr("localhost", prefer_ipv6) == "127.0.0.1" 13 | assert get_host_addr("example.net", prefer_ipv6) == "93.184.216.34" 14 | 15 | @pytest.mark.asyncio 16 | async def test_get_host_addr6(self): 17 | # Run these tests forcing IPv6 resolution 18 | prefer_ipv6 = True 19 | assert get_host_addr("::1", prefer_ipv6) == "::1" 20 | assert get_host_addr("2000:1000::1234:abcd", prefer_ipv6) == "2000:1000::1234:abcd" 21 | # ip6-localhost is not always available, and localhost is IPv4 only 22 | # on some systems. Just test neither here. 23 | # assert get_host_addr("ip6-localhost", prefer_ipv6) == "::1" 24 | # assert get_host_addr("localhost", prefer_ipv6) == "::1" 25 | assert get_host_addr("example.net", prefer_ipv6) == "2606:2800:220:1:248:1893:25c8:1946" 26 | -------------------------------------------------------------------------------- /include/condition_codes.clib: -------------------------------------------------------------------------------- 1 | ; See chia/types/condition_opcodes.py 2 | 3 | ( 4 | (defconstant AGG_SIG_UNSAFE 49) 5 | (defconstant AGG_SIG_ME 50) 6 | 7 | ; the conditions below reserve coin amounts and have to be accounted for in output totals 8 | 9 | (defconstant CREATE_COIN 51) 10 | (defconstant RESERVE_FEE 52) 11 | 12 | ; the conditions below deal with announcements, for inter-coin communication 13 | 14 | ; coin announcements 15 | (defconstant CREATE_COIN_ANNOUNCEMENT 60) 16 | (defconstant ASSERT_COIN_ANNOUNCEMENT 61) 17 | 18 | ; puzzle announcements 19 | (defconstant CREATE_PUZZLE_ANNOUNCEMENT 62) 20 | (defconstant ASSERT_PUZZLE_ANNOUNCEMENT 63) 21 | 22 | ; the conditions below let coins inquire about themselves 23 | 24 | (defconstant ASSERT_MY_COIN_ID 70) 25 | (defconstant ASSERT_MY_PARENT_ID 71) 26 | (defconstant ASSERT_MY_PUZZLEHASH 72) 27 | (defconstant ASSERT_MY_AMOUNT 73) 28 | 29 | ; the conditions below ensure that we're "far enough" in the future 30 | 31 | ; wall-clock time 32 | (defconstant ASSERT_SECONDS_RELATIVE 80) 33 | (defconstant ASSERT_SECONDS_ABSOLUTE 81) 34 | 35 | ; block index 36 | (defconstant ASSERT_HEIGHT_RELATIVE 82) 37 | (defconstant ASSERT_HEIGHT_ABSOLUTE 83) 38 | ) 39 | -------------------------------------------------------------------------------- /include/cat_truths.clib: -------------------------------------------------------------------------------- 1 | ( 2 | (defun-inline cat_truth_data_to_truth_struct (innerpuzhash cat_struct my_id this_coin_info) 3 | (c 4 | (c 5 | innerpuzhash 6 | cat_struct 7 | ) 8 | (c 9 | my_id 10 | this_coin_info 11 | ) 12 | ) 13 | ) 14 | 15 | ; CAT Truths is: ((Inner puzzle hash . (MOD hash . (MOD hash hash . TAIL hash))) . (my_id . (my_parent_info my_puzhash my_amount))) 16 | 17 | (defun-inline my_inner_puzzle_hash_cat_truth (Truths) (f (f Truths))) 18 | (defun-inline cat_struct_truth (Truths) (r (f Truths))) 19 | (defun-inline my_id_cat_truth (Truths) (f (r Truths))) 20 | (defun-inline my_coin_info_truth (Truths) (r (r Truths))) 21 | (defun-inline my_amount_cat_truth (Truths) (f (r (r (my_coin_info_truth Truths))))) 22 | (defun-inline my_full_puzzle_hash_cat_truth (Truths) (f (r (my_coin_info_truth Truths)))) 23 | (defun-inline my_parent_cat_truth (Truths) (f (my_coin_info_truth Truths))) 24 | 25 | 26 | ; CAT mod_struct is: (MOD_HASH MOD_HASH_hash TAIL_PROGRAM TAIL_PROGRAM_hash) 27 | 28 | (defun-inline cat_mod_hash_truth (Truths) (f (cat_struct_truth Truths))) 29 | (defun-inline cat_mod_hash_hash_truth (Truths) (f (r (cat_struct_truth Truths)))) 30 | (defun-inline cat_tail_program_hash_truth (Truths) (f (r (r (cat_struct_truth Truths))))) 31 | ) -------------------------------------------------------------------------------- /clsp/p2_creator_nft.clsp: -------------------------------------------------------------------------------- 1 | (mod (SINGLETON_MOD_HASH LAUNCHER_ID LAUNCHER_PH inner_puzzle_hash my_id new_state) 2 | (include condition_codes.clib) 3 | (include sha256tree.clib) 4 | (include singleton_truths.clib) 5 | (include curry_and_treehash.clib) 6 | 7 | 8 | 9 | (defun calc_ph (SINGLETON_MOD_HASH 10 | LAUNCHER_ID 11 | LAUNCHER_PH 12 | inner_puzzle_hash) 13 | 14 | (puzzle-hash-of-curried-function 15 | SINGLETON_MOD_HASH 16 | inner_puzzle_hash 17 | (sha256tree (c SINGLETON_MOD_HASH (c LAUNCHER_ID LAUNCHER_PH)))) 18 | 19 | ) 20 | 21 | 22 | (defun make_payment (SINGLETON_MOD_HASH 23 | LAUNCHER_ID 24 | LAUNCHER_PH 25 | inner_puzzle_hash 26 | my_id 27 | new_state) 28 | (list 29 | 30 | (list ASSERT_PUZZLE_ANNOUNCEMENT 31 | (sha256 (calc_ph SINGLETON_MOD_HASH 32 | LAUNCHER_ID 33 | LAUNCHER_PH 34 | inner_puzzle_hash) 35 | my_id)) 36 | 37 | (list CREATE_COIN_ANNOUNCEMENT 38 | (sha256tree new_state)) 39 | 40 | (list ASSERT_MY_COIN_ID 41 | my_id) 42 | )) 43 | 44 | 45 | (if my_id 46 | (make_payment SINGLETON_MOD_HASH 47 | LAUNCHER_ID 48 | LAUNCHER_PH 49 | inner_puzzle_hash 50 | my_id 51 | new_state) 52 | (x) 53 | ) 54 | ) 55 | -------------------------------------------------------------------------------- /tests/util/generator_tools_testing.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple 2 | 3 | from chia.full_node.mempool_check_conditions import get_name_puzzle_conditions 4 | from chia.types.blockchain_format.coin import Coin 5 | from chia.types.blockchain_format.sized_bytes import bytes32 6 | from chia.types.full_block import FullBlock 7 | from chia.types.generator_types import BlockGenerator 8 | from chia.util.generator_tools import additions_for_npc 9 | 10 | 11 | def run_and_get_removals_and_additions( 12 | block: FullBlock, max_cost: int, cost_per_byte: int, mempool_mode=False 13 | ) -> Tuple[List[bytes32], List[Coin]]: 14 | removals: List[bytes32] = [] 15 | additions: List[Coin] = [] 16 | 17 | assert len(block.transactions_generator_ref_list) == 0 18 | if not block.is_transaction_block(): 19 | return [], [] 20 | 21 | if block.transactions_generator is not None: 22 | npc_result = get_name_puzzle_conditions( 23 | BlockGenerator(block.transactions_generator, []), 24 | max_cost, 25 | cost_per_byte=cost_per_byte, 26 | mempool_mode=mempool_mode, 27 | ) 28 | # build removals list 29 | for npc in npc_result.npc_list: 30 | removals.append(npc.coin_name) 31 | additions.extend(additions_for_npc(npc_result.npc_list)) 32 | 33 | rewards = block.get_included_reward_coins() 34 | additions.extend(rewards) 35 | return removals, additions 36 | -------------------------------------------------------------------------------- /include/singleton_truths.clib: -------------------------------------------------------------------------------- 1 | ( 2 | (defun-inline truth_data_to_truth_struct (my_id full_puzhash innerpuzhash my_amount lineage_proof singleton_struct) (c (c my_id full_puzhash) (c (c innerpuzhash my_amount) (c lineage_proof singleton_struct)))) 3 | 4 | (defun-inline my_id_truth (Truths) (f (f Truths))) 5 | (defun-inline my_full_puzzle_hash_truth (Truths) (r (f Truths))) 6 | (defun-inline my_inner_puzzle_hash_truth (Truths) (f (f (r Truths)))) 7 | (defun-inline my_amount_truth (Truths) (r (f (r Truths)))) 8 | (defun-inline my_lineage_proof_truth (Truths) (f (r (r Truths)))) 9 | (defun-inline singleton_struct_truth (Truths) (r (r (r Truths)))) 10 | 11 | (defun-inline singleton_mod_hash_truth (Truths) (f (singleton_struct_truth Truths))) 12 | (defun-inline singleton_launcher_id_truth (Truths) (f (r (singleton_struct_truth Truths)))) 13 | (defun-inline singleton_launcher_puzzle_hash_truth (Truths) (f (r (r (singleton_struct_truth Truths))))) 14 | 15 | (defun-inline parent_info_for_lineage_proof (lineage_proof) (f lineage_proof)) 16 | (defun-inline puzzle_hash_for_lineage_proof (lineage_proof) (f (r lineage_proof))) 17 | (defun-inline amount_for_lineage_proof (lineage_proof) (f (r (r lineage_proof)))) 18 | (defun-inline is_not_eve_proof (lineage_proof) (r (r lineage_proof))) 19 | (defun-inline parent_info_for_eve_proof (lineage_proof) (f lineage_proof)) 20 | (defun-inline amount_for_eve_proof (lineage_proof) (f (r lineage_proof))) 21 | ) -------------------------------------------------------------------------------- /clsp/nft_launcher.clsp.hex: -------------------------------------------------------------------------------- 1 | ff02ffff01ff02ffff03ffff09ffff02ff16ffff04ff02ffff04ff0bffff04ff17ffff04ff2fffff04ffff02ff3affff04ff02ffff04ff5fffff04ff82017fffff04ff81bfff808080808080ff80808080808080ff0580ffff01ff02ffff03ffff15ff82057fffff0181ff80ffff01ff02ffff03ffff15ffff0165ff82057f80ffff01ff04ffff04ff28ffff04ff05ffff04ff8202ffff80808080ffff04ffff04ff38ffff04ffff02ff3effff04ff02ffff04ffff04ff05ffff04ff0bffff04ff17ffff04ff2fffff04ff5fffff04ff81bfffff04ff82017fffff04ff8202ffffff04ff8205ffff80808080808080808080ff80808080ff808080ff808080ffff01ff08ffff018d726f79616c7479203e203130308080ff0180ffff01ff08ffff018b726f79616c7479203c20308080ff0180ffff01ff08ffff0196696e636f727265637420696e6e65722070757a7a6c658080ff0180ffff04ffff01ffffff02ff333cff04ff0101ffff02ffff02ffff03ff05ffff01ff02ff2affff04ff02ffff04ff0dffff04ffff0bff12ffff0bff2cff1480ffff0bff12ffff0bff12ffff0bff2cff3c80ff0980ffff0bff12ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ff02ff2effff04ff02ffff04ff05ffff04ffff02ff3effff04ff02ffff04ff0bff80808080ffff04ffff02ff3effff04ff02ffff04ff17ff80808080ffff04ffff0bffff0101ff0580ff80808080808080ffff02ff2effff04ff02ffff04ff05ffff04ff2fffff04ffff02ff3effff04ff02ffff04ffff04ff05ffff04ff0bff178080ff80808080ff808080808080ffff0bff12ffff0bff2cff1080ffff0bff12ffff0bff12ffff0bff2cff3c80ff0580ffff0bff12ffff02ff2affff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.0.1 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | exclude: ".*?(.hex|.clvm|.clib|.clsp)" 8 | - id: trailing-whitespace 9 | - id: check-merge-conflict 10 | - id: check-ast 11 | - id: debug-statements 12 | - repo: https://github.com/psf/black 13 | rev: 21.8b0 14 | hooks: 15 | - id: black 16 | args: # arguments to configure black 17 | - --line-length=120 18 | - --include='\.pyi?$' 19 | - repo: https://github.com/pre-commit/mirrors-mypy 20 | rev: v0.910 21 | hooks: 22 | - id: mypy 23 | additional_dependencies: [types-aiofiles, types-click, types-setuptools, types-PyYAML] 24 | # This intentionally counters the settings in mypy.ini to allow a loose local 25 | # check and a strict CI check. This difference may or may not be retained long 26 | # term. 27 | args: [--no-warn-unused-ignores] 28 | 29 | - repo: https://github.com/pre-commit/pre-commit-hooks 30 | rev: v2.3.0 31 | hooks: 32 | - id: flake8 33 | args: # arguments to configure flake8 34 | # making isort line length compatible with black 35 | - "--max-line-length=120" 36 | - "--max-complexity=18" 37 | - "--select=B,C,E,F,W,T4,B9" 38 | 39 | # these are errors that will be ignored by flake8 40 | # check out their meaning here 41 | # https://flake8.pycqa.org/en/latest/user/error-codes.html 42 | - "--ignore=E203,E266,E501,W503,F403,F401,E402" 43 | -------------------------------------------------------------------------------- /clsp/nft_launcher.clsp: -------------------------------------------------------------------------------- 1 | (mod (nft_full_puzzle_hash 2 | singleton_mod_hash 3 | launcher_id 4 | launcher_ph 5 | inner_mod_hash 6 | state 7 | royalty 8 | amount 9 | key_value_list) 10 | 11 | (include condition_codes.clib) 12 | (include curry_and_treehash.clib) 13 | (include sha256tree.clib) 14 | 15 | (defun outer_puzzle_hash (singleton_mod_hash 16 | launcher_id 17 | launcher_ph 18 | inner_puzzle_hash) 19 | (puzzle-hash-of-curried-function singleton_mod_hash 20 | inner_puzzle_hash 21 | (sha256tree (c singleton_mod_hash 22 | (c launcher_id launcher_ph))))) 23 | 24 | 25 | (defun inner_puzzle_hash (inner_mod_hash royalty state) 26 | (puzzle-hash-of-curried-function inner_mod_hash 27 | (sha256tree royalty) 28 | (sha256tree state) 29 | (sha256 1 inner_mod_hash))) 30 | 31 | ;; main 32 | (if (= (outer_puzzle_hash singleton_mod_hash 33 | launcher_id 34 | launcher_ph 35 | (inner_puzzle_hash inner_mod_hash 36 | royalty 37 | state)) 38 | nft_full_puzzle_hash) 39 | ;;then 40 | (if (> (f (r royalty)) -1) 41 | (if (> 101 (f (r royalty))) 42 | (list 43 | (list CREATE_COIN nft_full_puzzle_hash amount) 44 | (list CREATE_COIN_ANNOUNCEMENT (sha256tree (list nft_full_puzzle_hash 45 | singleton_mod_hash 46 | launcher_id 47 | launcher_ph 48 | inner_mod_hash 49 | state 50 | royalty 51 | amount 52 | key_value_list)))) 53 | (x "royalty > 100")) 54 | (x "royalty < 0")) 55 | ;;else 56 | (x "incorrect inner puzzle")) 57 | ) -------------------------------------------------------------------------------- /clsp/creator_nft.clsp.hex: -------------------------------------------------------------------------------- 1 | ff02ffff01ff02ffff03ff81bfffff01ff02ffff03ff13ffff01ff02ff2effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff8201afffff04ff5fffff04ff81bfff808080808080808080ffff01ff08ffff018c6e6f7420666f722073616c658080ff0180ffff01ff02ff3effff04ff02ffff04ff05ffff04ff0bffff04ff17ffff04ff8201afffff04ff5fff808080808080808080ff0180ffff04ffff01ffffffff323dff0233ffff3e04ff0101ffffff02ff02ffff03ff05ffff01ff02ff32ffff04ff02ffff04ff0dffff04ffff0bff22ffff0bff2cff3480ffff0bff22ffff0bff22ffff0bff2cff3c80ff0980ffff0bff22ff0bffff0bff2cff8080808080ff8080808080ffff010b80ff0180ffff02ffff03ffff06ffff14ff05ffff01028080ffff01ff11ff05ffff010180ffff010580ff0180ff02ff26ffff04ff02ffff04ff05ffff04ffff02ff36ffff04ff02ffff04ff0bff80808080ffff04ffff02ff36ffff04ff02ffff04ff17ff80808080ffff04ffff0bffff0101ff0580ff80808080808080ffffff0bff22ffff0bff2cff2880ffff0bff22ffff0bff22ffff0bff2cff3c80ff0580ffff0bff22ffff02ff32ffff04ff02ffff04ff07ffff04ffff0bff2cff2c80ff8080808080ffff0bff2cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff36ffff04ff02ffff04ff09ff80808080ffff02ff36ffff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ffff04ffff04ff38ffff04ffff02ff3affff04ff02ffff04ff05ffff04ff17ffff04ff5fff808080808080ffff04ff2fff80808080ffff04ffff04ff38ffff04ff5bffff04ffff02ff2affff04ff02ffff04ffff11ff2bffff02ffff03ff57ffff01ff05ffff14ffff12ff2bff5780ffff01648080ff8080ff018080ff80808080ff80808080ffff04ffff04ff38ffff04ff27ffff04ffff02ff2affff04ff02ffff04ffff02ffff03ff57ffff01ff05ffff14ffff12ff2bff5780ffff01648080ff8080ff0180ff80808080ff80808080ffff04ffff04ff24ffff04ff81bfff808080ffff04ffff04ff30ffff04ffff0bff81bfffff02ff36ffff04ff02ffff04ff5fff8080808080ff808080ffff04ffff04ff20ffff04ff8205dfffff04ffff0bff0580ff80808080ff80808080808080ff04ffff04ff38ffff04ffff02ff3affff04ff02ffff04ff05ffff04ff17ffff04ff5fff808080808080ffff04ff2fff80808080ffff04ffff04ff20ffff04ff81bbffff04ffff02ff36ffff04ff02ffff04ff5fff80808080ff80808080ff808080ff018080 -------------------------------------------------------------------------------- /tests/chia-start-sim: -------------------------------------------------------------------------------- 1 | _kill_servers() { 2 | PROCS=`ps -e | grep -E 'chia|vdf_client' -v "chia-start-sim" | awk '!/grep/' | awk '{print $1}'` 3 | if [ -n "$PROCS" ]; then 4 | echo "$PROCS" | xargs -L1 kill -KILL 5 | fi 6 | } 7 | 8 | _kill_servers 9 | 10 | BG_PIDS="" 11 | _run_bg_cmd() { 12 | "$@" & 13 | BG_PIDS="$BG_PIDS $!" 14 | } 15 | 16 | 17 | _term() { 18 | echo "Caught TERM or INT signal, killing all servers." 19 | for PID in $BG_PIDS; do 20 | kill -TERM "$PID" 21 | done 22 | _kill_servers 23 | } 24 | 25 | echo "Starting local blockchain simulation. Runs a local introducer and chia system." 26 | echo "Note that this simulation will not work if connected to external nodes." 27 | 28 | # Starts a harvester, farmer, timelord, introducer, and 3 full nodes, locally. 29 | # Please note that the simulation is meant to be run locally and not connected to external nodes. 30 | # NOTE: you must run install.sh when changing this file 31 | 32 | _run_bg_cmd chia_farmer --logging.log_stdout=True --logging.log_level=INFO 33 | _run_bg_cmd chia_harvester --logging.log_stdout=True --logging.log_level=INFO 34 | _run_bg_cmd chia_timelord --logging.log_stdout=True --logging.log_level=INFO 35 | _run_bg_cmd chia_timelord_launcher --logging.log_stdout=True --logging.log_level=INFO 36 | _run_bg_cmd chia_introducer --logging.log_stdout=True --logging.log_level=INFO 37 | _run_bg_cmd chia_full_node --port=8444 --database_path="simulation_1.db" --rpc_port=8555 --introducer_peer.host="127.0.0.1" --introducer_peer.port=8445 --logging.log_stdout=True --logging.log_level=INFO --logging.log_level=INFO 38 | sleep 1 39 | _run_bg_cmd chia_full_node --port=8002 --database_path="simulation_2.db" --rpc_port=8556 --introducer_peer.host="127.0.0.1" --introducer_peer.port=8445 --logging.log_stdout=True --logging.log_level=INFO 40 | _run_bg_cmd python -m chia.daemon.server --logging.log_stdout=True --logging.log_level=INFO 41 | 42 | wait 43 | -------------------------------------------------------------------------------- /tests/time_out_assert.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import time 4 | from typing import Callable 5 | 6 | from chia.protocols.protocol_message_types import ProtocolMessageTypes 7 | 8 | log = logging.getLogger(__name__) 9 | 10 | 11 | async def time_out_assert_custom_interval(timeout: int, interval, function, value=True, *args, **kwargs): 12 | __tracebackhide__ = True 13 | start = time.time() 14 | while time.time() - start < timeout: 15 | if asyncio.iscoroutinefunction(function): 16 | f_res = await function(*args, **kwargs) 17 | else: 18 | f_res = function(*args, **kwargs) 19 | if value == f_res: 20 | return None 21 | await asyncio.sleep(interval) 22 | assert False, f"Timed assertion timed out after {timeout} seconds: expected {value!r}, got {f_res!r}" 23 | 24 | 25 | async def time_out_assert(timeout: int, function, value=True, *args, **kwargs): 26 | __tracebackhide__ = True 27 | await time_out_assert_custom_interval(timeout, 0.05, function, value, *args, **kwargs) 28 | 29 | 30 | async def time_out_assert_not_none(timeout: int, function, *args, **kwargs): 31 | start = time.time() 32 | while time.time() - start < timeout: 33 | if asyncio.iscoroutinefunction(function): 34 | f_res = await function(*args, **kwargs) 35 | else: 36 | f_res = function(*args, **kwargs) 37 | if f_res is not None: 38 | return None 39 | await asyncio.sleep(0.05) 40 | assert False, "Timed assertion timed out" 41 | 42 | 43 | def time_out_messages(incoming_queue: asyncio.Queue, msg_name: str, count: int = 1) -> Callable: 44 | async def bool_f(): 45 | if incoming_queue.qsize() < count: 46 | return False 47 | for _ in range(count): 48 | response = (await incoming_queue.get())[0].type 49 | if ProtocolMessageTypes(response).name != msg_name: 50 | # log.warning(f"time_out_message: found {response} instead of {msg_name}") 51 | return False 52 | return True 53 | 54 | return bool_f 55 | -------------------------------------------------------------------------------- /tests/util/key_tool.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from blspy import AugSchemeMPL, G2Element, PrivateKey 4 | 5 | from chia.types.blockchain_format.sized_bytes import bytes32 6 | from chia.types.coin_spend import CoinSpend 7 | from chia.util.condition_tools import conditions_by_opcode, conditions_for_solution, pkm_pairs_for_conditions_dict 8 | from tests.core.make_block_generator import GROUP_ORDER, int_to_public_key 9 | from tests.block_tools import test_constants 10 | 11 | 12 | class KeyTool(dict): 13 | @classmethod 14 | def __new__(cls, *args): 15 | return dict.__new__(*args) 16 | 17 | def add_secret_exponents(self, secret_exponents: List[int]) -> None: 18 | for _ in secret_exponents: 19 | self[bytes(int_to_public_key(_))] = _ % GROUP_ORDER 20 | 21 | def sign(self, public_key: bytes, message_hash: bytes32) -> G2Element: 22 | secret_exponent = self.get(public_key) 23 | if not secret_exponent: 24 | raise ValueError("unknown pubkey %s" % public_key.hex()) 25 | bls_private_key = PrivateKey.from_bytes(secret_exponent.to_bytes(32, "big")) 26 | return AugSchemeMPL.sign(bls_private_key, message_hash) 27 | 28 | def signature_for_solution(self, coin_spend: CoinSpend, additional_data: bytes) -> AugSchemeMPL: 29 | signatures = [] 30 | err, conditions, cost = conditions_for_solution( 31 | coin_spend.puzzle_reveal, coin_spend.solution, test_constants.MAX_BLOCK_COST_CLVM 32 | ) 33 | assert conditions is not None 34 | conditions_dict = conditions_by_opcode(conditions) 35 | for public_key, message_hash in pkm_pairs_for_conditions_dict( 36 | conditions_dict, coin_spend.coin.name(), additional_data 37 | ): 38 | # TODO: address hint error and remove ignore 39 | # error: Argument 2 to "sign" of "KeyTool" has incompatible type "bytes"; expected "bytes32" 40 | # [arg-type] 41 | signature = self.sign(public_key, message_hash) # type: ignore[arg-type] 42 | signatures.append(signature) 43 | return AugSchemeMPL.aggregate(signatures) 44 | -------------------------------------------------------------------------------- /tests/util/alert_server.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import asyncio 3 | import logging 4 | from pathlib import Path 5 | from typing import Any 6 | 7 | from aiohttp import web 8 | 9 | log = logging.getLogger(__name__) 10 | 11 | 12 | class AlertServer: 13 | shut_down: bool 14 | shut_down_event: asyncio.Event 15 | log: Any 16 | app: Any 17 | alert_file_path: Path 18 | port: int 19 | 20 | @staticmethod 21 | async def create_alert_server(alert_file_path: Path, port): 22 | self = AlertServer() 23 | self.log = log 24 | self.shut_down = False 25 | self.app = web.Application() 26 | self.shut_down_event = asyncio.Event() 27 | self.port = port 28 | routes = [ 29 | web.get("/status", self.status), 30 | ] 31 | 32 | self.alert_file_path = alert_file_path 33 | self.app.add_routes(routes) 34 | 35 | return self 36 | 37 | async def status(self, request): 38 | file_text = self.alert_file_path.read_text() 39 | return web.Response(body=file_text, content_type="text/plain") 40 | 41 | async def stop(self): 42 | self.shut_down_event.set() 43 | 44 | async def run(self): 45 | runner = web.AppRunner(self.app, access_log=None) 46 | await runner.setup() 47 | site = web.TCPSite(runner, None, self.port) 48 | await site.start() 49 | 50 | 51 | async def run_and_wait(file_path, port): 52 | server = await AlertServer.create_alert_server(Path(file_path), port) 53 | await server.run() 54 | await server.shut_down_event.wait() 55 | 56 | 57 | def main(): 58 | parser = argparse.ArgumentParser() 59 | parser.add_argument("-file_path", type=str, dest="file_path") 60 | parser.add_argument("-port", type=str, dest="port") 61 | 62 | port = None 63 | file_path = None 64 | 65 | for key, value in vars(parser.parse_args()).items(): 66 | if key == "port": 67 | port = value 68 | elif key == "file_path": 69 | file_path = value 70 | else: 71 | print(f"Invalid argument {key}") 72 | 73 | if port is None or file_path is None: 74 | print( 75 | "Missing arguments, example usage:\n\n" 76 | "python chia/util/alert_server.py -p 4000 -file_path /home/user/alert.txt\n" 77 | ) 78 | quit() 79 | 80 | return asyncio.get_event_loop().run_until_complete(run_and_wait(file_path, port)) 81 | 82 | 83 | if __name__ == "__main__": 84 | main() 85 | -------------------------------------------------------------------------------- /tests/util/misc.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from chia.util.misc import format_bytes 3 | from chia.util.misc import format_minutes 4 | 5 | 6 | class TestMisc: 7 | @pytest.mark.asyncio 8 | async def test_format_bytes(self): 9 | assert format_bytes(None) == "Invalid" 10 | assert format_bytes(dict()) == "Invalid" 11 | assert format_bytes("some bytes") == "Invalid" 12 | assert format_bytes(-1024) == "Invalid" 13 | assert format_bytes(0) == "0.000 MiB" 14 | assert format_bytes(1024) == "0.001 MiB" 15 | assert format_bytes(1024 ** 2 - 1000) == "0.999 MiB" 16 | assert format_bytes(1024 ** 2) == "1.000 MiB" 17 | assert format_bytes(1024 ** 3) == "1.000 GiB" 18 | assert format_bytes(1024 ** 4) == "1.000 TiB" 19 | assert format_bytes(1024 ** 5) == "1.000 PiB" 20 | assert format_bytes(1024 ** 6) == "1.000 EiB" 21 | assert format_bytes(1024 ** 7) == "1.000 ZiB" 22 | assert format_bytes(1024 ** 8) == "1.000 YiB" 23 | assert format_bytes(1024 ** 9) == "1024.000 YiB" 24 | assert format_bytes(1024 ** 10) == "1048576.000 YiB" 25 | assert format_bytes(1024 ** 20).endswith("YiB") 26 | 27 | @pytest.mark.asyncio 28 | async def test_format_minutes(self): 29 | assert format_minutes(None) == "Invalid" 30 | assert format_minutes(dict()) == "Invalid" 31 | assert format_minutes("some minutes") == "Invalid" 32 | assert format_minutes(-1) == "Unknown" 33 | assert format_minutes(0) == "Now" 34 | assert format_minutes(1) == "1 minute" 35 | assert format_minutes(59) == "59 minutes" 36 | assert format_minutes(60) == "1 hour" 37 | assert format_minutes(61) == "1 hour and 1 minute" 38 | assert format_minutes(119) == "1 hour and 59 minutes" 39 | assert format_minutes(1380) == "23 hours" 40 | assert format_minutes(1440) == "1 day" 41 | assert format_minutes(2160) == "1 day and 12 hours" 42 | assert format_minutes(8640) == "6 days" 43 | assert format_minutes(10080) == "1 week" 44 | assert format_minutes(20160) == "2 weeks" 45 | assert format_minutes(40240) == "3 weeks and 6 days" 46 | assert format_minutes(40340) == "4 weeks" 47 | assert format_minutes(43800) == "1 month" 48 | assert format_minutes(102000) == "2 months and 1 week" 49 | assert format_minutes(481800) == "11 months" 50 | assert format_minutes(525600) == "1 year" 51 | assert format_minutes(1007400) == "1 year and 11 months" 52 | assert format_minutes(5256000) == "10 years" 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ChiaLisp 2 | *.sym 3 | \#* 4 | .mypy_cache/ 5 | .pytest_cache/ 6 | .ropeproject/ 7 | 8 | # Org Mode 9 | *~ 10 | *.html 11 | *.org 12 | 13 | # Python 14 | *.db 15 | 16 | 17 | # Byte-compiled / optimized / DLL files 18 | __pycache__/ 19 | *.py[cod] 20 | *$py.class 21 | 22 | # C extensions 23 | *.so 24 | 25 | # Distribution / packaging 26 | .Python 27 | build/ 28 | develop-eggs/ 29 | dist/ 30 | downloads/ 31 | eggs/ 32 | .eggs/ 33 | lib/ 34 | lib64/ 35 | parts/ 36 | sdist/ 37 | var/ 38 | wheels/ 39 | share/python-wheels/ 40 | *.egg-info/ 41 | .installed.cfg 42 | *.egg 43 | MANIFEST 44 | 45 | # PyInstaller 46 | # Usually these files are written by a python script from a template 47 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 48 | *.manifest 49 | *.spec 50 | 51 | # Installer logs 52 | pip-log.txt 53 | pip-delete-this-directory.txt 54 | 55 | # Unit test / coverage reports 56 | htmlcov/ 57 | .tox/ 58 | .nox/ 59 | .coverage 60 | .coverage.* 61 | .cache 62 | nosetests.xml 63 | coverage.xml 64 | *.cover 65 | *.py,cover 66 | .hypothesis/ 67 | .pytest_cache/ 68 | cover/ 69 | 70 | # Translations 71 | *.mo 72 | *.pot 73 | 74 | # Django stuff: 75 | *.log 76 | local_settings.py 77 | db.sqlite3 78 | db.sqlite3-journal 79 | 80 | # Flask stuff: 81 | instance/ 82 | .webassets-cache 83 | 84 | # Scrapy stuff: 85 | .scrapy 86 | 87 | # Sphinx documentation 88 | docs/_build/ 89 | 90 | # PyBuilder 91 | .pybuilder/ 92 | target/ 93 | 94 | # Jupyter Notebook 95 | .ipynb_checkpoints 96 | 97 | # IPython 98 | profile_default/ 99 | ipython_config.py 100 | 101 | # pyenv 102 | # For a library or package, you might want to ignore these files since the code is 103 | # intended to run in multiple environments; otherwise, check them in: 104 | # .python-version 105 | 106 | # pipenv 107 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 108 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 109 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 110 | # install all needed dependencies. 111 | #Pipfile.lock 112 | 113 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 114 | __pypackages__/ 115 | 116 | # Celery stuff 117 | celerybeat-schedule 118 | celerybeat.pid 119 | 120 | # SageMath parsed files 121 | *.sage.py 122 | 123 | # Environments 124 | .env 125 | .venv 126 | env/ 127 | venv/ 128 | ENV/ 129 | env.bak/ 130 | venv.bak/ 131 | 132 | # Spyder project settings 133 | .spyderproject 134 | .spyproject 135 | 136 | # Rope project settings 137 | .ropeproject 138 | 139 | # mkdocs documentation 140 | /site 141 | 142 | # mypy 143 | .mypy_cache/ 144 | .dmypy.json 145 | dmypy.json 146 | 147 | # Pyre type checker 148 | .pyre/ 149 | 150 | # pytype static type analyzer 151 | .pytype/ 152 | 153 | # Cython debug symbols 154 | cython_debug/ 155 | -------------------------------------------------------------------------------- /include/curry_and_treehash.clib: -------------------------------------------------------------------------------- 1 | ( 2 | ;; The code below is used to calculate of the tree hash of a curried function 3 | ;; without actually doing the curry, and using other optimization tricks 4 | ;; like unrolling `sha256tree`. 5 | 6 | (defconstant ONE 1) 7 | (defconstant TWO 2) 8 | (defconstant A_KW #a) 9 | (defconstant Q_KW #q) 10 | (defconstant C_KW #c) 11 | 12 | ;; Given the tree hash `environment-hash` of an environment tree E 13 | ;; and the tree hash `parameter-hash` of a constant parameter P 14 | ;; return the tree hash of the tree corresponding to 15 | ;; `(c (q . P) E)` 16 | ;; This is the new environment tree with the addition parameter P curried in. 17 | ;; 18 | ;; Note that `(c (q . P) E)` = `(c . ((q . P) . (E . 0)))` 19 | 20 | (defun-inline update-hash-for-parameter-hash (parameter-hash environment-hash) 21 | (sha256 TWO (sha256 ONE C_KW) 22 | (sha256 TWO (sha256 TWO (sha256 ONE Q_KW) parameter-hash) 23 | (sha256 TWO environment-hash (sha256 ONE 0)))) 24 | ) 25 | 26 | ;; This function recursively calls `update-hash-for-parameter-hash`, updating `environment-hash` 27 | ;; along the way. 28 | 29 | (defun build-curry-list (reversed-curry-parameter-hashes environment-hash) 30 | (if reversed-curry-parameter-hashes 31 | (build-curry-list (r reversed-curry-parameter-hashes) 32 | (update-hash-for-parameter-hash (f reversed-curry-parameter-hashes) environment-hash)) 33 | environment-hash 34 | ) 35 | ) 36 | 37 | ;; Given the tree hash `environment-hash` of an environment tree E 38 | ;; and the tree hash `function-hash` of a function tree F 39 | ;; return the tree hash of the tree corresponding to 40 | ;; `(a (q . F) E)` 41 | ;; This is the hash of a new function that adopts the new environment E. 42 | ;; This is used to build of the tree hash of a curried function. 43 | ;; 44 | ;; Note that `(a (q . F) E)` = `(a . ((q . F) . (E . 0)))` 45 | 46 | (defun-inline tree-hash-of-apply (function-hash environment-hash) 47 | (sha256 TWO (sha256 ONE A_KW) 48 | (sha256 TWO (sha256 TWO (sha256 ONE Q_KW) function-hash) 49 | (sha256 TWO environment-hash (sha256 ONE 0)))) 50 | ) 51 | 52 | ;; function-hash: 53 | ;; the hash of a puzzle function, ie. a `mod` 54 | ;; 55 | ;; reversed-curry-parameter-hashes: 56 | ;; a list of pre-hashed trees representing parameters to be curried into the puzzle. 57 | ;; Note that this must be applied in REVERSED order. This may seem strange, but it greatly simplifies 58 | ;; the underlying code, since we calculate the tree hash from the bottom nodes up, and the last 59 | ;; parameters curried must have their hashes calculated first. 60 | ;; 61 | ;; we return the hash of the curried expression 62 | ;; (a (q . function-hash) (c (cp1 (c cp2 (c ... 1)...)))) 63 | 64 | (defun puzzle-hash-of-curried-function (function-hash . reversed-curry-parameter-hashes) 65 | (tree-hash-of-apply function-hash 66 | (build-curry-list reversed-curry-parameter-hashes (sha256 ONE ONE))) 67 | ) 68 | ) 69 | -------------------------------------------------------------------------------- /clsp/creator_nft.clsp: -------------------------------------------------------------------------------- 1 | (mod (MOD_HASH STATE ROYALTY Truths new_state payment_info) 2 | 3 | 4 | 5 | ;; STATE = (for_sale_flag, price, owner_ph, owner_pk) 6 | ;; ROYALTY = (creator_puzhash, percentage) 7 | 8 | ;; new_state: update for new STATE 9 | ;; payment_info: p2_singleton_coin_id 10 | ;; fee: payment fee in mojo 11 | 12 | 13 | 14 | (include condition_codes.clib) 15 | (include sha256tree.clib) 16 | (include singleton_truths.clib) 17 | (include curry_and_treehash.clib) 18 | 19 | (defun new_puzzle_hash (MOD_HASH ROYALTY new_state) 20 | (puzzle-hash-of-curried-function MOD_HASH 21 | (sha256tree ROYALTY) 22 | (sha256tree new_state) 23 | (sha256 1 MOD_HASH)) 24 | ) 25 | 26 | (defun make_even (amt) 27 | (if (r (divmod amt 2)) 28 | (- amt 1) 29 | amt)) 30 | 31 | (defun-inline creator_amt (STATE ROYALTY) 32 | (if (f (r ROYALTY)) 33 | (f (divmod (* (f (r STATE)) (f (r ROYALTY))) 100)) 34 | 0 35 | ) 36 | ) 37 | 38 | 39 | 40 | ;; UPDATE STATE 41 | (defun update_state (MOD_HASH STATE ROYALTY my_amount new_state) 42 | (list 43 | (list CREATE_COIN 44 | (new_puzzle_hash MOD_HASH 45 | ROYALTY 46 | new_state) 47 | my_amount) 48 | 49 | (list AGG_SIG_ME 50 | (f (r (r (r STATE)))) 51 | (sha256tree new_state))) 52 | ) 53 | 54 | ;; RECREATE WITH NEW STATE and PAYOUT 55 | (defun trade_coin (MOD_HASH 56 | STATE 57 | ROYALTY 58 | my_amount 59 | new_state 60 | payment_info 61 | ) 62 | 63 | 64 | (list 65 | 66 | (list CREATE_COIN ;; Recreate singleton 67 | (new_puzzle_hash MOD_HASH 68 | ROYALTY 69 | new_state) 70 | my_amount) 71 | 72 | (list CREATE_COIN ;; Payout current owner 73 | (f (r (r STATE))) 74 | (make_even (- (f (r STATE)) 75 | (creator_amt STATE 76 | ROYALTY)))) 77 | 78 | (list CREATE_COIN ;; Payout Creator 79 | (f ROYALTY) 80 | (make_even (creator_amt STATE ROYALTY))) 81 | 82 | 83 | (list CREATE_PUZZLE_ANNOUNCEMENT ;; Announce the p2 coin id 84 | payment_info) 85 | 86 | (list ASSERT_COIN_ANNOUNCEMENT ;; Assert the p2_coin spend 87 | (sha256 payment_info 88 | (sha256tree new_state))) 89 | 90 | (list AGG_SIG_ME ;; Buyer signs 91 | (f (r (r (r new_state)))) 92 | (sha256 MOD_HASH)) 93 | )) 94 | 95 | 96 | ;; --------------------------------------------------------------------------- 97 | ;; MAIN 98 | 99 | 100 | (if payment_info 101 | (if (f STATE) 102 | (trade_coin MOD_HASH 103 | STATE 104 | ROYALTY 105 | (my_amount_truth Truths) 106 | new_state 107 | payment_info 108 | ) 109 | (x "not for sale")) 110 | (update_state MOD_HASH 111 | STATE 112 | ROYALTY 113 | (my_amount_truth Truths) 114 | new_state 115 | )) 116 | 117 | ) 118 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import tempfile 3 | from pathlib import Path 4 | 5 | 6 | # TODO: tests.setup_nodes (which is also imported by tests.util.blockchain) creates a 7 | # global BlockTools at tests.setup_nodes.bt. This results in an attempt to create 8 | # the chia root directory which the build scripts symlink to a sometimes-not-there 9 | # directory. When not there Python complains since, well, the symlink is a file 10 | # not a directory and also not pointing to a directory. In those same cases, 11 | # these fixtures are not used. It would be good to refactor that global state 12 | # creation, including the filesystem modification, away from the import but 13 | # that seems like a separate step and until then locating the imports in the 14 | # fixtures avoids the issue. 15 | 16 | 17 | @pytest.fixture(scope="function", params=[1, 2]) 18 | async def empty_blockchain(request): 19 | """ 20 | Provides a list of 10 valid blocks, as well as a blockchain with 9 blocks added to it. 21 | """ 22 | from tests.util.blockchain import create_blockchain 23 | from tests.setup_nodes import test_constants 24 | 25 | bc1, connection, db_path = await create_blockchain(test_constants, request.param) 26 | yield bc1 27 | 28 | await connection.close() 29 | bc1.shut_down() 30 | db_path.unlink() 31 | 32 | 33 | block_format_version = "rc4" 34 | 35 | 36 | @pytest.fixture(scope="session") 37 | async def default_400_blocks(): 38 | from tests.util.blockchain import persistent_blocks 39 | 40 | return persistent_blocks(400, f"test_blocks_400_{block_format_version}.db", seed=b"alternate2") 41 | 42 | 43 | @pytest.fixture(scope="session") 44 | async def default_1000_blocks(): 45 | from tests.util.blockchain import persistent_blocks 46 | 47 | return persistent_blocks(1000, f"test_blocks_1000_{block_format_version}.db") 48 | 49 | 50 | @pytest.fixture(scope="session") 51 | async def pre_genesis_empty_slots_1000_blocks(): 52 | from tests.util.blockchain import persistent_blocks 53 | 54 | return persistent_blocks( 55 | 1000, f"pre_genesis_empty_slots_1000_blocks{block_format_version}.db", seed=b"alternate2", empty_sub_slots=1 56 | ) 57 | 58 | 59 | @pytest.fixture(scope="session") 60 | async def default_10000_blocks(): 61 | from tests.util.blockchain import persistent_blocks 62 | 63 | return persistent_blocks(10000, f"test_blocks_10000_{block_format_version}.db") 64 | 65 | 66 | @pytest.fixture(scope="session") 67 | async def default_20000_blocks(): 68 | from tests.util.blockchain import persistent_blocks 69 | 70 | return persistent_blocks(20000, f"test_blocks_20000_{block_format_version}.db") 71 | 72 | 73 | @pytest.fixture(scope="session") 74 | async def default_10000_blocks_compact(): 75 | from tests.util.blockchain import persistent_blocks 76 | 77 | return persistent_blocks( 78 | 10000, 79 | f"test_blocks_10000_compact_{block_format_version}.db", 80 | normalized_to_identity_cc_eos=True, 81 | normalized_to_identity_icc_eos=True, 82 | normalized_to_identity_cc_ip=True, 83 | normalized_to_identity_cc_sp=True, 84 | ) 85 | 86 | 87 | @pytest.fixture(scope="function") 88 | async def tmp_dir(): 89 | with tempfile.TemporaryDirectory() as folder: 90 | yield Path(folder) 91 | -------------------------------------------------------------------------------- /tests/connection_utils.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from typing import Tuple 4 | 5 | import aiohttp 6 | from cryptography import x509 7 | from cryptography.hazmat.backends import default_backend 8 | from cryptography.hazmat.primitives import hashes, serialization 9 | 10 | from chia.protocols.shared_protocol import protocol_version 11 | from chia.server.outbound_message import NodeType 12 | from chia.server.server import ChiaServer, ssl_context_for_client 13 | from chia.server.ws_connection import WSChiaConnection 14 | from chia.ssl.create_ssl import generate_ca_signed_cert 15 | from chia.types.blockchain_format.sized_bytes import bytes32 16 | from chia.types.peer_info import PeerInfo 17 | from chia.util.ints import uint16 18 | from tests.setup_nodes import self_hostname 19 | from tests.time_out_assert import time_out_assert 20 | 21 | log = logging.getLogger(__name__) 22 | 23 | 24 | async def disconnect_all_and_reconnect(server: ChiaServer, reconnect_to: ChiaServer) -> bool: 25 | cons = list(server.all_connections.values())[:] 26 | for con in cons: 27 | await con.close() 28 | return await server.start_client(PeerInfo(self_hostname, uint16(reconnect_to._port)), None) 29 | 30 | 31 | async def add_dummy_connection( 32 | server: ChiaServer, dummy_port: int, type: NodeType = NodeType.FULL_NODE 33 | ) -> Tuple[asyncio.Queue, bytes32]: 34 | timeout = aiohttp.ClientTimeout(total=10) 35 | session = aiohttp.ClientSession(timeout=timeout) 36 | incoming_queue: asyncio.Queue = asyncio.Queue() 37 | dummy_crt_path = server._private_key_path.parent / "dummy.crt" 38 | dummy_key_path = server._private_key_path.parent / "dummy.key" 39 | generate_ca_signed_cert( 40 | server.chia_ca_crt_path.read_bytes(), server.chia_ca_key_path.read_bytes(), dummy_crt_path, dummy_key_path 41 | ) 42 | ssl_context = ssl_context_for_client( 43 | server.chia_ca_crt_path, server.chia_ca_key_path, dummy_crt_path, dummy_key_path 44 | ) 45 | pem_cert = x509.load_pem_x509_certificate(dummy_crt_path.read_bytes(), default_backend()) 46 | der_cert = x509.load_der_x509_certificate(pem_cert.public_bytes(serialization.Encoding.DER), default_backend()) 47 | peer_id = bytes32(der_cert.fingerprint(hashes.SHA256())) 48 | url = f"wss://{self_hostname}:{server._port}/ws" 49 | ws = await session.ws_connect(url, autoclose=True, autoping=True, ssl=ssl_context) 50 | wsc = WSChiaConnection( 51 | type, 52 | ws, 53 | server._port, 54 | log, 55 | True, 56 | False, 57 | self_hostname, 58 | incoming_queue, 59 | lambda x, y: x, 60 | peer_id, 61 | 100, 62 | 30, 63 | ) 64 | handshake = await wsc.perform_handshake(server._network_id, protocol_version, dummy_port, NodeType.FULL_NODE) 65 | assert handshake is True 66 | return incoming_queue, peer_id 67 | 68 | 69 | async def connect_and_get_peer(server_1: ChiaServer, server_2: ChiaServer) -> WSChiaConnection: 70 | """ 71 | Connect server_2 to server_1, and get return the connection in server_1. 72 | """ 73 | await server_2.start_client(PeerInfo(self_hostname, uint16(server_1._port))) 74 | 75 | async def connected(): 76 | for node_id_c, _ in server_1.all_connections.items(): 77 | if node_id_c == server_2.node_id: 78 | return True 79 | return False 80 | 81 | await time_out_assert(10, connected, True) 82 | for node_id, wsc in server_1.all_connections.items(): 83 | if node_id == server_2.node_id: 84 | return wsc 85 | assert False 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Chialisp NFT with Perpetual Creator Royalties 2 | 3 | This is a chialisp NFT in which the creator/minter defines a puzzle hash which will capture a fixed percentage of the value each time the singleton is traded. 4 | 5 | There's also a video giving an overview of the functionality and a look at the chialisp, [here](https://drive.google.com/file/d/120Ky-LiDOOwsTEBtSmEKLBTChcrNUQda/view?usp=sharing) (Note the video quality is better if you download it... for some reason streaming it lowers the resolution making it unreadable) 6 | 7 | 8 | Coins locked with the NFT hold the usual key/value data as well as some simple state: 9 | * For sale/Not for sale 10 | * Price 11 | * Owner Puzzlehash 12 | * Owner Pubkey 13 | * Royalty percentage (immutable) 14 | * Creator puzzle hash (immutable) 15 | 16 | If the puzzle is flagged as for sale, anyone can buy the nft for the price set by the owner. When the transaction is made, the puzzle outputs conditions which pay the royalty percentage to the creator, the remainder to the owner, and recreates the nft with the details of the new owner. 17 | 18 | There is basic wallet functionality to identify coins marked as for-sale on the block chain, and keeping track of owned coins. It isn't built to work with offer files, just intended to be a simple way to have something on chain that you can interact with a bit. 19 | 20 | ![Screenshot](screenshot.png) 21 | 22 | 23 | ## Mainnet Installation 24 | To run the wallet on mainnet, you have to checkout the `setup_for_mainnet` branch. The key derivation for the wallet will work better once the chia `protocol_and_cats_rebased` branch is merged into chia main branch. 25 | 26 | ``` 27 | git clone https://github.com/geoffwalmsley/CreatorNFT.git 28 | cd CreatorNFT/ 29 | git checkout setup_for_mainnet 30 | pip install --editable . 31 | ``` 32 | 33 | To start the database run: 34 | 35 | ``` 36 | nft init 37 | ``` 38 | 39 | To see all the listed NFTs run: 40 | 41 | ``` 42 | nft list-for-sale 43 | ``` 44 | 45 | 46 | 47 | ### Testnet Installation 48 | 49 | To set up testnet10, best to follow the instructions for the CAT tutorial at chialisp.com. From there you can just use the venv you use for the protocol_and_cats_rebased branch. 50 | 51 | 52 | 53 | ``` 54 | git clone https://github.com/geoffwalmsley/CreatorNFT.git 55 | cd CreatorNFT/ 56 | pip install --editable . 57 | ``` 58 | 59 | Once that's done, make sure you're in the CreatorNFT directory, and you can start the DB and sync the current NFTs with: 60 | 61 | ``` 62 | nft init 63 | ``` 64 | 65 | 66 | ## Usage 67 | 68 | 69 | ``` 70 | # Launch a new NFT 71 | nft launch -d -r 10 -p 1200 -a 101 72 | 73 | # List owned NFTs 74 | nft list 75 | 76 | # List for-sale NFTS 77 | nft list-for-sale 78 | 79 | # View a specific NFT 80 | nft view -n 81 | 82 | # Update an owned nft 83 | nft update -n -p price --for-sale 84 | 85 | # Buy NFT 86 | nft buy -n 87 | ``` 88 | 89 | ## Testing 90 | 91 | For testing make sure to remove references to master_sk_to_wallet_sk_unhardened as its only available in the protovol_and_cats_branch. Tests need main branch to run. 92 | 93 | 94 | ``` 95 | pytest tests/ 96 | ``` 97 | 98 | ### License 99 | Copyright 2021 Geoff Walmsley 100 | 101 | Licensed under the Apache License, Version 2.0 (the "License"); 102 | you may not use these files except in compliance with the License. 103 | You may obtain a copy of the License at 104 | 105 | http://www.apache.org/licenses/LICENSE-2.0 106 | 107 | Unless required by applicable law or agreed to in writing, software 108 | distributed under the License is distributed on an "AS IS" BASIS, 109 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 110 | See the License for the specific language governing permissions and 111 | limitations under the License. 112 | -------------------------------------------------------------------------------- /tests/util/blockchain.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | from os import path 3 | from pathlib import Path 4 | from typing import List 5 | 6 | import aiosqlite 7 | 8 | from chia.consensus.blockchain import Blockchain 9 | from chia.consensus.constants import ConsensusConstants 10 | from chia.full_node.block_store import BlockStore 11 | from chia.full_node.coin_store import CoinStore 12 | from chia.full_node.hint_store import HintStore 13 | from chia.types.full_block import FullBlock 14 | from chia.util.db_wrapper import DBWrapper 15 | from chia.util.path import mkdir 16 | 17 | from tests.setup_nodes import bt 18 | 19 | blockchain_db_counter: int = 0 20 | 21 | 22 | async def create_blockchain(constants: ConsensusConstants, db_version: int): 23 | global blockchain_db_counter 24 | db_path = Path(f"blockchain_test-{blockchain_db_counter}.db") 25 | if db_path.exists(): 26 | db_path.unlink() 27 | blockchain_db_counter += 1 28 | connection = await aiosqlite.connect(db_path) 29 | wrapper = DBWrapper(connection, False, db_version) 30 | coin_store = await CoinStore.create(wrapper) 31 | store = await BlockStore.create(wrapper) 32 | hint_store = await HintStore.create(wrapper) 33 | bc1 = await Blockchain.create(coin_store, store, constants, hint_store, Path(".")) 34 | assert bc1.get_peak() is None 35 | return bc1, connection, db_path 36 | 37 | 38 | def persistent_blocks( 39 | num_of_blocks: int, 40 | db_name: str, 41 | seed: bytes = b"", 42 | empty_sub_slots=0, 43 | normalized_to_identity_cc_eos: bool = False, 44 | normalized_to_identity_icc_eos: bool = False, 45 | normalized_to_identity_cc_sp: bool = False, 46 | normalized_to_identity_cc_ip: bool = False, 47 | ): 48 | # try loading from disc, if not create new blocks.db file 49 | # TODO hash fixtures.py and blocktool.py, add to path, delete if the files changed 50 | block_path_dir = Path("~/.chia/blocks").expanduser() 51 | file_path = Path(f"~/.chia/blocks/{db_name}").expanduser() 52 | if not path.exists(block_path_dir): 53 | mkdir(block_path_dir.parent) 54 | mkdir(block_path_dir) 55 | 56 | if file_path.exists(): 57 | try: 58 | bytes_list = file_path.read_bytes() 59 | block_bytes_list: List[bytes] = pickle.loads(bytes_list) 60 | blocks: List[FullBlock] = [] 61 | for block_bytes in block_bytes_list: 62 | blocks.append(FullBlock.from_bytes(block_bytes)) 63 | if len(blocks) == num_of_blocks: 64 | print(f"\n loaded {file_path} with {len(blocks)} blocks") 65 | return blocks 66 | except EOFError: 67 | print("\n error reading db file") 68 | 69 | return new_test_db( 70 | file_path, 71 | num_of_blocks, 72 | seed, 73 | empty_sub_slots, 74 | normalized_to_identity_cc_eos, 75 | normalized_to_identity_icc_eos, 76 | normalized_to_identity_cc_sp, 77 | normalized_to_identity_cc_ip, 78 | ) 79 | 80 | 81 | def new_test_db( 82 | path: Path, 83 | num_of_blocks: int, 84 | seed: bytes, 85 | empty_sub_slots: int, 86 | normalized_to_identity_cc_eos: bool = False, # CC_EOS, 87 | normalized_to_identity_icc_eos: bool = False, # ICC_EOS 88 | normalized_to_identity_cc_sp: bool = False, # CC_SP, 89 | normalized_to_identity_cc_ip: bool = False, # CC_IP 90 | ): 91 | print(f"create {path} with {num_of_blocks} blocks with ") 92 | blocks: List[FullBlock] = bt.get_consecutive_blocks( 93 | num_of_blocks, 94 | seed=seed, 95 | skip_slots=empty_sub_slots, 96 | normalized_to_identity_cc_eos=normalized_to_identity_cc_eos, 97 | normalized_to_identity_icc_eos=normalized_to_identity_icc_eos, 98 | normalized_to_identity_cc_sp=normalized_to_identity_cc_sp, 99 | normalized_to_identity_cc_ip=normalized_to_identity_cc_ip, 100 | ) 101 | block_bytes_list: List[bytes] = [] 102 | for block in blocks: 103 | block_bytes_list.append(bytes(block)) 104 | bytes_fn = pickle.dumps(block_bytes_list) 105 | path.write_bytes(bytes_fn) 106 | return blocks 107 | -------------------------------------------------------------------------------- /tests/build-workflows.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Run from the current directory. 4 | 5 | import argparse 6 | import sys 7 | 8 | import testconfig 9 | import logging 10 | from pathlib import Path 11 | from typing import Dict, List 12 | 13 | root_path = Path(__file__).parent.resolve() 14 | 15 | 16 | def subdirs() -> List[Path]: 17 | dirs: List[Path] = [] 18 | for r in root_path.iterdir(): 19 | if r.is_dir(): 20 | dirs.extend(Path(r).rglob("**/")) 21 | return [d for d in dirs if not (any(c.startswith("_") for c in d.parts) or any(c.startswith(".") for c in d.parts))] 22 | 23 | 24 | def module_dict(module): 25 | return {k: v for k, v in module.__dict__.items() if not k.startswith("_")} 26 | 27 | 28 | def dir_config(dir): 29 | import importlib 30 | 31 | module_name = ".".join([*dir.relative_to(root_path).parts, "config"]) 32 | try: 33 | return module_dict(importlib.import_module(module_name)) 34 | except ModuleNotFoundError: 35 | return {} 36 | 37 | 38 | def read_file(filename: Path) -> str: 39 | return filename.read_bytes().decode("utf8") 40 | 41 | 42 | # Input file 43 | def workflow_yaml_template_text(os): 44 | return read_file(Path(root_path / f"runner-templates/build-test-{os}")) 45 | 46 | 47 | # Output files 48 | def workflow_yaml_file(dir, os, test_name): 49 | return Path(dir / f"build-test-{os}-{test_name}.yml") 50 | 51 | 52 | # String function from test dir to test name 53 | def test_name(dir): 54 | return "-".join(dir.relative_to(root_path).parts) 55 | 56 | 57 | def transform_template(template_text, replacements): 58 | t = template_text 59 | for r, v in replacements.items(): 60 | t = t.replace(r, v) 61 | return t 62 | 63 | 64 | # Replace with update_config 65 | def generate_replacements(conf, dir): 66 | replacements = { 67 | "INSTALL_TIMELORD": read_file(Path(root_path / "runner-templates/install-timelord.include.yml")).rstrip(), 68 | "CHECKOUT_TEST_BLOCKS_AND_PLOTS": read_file( 69 | Path(root_path / "runner-templates/checkout-test-plots.include.yml") 70 | ).rstrip(), 71 | "TEST_DIR": "", 72 | "TEST_NAME": "", 73 | "PYTEST_PARALLEL_ARGS": "", 74 | } 75 | 76 | if not conf["checkout_blocks_and_plots"]: 77 | replacements[ 78 | "CHECKOUT_TEST_BLOCKS_AND_PLOTS" 79 | ] = "# Omitted checking out blocks and plots repo Chia-Network/test-cache" 80 | if not conf["install_timelord"]: 81 | replacements["INSTALL_TIMELORD"] = "# Omitted installing Timelord" 82 | if conf["parallel"]: 83 | replacements["PYTEST_PARALLEL_ARGS"] = " -n auto" 84 | if conf["job_timeout"]: 85 | replacements["JOB_TIMEOUT"] = str(conf["job_timeout"]) 86 | replacements["TEST_DIR"] = "/".join([*dir.relative_to(root_path.parent).parts, "test_*.py"]) 87 | replacements["TEST_NAME"] = test_name(dir) 88 | if "test_name" in conf: 89 | replacements["TEST_NAME"] = conf["test_name"] 90 | for var in conf["custom_vars"]: 91 | replacements[var] = conf[var] if var in conf else "" 92 | return replacements 93 | 94 | 95 | # Overwrite with directory specific values 96 | def update_config(parent, child): 97 | if child is None: 98 | return parent 99 | conf = child 100 | for k, v in parent.items(): 101 | if k not in child: 102 | conf[k] = v 103 | return conf 104 | 105 | 106 | def dir_path(string): 107 | p = Path(root_path / string) 108 | if p.is_dir(): 109 | return p 110 | else: 111 | raise NotADirectoryError(string) 112 | 113 | 114 | # args 115 | arg_parser = argparse.ArgumentParser(description="Build github workflows") 116 | arg_parser.add_argument("--output-dir", "-d", default="../.github/workflows", type=dir_path) 117 | arg_parser.add_argument("--fail-on-update", "-f", action="store_true") 118 | arg_parser.add_argument("--verbose", "-v", action="store_true") 119 | args = arg_parser.parse_args() 120 | 121 | if args.verbose: 122 | logging.basicConfig(format="%(asctime)s:%(message)s", level=logging.DEBUG) 123 | 124 | # main 125 | test_dirs = subdirs() 126 | current_workflows: Dict[Path, str] = {file: read_file(file) for file in args.output_dir.iterdir()} 127 | changed: bool = False 128 | 129 | for os in testconfig.oses: 130 | template_text = workflow_yaml_template_text(os) 131 | for dir in test_dirs: 132 | if len([f for f in Path(root_path / dir).glob("test_*.py")]) == 0: 133 | logging.info(f"Skipping {dir}: no tests collected") 134 | continue 135 | conf = update_config(module_dict(testconfig), dir_config(dir)) 136 | replacements = generate_replacements(conf, dir) 137 | txt = transform_template(template_text, replacements) 138 | logging.info(f"Writing {os}-{test_name(dir)}") 139 | workflow_yaml_path: Path = workflow_yaml_file(args.output_dir, os, test_name(dir)) 140 | if workflow_yaml_path not in current_workflows or current_workflows[workflow_yaml_path] != txt: 141 | changed = True 142 | workflow_yaml_path.write_bytes(txt.encode("utf8")) 143 | 144 | if changed: 145 | print("New workflow updates available.") 146 | if args.fail_on_update: 147 | sys.exit(1) 148 | else: 149 | print("Nothing to do.") 150 | -------------------------------------------------------------------------------- /nft.py: -------------------------------------------------------------------------------- 1 | import click 2 | import asyncio 3 | from functools import wraps 4 | from pathlib import Path 5 | 6 | from chia.util.byte_types import hexstr_to_bytes 7 | 8 | from nft_manager import NFTManager 9 | from nft_wallet import NFT 10 | 11 | CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"]) 12 | 13 | 14 | def coro(f): 15 | @wraps(f) 16 | def wrapper(*args, **kwargs): 17 | return asyncio.run(f(*args, **kwargs)) 18 | 19 | return wrapper 20 | 21 | 22 | def print_nft(nft: NFT): 23 | print("\n") 24 | print("-" * 64) 25 | print(f"NFT ID:\n{nft.launcher_id.hex()}\n") 26 | print(f"Owner: {nft.owner_fingerprint()}") 27 | if nft.is_for_sale(): 28 | print("Status: For Sale") 29 | else: 30 | print("Status: Not for sale") 31 | print(f"Price: {nft.price()}") 32 | print(f"Royalty: {nft.royalty_pc()}%\n") 33 | print(f"Chialisp: {str(nft.data[0])}\n") 34 | print(f"Data:\n") 35 | print(nft.data[1].decode("utf-8")) 36 | print("-" * 64) 37 | print("\n") 38 | 39 | 40 | @click.group( 41 | help=f"\n CreatorNFT v0.1\n", 42 | epilog="Try 'nft list' or 'nft sale' to see some NFTs", 43 | context_settings=CONTEXT_SETTINGS, 44 | ) 45 | @click.pass_context 46 | def cli(ctx: click.Context): 47 | ctx.ensure_object(dict) 48 | 49 | 50 | @cli.command("init", short_help="Start the nft database") 51 | @coro 52 | async def init_cmd(): 53 | manager = NFTManager() 54 | await manager.connect() 55 | await manager.sync() 56 | await manager.close() 57 | 58 | 59 | @cli.command("view", short_help="View a single NFT by id") 60 | @click.option("-n", "--nft-id", required=True, type=str) 61 | @click.pass_context 62 | @coro 63 | async def view_cmd(ctx, nft_id): 64 | manager = NFTManager() 65 | await manager.connect() 66 | nft = await manager.view_nft(hexstr_to_bytes(nft_id)) 67 | if nft: 68 | print_nft(nft) 69 | else: 70 | print(f"\nNo record found for:\n{nft_id}") 71 | await manager.close() 72 | 73 | 74 | @cli.command("list", short_help="Show CreatorNFT version") 75 | @click.pass_context 76 | @coro 77 | async def list_cmd(ctx) -> None: 78 | manager = NFTManager() 79 | await manager.connect() 80 | nfts = await manager.get_my_nfts() 81 | await manager.close() 82 | for nft in nfts: 83 | print_nft(nft) 84 | 85 | 86 | @cli.command("list-for-sale", short_help="Show some NFTs for sale") 87 | @click.pass_context 88 | @coro 89 | async def sale_cmd(ctx) -> None: 90 | manager = NFTManager() 91 | await manager.connect() 92 | nfts = await manager.get_for_sale_nfts() 93 | for nft in nfts: 94 | print_nft(nft) 95 | await manager.close() 96 | 97 | 98 | @cli.command("launch", short_help="Launch a new NFT") 99 | @click.option("-d", "--data", required=True, type=str) 100 | @click.option("-r", "--royalty", required=True, type=int) 101 | @click.option("-a", "--amount", type=int, default=101) 102 | @click.option("-p", "--price", type=int, default=1000) 103 | @click.option("--for-sale/--not-for-sale", type=bool, default=False) 104 | @click.pass_context 105 | @coro 106 | async def launch_cmd(ctx, data, royalty, amount, price, for_sale) -> None: 107 | assert price > 0 108 | assert amount % 2 == 1 109 | 110 | manager = NFTManager() 111 | await manager.connect() 112 | with open(Path(data), "r") as f: 113 | datastr = f.readlines() 114 | nft_data = ("CreatorNFT", "".join(datastr)) 115 | if for_sale: 116 | launch_state = [10, price] 117 | else: 118 | launch_state = [0, price] 119 | royalty = [royalty] 120 | tx_id, launcher_id = await manager.launch_nft(amount, nft_data, launch_state, royalty) 121 | print(f"Transaction id: {tx_id}") 122 | nft = await manager.wait_for_confirmation(tx_id, launcher_id) 123 | print("\n\n NFT Launched!!") 124 | print_nft(nft) 125 | await manager.close() 126 | 127 | 128 | @cli.command("update", short_help="Update one of your NFTs") 129 | @click.option("-n", "--nft-id", required=True, type=str) 130 | @click.option("-p", "--price", required=True, type=int) 131 | @click.option("--for-sale/--not-for-sale", required=True, type=bool, default=False) 132 | @click.pass_context 133 | @coro 134 | async def update_cmd(ctx, nft_id, price, for_sale): 135 | assert price > 0 136 | manager = NFTManager() 137 | await manager.connect() 138 | if for_sale: 139 | new_state = [10, price] 140 | else: 141 | new_state = [0, price] 142 | tx_id = await manager.update_nft(hexstr_to_bytes(nft_id), new_state) 143 | print(f"Transaction id: {tx_id}") 144 | nft = await manager.wait_for_confirmation(tx_id, hexstr_to_bytes(nft_id)) 145 | print("\n\n NFT Updated!!") 146 | print_nft(nft) 147 | await manager.close() 148 | 149 | 150 | @cli.command("buy", short_help="Update one of your NFTs") 151 | @click.option("-n", "--nft-id", required=True, type=str) 152 | @click.option("-p", "--price", required=True, type=int) 153 | @click.option("--for-sale/--not-for-sale", required=True, type=bool, default=False) 154 | @click.pass_context 155 | @coro 156 | async def buy_cmd(ctx, nft_id, price, for_sale): 157 | assert price > 0 158 | manager = NFTManager() 159 | await manager.connect() 160 | if for_sale: 161 | new_state = [10, price] 162 | else: 163 | new_state = [0, price] 164 | tx_id = await manager.buy_nft(hexstr_to_bytes(nft_id), new_state) 165 | print(f"Transaction id: {tx_id}") 166 | nft = await manager.wait_for_confirmation(tx_id, hexstr_to_bytes(nft_id)) 167 | print("\n\n NFT Purchased!!") 168 | print_nft(nft) 169 | await manager.close() 170 | 171 | 172 | def monkey_patch_click() -> None: 173 | import click.core 174 | 175 | click.core._verify_python3_env = lambda *args, **kwargs: 0 # type: ignore[attr-defined] 176 | 177 | 178 | def main() -> None: 179 | monkey_patch_click() 180 | asyncio.run(cli()) 181 | 182 | 183 | if __name__ == "__main__": 184 | main() 185 | -------------------------------------------------------------------------------- /tests/util/build_network_protocol_files.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | import os 4 | from pathlib import Path 5 | from chia.util.streamable import Streamable, streamable 6 | from tests.util.network_protocol_data import * 7 | from chia.util.ints import uint32 8 | 9 | version = "1.0" 10 | 11 | 12 | def get_network_protocol_filename() -> Path: 13 | tests_dir = Path(os.path.dirname(os.path.abspath(__file__))) 14 | return tests_dir / Path("protocol_messages_bytes-v" + version) 15 | 16 | 17 | def encode_data(data) -> bytes: 18 | data_bytes = bytes(data) 19 | size = uint32(len(data_bytes)) 20 | return size.to_bytes(4, "big") + data_bytes 21 | 22 | 23 | def get_farmer_protocol_bytes() -> bytes: 24 | result = b"" 25 | result += encode_data(new_signage_point) 26 | result += encode_data(declare_proof_of_space) 27 | result += encode_data(request_signed_values) 28 | result += encode_data(farming_info) 29 | result += encode_data(signed_values) 30 | return result 31 | 32 | 33 | def get_full_node_bytes() -> bytes: 34 | result = b"" 35 | result += encode_data(new_peak) 36 | result += encode_data(new_transaction) 37 | result += encode_data(request_transaction) 38 | result += encode_data(respond_transaction) 39 | result += encode_data(request_proof_of_weight) 40 | result += encode_data(respond_proof_of_weight) 41 | result += encode_data(request_block) 42 | result += encode_data(reject_block) 43 | result += encode_data(request_blocks) 44 | result += encode_data(respond_blocks) 45 | result += encode_data(reject_blocks) 46 | result += encode_data(respond_block) 47 | result += encode_data(new_unfinished_block) 48 | result += encode_data(request_unfinished_block) 49 | result += encode_data(respond_unfinished_block) 50 | result += encode_data(new_signage_point_or_end_of_subslot) 51 | result += encode_data(request_signage_point_or_end_of_subslot) 52 | result += encode_data(respond_signage_point) 53 | result += encode_data(respond_end_of_subslot) 54 | result += encode_data(request_mempool_transaction) 55 | result += encode_data(new_compact_vdf) 56 | result += encode_data(request_compact_vdf) 57 | result += encode_data(respond_compact_vdf) 58 | result += encode_data(request_peers) 59 | result += encode_data(respond_peers) 60 | return result 61 | 62 | 63 | def get_wallet_protocol_bytes() -> bytes: 64 | result = b"" 65 | result += encode_data(request_puzzle_solution) 66 | result += encode_data(puzzle_solution_response) 67 | result += encode_data(respond_puzzle_solution) 68 | result += encode_data(reject_puzzle_solution) 69 | result += encode_data(send_transaction) 70 | result += encode_data(transaction_ack) 71 | result += encode_data(new_peak_wallet) 72 | result += encode_data(request_block_header) 73 | result += encode_data(respond_header_block) 74 | result += encode_data(reject_header_request) 75 | result += encode_data(request_removals) 76 | result += encode_data(respond_removals) 77 | result += encode_data(reject_removals_request) 78 | result += encode_data(request_additions) 79 | result += encode_data(respond_additions) 80 | result += encode_data(reject_additions) 81 | result += encode_data(request_header_blocks) 82 | result += encode_data(reject_header_blocks) 83 | result += encode_data(respond_header_blocks) 84 | result += encode_data(coin_state) 85 | result += encode_data(register_for_ph_updates) 86 | result += encode_data(respond_to_ph_updates) 87 | result += encode_data(register_for_coin_updates) 88 | result += encode_data(respond_to_coin_updates) 89 | result += encode_data(coin_state_update) 90 | result += encode_data(request_children) 91 | result += encode_data(respond_children) 92 | result += encode_data(request_ses_info) 93 | result += encode_data(respond_ses_info) 94 | return result 95 | 96 | 97 | def get_harvester_protocol_bytes() -> bytes: 98 | result = b"" 99 | result += encode_data(pool_difficulty) 100 | result += encode_data(harvester_handhsake) 101 | result += encode_data(new_signage_point_harvester) 102 | result += encode_data(new_proof_of_space) 103 | result += encode_data(request_signatures) 104 | result += encode_data(respond_signatures) 105 | result += encode_data(plot) 106 | result += encode_data(request_plots) 107 | result += encode_data(respond_plots) 108 | return result 109 | 110 | 111 | def get_introducer_protocol_bytes() -> bytes: 112 | result = b"" 113 | result += encode_data(request_peers_introducer) 114 | result += encode_data(respond_peers_introducer) 115 | return result 116 | 117 | 118 | def get_pool_protocol_bytes() -> bytes: 119 | result = b"" 120 | result += encode_data(authentication_payload) 121 | result += encode_data(get_pool_info_response) 122 | result += encode_data(post_partial_payload) 123 | result += encode_data(post_partial_request) 124 | result += encode_data(post_partial_response) 125 | result += encode_data(get_farmer_response) 126 | result += encode_data(post_farmer_payload) 127 | result += encode_data(post_farmer_request) 128 | result += encode_data(post_farmer_response) 129 | result += encode_data(put_farmer_payload) 130 | result += encode_data(put_farmer_request) 131 | result += encode_data(put_farmer_response) 132 | result += encode_data(error_response) 133 | return result 134 | 135 | 136 | def get_timelord_protocol_bytes() -> bytes: 137 | result = b"" 138 | result += encode_data(new_peak_timelord) 139 | result += encode_data(new_unfinished_block_timelord) 140 | result += encode_data(new_infusion_point_vdf) 141 | result += encode_data(new_signage_point_vdf) 142 | result += encode_data(new_end_of_sub_slot_bundle) 143 | result += encode_data(request_compact_proof_of_time) 144 | result += encode_data(respond_compact_proof_of_time) 145 | return result 146 | 147 | 148 | def get_protocol_bytes() -> bytes: 149 | return ( 150 | get_farmer_protocol_bytes() 151 | + get_full_node_bytes() 152 | + get_wallet_protocol_bytes() 153 | + get_harvester_protocol_bytes() 154 | + get_introducer_protocol_bytes() 155 | + get_pool_protocol_bytes() 156 | + get_timelord_protocol_bytes() 157 | ) 158 | 159 | 160 | if __name__ == "__main__": 161 | filename = get_network_protocol_filename() 162 | data = get_protocol_bytes() 163 | if os.path.exists(filename): 164 | print("Deleting old file.") 165 | os.remove(filename) 166 | f = open(filename, "wb") 167 | f.write(data) 168 | f.close() 169 | print(f"Written {len(data)} bytes.") 170 | -------------------------------------------------------------------------------- /tests/util/benchmark_cost.py: -------------------------------------------------------------------------------- 1 | import time 2 | from secrets import token_bytes 3 | 4 | from blspy import AugSchemeMPL, PrivateKey 5 | from clvm_tools import binutils 6 | 7 | from chia.consensus.default_constants import DEFAULT_CONSTANTS 8 | from chia.types.blockchain_format.program import Program, INFINITE_COST 9 | from chia.types.condition_opcodes import ConditionOpcode 10 | from chia.types.condition_with_args import ConditionWithArgs 11 | from chia.util.ints import uint32 12 | from tests.wallet_tools import WalletTool 13 | from chia.wallet.derive_keys import master_sk_to_wallet_sk 14 | from chia.wallet.puzzles.p2_delegated_puzzle import puzzle_for_pk 15 | 16 | 17 | def float_to_str(f): 18 | float_string = repr(f) 19 | if "e" in float_string: # detect scientific notation 20 | digits, exp_str = float_string.split("e") 21 | digits = digits.replace(".", "").replace("-", "") 22 | exp = int(exp_str) 23 | zero_padding = "0" * (abs(int(exp)) - 1) # minus 1 for decimal point in the sci notation 24 | sign = "-" if f < 0 else "" 25 | if exp > 0: 26 | float_string = "{}{}{}.0".format(sign, digits, zero_padding) 27 | else: 28 | float_string = "{}0.{}{}".format(sign, zero_padding, digits) 29 | return float_string 30 | 31 | 32 | def run_and_return_cost_time(chialisp): 33 | 34 | start = time.time() 35 | clvm_loop = "((c (q ((c (f (a)) (c (f (a)) (c (f (r (a))) (c (f (r (r (a))))" 36 | " (q ()))))))) (c (q ((c (i (f (r (a))) (q (i (q 1) ((c (f (a)) (c (f (a))" 37 | " (c (- (f (r (a))) (q 1)) (c (f (r (r (a)))) (q ()))))))" 38 | " ((c (f (r (r (a)))) (q ()))))) (q (q ()))) (a)))) (a))))" 39 | loop_program = Program.to(binutils.assemble(clvm_loop)) 40 | clvm_loop_solution = f"(1000 {chialisp})" 41 | solution_program = Program.to(binutils.assemble(clvm_loop_solution)) 42 | 43 | cost, sexp = loop_program.run_with_cost(solution_program, INFINITE_COST) 44 | 45 | end = time.time() 46 | total_time = end - start 47 | 48 | return cost, total_time 49 | 50 | 51 | def get_cost_compared_to_addition(addition_cost, addition_time, other_time): 52 | return (addition_cost * other_time) / addition_time 53 | 54 | 55 | def benchmark_all_operators(): 56 | addition = "(+ (q 1000000000) (q 1000000000))" 57 | substraction = "(- (q 1000000000) (q 1000000000))" 58 | multiply = "(* (q 1000000000) (q 1000000000))" 59 | greater = "(> (q 1000000000) (q 1000000000))" 60 | equal = "(= (q 1000000000) (q 1000000000))" 61 | if_clvm = "(i (= (q 1000000000) (q 1000000000)) (q 1000000000) (q 1000000000))" 62 | sha256tree = "(sha256 (q 1000000000))" 63 | pubkey_for_exp = "(pubkey_for_exp (q 1))" 64 | point_add = "(point_add" 65 | " (q 0x17f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb)" 66 | " (q 0x17f1d3a73197d7942695638c4fa9ac0fc3688c4f9774b905a14e3a3f171bac586c55e83ff97a1aeffb3af00adb22c6bb))" 67 | point_add_cost, point_add_time = run_and_return_cost_time(point_add) 68 | addition_cost, addition_time = run_and_return_cost_time(addition) 69 | substraction_cost, substraction_time = run_and_return_cost_time(substraction) 70 | multiply_cost, multiply_time = run_and_return_cost_time(multiply) 71 | greater_cost, greater_time = run_and_return_cost_time(greater) 72 | equal_cost, equal_time = run_and_return_cost_time(equal) 73 | if_cost, if_time = run_and_return_cost_time(if_clvm) 74 | sha256tree_cost, sha256tree_time = run_and_return_cost_time(sha256tree) 75 | pubkey_for_exp_cost, pubkey_for_exp_time = run_and_return_cost_time(pubkey_for_exp) 76 | 77 | one_addition = 1 78 | one_substraction = get_cost_compared_to_addition(addition_cost, addition_time, substraction_time) / addition_cost 79 | one_multiply = get_cost_compared_to_addition(addition_cost, addition_time, multiply_time) / addition_cost 80 | one_greater = get_cost_compared_to_addition(addition_cost, addition_time, greater_time) / addition_cost 81 | one_equal = get_cost_compared_to_addition(addition_cost, addition_time, equal_time) / addition_cost 82 | one_if = get_cost_compared_to_addition(addition_cost, addition_time, if_time) / addition_cost 83 | one_sha256 = get_cost_compared_to_addition(addition_cost, addition_time, sha256tree_time) / addition_cost 84 | one_pubkey_for_exp = ( 85 | get_cost_compared_to_addition(addition_cost, addition_time, pubkey_for_exp_time) / addition_cost 86 | ) 87 | one_point_add = get_cost_compared_to_addition(addition_cost, addition_time, point_add_time) / addition_cost 88 | 89 | print(f"cost of addition is: {one_addition}") 90 | print(f"cost of one_substraction is: {one_substraction}") 91 | print(f"cost of one_multiply is: {one_multiply}") 92 | print(f"cost of one_greater is: {one_greater}") 93 | print(f"cost of one_equal is: {one_equal}") 94 | print(f"cost of one_if is: {one_if}") 95 | print(f"cost of one_sha256 is: {one_sha256}") 96 | print(f"cost of one_pubkey_for_exp is: {one_pubkey_for_exp}") 97 | print(f"cost of one_point_add is: {one_point_add}") 98 | 99 | 100 | if __name__ == "__main__": 101 | """ 102 | Naive way to calculate cost ratio between vByte and CLVM cost unit. 103 | AggSig has assigned cost of 20vBytes, simple CLVM program is benchmarked against it. 104 | """ 105 | wallet_tool = WalletTool(DEFAULT_CONSTANTS) 106 | benchmark_all_operators() 107 | secret_key: PrivateKey = AugSchemeMPL.key_gen(bytes([2] * 32)) 108 | puzzles = [] 109 | solutions = [] 110 | private_keys = [] 111 | public_keys = [] 112 | 113 | for i in range(0, 1000): 114 | private_key: PrivateKey = master_sk_to_wallet_sk(secret_key, uint32(i)) 115 | public_key = private_key.public_key() 116 | solution = wallet_tool.make_solution( 117 | {ConditionOpcode.ASSERT_MY_COIN_ID: [ConditionWithArgs(ConditionOpcode.ASSERT_MY_COIN_ID, [token_bytes()])]} 118 | ) 119 | puzzle = puzzle_for_pk(bytes(public_key)) 120 | puzzles.append(puzzle) 121 | solutions.append(solution) 122 | private_keys.append(private_key) 123 | public_keys.append(public_key) 124 | 125 | # Run Puzzle 1000 times 126 | puzzle_start = time.time() 127 | clvm_cost = 0 128 | for i in range(0, 1000): 129 | cost_run, sexp = puzzles[i].run_with_cost(solutions[i], INFINITE_COST) 130 | clvm_cost += cost_run 131 | 132 | puzzle_end = time.time() 133 | puzzle_time = puzzle_end - puzzle_start 134 | print(f"Puzzle_time is: {puzzle_time}") 135 | print(f"Puzzle cost sum is: {clvm_cost}") 136 | 137 | private_key = master_sk_to_wallet_sk(secret_key, uint32(0)) 138 | public_key = private_key.get_g1() 139 | message = token_bytes() 140 | signature = AugSchemeMPL.sign(private_key, message) 141 | pk_message_pair = (public_key, message) 142 | 143 | # Run AggSig 1000 times 144 | agg_sig_start = time.time() 145 | agg_sig_cost = 0 146 | for i in range(0, 1000): 147 | valid = AugSchemeMPL.verify(public_key, message, signature) 148 | assert valid 149 | agg_sig_cost += 20 150 | agg_sig_end = time.time() 151 | agg_sig_time = agg_sig_end - agg_sig_start 152 | print(f"Aggsig Cost: {agg_sig_cost}") 153 | print(f"Aggsig time is: {agg_sig_time}") 154 | 155 | # clvm_should_cost = agg_sig_cost * puzzle_time / agg_sig_time 156 | clvm_should_cost = (agg_sig_cost * puzzle_time) / agg_sig_time 157 | print(f"Puzzle should cost: {clvm_should_cost}") 158 | constant = clvm_should_cost / clvm_cost 159 | format = float_to_str(constant) 160 | print(f"Constant factor: {format}") 161 | print(f"CLVM RATIO MULTIPLIER: {1/constant}") 162 | -------------------------------------------------------------------------------- /tests/util/keyring.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import tempfile 4 | 5 | from chia.util.file_keyring import FileKeyring 6 | from chia.util.keychain import Keychain, default_keychain_service, default_keychain_user, get_private_key_user 7 | from chia.util.keyring_wrapper import KeyringWrapper 8 | from functools import wraps 9 | from keyring.util import platform_ 10 | from keyrings.cryptfile.cryptfile import CryptFileKeyring # pyright: reportMissingImports=false 11 | from pathlib import Path 12 | from typing import Any, Optional 13 | from unittest.mock import patch 14 | 15 | 16 | def create_empty_cryptfilekeyring() -> CryptFileKeyring: 17 | """ 18 | Create an empty legacy keyring 19 | """ 20 | crypt_file_keyring = CryptFileKeyring() 21 | fd = os.open(crypt_file_keyring.file_path, os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o600) 22 | os.close(fd) 23 | assert Path(crypt_file_keyring.file_path).exists() 24 | return crypt_file_keyring 25 | 26 | 27 | def add_dummy_key_to_cryptfilekeyring(crypt_file_keyring: CryptFileKeyring): 28 | """ 29 | Add a fake key to the CryptFileKeyring 30 | """ 31 | crypt_file_keyring.keyring_key = "your keyring password" 32 | user: str = get_private_key_user(default_keychain_user(), 0) 33 | crypt_file_keyring.set_password(default_keychain_service(), user, "abc123") 34 | 35 | 36 | def setup_mock_file_keyring(mock_configure_backend, temp_file_keyring_dir, populate=False): 37 | if populate: 38 | # Populate the file keyring with an empty (but encrypted) data set 39 | file_keyring_path = FileKeyring.keyring_path_from_root(Path(temp_file_keyring_dir)) 40 | os.makedirs(os.path.dirname(file_keyring_path), 0o700, True) 41 | with open( 42 | os.open( 43 | FileKeyring.keyring_path_from_root(Path(temp_file_keyring_dir)), 44 | os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 45 | 0o600, 46 | ), 47 | "w", 48 | ) as f: 49 | f.write( 50 | # Encrypted using DEFAULT_PASSPHRASE_IF_NO_MASTER_PASSPHRASE. Data holds an empty keyring. 51 | "data: xtcxYOWtbeO9ruv4Nkwhw1pcTJCNh/fvPSdFxez/L0ysnag=\n" 52 | "nonce: 17ecac58deb7a392fccef49e\n" 53 | "salt: b1aa32d5730288d653e82017e4a4057c\n" 54 | "version: 1" 55 | ) 56 | 57 | # Create the file keyring 58 | mock_configure_backend.return_value = FileKeyring(keys_root_path=Path(temp_file_keyring_dir)) 59 | 60 | 61 | def using_temp_file_keyring(populate=False): 62 | """ 63 | Decorator that will create a temporary directory with a temporary keyring that is 64 | automatically cleaned-up after invoking the decorated function. If `populate` is 65 | true, the newly created keyring will be populated with a payload containing 0 keys 66 | using the default passphrase. 67 | """ 68 | 69 | def outer(method): 70 | @wraps(method) 71 | def inner(self, *args, **kwargs): 72 | with TempKeyring(populate=populate): 73 | return method(self, *args, **kwargs) 74 | 75 | return inner 76 | 77 | return outer 78 | 79 | 80 | def using_temp_file_keyring_and_cryptfilekeyring(populate=False): 81 | """ 82 | Like the `using_temp_file_keyring` decorator, this decorator will create a temp 83 | dir and temp keyring. Additionally, an empty legacy Cryptfile keyring will be 84 | created in the temp directory. 85 | """ 86 | 87 | def outer(method): 88 | @wraps(method) 89 | def inner(self, *args, **kwargs): 90 | with TempKeyring(populate=populate, setup_cryptfilekeyring=True): 91 | return method(self, *args, **kwargs) 92 | 93 | return inner 94 | 95 | return outer 96 | 97 | 98 | class TempKeyring: 99 | def __init__( 100 | self, 101 | *, 102 | user: str = "testing-1.8.0", 103 | service: str = "testing-chia-1.8.0", 104 | populate: bool = False, 105 | setup_cryptfilekeyring: bool = False, 106 | existing_keyring_path: str = None, 107 | delete_on_cleanup: bool = True, 108 | use_os_credential_store: bool = False, 109 | ): 110 | self.keychain = self._patch_and_create_keychain( 111 | user=user, 112 | service=service, 113 | populate=populate, 114 | existing_keyring_path=existing_keyring_path, 115 | use_os_credential_store=use_os_credential_store, 116 | setup_cryptfilekeyring=setup_cryptfilekeyring, 117 | ) 118 | self.delete_on_cleanup = delete_on_cleanup 119 | self.cleaned_up = False 120 | 121 | def _patch_and_create_keychain( 122 | self, 123 | *, 124 | user: str, 125 | service: str, 126 | populate: bool, 127 | setup_cryptfilekeyring: bool, 128 | existing_keyring_path: Optional[str], 129 | use_os_credential_store: bool, 130 | ): 131 | existing_keyring_dir = Path(existing_keyring_path).parent if existing_keyring_path else None 132 | temp_dir = existing_keyring_dir or tempfile.mkdtemp(prefix="test_keyring_wrapper") 133 | 134 | mock_supports_keyring_passphrase_patch = patch("chia.util.keychain.supports_keyring_passphrase") 135 | mock_supports_keyring_passphrase = mock_supports_keyring_passphrase_patch.start() 136 | 137 | # Patch supports_keyring_passphrase() to return True 138 | mock_supports_keyring_passphrase.return_value = True 139 | 140 | mock_supports_os_passphrase_storage_patch = patch("chia.util.keychain.supports_os_passphrase_storage") 141 | mock_supports_os_passphrase_storage = mock_supports_os_passphrase_storage_patch.start() 142 | 143 | # Patch supports_os_passphrase_storage() to return use_os_credential_store 144 | mock_supports_os_passphrase_storage.return_value = use_os_credential_store 145 | 146 | mock_configure_backend_patch = patch.object(KeyringWrapper, "_configure_backend") 147 | mock_configure_backend = mock_configure_backend_patch.start() 148 | setup_mock_file_keyring(mock_configure_backend, temp_dir, populate=populate) 149 | 150 | mock_configure_legacy_backend_patch: Any = None 151 | if setup_cryptfilekeyring is False: 152 | mock_configure_legacy_backend_patch = patch.object(KeyringWrapper, "_configure_legacy_backend") 153 | mock_configure_legacy_backend = mock_configure_legacy_backend_patch.start() 154 | mock_configure_legacy_backend.return_value = None 155 | 156 | mock_data_root_patch = patch.object(platform_, "data_root") 157 | mock_data_root = mock_data_root_patch.start() 158 | 159 | # Mock CryptFileKeyring's file_path indirectly by changing keyring.util.platform_.data_root 160 | # We don't want CryptFileKeyring finding the real legacy keyring 161 | mock_data_root.return_value = temp_dir 162 | 163 | if setup_cryptfilekeyring is True: 164 | crypt_file_keyring = create_empty_cryptfilekeyring() 165 | add_dummy_key_to_cryptfilekeyring(crypt_file_keyring) 166 | 167 | keychain = Keychain(user=user, service=service) 168 | keychain.keyring_wrapper = KeyringWrapper(keys_root_path=Path(temp_dir)) 169 | 170 | # Stash the temp_dir in the keychain instance 171 | keychain._temp_dir = temp_dir # type: ignore 172 | 173 | # Stash the patches in the keychain instance 174 | keychain._mock_supports_keyring_passphrase_patch = mock_supports_keyring_passphrase_patch # type: ignore 175 | keychain._mock_supports_os_passphrase_storage_patch = mock_supports_os_passphrase_storage_patch # type: ignore 176 | keychain._mock_configure_backend_patch = mock_configure_backend_patch # type: ignore 177 | keychain._mock_configure_legacy_backend_patch = mock_configure_legacy_backend_patch # type: ignore 178 | keychain._mock_data_root_patch = mock_data_root_patch # type: ignore 179 | 180 | return keychain 181 | 182 | def __enter__(self): 183 | assert not self.cleaned_up 184 | return self.get_keychain() 185 | 186 | def __exit__(self, exc_type, exc_value, exc_tb): 187 | self.cleanup() 188 | 189 | def get_keychain(self): 190 | return self.keychain 191 | 192 | def cleanup(self): 193 | assert not self.cleaned_up 194 | 195 | if self.delete_on_cleanup: 196 | self.keychain.keyring_wrapper.keyring.cleanup_keyring_file_watcher() 197 | temp_dir = self.keychain._temp_dir 198 | print(f"Cleaning up temp keychain in dir: {temp_dir}") 199 | shutil.rmtree(temp_dir) 200 | 201 | self.keychain._mock_supports_keyring_passphrase_patch.stop() 202 | self.keychain._mock_supports_os_passphrase_storage_patch.stop() 203 | self.keychain._mock_configure_backend_patch.stop() 204 | if self.keychain._mock_configure_legacy_backend_patch is not None: 205 | self.keychain._mock_configure_legacy_backend_patch.stop() 206 | self.keychain._mock_data_root_patch.stop() 207 | 208 | self.cleaned_up = True 209 | -------------------------------------------------------------------------------- /driver.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Dict, List, Tuple, Optional, Union, Any 3 | 4 | from chia.types.blockchain_format.coin import Coin 5 | from chia.types.spend_bundle import SpendBundle 6 | from chia.types.blockchain_format.program import Program, SerializedProgram 7 | from chia.util.hash import std_hash 8 | from clvm.casts import int_to_bytes, int_from_bytes 9 | from chia.util.byte_types import hexstr_to_bytes 10 | from chia.consensus.default_constants import DEFAULT_CONSTANTS 11 | from chia.wallet.puzzles.load_clvm import load_clvm 12 | from chia.util.condition_tools import ConditionOpcode 13 | from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( # standard_transaction 14 | puzzle_for_pk, 15 | calculate_synthetic_secret_key, 16 | DEFAULT_HIDDEN_PUZZLE_HASH, 17 | ) 18 | from chia.types.blockchain_format.sized_bytes import bytes32 19 | from chia.wallet.derive_keys import master_sk_to_wallet_sk 20 | from chia.types.coin_spend import CoinSpend 21 | from chia.wallet.sign_coin_spends import sign_coin_spends 22 | from chia.wallet.lineage_proof import LineageProof 23 | from chia.wallet.puzzles import singleton_top_layer 24 | from chia.types.announcement import Announcement 25 | 26 | from sim import load_clsp_relative 27 | from nft_wallet import NFT 28 | 29 | 30 | SINGLETON_MOD = load_clvm("singleton_top_layer.clvm") 31 | SINGLETON_MOD_HASH = SINGLETON_MOD.get_tree_hash() 32 | LAUNCHER_PUZZLE = load_clsp_relative("clsp/nft_launcher.clsp") 33 | LAUNCHER_PUZZLE_HASH = LAUNCHER_PUZZLE.get_tree_hash() 34 | 35 | ESCAPE_VALUE = -113 36 | MELT_CONDITION = [ConditionOpcode.CREATE_COIN, 0, ESCAPE_VALUE] 37 | 38 | INNER_MOD = load_clsp_relative("clsp/creator_nft.clsp") 39 | P2_MOD = load_clsp_relative("clsp/p2_creator_nft.clsp") 40 | 41 | 42 | def run_singleton(full_puzzle: Program, solution: Program) -> List: 43 | k = full_puzzle.run(solution) 44 | conds = [] 45 | for x in k.as_iter(): 46 | code = int.from_bytes(x.first(), "big") 47 | 48 | if code == 51: 49 | ph = x.rest().first().as_python() 50 | amt = int.from_bytes(x.rest().rest().first().as_python(), "big") 51 | conds.append([code, ph, amt]) 52 | elif code == 50: 53 | pk = x.rest().first().as_python() 54 | msg = x.rest().rest().first().as_python() 55 | conds.append([code, pk, msg]) 56 | elif code == 61: 57 | a_id = x.rest().first().as_python().hex() 58 | conds.append([code, a_id]) 59 | elif code in [60, 62, 63, 70]: 60 | msg = x.rest().first().as_python() 61 | conds.append([code, msg]) 62 | 63 | return conds 64 | 65 | 66 | def make_inner(state: List, royalty: List) -> Program: 67 | args = [INNER_MOD.get_tree_hash(), state, royalty] 68 | return INNER_MOD.curry(*args) 69 | 70 | 71 | def make_solution(new_state, payment_info): 72 | return [new_state, payment_info] 73 | 74 | 75 | def get_eve_coin_from_launcher(launcher_spend): 76 | conds = run_singleton(launcher_spend.puzzle_reveal.to_program(), launcher_spend.solution.to_program()) 77 | create_cond = next(c for c in conds if c[0] == 51) 78 | return Coin(launcher_spend.coin.name(), create_cond[1], create_cond[2]) 79 | 80 | 81 | def make_launcher_spend(found_coin: Coin, amount: int, state: List, royalty: List, key_value_list: Tuple): 82 | # key_value_list must be a tuple, which can contain lists, but the top-level 83 | # must be 2 elements 84 | launcher_coin = Coin(found_coin.name(), LAUNCHER_PUZZLE_HASH, amount) 85 | args = [INNER_MOD.get_tree_hash(), state, royalty] 86 | curried = INNER_MOD.curry(*args) 87 | full_puzzle = SINGLETON_MOD.curry((SINGLETON_MOD_HASH, (launcher_coin.name(), LAUNCHER_PUZZLE_HASH)), curried) 88 | 89 | solution = Program.to( 90 | [ 91 | full_puzzle.get_tree_hash(), 92 | SINGLETON_MOD_HASH, 93 | launcher_coin.name(), 94 | LAUNCHER_PUZZLE_HASH, 95 | INNER_MOD.get_tree_hash(), 96 | state, 97 | royalty, 98 | amount, 99 | key_value_list, 100 | ] 101 | ) 102 | 103 | return CoinSpend(launcher_coin, LAUNCHER_PUZZLE, solution) 104 | 105 | 106 | def make_found_spend( 107 | found_coin: Coin, found_coin_puzzle: Program, launcher_coin_spend: CoinSpend, amount: int 108 | ) -> CoinSpend: 109 | launcher_announcement = Announcement(launcher_coin_spend.coin.name(), launcher_coin_spend.solution.get_tree_hash()) 110 | 111 | conditions = [ 112 | [ 113 | ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, 114 | std_hash(launcher_coin_spend.coin.name() + launcher_announcement.message), 115 | ], 116 | [ConditionOpcode.CREATE_COIN, launcher_coin_spend.coin.puzzle_hash, amount], 117 | [ConditionOpcode.CREATE_COIN, found_coin.puzzle_hash, found_coin.amount - amount], 118 | ] 119 | delegated_puzzle = Program.to((1, conditions)) 120 | found_coin_solution = Program.to([[], delegated_puzzle, []]) 121 | return CoinSpend(found_coin, found_coin_puzzle, found_coin_solution) 122 | 123 | 124 | def make_eve_spend(state: List, royalty: List, launcher_spend: CoinSpend): 125 | eve_coin = get_eve_coin_from_launcher(launcher_spend) 126 | args = [INNER_MOD.get_tree_hash(), state, royalty] 127 | eve_inner_puzzle = INNER_MOD.curry(*args) 128 | full_puzzle = SINGLETON_MOD.curry( 129 | (SINGLETON_MOD_HASH, (launcher_spend.coin.name(), LAUNCHER_PUZZLE_HASH)), eve_inner_puzzle 130 | ) 131 | 132 | assert full_puzzle.get_tree_hash() == eve_coin.puzzle_hash 133 | 134 | eve_solution = [state, [], 0] # [state, pmt_id, fee] 135 | eve_proof = LineageProof(launcher_spend.coin.parent_coin_info, None, launcher_spend.coin.amount) 136 | # eve_proof = singleton_top_layer.lineage_proof_for_coinsol(launcher_spend) 137 | solution = singleton_top_layer.solution_for_singleton(eve_proof, eve_coin.amount, eve_solution) 138 | eve_spend = CoinSpend(eve_coin, full_puzzle, solution) 139 | return eve_spend 140 | 141 | 142 | def uncurry_inner_from_singleton(puzzle: Program): 143 | _, args = puzzle.uncurry() 144 | _, inner_puzzle = list(args.as_iter()) 145 | return inner_puzzle 146 | 147 | 148 | def uncurry_state_and_royalty(puzzle: Program): 149 | """Uncurry the data from a full singleton puzzle""" 150 | _, args = puzzle.uncurry() 151 | _, inner_puzzle = list(args.as_iter()) 152 | _, inner_args = inner_puzzle.uncurry() 153 | state = inner_args.rest().first().as_python() 154 | royalty = inner_args.rest().rest().first().as_python() 155 | return (state, royalty) 156 | 157 | 158 | def uncurry_solution(solution: Program): 159 | mod, args = solution.uncurry() 160 | return mod.as_python()[-1][0] 161 | 162 | 163 | def make_buy_spend(nft: NFT, new_state, payment_coin, payment_coin_puzzle): 164 | old_state, royalty = uncurry_state_and_royalty(nft.last_spend.puzzle_reveal.to_program()) 165 | current_state = uncurry_solution(nft.last_spend.solution.to_program()) 166 | args = [INNER_MOD.get_tree_hash(), current_state, royalty] 167 | 168 | current_inner_puzzle = INNER_MOD.curry(*args) 169 | current_singleton_puzzle = SINGLETON_MOD.curry( 170 | (SINGLETON_MOD_HASH, (nft.launcher_id, LAUNCHER_PUZZLE_HASH)), current_inner_puzzle 171 | ) 172 | 173 | assert current_singleton_puzzle.get_tree_hash() == nft.puzzle_hash 174 | assert nft.state()[0] != int_to_bytes(0) # is for sale 175 | 176 | price = int_from_bytes(nft.state()[1]) 177 | 178 | p2_puzzle = P2_MOD.curry(SINGLETON_MOD_HASH, nft.launcher_id, LAUNCHER_PUZZLE_HASH) 179 | p2_coin = Coin(payment_coin.name(), p2_puzzle.get_tree_hash(), price) 180 | 181 | r = nft.last_spend.puzzle_reveal.to_program().uncurry() 182 | if r is not None: 183 | _, args = r 184 | _, inner_puzzle = list(args.as_iter()) 185 | inner_puzzle_hash = inner_puzzle.get_tree_hash() 186 | 187 | lineage_proof = LineageProof(nft.last_spend.coin.parent_coin_info, inner_puzzle_hash, nft.amount) 188 | 189 | # lineage_proof = singleton_top_layer.lineage_proof_for_coinsol(nft.last_spend) 190 | 191 | inner_solution = [new_state, p2_coin.name()] 192 | singleton_solution = singleton_top_layer.solution_for_singleton(lineage_proof, nft.as_coin().amount, inner_solution) 193 | 194 | # conds = run_singleton(current_singleton_puzzle, singleton_solution) 195 | # print(conds) 196 | 197 | p2_solution = Program.to([current_inner_puzzle.get_tree_hash(), p2_coin.name(), new_state]) 198 | delegated_cond = [ 199 | [ConditionOpcode.CREATE_COIN, p2_puzzle.get_tree_hash(), price], 200 | [ConditionOpcode.CREATE_COIN, payment_coin_puzzle.get_tree_hash(), payment_coin.amount - price], 201 | ] 202 | delegated_puz = Program.to((1, delegated_cond)) 203 | delegated_sol = Program.to([[], delegated_puz, []]) 204 | # make coin spends 205 | nft_spend = CoinSpend(nft.as_coin(), current_singleton_puzzle, singleton_solution) 206 | p2_spend = CoinSpend(p2_coin, p2_puzzle, p2_solution) 207 | payment_spend = CoinSpend(payment_coin, payment_coin_puzzle, delegated_sol) 208 | 209 | return (nft_spend, p2_spend, payment_spend) 210 | 211 | 212 | def make_update_spend(nft: NFT, new_state): 213 | old_state, royalty = uncurry_state_and_royalty(nft.last_spend.puzzle_reveal.to_program()) 214 | current_state = uncurry_solution(nft.last_spend.solution.to_program()) 215 | args = [INNER_MOD.get_tree_hash(), current_state, royalty] 216 | 217 | current_inner_puzzle = INNER_MOD.curry(*args) 218 | current_singleton_puzzle = SINGLETON_MOD.curry( 219 | (SINGLETON_MOD_HASH, (nft.launcher_id, LAUNCHER_PUZZLE_HASH)), current_inner_puzzle 220 | ) 221 | 222 | assert current_singleton_puzzle.get_tree_hash() == nft.puzzle_hash 223 | 224 | r = nft.last_spend.puzzle_reveal.to_program().uncurry() 225 | if r is not None: 226 | _, args = r 227 | _, inner_puzzle = list(args.as_iter()) 228 | inner_puzzle_hash = inner_puzzle.get_tree_hash() 229 | 230 | lineage_proof = LineageProof(nft.last_spend.coin.parent_coin_info, inner_puzzle_hash, nft.amount) 231 | 232 | inner_solution = [new_state, [], []] 233 | singleton_solution = singleton_top_layer.solution_for_singleton(lineage_proof, nft.as_coin().amount, inner_solution) 234 | 235 | return CoinSpend(nft.as_coin(), current_singleton_puzzle, singleton_solution) 236 | -------------------------------------------------------------------------------- /tests/wallet_tools.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, List, Optional, Tuple, Any 2 | 3 | from blspy import AugSchemeMPL, G2Element, PrivateKey 4 | 5 | from chia.consensus.constants import ConsensusConstants 6 | from chia.util.hash import std_hash 7 | from chia.types.announcement import Announcement 8 | from chia.types.blockchain_format.coin import Coin 9 | from chia.types.blockchain_format.program import Program 10 | from chia.types.blockchain_format.sized_bytes import bytes32 11 | from chia.types.coin_spend import CoinSpend 12 | from chia.types.condition_opcodes import ConditionOpcode 13 | from chia.types.condition_with_args import ConditionWithArgs 14 | from chia.types.spend_bundle import SpendBundle 15 | from chia.util.clvm import int_from_bytes, int_to_bytes 16 | from chia.util.condition_tools import conditions_by_opcode, conditions_for_solution 17 | from chia.util.ints import uint32, uint64 18 | from chia.wallet.derive_keys import master_sk_to_wallet_sk 19 | from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( 20 | DEFAULT_HIDDEN_PUZZLE_HASH, 21 | calculate_synthetic_secret_key, 22 | puzzle_for_pk, 23 | solution_for_conditions, 24 | ) 25 | 26 | DEFAULT_SEED = b"seed" * 8 27 | assert len(DEFAULT_SEED) == 32 28 | 29 | 30 | class WalletTool: 31 | next_address = 0 32 | pubkey_num_lookup: Dict[bytes, uint32] = {} 33 | 34 | def __init__(self, constants: ConsensusConstants, sk: Optional[PrivateKey] = None): 35 | self.constants = constants 36 | self.current_balance = 0 37 | self.my_utxos: set = set() 38 | if sk is not None: 39 | self.private_key = sk 40 | else: 41 | self.private_key = AugSchemeMPL.key_gen(DEFAULT_SEED) 42 | self.generator_lookups: Dict = {} 43 | self.puzzle_pk_cache: Dict = {} 44 | self.get_new_puzzle() 45 | 46 | def get_next_address_index(self) -> uint32: 47 | self.next_address = uint32(self.next_address + 1) 48 | return self.next_address 49 | 50 | def get_private_key_for_puzzle_hash(self, puzzle_hash: bytes32) -> PrivateKey: 51 | if puzzle_hash in self.puzzle_pk_cache: 52 | child = self.puzzle_pk_cache[puzzle_hash] 53 | private = master_sk_to_wallet_sk(self.private_key, uint32(child)) 54 | # pubkey = private.get_g1() 55 | return private 56 | else: 57 | for child in range(self.next_address): 58 | pubkey = master_sk_to_wallet_sk(self.private_key, uint32(child)).get_g1() 59 | if puzzle_hash == puzzle_for_pk(bytes(pubkey)).get_tree_hash(): 60 | return master_sk_to_wallet_sk(self.private_key, uint32(child)) 61 | raise ValueError(f"Do not have the keys for puzzle hash {puzzle_hash}") 62 | 63 | def puzzle_for_pk(self, pubkey: bytes) -> Program: 64 | return puzzle_for_pk(pubkey) 65 | 66 | def get_new_puzzle(self) -> bytes32: 67 | next_address_index: uint32 = self.get_next_address_index() 68 | pubkey = master_sk_to_wallet_sk(self.private_key, next_address_index).get_g1() 69 | self.pubkey_num_lookup[bytes(pubkey)] = next_address_index 70 | 71 | puzzle = puzzle_for_pk(bytes(pubkey)) 72 | 73 | self.puzzle_pk_cache[puzzle.get_tree_hash()] = next_address_index 74 | return puzzle 75 | 76 | def get_new_puzzlehash(self) -> bytes32: 77 | puzzle = self.get_new_puzzle() 78 | # TODO: address hint error and remove ignore 79 | # error: "bytes32" has no attribute "get_tree_hash" [attr-defined] 80 | return puzzle.get_tree_hash() # type: ignore[attr-defined] 81 | 82 | def sign(self, value: bytes, pubkey: bytes) -> G2Element: 83 | privatekey: PrivateKey = master_sk_to_wallet_sk(self.private_key, self.pubkey_num_lookup[pubkey]) 84 | return AugSchemeMPL.sign(privatekey, value) 85 | 86 | def make_solution(self, condition_dic: Dict[ConditionOpcode, List[ConditionWithArgs]]) -> Program: 87 | ret = [] 88 | 89 | for con_list in condition_dic.values(): 90 | for cvp in con_list: 91 | if cvp.opcode == ConditionOpcode.CREATE_COIN and len(cvp.vars) > 2: 92 | formatted: List[Any] = [] 93 | formatted.extend(cvp.vars) 94 | formatted[2] = cvp.vars[2:] 95 | ret.append([cvp.opcode.value] + formatted) 96 | else: 97 | ret.append([cvp.opcode.value] + cvp.vars) 98 | return solution_for_conditions(Program.to(ret)) 99 | 100 | def generate_unsigned_transaction( 101 | self, 102 | amount: uint64, 103 | new_puzzle_hash: bytes32, 104 | coins: List[Coin], 105 | condition_dic: Dict[ConditionOpcode, List[ConditionWithArgs]], 106 | fee: int = 0, 107 | secret_key: Optional[PrivateKey] = None, 108 | additional_outputs: Optional[List[Tuple[bytes32, int]]] = None, 109 | ) -> List[CoinSpend]: 110 | spends = [] 111 | 112 | spend_value = sum([c.amount for c in coins]) 113 | 114 | if ConditionOpcode.CREATE_COIN not in condition_dic: 115 | condition_dic[ConditionOpcode.CREATE_COIN] = [] 116 | if ConditionOpcode.CREATE_COIN_ANNOUNCEMENT not in condition_dic: 117 | condition_dic[ConditionOpcode.CREATE_COIN_ANNOUNCEMENT] = [] 118 | 119 | output = ConditionWithArgs(ConditionOpcode.CREATE_COIN, [new_puzzle_hash, int_to_bytes(amount)]) 120 | condition_dic[output.opcode].append(output) 121 | if additional_outputs is not None: 122 | for o in additional_outputs: 123 | out = ConditionWithArgs(ConditionOpcode.CREATE_COIN, [o[0], int_to_bytes(o[1])]) 124 | condition_dic[out.opcode].append(out) 125 | 126 | amount_total = sum(int_from_bytes(cvp.vars[1]) for cvp in condition_dic[ConditionOpcode.CREATE_COIN]) 127 | change = spend_value - amount_total - fee 128 | if change > 0: 129 | change_puzzle_hash = self.get_new_puzzlehash() 130 | change_output = ConditionWithArgs(ConditionOpcode.CREATE_COIN, [change_puzzle_hash, int_to_bytes(change)]) 131 | condition_dic[output.opcode].append(change_output) 132 | 133 | secondary_coins_cond_dic: Dict[ConditionOpcode, List[ConditionWithArgs]] = dict() 134 | secondary_coins_cond_dic[ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT] = [] 135 | for n, coin in enumerate(coins): 136 | puzzle_hash = coin.puzzle_hash 137 | if secret_key is None: 138 | secret_key = self.get_private_key_for_puzzle_hash(puzzle_hash) 139 | pubkey = secret_key.get_g1() 140 | puzzle = puzzle_for_pk(bytes(pubkey)) 141 | if n == 0: 142 | message_list = [c.name() for c in coins] 143 | for outputs in condition_dic[ConditionOpcode.CREATE_COIN]: 144 | # TODO: address hint error and remove ignore 145 | # error: Argument 2 to "Coin" has incompatible type "bytes"; expected "bytes32" [arg-type] 146 | coin_to_append = Coin( 147 | coin.name(), 148 | outputs.vars[0], # type: ignore[arg-type] 149 | int_from_bytes(outputs.vars[1]), 150 | ) 151 | message_list.append(coin_to_append.name()) 152 | message = std_hash(b"".join(message_list)) 153 | condition_dic[ConditionOpcode.CREATE_COIN_ANNOUNCEMENT].append( 154 | ConditionWithArgs(ConditionOpcode.CREATE_COIN_ANNOUNCEMENT, [message]) 155 | ) 156 | primary_announcement_hash = Announcement(coin.name(), message).name() 157 | secondary_coins_cond_dic[ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT].append( 158 | ConditionWithArgs(ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, [primary_announcement_hash]) 159 | ) 160 | main_solution = self.make_solution(condition_dic) 161 | spends.append(CoinSpend(coin, puzzle, main_solution)) 162 | else: 163 | spends.append(CoinSpend(coin, puzzle, self.make_solution(secondary_coins_cond_dic))) 164 | return spends 165 | 166 | def sign_transaction(self, coin_spends: List[CoinSpend]) -> SpendBundle: 167 | signatures = [] 168 | solution: Program 169 | puzzle: Program 170 | for coin_spend in coin_spends: # noqa 171 | secret_key = self.get_private_key_for_puzzle_hash(coin_spend.coin.puzzle_hash) 172 | synthetic_secret_key = calculate_synthetic_secret_key(secret_key, DEFAULT_HIDDEN_PUZZLE_HASH) 173 | err, con, cost = conditions_for_solution( 174 | coin_spend.puzzle_reveal, coin_spend.solution, self.constants.MAX_BLOCK_COST_CLVM 175 | ) 176 | if not con: 177 | raise ValueError(err) 178 | conditions_dict = conditions_by_opcode(con) 179 | 180 | for cwa in conditions_dict.get(ConditionOpcode.AGG_SIG_UNSAFE, []): 181 | msg = cwa.vars[1] 182 | signature = AugSchemeMPL.sign(synthetic_secret_key, msg) 183 | signatures.append(signature) 184 | 185 | for cwa in conditions_dict.get(ConditionOpcode.AGG_SIG_ME, []): 186 | msg = cwa.vars[1] + bytes(coin_spend.coin.name()) + self.constants.AGG_SIG_ME_ADDITIONAL_DATA 187 | signature = AugSchemeMPL.sign(synthetic_secret_key, msg) 188 | signatures.append(signature) 189 | 190 | aggsig = AugSchemeMPL.aggregate(signatures) 191 | spend_bundle = SpendBundle(coin_spends, aggsig) 192 | return spend_bundle 193 | 194 | def generate_signed_transaction( 195 | self, 196 | amount: uint64, 197 | new_puzzle_hash: bytes32, 198 | coin: Coin, 199 | condition_dic: Dict[ConditionOpcode, List[ConditionWithArgs]] = None, 200 | fee: int = 0, 201 | additional_outputs: Optional[List[Tuple[bytes32, int]]] = None, 202 | ) -> SpendBundle: 203 | if condition_dic is None: 204 | condition_dic = {} 205 | transaction = self.generate_unsigned_transaction( 206 | amount, new_puzzle_hash, [coin], condition_dic, fee, additional_outputs=additional_outputs 207 | ) 208 | assert transaction is not None 209 | return self.sign_transaction(transaction) 210 | 211 | def generate_signed_transaction_multiple_coins( 212 | self, 213 | amount: uint64, 214 | new_puzzle_hash: bytes32, 215 | coins: List[Coin], 216 | condition_dic: Dict[ConditionOpcode, List[ConditionWithArgs]] = None, 217 | fee: int = 0, 218 | additional_outputs: Optional[List[Tuple[bytes32, int]]] = None, 219 | ) -> SpendBundle: 220 | if condition_dic is None: 221 | condition_dic = {} 222 | transaction = self.generate_unsigned_transaction( 223 | amount, new_puzzle_hash, coins, condition_dic, fee, additional_outputs=additional_outputs 224 | ) 225 | assert transaction is not None 226 | return self.sign_transaction(transaction) 227 | -------------------------------------------------------------------------------- /nft_wallet.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import List, Tuple, Dict, Optional 3 | from blspy import AugSchemeMPL, G1Element, G2Element, PrivateKey 4 | import aiosqlite 5 | 6 | from chia.types.blockchain_format.coin import Coin 7 | from chia.types.coin_spend import CoinSpend 8 | from chia.types.blockchain_format.sized_bytes import bytes32 9 | from chia.util.db_wrapper import DBWrapper 10 | from chia.util.ints import uint32 11 | from chia.wallet.puzzles.load_clvm import load_clvm 12 | from clvm.casts import int_to_bytes, int_from_bytes 13 | 14 | from sim import load_clsp_relative 15 | 16 | 17 | log = logging.getLogger(__name__) 18 | 19 | SINGLETON_MOD = load_clvm("singleton_top_layer.clvm") 20 | SINGLETON_MOD_HASH = SINGLETON_MOD.get_tree_hash() 21 | LAUNCHER_PUZZLE = load_clsp_relative("clsp/nft_launcher.clsp") 22 | LAUNCHER_PUZZLE_HASH = LAUNCHER_PUZZLE.get_tree_hash() 23 | 24 | INNER_MOD = load_clsp_relative("clsp/creator_nft.clsp") 25 | P2_MOD = load_clsp_relative("clsp/p2_creator_nft.clsp") 26 | 27 | 28 | class NFT(Coin): 29 | def __init__(self, launcher_id: bytes32, coin: Coin, last_spend: CoinSpend = None, nft_data=None, royalty=None): 30 | super().__init__(coin.parent_coin_info, coin.puzzle_hash, coin.amount) 31 | self.launcher_id = launcher_id 32 | self.last_spend = last_spend 33 | self.data = nft_data 34 | self.royalty = royalty 35 | 36 | def conditions(self): 37 | if self.last_spend: 38 | return conditions_dict_for_solution( 39 | self.last_spend.puzzle_reveal.to_program(), self.last_spend.solution.to_program() 40 | ) 41 | 42 | def as_coin(self): 43 | return Coin(self.parent_coin_info, self.puzzle_hash, self.amount) 44 | 45 | def state(self): 46 | mod, args = self.last_spend.solution.to_program().uncurry() 47 | return mod.as_python()[-1][0] 48 | 49 | # def royalty(self): 50 | # mod, args = self.last_spend.solution.to_program().uncurry() 51 | # return mod.as_python()[0] 52 | 53 | def is_for_sale(self): 54 | if int_from_bytes(self.state()[0]) != 0: 55 | return True 56 | 57 | def royalty_pc(self): 58 | return int_from_bytes(self.royalty[1]) 59 | 60 | def owner_pk(self): 61 | return self.state()[-1] 62 | 63 | def owner_fingerprint(self): 64 | return G1Element(self.owner_pk()).get_fingerprint() 65 | 66 | def owner_puzzle_hash(self): 67 | return self.state()[-2] 68 | 69 | def price(self): 70 | return int_from_bytes(self.state()[1]) 71 | 72 | 73 | class NFTWallet: 74 | db_connection: aiosqlite.Connection 75 | db_wrapper: DBWrapper 76 | _state_transitions_cache: Dict[int, List[Tuple[uint32, CoinSpend]]] 77 | 78 | @classmethod 79 | async def create(cls, wrapper: DBWrapper, node_client): 80 | self = cls() 81 | 82 | self.db_connection = wrapper.db 83 | self.db_wrapper = wrapper 84 | self.node_client = node_client 85 | 86 | await self.db_connection.execute( 87 | """CREATE TABLE IF NOT EXISTS 88 | nft_state_transitions(transition_index integer, 89 | wallet_id integer, 90 | height bigint, 91 | coin_spend blob, 92 | PRIMARY KEY(transition_index, wallet_id))""" 93 | ) 94 | 95 | await self.db_connection.execute( 96 | """CREATE TABLE IF NOT EXISTS 97 | nft_coins (launcher_id text PRIMARY KEY, 98 | owner_pk text)""" 99 | ) 100 | 101 | await self.db_connection.execute( 102 | """CREATE TABLE IF NOT EXISTS 103 | height (block integer)""" 104 | ) 105 | 106 | await self.db_connection.commit() 107 | 108 | return self 109 | 110 | async def _clear_database(self): 111 | cursor = await self.db_connection.execute("DELETE FROM nft_coins") 112 | await cursor.close() 113 | await self.db_connection.commit() 114 | 115 | async def get_current_height_from_node(self): 116 | blockchain_state = await self.node_client.get_blockchain_state() 117 | new_height = blockchain_state["peak"].height 118 | return new_height 119 | 120 | async def set_new_height(self, new_height: int): 121 | cursor = await self.db_connection.execute("INSERT OR REPLACE INTO height (block) VALUES (?)", (new_height,)) 122 | await cursor.close() 123 | await self.db_connection.commit() 124 | 125 | async def retrieve_current_block(self): 126 | current_block = None 127 | cursor = await self.db_connection.execute("SELECT block FROM height ORDER BY block DESC LIMIT 1") 128 | 129 | returned_block = await cursor.fetchone() 130 | await cursor.close() 131 | 132 | if returned_block is None: 133 | current_block = await self.get_current_height_from_node() 134 | current_block -= 1 135 | else: 136 | current_block = returned_block[0] 137 | 138 | return current_block 139 | 140 | async def update_to_current_block(self): 141 | current_block = await self.retrieve_current_block() 142 | new_height = await self.get_current_height_from_node() 143 | if new_height - 1 < current_block: 144 | current_block = max(new_height - 1, 1) 145 | 146 | if current_block is None: 147 | current_block = await self.get_current_height_from_node() 148 | current_block -= 1 149 | 150 | singletons = await self.node_client.get_coin_records_by_puzzle_hash( 151 | LAUNCHER_PUZZLE_HASH, start_height=current_block, end_height=new_height 152 | ) 153 | await self.filter_singletons(singletons) 154 | 155 | while new_height > current_block: 156 | if new_height - current_block > 1: 157 | new_height = current_block + 1 158 | 159 | # ADD FUNCTIONS TO UPDATE SINGLE STATES HERE 160 | 161 | await self.set_new_height(new_height) 162 | current_block = new_height 163 | blockchain_state = await self.node_client.get_blockchain_state() 164 | new_height = blockchain_state["peak"].height 165 | 166 | async def filter_singletons(self, singletons: List): 167 | print(f"Updating {len(singletons)} CreatorNFTs") 168 | for cr in singletons: 169 | eve_cr = await self.node_client.get_coin_records_by_parent_ids([cr.coin.name()]) 170 | assert len(eve_cr) > 0 171 | if eve_cr[0].spent: 172 | eve_spend = await self.node_client.get_puzzle_and_solution( 173 | eve_cr[0].coin.name(), eve_cr[0].spent_block_index 174 | ) 175 | # uncurry the singletons inner puzzle 176 | _, args = eve_spend.puzzle_reveal.to_program().uncurry() 177 | _, inner_puzzle = list(args.as_iter()) 178 | mod, _ = inner_puzzle.uncurry() 179 | if mod.get_tree_hash() == INNER_MOD.get_tree_hash(): 180 | mod, _ = eve_spend.solution.to_program().uncurry() 181 | state = mod.as_python()[-1][0] 182 | await self.save_launcher(cr.coin.name(), state[-1]) 183 | 184 | async def get_nft_by_launcher_id(self, launcher_id: bytes32): 185 | nft_id = launcher_id 186 | launcher_rec = await self.node_client.get_coin_record_by_name(launcher_id) 187 | launcher_spend = await self.node_client.get_puzzle_and_solution( 188 | launcher_rec.coin.name(), launcher_rec.spent_block_index 189 | ) 190 | nft_data = launcher_spend.solution.to_program().uncurry()[0].as_python()[-1] 191 | 192 | while True: 193 | current_coin_record = await self.node_client.get_coin_record_by_name(nft_id) 194 | if current_coin_record.spent: 195 | next_coin_records = await self.node_client.get_coin_records_by_parent_ids([nft_id]) 196 | last_spend = await self.node_client.get_puzzle_and_solution( 197 | current_coin_record.coin.name(), current_coin_record.spent_block_index 198 | ) 199 | if len(next_coin_records) == 3: 200 | # last spend was purchase spend, so separate out the puzzlehashes 201 | _, args = last_spend.puzzle_reveal.to_program().uncurry() 202 | _, inner_puzzle = list(args.as_iter()) 203 | _, inner_args = inner_puzzle.uncurry() 204 | state = inner_args.rest().first().as_python() 205 | royalty = inner_args.rest().rest().first().as_python() 206 | for rec in next_coin_records: 207 | if rec.coin.puzzle_hash not in [state[2], royalty[0]]: 208 | next_parent = rec.coin 209 | if len(next_coin_records) == 1: 210 | next_parent = next_coin_records[0].coin 211 | nft_id = next_parent.name() 212 | last_coin_record = current_coin_record 213 | else: 214 | last_spend = await self.node_client.get_puzzle_and_solution( 215 | last_coin_record.coin.name(), last_coin_record.spent_block_index 216 | ) 217 | _, args = last_spend.puzzle_reveal.to_program().uncurry() 218 | _, inner_puzzle = list(args.as_iter()) 219 | _, inner_args = inner_puzzle.uncurry() 220 | # state = inner_args.rest().first().as_python() 221 | royalty = inner_args.rest().rest().first().as_python() 222 | nft = NFT(launcher_id, current_coin_record.coin, last_spend, nft_data, royalty) 223 | await self.save_nft(nft) 224 | return nft 225 | 226 | async def basic_sync(self): 227 | all_nfts = await self.node_client.get_coin_records_by_puzzle_hash(LAUNCHER_PUZZLE_HASH) 228 | await self.filter_singletons(all_nfts) 229 | await self.update_to_current_block() 230 | 231 | async def save_launcher(self, launcher_id, pk=b""): 232 | cursor = await self.db_connection.execute( 233 | "INSERT OR REPLACE INTO nft_coins (launcher_id, owner_pk) VALUES (?, ?)", (bytes(launcher_id), bytes(pk)) 234 | ) 235 | await cursor.close() 236 | await self.db_connection.commit() 237 | 238 | async def save_nft(self, nft: NFT): 239 | # add launcher_id, owner_pk to db 240 | cursor = await self.db_connection.execute( 241 | "INSERT OR REPLACE INTO nft_coins (launcher_id, owner_pk) VALUES (?,?)", 242 | (bytes(nft.launcher_id), bytes(nft.owner_pk())), 243 | ) 244 | await cursor.close() 245 | await self.db_connection.commit() 246 | 247 | async def get_all_nft_ids(self): 248 | query = "SELECT launcher_id FROM nft_coins" 249 | cursor = await self.db_connection.execute(query) 250 | rows = await cursor.fetchall() 251 | await cursor.close() 252 | return list(map(lambda x: x[0], rows)) 253 | 254 | async def get_nft_ids_by_pk(self, pk: G1Element = None): 255 | query = f"SELECT launcher_id FROM nft_coins WHERE owner_pk = ?" 256 | cursor = await self.db_connection.execute(query, (bytes(pk),)) 257 | rows = await cursor.fetchall() 258 | await cursor.close() 259 | return list(map(lambda x: x[0], rows)) 260 | -------------------------------------------------------------------------------- /tests/util/bip39_test_vectors.json: -------------------------------------------------------------------------------- 1 | { 2 | "english": [ 3 | [ 4 | "00000000000000000000000000000000", 5 | "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", 6 | "c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04", 7 | "xprv9s21ZrQH143K3h3fDYiay8mocZ3afhfULfb5GX8kCBdno77K4HiA15Tg23wpbeF1pLfs1c5SPmYHrEpTuuRhxMwvKDwqdKiGJS9XFKzUsAF" 8 | ], 9 | [ 10 | "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f", 11 | "legal winner thank year wave sausage worth useful legal winner thank yellow", 12 | "2e8905819b8723fe2c1d161860e5ee1830318dbf49a83bd451cfb8440c28bd6fa457fe1296106559a3c80937a1c1069be3a3a5bd381ee6260e8d9739fce1f607", 13 | "xprv9s21ZrQH143K2gA81bYFHqU68xz1cX2APaSq5tt6MFSLeXnCKV1RVUJt9FWNTbrrryem4ZckN8k4Ls1H6nwdvDTvnV7zEXs2HgPezuVccsq" 14 | ], 15 | [ 16 | "80808080808080808080808080808080", 17 | "letter advice cage absurd amount doctor acoustic avoid letter advice cage above", 18 | "d71de856f81a8acc65e6fc851a38d4d7ec216fd0796d0a6827a3ad6ed5511a30fa280f12eb2e47ed2ac03b5c462a0358d18d69fe4f985ec81778c1b370b652a8", 19 | "xprv9s21ZrQH143K2shfP28KM3nr5Ap1SXjz8gc2rAqqMEynmjt6o1qboCDpxckqXavCwdnYds6yBHZGKHv7ef2eTXy461PXUjBFQg6PrwY4Gzq" 20 | ], 21 | [ 22 | "ffffffffffffffffffffffffffffffff", 23 | "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong", 24 | "ac27495480225222079d7be181583751e86f571027b0497b5b5d11218e0a8a13332572917f0f8e5a589620c6f15b11c61dee327651a14c34e18231052e48c069", 25 | "xprv9s21ZrQH143K2V4oox4M8Zmhi2Fjx5XK4Lf7GKRvPSgydU3mjZuKGCTg7UPiBUD7ydVPvSLtg9hjp7MQTYsW67rZHAXeccqYqrsx8LcXnyd" 26 | ], 27 | [ 28 | "000000000000000000000000000000000000000000000000", 29 | "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent", 30 | "035895f2f481b1b0f01fcf8c289c794660b289981a78f8106447707fdd9666ca06da5a9a565181599b79f53b844d8a71dd9f439c52a3d7b3e8a79c906ac845fa", 31 | "xprv9s21ZrQH143K3mEDrypcZ2usWqFgzKB6jBBx9B6GfC7fu26X6hPRzVjzkqkPvDqp6g5eypdk6cyhGnBngbjeHTe4LsuLG1cCmKJka5SMkmU" 32 | ], 33 | [ 34 | "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f", 35 | "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will", 36 | "f2b94508732bcbacbcc020faefecfc89feafa6649a5491b8c952cede496c214a0c7b3c392d168748f2d4a612bada0753b52a1c7ac53c1e93abd5c6320b9e95dd", 37 | "xprv9s21ZrQH143K3Lv9MZLj16np5GzLe7tDKQfVusBni7toqJGcnKRtHSxUwbKUyUWiwpK55g1DUSsw76TF1T93VT4gz4wt5RM23pkaQLnvBh7" 38 | ], 39 | [ 40 | "808080808080808080808080808080808080808080808080", 41 | "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always", 42 | "107d7c02a5aa6f38c58083ff74f04c607c2d2c0ecc55501dadd72d025b751bc27fe913ffb796f841c49b1d33b610cf0e91d3aa239027f5e99fe4ce9e5088cd65", 43 | "xprv9s21ZrQH143K3VPCbxbUtpkh9pRG371UCLDz3BjceqP1jz7XZsQ5EnNkYAEkfeZp62cDNj13ZTEVG1TEro9sZ9grfRmcYWLBhCocViKEJae" 44 | ], 45 | [ 46 | "ffffffffffffffffffffffffffffffffffffffffffffffff", 47 | "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo when", 48 | "0cd6e5d827bb62eb8fc1e262254223817fd068a74b5b449cc2f667c3f1f985a76379b43348d952e2265b4cd129090758b3e3c2c49103b5051aac2eaeb890a528", 49 | "xprv9s21ZrQH143K36Ao5jHRVhFGDbLP6FCx8BEEmpru77ef3bmA928BxsqvVM27WnvvyfWywiFN8K6yToqMaGYfzS6Db1EHAXT5TuyCLBXUfdm" 50 | ], 51 | [ 52 | "0000000000000000000000000000000000000000000000000000000000000000", 53 | "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art", 54 | "bda85446c68413707090a52022edd26a1c9462295029f2e60cd7c4f2bbd3097170af7a4d73245cafa9c3cca8d561a7c3de6f5d4a10be8ed2a5e608d68f92fcc8", 55 | "xprv9s21ZrQH143K32qBagUJAMU2LsHg3ka7jqMcV98Y7gVeVyNStwYS3U7yVVoDZ4btbRNf4h6ibWpY22iRmXq35qgLs79f312g2kj5539ebPM" 56 | ], 57 | [ 58 | "7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f", 59 | "legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title", 60 | "bc09fca1804f7e69da93c2f2028eb238c227f2e9dda30cd63699232578480a4021b146ad717fbb7e451ce9eb835f43620bf5c514db0f8add49f5d121449d3e87", 61 | "xprv9s21ZrQH143K3Y1sd2XVu9wtqxJRvybCfAetjUrMMco6r3v9qZTBeXiBZkS8JxWbcGJZyio8TrZtm6pkbzG8SYt1sxwNLh3Wx7to5pgiVFU" 62 | ], 63 | [ 64 | "8080808080808080808080808080808080808080808080808080808080808080", 65 | "letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless", 66 | "c0c519bd0e91a2ed54357d9d1ebef6f5af218a153624cf4f2da911a0ed8f7a09e2ef61af0aca007096df430022f7a2b6fb91661a9589097069720d015e4e982f", 67 | "xprv9s21ZrQH143K3CSnQNYC3MqAAqHwxeTLhDbhF43A4ss4ciWNmCY9zQGvAKUSqVUf2vPHBTSE1rB2pg4avopqSiLVzXEU8KziNnVPauTqLRo" 68 | ], 69 | [ 70 | "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 71 | "zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote", 72 | "dd48c104698c30cfe2b6142103248622fb7bb0ff692eebb00089b32d22484e1613912f0a5b694407be899ffd31ed3992c456cdf60f5d4564b8ba3f05a69890ad", 73 | "xprv9s21ZrQH143K2WFF16X85T2QCpndrGwx6GueB72Zf3AHwHJaknRXNF37ZmDrtHrrLSHvbuRejXcnYxoZKvRquTPyp2JiNG3XcjQyzSEgqCB" 74 | ], 75 | [ 76 | "9e885d952ad362caeb4efe34a8e91bd2", 77 | "ozone drill grab fiber curtain grace pudding thank cruise elder eight picnic", 78 | "274ddc525802f7c828d8ef7ddbcdc5304e87ac3535913611fbbfa986d0c9e5476c91689f9c8a54fd55bd38606aa6a8595ad213d4c9c9f9aca3fb217069a41028", 79 | "xprv9s21ZrQH143K2oZ9stBYpoaZ2ktHj7jLz7iMqpgg1En8kKFTXJHsjxry1JbKH19YrDTicVwKPehFKTbmaxgVEc5TpHdS1aYhB2s9aFJBeJH" 80 | ], 81 | [ 82 | "6610b25967cdcca9d59875f5cb50b0ea75433311869e930b", 83 | "gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog", 84 | "628c3827a8823298ee685db84f55caa34b5cc195a778e52d45f59bcf75aba68e4d7590e101dc414bc1bbd5737666fbbef35d1f1903953b66624f910feef245ac", 85 | "xprv9s21ZrQH143K3uT8eQowUjsxrmsA9YUuQQK1RLqFufzybxD6DH6gPY7NjJ5G3EPHjsWDrs9iivSbmvjc9DQJbJGatfa9pv4MZ3wjr8qWPAK" 86 | ], 87 | [ 88 | "68a79eaca2324873eacc50cb9c6eca8cc68ea5d936f98787c60c7ebc74e6ce7c", 89 | "hamster diagram private dutch cause delay private meat slide toddler razor book happy fancy gospel tennis maple dilemma loan word shrug inflict delay length", 90 | "64c87cde7e12ecf6704ab95bb1408bef047c22db4cc7491c4271d170a1b213d20b385bc1588d9c7b38f1b39d415665b8a9030c9ec653d75e65f847d8fc1fc440", 91 | "xprv9s21ZrQH143K2XTAhys3pMNcGn261Fi5Ta2Pw8PwaVPhg3D8DWkzWQwjTJfskj8ofb81i9NP2cUNKxwjueJHHMQAnxtivTA75uUFqPFeWzk" 92 | ], 93 | [ 94 | "c0ba5a8e914111210f2bd131f3d5e08d", 95 | "scheme spot photo card baby mountain device kick cradle pact join borrow", 96 | "ea725895aaae8d4c1cf682c1bfd2d358d52ed9f0f0591131b559e2724bb234fca05aa9c02c57407e04ee9dc3b454aa63fbff483a8b11de949624b9f1831a9612", 97 | "xprv9s21ZrQH143K3FperxDp8vFsFycKCRcJGAFmcV7umQmcnMZaLtZRt13QJDsoS5F6oYT6BB4sS6zmTmyQAEkJKxJ7yByDNtRe5asP2jFGhT6" 98 | ], 99 | [ 100 | "6d9be1ee6ebd27a258115aad99b7317b9c8d28b6d76431c3", 101 | "horn tenant knee talent sponsor spell gate clip pulse soap slush warm silver nephew swap uncle crack brave", 102 | "fd579828af3da1d32544ce4db5c73d53fc8acc4ddb1e3b251a31179cdb71e853c56d2fcb11aed39898ce6c34b10b5382772db8796e52837b54468aeb312cfc3d", 103 | "xprv9s21ZrQH143K3R1SfVZZLtVbXEB9ryVxmVtVMsMwmEyEvgXN6Q84LKkLRmf4ST6QrLeBm3jQsb9gx1uo23TS7vo3vAkZGZz71uuLCcywUkt" 104 | ], 105 | [ 106 | "9f6a2878b2520799a44ef18bc7df394e7061a224d2c33cd015b157d746869863", 107 | "panda eyebrow bullet gorilla call smoke muffin taste mesh discover soft ostrich alcohol speed nation flash devote level hobby quick inner drive ghost inside", 108 | "72be8e052fc4919d2adf28d5306b5474b0069df35b02303de8c1729c9538dbb6fc2d731d5f832193cd9fb6aeecbc469594a70e3dd50811b5067f3b88b28c3e8d", 109 | "xprv9s21ZrQH143K2WNnKmssvZYM96VAr47iHUQUTUyUXH3sAGNjhJANddnhw3i3y3pBbRAVk5M5qUGFr4rHbEWwXgX4qrvrceifCYQJbbFDems" 110 | ], 111 | [ 112 | "23db8160a31d3e0dca3688ed941adbf3", 113 | "cat swing flag economy stadium alone churn speed unique patch report train", 114 | "deb5f45449e615feff5640f2e49f933ff51895de3b4381832b3139941c57b59205a42480c52175b6efcffaa58a2503887c1e8b363a707256bdd2b587b46541f5", 115 | "xprv9s21ZrQH143K4G28omGMogEoYgDQuigBo8AFHAGDaJdqQ99QKMQ5J6fYTMfANTJy6xBmhvsNZ1CJzRZ64PWbnTFUn6CDV2FxoMDLXdk95DQ" 116 | ], 117 | [ 118 | "8197a4a47f0425faeaa69deebc05ca29c0a5b5cc76ceacc0", 119 | "light rule cinnamon wrap drastic word pride squirrel upgrade then income fatal apart sustain crack supply proud access", 120 | "4cbdff1ca2db800fd61cae72a57475fdc6bab03e441fd63f96dabd1f183ef5b782925f00105f318309a7e9c3ea6967c7801e46c8a58082674c860a37b93eda02", 121 | "xprv9s21ZrQH143K3wtsvY8L2aZyxkiWULZH4vyQE5XkHTXkmx8gHo6RUEfH3Jyr6NwkJhvano7Xb2o6UqFKWHVo5scE31SGDCAUsgVhiUuUDyh" 122 | ], 123 | [ 124 | "066dca1a2bb7e8a1db2832148ce9933eea0f3ac9548d793112d9a95c9407efad", 125 | "all hour make first leader extend hole alien behind guard gospel lava path output census museum junior mass reopen famous sing advance salt reform", 126 | "26e975ec644423f4a4c4f4215ef09b4bd7ef924e85d1d17c4cf3f136c2863cf6df0a475045652c57eb5fb41513ca2a2d67722b77e954b4b3fc11f7590449191d", 127 | "xprv9s21ZrQH143K3rEfqSM4QZRVmiMuSWY9wugscmaCjYja3SbUD3KPEB1a7QXJoajyR2T1SiXU7rFVRXMV9XdYVSZe7JoUXdP4SRHTxsT1nzm" 128 | ], 129 | [ 130 | "f30f8c1da665478f49b001d94c5fc452", 131 | "vessel ladder alter error federal sibling chat ability sun glass valve picture", 132 | "2aaa9242daafcee6aa9d7269f17d4efe271e1b9a529178d7dc139cd18747090bf9d60295d0ce74309a78852a9caadf0af48aae1c6253839624076224374bc63f", 133 | "xprv9s21ZrQH143K2QWV9Wn8Vvs6jbqfF1YbTCdURQW9dLFKDovpKaKrqS3SEWsXCu6ZNky9PSAENg6c9AQYHcg4PjopRGGKmdD313ZHszymnps" 134 | ], 135 | [ 136 | "c10ec20dc3cd9f652c7fac2f1230f7a3c828389a14392f05", 137 | "scissors invite lock maple supreme raw rapid void congress muscle digital elegant little brisk hair mango congress clump", 138 | "7b4a10be9d98e6cba265566db7f136718e1398c71cb581e1b2f464cac1ceedf4f3e274dc270003c670ad8d02c4558b2f8e39edea2775c9e232c7cb798b069e88", 139 | "xprv9s21ZrQH143K4aERa2bq7559eMCCEs2QmmqVjUuzfy5eAeDX4mqZffkYwpzGQRE2YEEeLVRoH4CSHxianrFaVnMN2RYaPUZJhJx8S5j6puX" 140 | ], 141 | [ 142 | "f585c11aec520db57dd353c69554b21a89b20fb0650966fa0a9d6f74fd989d8f", 143 | "void come effort suffer camp survey warrior heavy shoot primary clutch crush open amazing screen patrol group space point ten exist slush involve unfold", 144 | "01f5bced59dec48e362f2c45b5de68b9fd6c92c6634f44d6d40aab69056506f0e35524a518034ddc1192e1dacd32c1ed3eaa3c3b131c88ed8e7e54c49a5d0998", 145 | "xprv9s21ZrQH143K39rnQJknpH1WEPFJrzmAqqasiDcVrNuk926oizzJDDQkdiTvNPr2FYDYzWgiMiC63YmfPAa2oPyNB23r2g7d1yiK6WpqaQS" 146 | ] 147 | ] 148 | } 149 | -------------------------------------------------------------------------------- /nft_manager.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import asyncio 3 | import aiosqlite 4 | from pathlib import Path 5 | import binascii 6 | import sqlite3 7 | from typing import Dict, List, Tuple, Optional, Union, Any 8 | from blspy import AugSchemeMPL, G1Element, G2Element, PrivateKey 9 | 10 | from chia.types.blockchain_format.coin import Coin 11 | from chia.types.spend_bundle import SpendBundle 12 | from chia.types.blockchain_format.program import Program, SerializedProgram 13 | from chia.util.hash import std_hash 14 | from clvm.casts import int_to_bytes, int_from_bytes 15 | from chia.util.byte_types import hexstr_to_bytes 16 | from chia.consensus.default_constants import DEFAULT_CONSTANTS 17 | from chia.wallet.puzzles.load_clvm import load_clvm 18 | from chia.util.condition_tools import ConditionOpcode 19 | from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( # standard_transaction 20 | puzzle_for_pk, 21 | calculate_synthetic_secret_key, 22 | DEFAULT_HIDDEN_PUZZLE_HASH, 23 | ) 24 | from chia.util.db_wrapper import DBWrapper 25 | from chia.full_node.coin_store import CoinStore 26 | from chia.wallet.derive_keys import ( 27 | master_sk_to_wallet_sk, 28 | master_sk_to_singleton_owner_sk, 29 | ) 30 | from chia.wallet.derive_keys import master_sk_to_wallet_sk_unhardened 31 | from chia.types.coin_spend import CoinSpend 32 | from chia.wallet.sign_coin_spends import sign_coin_spends 33 | from chia.wallet.lineage_proof import LineageProof 34 | from chia.wallet.puzzles import singleton_top_layer 35 | from chia.types.announcement import Announcement 36 | from chia.types.blockchain_format.sized_bytes import bytes32 37 | from chia.util.default_root import DEFAULT_ROOT_PATH 38 | from chia.rpc.rpc_client import RpcClient 39 | from chia.rpc.full_node_rpc_client import FullNodeRpcClient 40 | from chia.rpc.wallet_rpc_client import WalletRpcClient 41 | from chia.util.config import load_config 42 | from chia.util.ints import uint16, uint64 43 | from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash 44 | 45 | from sim import load_clsp_relative 46 | from nft_wallet import NFT, NFTWallet 47 | import driver 48 | 49 | 50 | SINGLETON_MOD = load_clvm("singleton_top_layer.clvm") 51 | SINGLETON_MOD_HASH = SINGLETON_MOD.get_tree_hash() 52 | LAUNCHER_PUZZLE = load_clsp_relative("clsp/nft_launcher.clsp") 53 | LAUNCHER_PUZZLE_HASH = LAUNCHER_PUZZLE.get_tree_hash() 54 | 55 | 56 | config = load_config(Path(DEFAULT_ROOT_PATH), "config.yaml") 57 | testnet_agg_sig_data = config["network_overrides"]["constants"]["testnet10"]["AGG_SIG_ME_ADDITIONAL_DATA"] 58 | DEFAULT_CONSTANTS = DEFAULT_CONSTANTS.replace_str_to_bytes(**{"AGG_SIG_ME_ADDITIONAL_DATA": testnet_agg_sig_data}) 59 | 60 | 61 | class NFTManager: 62 | def __init__( 63 | self, 64 | wallet_client: WalletRpcClient = None, 65 | node_client: FullNodeRpcClient = None, 66 | db_name: str = "nft_store.db", 67 | ) -> None: 68 | self.wallet_client = wallet_client 69 | self.node_client = node_client 70 | self.db_name = db_name 71 | self.connection = None 72 | self.key_dict = {} 73 | 74 | async def connect(self, wallet_index: int = 0) -> None: 75 | config = load_config(Path(DEFAULT_ROOT_PATH), "config.yaml") 76 | rpc_host = config["self_hostname"] 77 | full_node_rpc_port = config["full_node"]["rpc_port"] 78 | wallet_rpc_port = config["wallet"]["rpc_port"] 79 | if not self.node_client: 80 | self.node_client = await FullNodeRpcClient.create( 81 | rpc_host, uint16(full_node_rpc_port), Path(DEFAULT_ROOT_PATH), config 82 | ) 83 | if not self.wallet_client: 84 | self.wallet_client = await WalletRpcClient.create( 85 | rpc_host, uint16(wallet_rpc_port), Path(DEFAULT_ROOT_PATH), config 86 | ) 87 | self.connection = await aiosqlite.connect(Path(self.db_name)) 88 | self.db_wrapper = DBWrapper(self.connection) 89 | self.nft_wallet = await NFTWallet.create(self.db_wrapper, self.node_client) 90 | self.fingerprints = await self.wallet_client.get_public_keys() 91 | fp = self.fingerprints[wallet_index] 92 | private_key = await self.wallet_client.get_private_key(fp) 93 | sk_data = binascii.unhexlify(private_key["sk"]) 94 | self.master_sk = PrivateKey.from_bytes(sk_data) 95 | await self.derive_nft_keys() 96 | await self.derive_wallet_keys() 97 | await self.derive_unhardened_keys() 98 | await self.nft_wallet.update_to_current_block() 99 | 100 | async def close(self) -> None: 101 | if self.node_client: 102 | self.node_client.close() 103 | 104 | if self.wallet_client: 105 | self.wallet_client.close() 106 | 107 | if self.connection: 108 | await self.connection.close() 109 | 110 | async def sync(self) -> None: 111 | await self.nft_wallet.basic_sync() 112 | 113 | async def derive_nft_keys(self, index: int = 0) -> None: 114 | _sk = master_sk_to_singleton_owner_sk(self.master_sk, index) 115 | synth_sk = calculate_synthetic_secret_key(_sk, driver.INNER_MOD.get_tree_hash()) 116 | self.key_dict[bytes(synth_sk.get_g1())] = synth_sk 117 | self.nft_sk = synth_sk 118 | self.nft_pk = synth_sk.get_g1() 119 | 120 | async def derive_wallet_keys(self, index=0): 121 | _sk = master_sk_to_wallet_sk(self.master_sk, index) 122 | synth_sk = calculate_synthetic_secret_key(_sk, DEFAULT_HIDDEN_PUZZLE_HASH) 123 | self.key_dict[bytes(synth_sk.get_g1())] = synth_sk 124 | self.key_dict[bytes(_sk.get_g1())] = _sk 125 | self.wallet_sk = _sk 126 | 127 | async def derive_unhardened_keys(self, n=10): 128 | for i in range(n): 129 | #_sk = AugSchemeMPL.derive_child_sk_unhardened(self.master_sk, i) # TESTING on main branch 130 | _sk = master_sk_to_wallet_sk_unhardened(self.master_sk, i) # protocol_and_cats_branch 131 | synth_sk = calculate_synthetic_secret_key(_sk, DEFAULT_HIDDEN_PUZZLE_HASH) 132 | self.key_dict[bytes(_sk.get_g1())] = _sk 133 | self.key_dict[bytes(synth_sk.get_g1())] = synth_sk 134 | 135 | async def pk_to_sk(self, pk): 136 | return self.key_dict.get(bytes(pk)) 137 | 138 | async def available_balance(self) -> int: 139 | balance_data = await self.wallet_client.get_wallet_balance(1) 140 | return balance_data["confirmed_wallet_balance"] 141 | 142 | async def choose_std_coin(self, amount: int) -> Tuple[Coin, Program]: 143 | # check that wallet_balance is greater than amount 144 | assert await self.available_balance() > amount 145 | for k in self.key_dict.keys(): 146 | puzzle = puzzle_for_pk(k) 147 | my_coins = await self.node_client.get_coin_records_by_puzzle_hash( 148 | puzzle.get_tree_hash(), include_spent_coins=False 149 | ) 150 | if my_coins: 151 | coin_record = next((cr for cr in my_coins if (cr.coin.amount >= amount) and (not cr.spent)), None) 152 | if coin_record: 153 | assert not coin_record.spent 154 | assert coin_record.coin.puzzle_hash == puzzle.get_tree_hash() 155 | synth_sk = calculate_synthetic_secret_key(self.key_dict[k], DEFAULT_HIDDEN_PUZZLE_HASH) 156 | self.key_dict[bytes(synth_sk.get_g1())] = synth_sk 157 | return (coin_record.coin, puzzle) 158 | raise ValueError("No spendable coins found") 159 | 160 | async def launch_nft(self, amount: int, nft_data: Tuple, launch_state: List, royalty: List) -> bytes: 161 | addr = await self.wallet_client.get_next_address(1, False) 162 | puzzle_hash = decode_puzzle_hash(addr) 163 | launch_state += [puzzle_hash, self.nft_pk] 164 | royalty.insert(0, puzzle_hash) 165 | 166 | found_coin, found_coin_puzzle = await self.choose_std_coin(amount) 167 | 168 | launcher_coin = Coin(found_coin.name(), LAUNCHER_PUZZLE_HASH, amount) 169 | 170 | launcher_spend = driver.make_launcher_spend(found_coin, amount, launch_state, royalty, nft_data) 171 | found_spend = driver.make_found_spend(found_coin, found_coin_puzzle, launcher_spend, amount) 172 | eve_spend = driver.make_eve_spend(launch_state, royalty, launcher_spend) 173 | 174 | sb = await sign_coin_spends( 175 | [launcher_spend, found_spend, eve_spend], 176 | self.pk_to_sk, 177 | DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA, 178 | DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, 179 | ) 180 | 181 | res = await self.node_client.push_tx(sb) 182 | if res["success"]: 183 | # add launcher_id and pk to nft_coins 184 | await self.nft_wallet.save_launcher(launcher_coin.name(), self.nft_pk) 185 | tx_id = await self.get_tx_from_mempool(sb.name()) 186 | return (tx_id, launcher_coin.name()) 187 | 188 | async def get_tx_from_mempool(self, sb_name): 189 | # get mempool txn 190 | mempool_items = await self.node_client.get_all_mempool_items() 191 | for tx_id in mempool_items.keys(): 192 | mem_sb_name = bytes32(hexstr_to_bytes(mempool_items[tx_id]["spend_bundle_name"])) 193 | if mem_sb_name == sb_name: 194 | return tx_id 195 | raise ValueError("No tx found in mempool. Check if confirmed") 196 | 197 | async def wait_for_confirmation(self, tx_id, launcher_id): 198 | while True: 199 | item = await self.node_client.get_mempool_item_by_tx_id(tx_id) 200 | if not item: 201 | return await self.nft_wallet.get_nft_by_launcher_id(launcher_id) 202 | else: 203 | print("Waiting for block (30s)") 204 | await asyncio.sleep(30) 205 | 206 | async def update_nft(self, nft_id: bytes, new_state: List) -> bytes: 207 | nft = await self.nft_wallet.get_nft_by_launcher_id(nft_id) 208 | addr = await self.wallet_client.get_next_address(1, False) 209 | puzzle_hash = decode_puzzle_hash(addr) 210 | new_state += [puzzle_hash, self.nft_pk] 211 | update_spend = driver.make_update_spend(nft, new_state) 212 | conds = driver.run_singleton(update_spend.puzzle_reveal.to_program(), update_spend.solution.to_program()) 213 | target_pk = conds[-1][1] 214 | 215 | sb = await sign_coin_spends( 216 | [update_spend], 217 | self.pk_to_sk, 218 | DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA, 219 | DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, 220 | ) 221 | res = await self.node_client.push_tx(sb) 222 | if res["success"]: 223 | tx_id = await self.get_tx_from_mempool(sb.name()) 224 | return tx_id 225 | 226 | async def get_my_nfts(self) -> List[NFT]: 227 | launcher_ids = await self.nft_wallet.get_all_nft_ids() 228 | my_nfts = [] 229 | for launcher_id in launcher_ids: 230 | nft = await self.nft_wallet.get_nft_by_launcher_id(launcher_id) 231 | if nft.owner_pk() == bytes(self.nft_pk): 232 | my_nfts.append(nft) 233 | return my_nfts 234 | 235 | async def get_for_sale_nfts(self) -> List[NFT]: 236 | launcher_ids = await self.nft_wallet.get_all_nft_ids() 237 | for_sale_nfts = [] 238 | for launcher_id in launcher_ids: 239 | nft = await self.nft_wallet.get_nft_by_launcher_id(launcher_id) 240 | if (nft.is_for_sale()) and (nft.owner_pk() != bytes(self.nft_pk)): 241 | for_sale_nfts.append(nft) 242 | return for_sale_nfts 243 | 244 | async def buy_nft(self, launcher_id: bytes, new_state: List) -> bytes: 245 | nft = await self.nft_wallet.get_nft_by_launcher_id(launcher_id) 246 | addr = await self.wallet_client.get_next_address(1, False) 247 | ph = decode_puzzle_hash(addr) 248 | new_state += [ph, self.nft_pk] 249 | payment_coin, payment_coin_puzzle = await self.choose_std_coin(nft.price()) 250 | nft_spend, p2_spend, payment_spend = driver.make_buy_spend(nft, new_state, payment_coin, payment_coin_puzzle) 251 | 252 | sb = await sign_coin_spends( 253 | [nft_spend, p2_spend, payment_spend], 254 | self.pk_to_sk, 255 | DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA, 256 | DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, 257 | ) 258 | res = await self.node_client.push_tx(sb) 259 | if res["success"]: 260 | tx_id = await self.get_tx_from_mempool(sb.name()) 261 | return tx_id 262 | 263 | async def view_nft(self, launcher_id: bytes) -> NFT: 264 | nft = await self.nft_wallet.get_nft_by_launcher_id(launcher_id) 265 | return nft 266 | 267 | 268 | async def main(func): 269 | # DATA 270 | amount = 101 271 | with open(Path("art/bird1.txt"), "r") as f: 272 | k = f.readlines() 273 | data = "".join(k) 274 | nft_data = ("CreatorNFT", data) 275 | launch_state = [100, 1000] # append ph and pk later 276 | royalty = [10] 277 | 278 | manager = NFTManager() 279 | await manager.connect() 280 | 281 | if func == "test3": 282 | print(await manager.available_balance()) 283 | txns = await manager.wallet_client.get_transactions("1") 284 | for tx in txns: 285 | if (tx.type == 0) and (tx.removals != []): 286 | print(tx) 287 | await manager.close() 288 | 289 | return manager 290 | 291 | 292 | if __name__ == "__main__": 293 | 294 | func = sys.argv[1] 295 | 296 | m = asyncio.run(main(func)) 297 | -------------------------------------------------------------------------------- /tests/setup_nodes.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import atexit 3 | import signal 4 | 5 | from secrets import token_bytes 6 | from typing import Dict, List, Optional 7 | 8 | from chia.consensus.constants import ConsensusConstants 9 | from chia.daemon.server import WebSocketServer, create_server_for_daemon, daemon_launch_lock_path, singleton 10 | from chia.full_node.full_node_api import FullNodeAPI 11 | from chia.server.start_farmer import service_kwargs_for_farmer 12 | from chia.server.start_full_node import service_kwargs_for_full_node 13 | from chia.server.start_harvester import service_kwargs_for_harvester 14 | from chia.server.start_introducer import service_kwargs_for_introducer 15 | from chia.server.start_service import Service 16 | from chia.server.start_timelord import service_kwargs_for_timelord 17 | from chia.server.start_wallet import service_kwargs_for_wallet 18 | from chia.simulator.start_simulator import service_kwargs_for_full_node_simulator 19 | from chia.timelord.timelord_launcher import kill_processes, spawn_process 20 | from chia.types.peer_info import PeerInfo 21 | from chia.util.bech32m import encode_puzzle_hash 22 | from tests.block_tools import create_block_tools, create_block_tools_async, test_constants 23 | from tests.util.keyring import TempKeyring 24 | from chia.util.hash import std_hash 25 | from chia.util.ints import uint16, uint32 26 | from chia.util.keychain import bytes_to_mnemonic 27 | from tests.time_out_assert import time_out_assert_custom_interval 28 | 29 | 30 | def cleanup_keyring(keyring: TempKeyring): 31 | keyring.cleanup() 32 | 33 | 34 | temp_keyring = TempKeyring() 35 | keychain = temp_keyring.get_keychain() 36 | atexit.register(cleanup_keyring, temp_keyring) # Attempt to cleanup the temp keychain 37 | bt = create_block_tools(constants=test_constants, keychain=keychain) 38 | 39 | self_hostname = bt.config["self_hostname"] 40 | 41 | 42 | def constants_for_dic(dic): 43 | return test_constants.replace(**dic) 44 | 45 | 46 | async def _teardown_nodes(node_aiters: List) -> None: 47 | awaitables = [node_iter.__anext__() for node_iter in node_aiters] 48 | for sublist_awaitable in asyncio.as_completed(awaitables): 49 | try: 50 | await sublist_awaitable 51 | except StopAsyncIteration: 52 | pass 53 | 54 | 55 | async def setup_daemon(btools): 56 | root_path = btools.root_path 57 | config = btools.config 58 | lockfile = singleton(daemon_launch_lock_path(root_path)) 59 | crt_path = root_path / config["daemon_ssl"]["private_crt"] 60 | key_path = root_path / config["daemon_ssl"]["private_key"] 61 | ca_crt_path = root_path / config["private_ssl_ca"]["crt"] 62 | ca_key_path = root_path / config["private_ssl_ca"]["key"] 63 | assert lockfile is not None 64 | create_server_for_daemon(btools.root_path) 65 | ws_server = WebSocketServer(root_path, ca_crt_path, ca_key_path, crt_path, key_path) 66 | await ws_server.start() 67 | 68 | yield ws_server 69 | 70 | await ws_server.stop() 71 | 72 | 73 | async def setup_full_node( 74 | consensus_constants: ConsensusConstants, 75 | db_name, 76 | port, 77 | local_bt, 78 | introducer_port=None, 79 | simulator=False, 80 | send_uncompact_interval=0, 81 | sanitize_weight_proof_only=False, 82 | connect_to_daemon=False, 83 | ): 84 | db_path = local_bt.root_path / f"{db_name}" 85 | if db_path.exists(): 86 | db_path.unlink() 87 | config = local_bt.config["full_node"] 88 | config["database_path"] = db_name 89 | config["send_uncompact_interval"] = send_uncompact_interval 90 | config["target_uncompact_proofs"] = 30 91 | config["peer_connect_interval"] = 50 92 | config["sanitize_weight_proof_only"] = sanitize_weight_proof_only 93 | if introducer_port is not None: 94 | config["introducer_peer"]["host"] = self_hostname 95 | config["introducer_peer"]["port"] = introducer_port 96 | else: 97 | config["introducer_peer"] = None 98 | config["dns_servers"] = [] 99 | config["port"] = port 100 | config["rpc_port"] = port + 1000 101 | overrides = config["network_overrides"]["constants"][config["selected_network"]] 102 | updated_constants = consensus_constants.replace_str_to_bytes(**overrides) 103 | if simulator: 104 | kwargs = service_kwargs_for_full_node_simulator(local_bt.root_path, config, local_bt) 105 | else: 106 | kwargs = service_kwargs_for_full_node(local_bt.root_path, config, updated_constants) 107 | 108 | kwargs.update( 109 | parse_cli_args=False, 110 | connect_to_daemon=connect_to_daemon, 111 | ) 112 | 113 | service = Service(**kwargs) 114 | 115 | await service.start() 116 | 117 | yield service._api 118 | 119 | service.stop() 120 | await service.wait_closed() 121 | if db_path.exists(): 122 | db_path.unlink() 123 | 124 | 125 | async def setup_wallet_node( 126 | port, 127 | consensus_constants: ConsensusConstants, 128 | local_bt, 129 | full_node_port=None, 130 | introducer_port=None, 131 | key_seed=None, 132 | starting_height=None, 133 | ): 134 | with TempKeyring() as keychain: 135 | config = bt.config["wallet"] 136 | config["port"] = port 137 | config["rpc_port"] = port + 1000 138 | if starting_height is not None: 139 | config["starting_height"] = starting_height 140 | config["initial_num_public_keys"] = 5 141 | 142 | entropy = token_bytes(32) 143 | if key_seed is None: 144 | key_seed = entropy 145 | keychain.add_private_key(bytes_to_mnemonic(key_seed), "") 146 | first_pk = keychain.get_first_public_key() 147 | assert first_pk is not None 148 | db_path_key_suffix = str(first_pk.get_fingerprint()) 149 | db_name = f"test-wallet-db-{port}-KEY.sqlite" 150 | db_path_replaced: str = db_name.replace("KEY", db_path_key_suffix) 151 | db_path = bt.root_path / db_path_replaced 152 | 153 | if db_path.exists(): 154 | db_path.unlink() 155 | config["database_path"] = str(db_name) 156 | config["testing"] = True 157 | 158 | config["introducer_peer"]["host"] = self_hostname 159 | if introducer_port is not None: 160 | config["introducer_peer"]["port"] = introducer_port 161 | config["peer_connect_interval"] = 10 162 | else: 163 | config["introducer_peer"] = None 164 | 165 | if full_node_port is not None: 166 | config["full_node_peer"] = {} 167 | config["full_node_peer"]["host"] = self_hostname 168 | config["full_node_peer"]["port"] = full_node_port 169 | else: 170 | del config["full_node_peer"] 171 | 172 | kwargs = service_kwargs_for_wallet(local_bt.root_path, config, consensus_constants, keychain) 173 | kwargs.update( 174 | parse_cli_args=False, 175 | connect_to_daemon=False, 176 | ) 177 | 178 | service = Service(**kwargs) 179 | 180 | await service.start(new_wallet=True) 181 | 182 | yield service._node, service._node.server 183 | 184 | service.stop() 185 | await service.wait_closed() 186 | if db_path.exists(): 187 | db_path.unlink() 188 | keychain.delete_all_keys() 189 | 190 | 191 | async def setup_harvester( 192 | port, farmer_port, consensus_constants: ConsensusConstants, b_tools, start_service: bool = True 193 | ): 194 | kwargs = service_kwargs_for_harvester(b_tools.root_path, b_tools.config["harvester"], consensus_constants) 195 | kwargs.update( 196 | server_listen_ports=[port], 197 | advertised_port=port, 198 | connect_peers=[PeerInfo(self_hostname, farmer_port)], 199 | parse_cli_args=False, 200 | connect_to_daemon=False, 201 | ) 202 | 203 | service = Service(**kwargs) 204 | 205 | if start_service: 206 | await service.start() 207 | 208 | yield service 209 | 210 | service.stop() 211 | await service.wait_closed() 212 | 213 | 214 | async def setup_farmer( 215 | port, 216 | consensus_constants: ConsensusConstants, 217 | b_tools, 218 | full_node_port: Optional[uint16] = None, 219 | start_service: bool = True, 220 | ): 221 | config = bt.config["farmer"] 222 | config_pool = bt.config["pool"] 223 | 224 | config["xch_target_address"] = encode_puzzle_hash(b_tools.farmer_ph, "xch") 225 | config["pool_public_keys"] = [bytes(pk).hex() for pk in b_tools.pool_pubkeys] 226 | config["port"] = port 227 | config_pool["xch_target_address"] = encode_puzzle_hash(b_tools.pool_ph, "xch") 228 | 229 | if full_node_port: 230 | config["full_node_peer"]["host"] = self_hostname 231 | config["full_node_peer"]["port"] = full_node_port 232 | else: 233 | del config["full_node_peer"] 234 | 235 | kwargs = service_kwargs_for_farmer( 236 | b_tools.root_path, config, config_pool, consensus_constants, b_tools.local_keychain 237 | ) 238 | kwargs.update( 239 | parse_cli_args=False, 240 | connect_to_daemon=False, 241 | ) 242 | 243 | service = Service(**kwargs) 244 | 245 | if start_service: 246 | await service.start() 247 | 248 | yield service 249 | 250 | service.stop() 251 | await service.wait_closed() 252 | 253 | 254 | async def setup_introducer(port): 255 | kwargs = service_kwargs_for_introducer( 256 | bt.root_path, 257 | bt.config["introducer"], 258 | ) 259 | kwargs.update( 260 | advertised_port=port, 261 | parse_cli_args=False, 262 | connect_to_daemon=False, 263 | ) 264 | 265 | service = Service(**kwargs) 266 | 267 | await service.start() 268 | 269 | yield service._api, service._node.server 270 | 271 | service.stop() 272 | await service.wait_closed() 273 | 274 | 275 | async def setup_vdf_client(port): 276 | vdf_task_1 = asyncio.create_task(spawn_process(self_hostname, port, 1, bt.config.get("prefer_ipv6"))) 277 | 278 | def stop(): 279 | asyncio.create_task(kill_processes()) 280 | 281 | asyncio.get_running_loop().add_signal_handler(signal.SIGTERM, stop) 282 | asyncio.get_running_loop().add_signal_handler(signal.SIGINT, stop) 283 | 284 | yield vdf_task_1 285 | await kill_processes() 286 | 287 | 288 | async def setup_vdf_clients(port): 289 | vdf_task_1 = asyncio.create_task(spawn_process(self_hostname, port, 1, bt.config.get("prefer_ipv6"))) 290 | vdf_task_2 = asyncio.create_task(spawn_process(self_hostname, port, 2, bt.config.get("prefer_ipv6"))) 291 | vdf_task_3 = asyncio.create_task(spawn_process(self_hostname, port, 3, bt.config.get("prefer_ipv6"))) 292 | 293 | def stop(): 294 | asyncio.create_task(kill_processes()) 295 | 296 | asyncio.get_running_loop().add_signal_handler(signal.SIGTERM, stop) 297 | asyncio.get_running_loop().add_signal_handler(signal.SIGINT, stop) 298 | 299 | yield vdf_task_1, vdf_task_2, vdf_task_3 300 | 301 | await kill_processes() 302 | 303 | 304 | async def setup_timelord(port, full_node_port, sanitizer, consensus_constants: ConsensusConstants, b_tools): 305 | config = b_tools.config["timelord"] 306 | config["port"] = port 307 | config["full_node_peer"]["port"] = full_node_port 308 | config["bluebox_mode"] = sanitizer 309 | config["fast_algorithm"] = False 310 | if sanitizer: 311 | config["vdf_server"]["port"] = 7999 312 | 313 | kwargs = service_kwargs_for_timelord(b_tools.root_path, config, consensus_constants) 314 | kwargs.update( 315 | parse_cli_args=False, 316 | connect_to_daemon=False, 317 | ) 318 | 319 | service = Service(**kwargs) 320 | 321 | await service.start() 322 | 323 | yield service._api, service._node.server 324 | 325 | service.stop() 326 | await service.wait_closed() 327 | 328 | 329 | async def setup_two_nodes(consensus_constants: ConsensusConstants): 330 | """ 331 | Setup and teardown of two full nodes, with blockchains and separate DBs. 332 | """ 333 | 334 | with TempKeyring() as keychain1, TempKeyring() as keychain2: 335 | node_iters = [ 336 | setup_full_node( 337 | consensus_constants, 338 | "blockchain_test.db", 339 | 21234, 340 | await create_block_tools_async(constants=test_constants, keychain=keychain1), 341 | simulator=False, 342 | ), 343 | setup_full_node( 344 | consensus_constants, 345 | "blockchain_test_2.db", 346 | 21235, 347 | await create_block_tools_async(constants=test_constants, keychain=keychain2), 348 | simulator=False, 349 | ), 350 | ] 351 | 352 | fn1 = await node_iters[0].__anext__() 353 | fn2 = await node_iters[1].__anext__() 354 | 355 | yield fn1, fn2, fn1.full_node.server, fn2.full_node.server 356 | 357 | await _teardown_nodes(node_iters) 358 | 359 | 360 | async def setup_n_nodes(consensus_constants: ConsensusConstants, n: int): 361 | """ 362 | Setup and teardown of n full nodes, with blockchains and separate DBs. 363 | """ 364 | port_start = 21244 365 | node_iters = [] 366 | keyrings_to_cleanup = [] 367 | for i in range(n): 368 | keyring = TempKeyring() 369 | keyrings_to_cleanup.append(keyring) 370 | node_iters.append( 371 | setup_full_node( 372 | consensus_constants, 373 | f"blockchain_test_{i}.db", 374 | port_start + i, 375 | await create_block_tools_async(constants=test_constants, keychain=keyring.get_keychain()), 376 | simulator=False, 377 | ) 378 | ) 379 | nodes = [] 380 | for ni in node_iters: 381 | nodes.append(await ni.__anext__()) 382 | 383 | yield nodes 384 | 385 | await _teardown_nodes(node_iters) 386 | 387 | for keyring in keyrings_to_cleanup: 388 | keyring.cleanup() 389 | 390 | 391 | async def setup_node_and_wallet(consensus_constants: ConsensusConstants, starting_height=None, key_seed=None): 392 | with TempKeyring() as keychain: 393 | btools = await create_block_tools_async(constants=test_constants, keychain=keychain) 394 | node_iters = [ 395 | setup_full_node(consensus_constants, "blockchain_test.db", 21234, btools, simulator=False), 396 | setup_wallet_node( 397 | 21235, consensus_constants, btools, None, starting_height=starting_height, key_seed=key_seed 398 | ), 399 | ] 400 | 401 | full_node_api = await node_iters[0].__anext__() 402 | wallet, s2 = await node_iters[1].__anext__() 403 | 404 | yield full_node_api, wallet, full_node_api.full_node.server, s2 405 | 406 | await _teardown_nodes(node_iters) 407 | 408 | 409 | async def setup_simulators_and_wallets( 410 | simulator_count: int, 411 | wallet_count: int, 412 | dic: Dict, 413 | starting_height=None, 414 | key_seed=None, 415 | starting_port=50000, 416 | ): 417 | with TempKeyring() as keychain1, TempKeyring() as keychain2: 418 | simulators: List[FullNodeAPI] = [] 419 | wallets = [] 420 | node_iters = [] 421 | 422 | consensus_constants = constants_for_dic(dic) 423 | for index in range(0, simulator_count): 424 | port = starting_port + index 425 | db_name = f"blockchain_test_{port}.db" 426 | bt_tools = await create_block_tools_async( 427 | consensus_constants, const_dict=dic, keychain=keychain1 428 | ) # block tools modifies constants 429 | sim = setup_full_node( 430 | bt_tools.constants, 431 | db_name, 432 | port, 433 | bt_tools, 434 | simulator=True, 435 | ) 436 | simulators.append(await sim.__anext__()) 437 | node_iters.append(sim) 438 | 439 | for index in range(0, wallet_count): 440 | if key_seed is None: 441 | seed = std_hash(uint32(index)) 442 | else: 443 | seed = key_seed 444 | port = starting_port + 5000 + index 445 | bt_tools = await create_block_tools_async( 446 | consensus_constants, const_dict=dic, keychain=keychain2 447 | ) # block tools modifies constants 448 | wlt = setup_wallet_node( 449 | port, 450 | bt_tools.constants, 451 | bt_tools, 452 | None, 453 | key_seed=seed, 454 | starting_height=starting_height, 455 | ) 456 | wallets.append(await wlt.__anext__()) 457 | node_iters.append(wlt) 458 | 459 | yield simulators, wallets 460 | 461 | await _teardown_nodes(node_iters) 462 | 463 | 464 | async def setup_farmer_harvester(consensus_constants: ConsensusConstants, start_services: bool = True): 465 | node_iters = [ 466 | setup_harvester(21234, 21235, consensus_constants, bt, start_services), 467 | setup_farmer(21235, consensus_constants, bt, start_service=start_services), 468 | ] 469 | 470 | harvester_service = await node_iters[0].__anext__() 471 | farmer_service = await node_iters[1].__anext__() 472 | 473 | yield harvester_service, farmer_service 474 | 475 | await _teardown_nodes(node_iters) 476 | 477 | 478 | async def setup_full_system( 479 | consensus_constants: ConsensusConstants, b_tools=None, b_tools_1=None, connect_to_daemon=False 480 | ): 481 | with TempKeyring() as keychain1, TempKeyring() as keychain2: 482 | if b_tools is None: 483 | b_tools = await create_block_tools_async(constants=test_constants, keychain=keychain1) 484 | if b_tools_1 is None: 485 | b_tools_1 = await create_block_tools_async(constants=test_constants, keychain=keychain2) 486 | node_iters = [ 487 | setup_introducer(21233), 488 | setup_harvester(21234, 21235, consensus_constants, b_tools), 489 | setup_farmer(21235, consensus_constants, b_tools, uint16(21237)), 490 | setup_vdf_clients(8000), 491 | setup_timelord(21236, 21237, False, consensus_constants, b_tools), 492 | setup_full_node( 493 | consensus_constants, "blockchain_test.db", 21237, b_tools, 21233, False, 10, True, connect_to_daemon 494 | ), 495 | setup_full_node( 496 | consensus_constants, "blockchain_test_2.db", 21238, b_tools_1, 21233, False, 10, True, connect_to_daemon 497 | ), 498 | setup_vdf_client(7999), 499 | setup_timelord(21239, 21238, True, consensus_constants, b_tools_1), 500 | ] 501 | 502 | introducer, introducer_server = await node_iters[0].__anext__() 503 | harvester_service = await node_iters[1].__anext__() 504 | harvester = harvester_service._node 505 | farmer_service = await node_iters[2].__anext__() 506 | farmer = farmer_service._node 507 | 508 | async def num_connections(): 509 | count = len(harvester.server.all_connections.items()) 510 | return count 511 | 512 | await time_out_assert_custom_interval(10, 3, num_connections, 1) 513 | 514 | vdf_clients = await node_iters[3].__anext__() 515 | timelord, timelord_server = await node_iters[4].__anext__() 516 | node_api_1 = await node_iters[5].__anext__() 517 | node_api_2 = await node_iters[6].__anext__() 518 | vdf_sanitizer = await node_iters[7].__anext__() 519 | sanitizer, sanitizer_server = await node_iters[8].__anext__() 520 | 521 | yield ( 522 | node_api_1, 523 | node_api_2, 524 | harvester, 525 | farmer, 526 | introducer, 527 | timelord, 528 | vdf_clients, 529 | vdf_sanitizer, 530 | sanitizer, 531 | node_api_1.full_node.server, 532 | ) 533 | 534 | await _teardown_nodes(node_iters) 535 | -------------------------------------------------------------------------------- /tests/test_manager.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from operator import attrgetter 4 | from chia.util.config import load_config, save_config 5 | import logging 6 | from pathlib import Path 7 | 8 | import pytest 9 | 10 | from chia.consensus.block_rewards import calculate_base_farmer_reward, calculate_pool_reward 11 | from chia.rpc.full_node_rpc_api import FullNodeRpcApi 12 | from chia.rpc.full_node_rpc_client import FullNodeRpcClient 13 | from chia.rpc.rpc_server import start_rpc_server 14 | from chia.rpc.wallet_rpc_api import WalletRpcApi 15 | from chia.rpc.wallet_rpc_client import WalletRpcClient 16 | from chia.simulator.simulator_protocol import FarmNewBlockProtocol 17 | from chia.types.peer_info import PeerInfo 18 | from chia.util.bech32m import encode_puzzle_hash, decode_puzzle_hash 19 | from chia.types.blockchain_format.sized_bytes import bytes32 20 | from chia.consensus.coinbase import create_puzzlehash_for_pk 21 | from chia.wallet.derive_keys import master_sk_to_wallet_sk 22 | from chia.util.ints import uint16, uint32 23 | from chia.wallet.transaction_record import TransactionRecord 24 | from chia.protocols.full_node_protocol import RespondBlock 25 | from clvm.casts import int_to_bytes, int_from_bytes 26 | 27 | # from chia.wallet.transaction_sorting import SortKey 28 | from tests.setup_nodes import bt, setup_simulators_and_wallets, self_hostname 29 | from tests.time_out_assert import time_out_assert 30 | from tests.util.rpc import validate_get_routes 31 | from tests.connection_utils import connect_and_get_peer 32 | from nft_manager import NFTManager 33 | 34 | 35 | class TestNFTWallet: 36 | @pytest.fixture(scope="function") 37 | async def two_wallet_nodes(self): 38 | async for _ in setup_simulators_and_wallets(1, 2, {}): 39 | yield _ 40 | 41 | @pytest.fixture(scope="function") 42 | async def three_wallet_nodes(self): 43 | async for _ in setup_simulators_and_wallets(3, 3, {}): 44 | yield _ 45 | 46 | # @pytest.mark.asyncio 47 | @pytest.fixture(scope="function") 48 | async def three_nft_managers(self, three_wallet_nodes, tmp_path): 49 | num_blocks = 5 50 | config = bt.config 51 | hostname = config["self_hostname"] 52 | daemon_port = config["daemon_port"] 53 | wallet_rpc_port_0 = 21520 54 | wallet_rpc_port_1 = 21521 55 | wallet_rpc_port_2 = 21522 56 | 57 | node_rpc_port_0 = 21530 58 | node_rpc_port_1 = 21531 59 | node_rpc_port_2 = 21532 60 | 61 | full_nodes, wallets = three_wallet_nodes 62 | 63 | wallet_0, wallet_server_0 = wallets[0] 64 | wallet_1, wallet_server_1 = wallets[1] 65 | wallet_2, wallet_server_2 = wallets[2] 66 | 67 | full_node_api_0 = full_nodes[0] 68 | full_node_api_1 = full_nodes[1] 69 | full_node_api_2 = full_nodes[2] 70 | 71 | full_node_0 = full_node_api_0.full_node 72 | full_node_1 = full_node_api_1.full_node 73 | full_node_2 = full_node_api_2.full_node 74 | 75 | server_0 = full_node_0.server 76 | server_1 = full_node_1.server 77 | server_2 = full_node_2.server 78 | 79 | # wallet_0 <-> server_0 80 | await wallet_server_0.start_client(PeerInfo(self_hostname, uint16(server_0._port)), None) 81 | # wallet_1 <-> server_1 82 | await wallet_server_1.start_client(PeerInfo(self_hostname, uint16(server_1._port)), None) 83 | # wallet_2 <-> server_2 84 | await wallet_server_2.start_client(PeerInfo(self_hostname, uint16(server_2._port)), None) 85 | 86 | await server_0.start_client(PeerInfo(self_hostname, uint16(server_1._port))) 87 | await server_0.start_client(PeerInfo(self_hostname, uint16(server_2._port))) 88 | await server_1.start_client(PeerInfo(self_hostname, uint16(server_2._port))) 89 | await server_1.start_client(PeerInfo(self_hostname, uint16(server_0._port))) 90 | await server_2.start_client(PeerInfo(self_hostname, uint16(server_0._port))) 91 | await server_2.start_client(PeerInfo(self_hostname, uint16(server_1._port))) 92 | 93 | def stop_node_cb(): 94 | pass 95 | 96 | wallet_rpc_api_0 = WalletRpcApi(wallet_0) 97 | wallet_rpc_api_1 = WalletRpcApi(wallet_1) 98 | wallet_rpc_api_2 = WalletRpcApi(wallet_2) 99 | 100 | full_node_rpc_api_0 = FullNodeRpcApi(full_node_0) 101 | full_node_rpc_api_1 = FullNodeRpcApi(full_node_1) 102 | full_node_rpc_api_2 = FullNodeRpcApi(full_node_2) 103 | 104 | rpc_cleanup_node_0 = await start_rpc_server( 105 | full_node_rpc_api_0, 106 | hostname, 107 | daemon_port, 108 | node_rpc_port_0, 109 | stop_node_cb, 110 | bt.root_path, 111 | config, 112 | connect_to_daemon=False, 113 | ) 114 | rpc_cleanup_node_1 = await start_rpc_server( 115 | full_node_rpc_api_1, 116 | hostname, 117 | daemon_port, 118 | node_rpc_port_1, 119 | stop_node_cb, 120 | bt.root_path, 121 | config, 122 | connect_to_daemon=False, 123 | ) 124 | rpc_cleanup_node_2 = await start_rpc_server( 125 | full_node_rpc_api_2, 126 | hostname, 127 | daemon_port, 128 | node_rpc_port_2, 129 | stop_node_cb, 130 | bt.root_path, 131 | config, 132 | connect_to_daemon=False, 133 | ) 134 | 135 | rpc_cleanup_wallet_0 = await start_rpc_server( 136 | wallet_rpc_api_0, 137 | hostname, 138 | daemon_port, 139 | wallet_rpc_port_0, 140 | stop_node_cb, 141 | bt.root_path, 142 | config, 143 | connect_to_daemon=False, 144 | ) 145 | rpc_cleanup_wallet_1 = await start_rpc_server( 146 | wallet_rpc_api_1, 147 | hostname, 148 | daemon_port, 149 | wallet_rpc_port_1, 150 | stop_node_cb, 151 | bt.root_path, 152 | config, 153 | connect_to_daemon=False, 154 | ) 155 | rpc_cleanup_wallet_2 = await start_rpc_server( 156 | wallet_rpc_api_2, 157 | hostname, 158 | daemon_port, 159 | wallet_rpc_port_2, 160 | stop_node_cb, 161 | bt.root_path, 162 | config, 163 | connect_to_daemon=False, 164 | ) 165 | 166 | wallet_client_0 = await WalletRpcClient.create(self_hostname, wallet_rpc_port_0, bt.root_path, config) 167 | wallet_client_1 = await WalletRpcClient.create(self_hostname, wallet_rpc_port_1, bt.root_path, config) 168 | wallet_client_2 = await WalletRpcClient.create(self_hostname, wallet_rpc_port_2, bt.root_path, config) 169 | 170 | node_client_0 = await FullNodeRpcClient.create(self_hostname, node_rpc_port_0, bt.root_path, config) 171 | node_client_1 = await FullNodeRpcClient.create(self_hostname, node_rpc_port_1, bt.root_path, config) 172 | node_client_2 = await FullNodeRpcClient.create(self_hostname, node_rpc_port_2, bt.root_path, config) 173 | 174 | try: 175 | # Setup Initial Balances 176 | # Wallet_0 has coinbase only 177 | # Wallet_1 has coinbase and received 178 | # Wallet_2 has received only 179 | ph_0 = await wallet_0.wallet_state_manager.main_wallet.get_new_puzzlehash() 180 | ph_1 = await wallet_1.wallet_state_manager.main_wallet.get_new_puzzlehash() 181 | ph_2 = await wallet_2.wallet_state_manager.main_wallet.get_new_puzzlehash() 182 | 183 | for i in range(0, num_blocks): 184 | await full_node_api_0.farm_new_transaction_block(FarmNewBlockProtocol(ph_0)) 185 | await full_node_api_0.farm_new_transaction_block(FarmNewBlockProtocol(ph_2)) 186 | 187 | assert await wallet_0.wallet_state_manager.main_wallet.get_confirmed_balance() > 0 188 | # assert await wallet_0.wallet_state_manager.main_wallet.get_confirmed_balance() > 0 189 | 190 | manager_0 = NFTManager(wallet_client_0, node_client_0, tmp_path / "nft_store_test_0.db") 191 | manager_1 = NFTManager(wallet_client_1, node_client_1, tmp_path / "nft_store_test_1.db") 192 | manager_2 = NFTManager(wallet_client_2, node_client_2, tmp_path / "nft_store_test_2.db") 193 | 194 | yield (manager_0, manager_1, manager_2, full_node_api_0, full_node_api_1, full_node_api_2) 195 | 196 | await manager_0.close() 197 | await manager_1.close() 198 | await manager_2.close() 199 | 200 | finally: 201 | await asyncio.sleep(2) # give the ongoing loops a second to finish. 202 | await rpc_cleanup_node_0() 203 | await rpc_cleanup_node_1() 204 | await rpc_cleanup_node_2() 205 | await rpc_cleanup_wallet_0() 206 | await rpc_cleanup_wallet_1() 207 | await rpc_cleanup_wallet_2() 208 | wallet_client_0.close() 209 | wallet_client_1.close() 210 | wallet_client_2.close() 211 | node_client_0.close() 212 | node_client_1.close() 213 | node_client_2.close() 214 | await wallet_client_0.await_closed() 215 | await wallet_client_1.await_closed() 216 | await wallet_client_2.await_closed() 217 | await node_client_0.await_closed() 218 | await node_client_1.await_closed() 219 | await node_client_2.await_closed() 220 | 221 | @pytest.mark.asyncio 222 | async def test_launch_and_find_on_other_nodes(self, three_nft_managers): 223 | man_0, man_1, man_2, full_node_api_0, full_node_api_1, full_node_api_2 = three_nft_managers 224 | await man_0.connect() 225 | await man_0.nft_wallet.basic_sync() 226 | await man_1.connect() 227 | await man_1.nft_wallet.basic_sync() 228 | await man_2.connect() 229 | await man_2.nft_wallet.basic_sync() 230 | amount = 101 231 | nft_data = ("CreatorNFT", "some data") 232 | for_sale_launch_state = [100, 1000] 233 | not_for_sale_launch_state = [0, 1000] 234 | royalty = [10] 235 | tx_id, launcher_id = await man_0.launch_nft(amount, nft_data, for_sale_launch_state, royalty) 236 | assert tx_id 237 | for i in range(0, 5): 238 | await full_node_api_0.farm_new_transaction_block(FarmNewBlockProtocol(bytes32(b"a" * 32))) 239 | await full_node_api_1.farm_new_transaction_block(FarmNewBlockProtocol(bytes32(b"a" * 32))) 240 | # Check other managers find 241 | await man_1.nft_wallet.update_to_current_block() 242 | await man_2.nft_wallet.update_to_current_block() 243 | coins_for_sale_1 = await man_1.get_for_sale_nfts() 244 | coins_for_sale_2 = await man_2.get_for_sale_nfts() 245 | print(coins_for_sale_1) 246 | assert coins_for_sale_1[0].launcher_id == launcher_id 247 | assert coins_for_sale_2[0].launcher_id == launcher_id 248 | 249 | @pytest.mark.asyncio 250 | async def test_basic_sync(self, three_nft_managers): 251 | man_0, man_1, man_2, full_node_api_0, full_node_api_1, full_node_api_2 = three_nft_managers 252 | await man_0.connect() 253 | await man_0.nft_wallet.basic_sync() 254 | amount = 101 255 | nft_data = ("CreatorNFT", "some data") 256 | for_sale_launch_state = [100, 1000] 257 | not_for_sale_launch_state = [0, 1000] 258 | royalty = [10] 259 | tx_id, launcher_id = await man_0.launch_nft(amount, nft_data, for_sale_launch_state, royalty) 260 | assert tx_id 261 | for i in range(0, 5): 262 | await full_node_api_0.farm_new_transaction_block(FarmNewBlockProtocol(bytes32(b"a" * 32))) 263 | 264 | await man_1.connect() 265 | await man_1.nft_wallet.basic_sync() 266 | 267 | nfts = await man_1.nft_wallet.get_all_nft_ids() 268 | assert len(nfts) == 1 269 | nft = await man_1.nft_wallet.get_nft_by_launcher_id(nfts[0]) 270 | 271 | assert nft.launcher_id == launcher_id 272 | 273 | @pytest.mark.asyncio 274 | async def test_update_state(self, three_nft_managers): 275 | man_0, man_1, man_2, full_node_api_0, full_node_api_1, full_node_api_2 = three_nft_managers 276 | await man_0.connect() 277 | await man_0.nft_wallet.basic_sync() 278 | amount = 101 279 | nft_data = ("CreatorNFT", "some data") 280 | for_sale_launch_state = [100, 1000] 281 | not_for_sale_launch_state = [0, 1000] 282 | royalty = [10] 283 | tx_id, launcher_id = await man_0.launch_nft(amount, nft_data, not_for_sale_launch_state, royalty) 284 | assert tx_id 285 | for i in range(0, 5): 286 | await full_node_api_0.farm_new_transaction_block(FarmNewBlockProtocol(bytes32(b"a" * 32))) 287 | # Check other managers find for_sale_nfts 288 | new_state = [100, 20000] 289 | tx_id = await man_0.update_nft(launcher_id, new_state) 290 | assert tx_id 291 | 292 | for i in range(0, 5): 293 | await full_node_api_0.farm_new_transaction_block(FarmNewBlockProtocol(bytes32(b"a" * 32))) 294 | 295 | nft = await man_0.view_nft(launcher_id) 296 | assert int_from_bytes(nft.state()[0]) == new_state[0] 297 | assert int_from_bytes(nft.state()[1]) == new_state[1] 298 | 299 | await man_1.connect() 300 | await man_1.nft_wallet.basic_sync() 301 | nft = await man_1.view_nft(launcher_id) 302 | assert int_from_bytes(nft.state()[0]) == new_state[0] 303 | assert int_from_bytes(nft.state()[1]) == new_state[1] 304 | 305 | @pytest.mark.asyncio 306 | async def test_buy_spends(self, three_nft_managers): 307 | man_0, man_1, man_2, full_node_api_0, full_node_api_1, full_node_api_2 = three_nft_managers 308 | await man_0.connect() 309 | await man_0.nft_wallet.basic_sync() 310 | await man_1.connect() 311 | await man_1.nft_wallet.basic_sync() 312 | # we will sync man_2 later to test buying an earlier coin 313 | amount_to_send = int(1e12) 314 | man_0_balance = await man_0.available_balance() 315 | 316 | # send 1xch from man_0 to man_1 317 | man_0_addr = await man_0.wallet_client.get_next_address(1, False) 318 | man_1_addr = await man_1.wallet_client.get_next_address(1, False) 319 | 320 | tx = await man_0.wallet_client.send_transaction("1", amount_to_send, man_1_addr) 321 | tx_id = tx.name 322 | 323 | async def tx_in_mempool(): 324 | tx = await man_0.wallet_client.get_transaction("1", tx_id) 325 | return tx.is_in_mempool() 326 | 327 | await time_out_assert(5, tx_in_mempool, True) 328 | 329 | for i in range(0, 5): 330 | await full_node_api_0.farm_new_transaction_block(FarmNewBlockProtocol(decode_puzzle_hash(man_0_addr))) 331 | 332 | async def tx_confirmed(): 333 | tx = await man_0.wallet_client.get_transaction("1", tx_id) 334 | return tx.confirmed 335 | 336 | await time_out_assert(10, tx_confirmed, True) 337 | 338 | assert await man_1.available_balance() == amount_to_send 339 | 340 | # MAn_0 launches a for sale nft 341 | amount = 101 342 | price = 2362383 343 | nft_data = ("CreatorNFT", "some data") 344 | for_sale_launch_state = [100, price] 345 | not_for_sale_launch_state = [0, price] 346 | royalty = [10] 347 | tx_id, launcher_id = await man_0.launch_nft(amount, nft_data, for_sale_launch_state, royalty) 348 | assert tx_id 349 | 350 | for i in range(0, 5): 351 | await full_node_api_0.farm_new_transaction_block(FarmNewBlockProtocol(bytes32(b"a" * 32))) 352 | 353 | nft = await man_0.view_nft(launcher_id) 354 | assert nft.is_for_sale() 355 | 356 | # Man_1 finds the launched nft on-chain: 357 | for i in range(0, 5): 358 | await full_node_api_1.farm_new_transaction_block(FarmNewBlockProtocol(bytes32(b"a" * 32))) 359 | await man_1.nft_wallet.update_to_current_block() 360 | for_sale_nfts = await man_1.get_for_sale_nfts() 361 | assert for_sale_nfts 362 | assert for_sale_nfts[0].launcher_id == launcher_id 363 | assert for_sale_nfts[0].is_for_sale() 364 | 365 | man_0_start_bal = await man_0.available_balance() 366 | man_1_start_bal = await man_1.available_balance() 367 | 368 | # Man_1 buys nft and sets to not-for-sale 369 | new_state = [0, 10000] 370 | tx_id = await man_1.buy_nft(launcher_id, new_state) 371 | assert tx_id 372 | for i in range(0, 5): 373 | await full_node_api_1.farm_new_transaction_block(FarmNewBlockProtocol(bytes32(b"a" * 32))) 374 | await full_node_api_0.farm_new_transaction_block(FarmNewBlockProtocol(bytes32(b"a" * 32))) 375 | 376 | nft = await man_0.view_nft(launcher_id) 377 | assert nft.owner_pk() == bytes(man_1.nft_pk) 378 | assert not nft.is_for_sale() 379 | 380 | # assert balannces 381 | assert (await man_0.available_balance()) == man_0_start_bal + price 382 | assert (await man_1.available_balance()) == man_1_start_bal - price 383 | 384 | # man_1 updates to for-sale 385 | new_state = [100, 10000] 386 | tx_id = await man_1.update_nft(launcher_id, new_state) 387 | assert tx_id 388 | for i in range(0, 5): 389 | await full_node_api_1.farm_new_transaction_block(FarmNewBlockProtocol(bytes32(b"a" * 32))) 390 | await full_node_api_0.farm_new_transaction_block(FarmNewBlockProtocol(bytes32(b"a" * 32))) 391 | 392 | nft = await man_0.view_nft(launcher_id) 393 | assert nft.is_for_sale() 394 | 395 | # man_2 comes online, buys nft, check royalty payment 396 | await man_2.connect() 397 | await man_2.nft_wallet.basic_sync() 398 | 399 | man_0_start_bal = await man_0.available_balance() 400 | man_1_start_bal = await man_1.available_balance() 401 | man_2_start_bal = await man_2.available_balance() 402 | 403 | nfts = await man_2.get_for_sale_nfts() 404 | assert len(nfts) == 1 405 | nft = nfts[0] 406 | 407 | assert nft.is_for_sale() 408 | new_state = [0, 15000] 409 | tx_id = await man_2.buy_nft(launcher_id, new_state) 410 | assert tx_id 411 | for i in range(0, 5): 412 | await full_node_api_2.farm_new_transaction_block(FarmNewBlockProtocol(bytes32(b"a" * 32))) 413 | await full_node_api_1.farm_new_transaction_block(FarmNewBlockProtocol(bytes32(b"a" * 32))) 414 | 415 | # assert royalties have been paid 416 | assert (await man_0.available_balance()) == man_0_start_bal + 1000 417 | assert (await man_1.available_balance()) == man_1_start_bal + 9000 418 | assert (await man_2.available_balance()) == man_2_start_bal - 10000 419 | 420 | @pytest.mark.asyncio 421 | async def test_coin_selection(self, three_nft_managers): 422 | man_0, man_1, man_2, full_node_api_0, full_node_api_1, full_node_api_2 = three_nft_managers 423 | await man_0.connect() 424 | await man_1.connect() 425 | await man_2.connect() 426 | amount_to_send = int(1e12) 427 | man_0_balance = await man_0.available_balance() 428 | assert man_0_balance > 0 429 | balance = await man_1.available_balance() 430 | assert balance == 0 431 | balance = await man_2.available_balance() 432 | assert balance == 8 * int(1e12) 433 | 434 | # send 1xch from man_0 to man_1 435 | man_1_addr = await man_1.wallet_client.get_next_address(1, False) 436 | man_0_addr = await man_0.wallet_client.get_next_address(1, False) 437 | 438 | tx = await man_0.wallet_client.send_transaction("1", amount_to_send, man_1_addr) 439 | tx_id = tx.name 440 | 441 | async def tx_in_mempool(): 442 | tx = await man_0.wallet_client.get_transaction("1", tx_id) 443 | return tx.is_in_mempool() 444 | 445 | await time_out_assert(5, tx_in_mempool, True) 446 | 447 | assert (await man_0.wallet_client.get_wallet_balance("1"))["unconfirmed_wallet_balance"] == man_0_balance - int( 448 | 1e12 449 | ) 450 | 451 | man_0_balance = (await man_0.wallet_client.get_wallet_balance("1"))["confirmed_wallet_balance"] 452 | print(man_0_balance) 453 | 454 | async def eventual_balance(): 455 | return (await man_0.wallet_client.get_wallet_balance("1"))["confirmed_wallet_balance"] 456 | 457 | for i in range(0, 5): 458 | await full_node_api_0.farm_new_transaction_block(FarmNewBlockProtocol(decode_puzzle_hash(man_0_addr))) 459 | 460 | async def tx_confirmed(): 461 | tx = await man_0.wallet_client.get_transaction("1", tx_id) 462 | return tx.confirmed 463 | 464 | await time_out_assert(10, tx_confirmed, True) 465 | txns = await man_1.wallet_client.get_transactions("1") 466 | print(txns) 467 | 468 | assert await man_1.available_balance() == amount_to_send 469 | 470 | # man_1 now has a balance of coins transferred from man_0, i.e. non-farmed coins 471 | # man_1 can test coin selection by launching a coin 472 | 473 | amount = 101 474 | nft_data = ("CreatorNFT", "some data") 475 | for_sale_launch_state = [100, 1000] 476 | not_for_sale_launch_state = [0, 1000] 477 | royalty = [10] 478 | tx_id, launcher_id = await man_1.launch_nft(amount, nft_data, for_sale_launch_state, royalty) 479 | assert tx_id 480 | for i in range(0, 5): 481 | await full_node_api_1.farm_new_transaction_block(FarmNewBlockProtocol(bytes32(b"a" * 32))) 482 | 483 | launched_nft = await man_1.get_my_nfts() 484 | assert launched_nft[0].price() == 1000 485 | assert launched_nft[0].is_for_sale() 486 | -------------------------------------------------------------------------------- /sim.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import time 3 | import datetime 4 | import pytimeparse 5 | from pathlib import Path 6 | 7 | from typing import Dict, List, Tuple, Optional, Union 8 | from blspy import BasicSchemeMPL, PrivateKey, G1Element, AugSchemeMPL, G2Element 9 | 10 | 11 | from chia.types.blockchain_format.sized_bytes import bytes32 12 | from chia.types.blockchain_format.coin import Coin 13 | from chia.types.blockchain_format.program import Program, SerializedProgram 14 | from chia.types.spend_bundle import SpendBundle 15 | from chia.types.coin_spend import CoinSpend 16 | from chia.types.coin_record import CoinRecord 17 | from chia.util.ints import uint64 18 | from chia.util.condition_tools import ConditionOpcode 19 | from chia.util.hash import std_hash 20 | from chia.wallet.sign_coin_spends import sign_coin_spends 21 | from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( # standard_transaction 22 | puzzle_for_pk, 23 | calculate_synthetic_secret_key, 24 | DEFAULT_HIDDEN_PUZZLE_HASH, 25 | ) 26 | from clvm_tools.clvmc import compile_clvm 27 | from chia.clvm.spend_sim import SpendSim, SimClient 28 | from chia.consensus.default_constants import DEFAULT_CONSTANTS 29 | 30 | duration_div = 86400.0 31 | block_time = (600.0 / 32.0) / duration_div 32 | # Allowed subdivisions of 1 coin 33 | 34 | 35 | def secret_exponent_for_index(index: int) -> int: 36 | blob = index.to_bytes(32, "big") 37 | hashed_blob = BasicSchemeMPL.key_gen(std_hash(b"foo" + blob)) 38 | r = int.from_bytes(hashed_blob, "big") 39 | return r 40 | 41 | 42 | def private_key_for_index(index: int) -> PrivateKey: 43 | r = secret_exponent_for_index(index) 44 | return PrivateKey.from_bytes(r.to_bytes(32, "big")) 45 | 46 | 47 | def public_key_for_index(index: int) -> G1Element: 48 | return private_key_for_index(index).get_g1() 49 | 50 | 51 | def sign_message_with_index(index: int, message: str) -> G2Element: 52 | sk = private_key_for_index(index) 53 | return AugSchemeMPL.sign(sk, bytes(message, "utf-8")) 54 | 55 | 56 | def sign_messages_with_indexes(sign_ops: List[Dict[int, str]]) -> G2Element: 57 | signatures = [] 58 | for _ in sign_ops: 59 | for index, message in _.items(): 60 | sk = private_key_for_index(index) 61 | signatures.append(AugSchemeMPL.sign(sk, bytes(message, "utf-8"))) 62 | return AugSchemeMPL.aggregate(signatures) 63 | 64 | 65 | def aggregate_signatures(signatures: List[G2Element]) -> G2Element: 66 | return AugSchemeMPL.aggregate(signatures) 67 | 68 | 69 | class SpendResult: 70 | def __init__(self, result: Dict): 71 | """Constructor for internal use. 72 | error - a string describing the error or None 73 | result - the raw result from Network::push_tx 74 | outputs - a list of new Coin objects surviving the transaction 75 | """ 76 | self.result = result 77 | if "error" in result: 78 | self.error: Optional[str] = result["error"] 79 | self.outputs: List[Coin] = [] 80 | else: 81 | self.error = None 82 | self.outputs = result["additions"] 83 | 84 | def find_standard_coins(self, puzzle_hash: bytes32) -> List[Coin]: 85 | """Given a Wallet's puzzle_hash, find standard coins usable by it. 86 | These coins are recognized as changing the Wallet's chia balance and are 87 | usable for any purpose.""" 88 | return list(filter(lambda x: x.puzzle_hash == puzzle_hash, self.outputs)) 89 | 90 | 91 | class CoinWrapper(Coin): 92 | """A class that provides some useful methods on coins.""" 93 | 94 | def __init__(self, parent: Coin, puzzle_hash: bytes32, amt: uint64, source: Program): 95 | """Given parent, puzzle_hash and amount, give an object representing the coin""" 96 | super().__init__(parent, puzzle_hash, amt) 97 | self.source = source 98 | 99 | def puzzle(self) -> Program: 100 | """Return the program that unlocks this coin""" 101 | return self.source 102 | 103 | def puzzle_hash(self) -> bytes32: 104 | """Return this coin's puzzle hash""" 105 | return self.puzzle().get_tree_hash() 106 | 107 | def smart_coin(self) -> "SmartCoinWrapper": 108 | """Return a smart coin object wrapping this coin's program""" 109 | return SmartCoinWrapper(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, self.source) 110 | 111 | def as_coin(self) -> Coin: 112 | return Coin( 113 | self.parent_coin_info, 114 | self.puzzle_hash, 115 | self.amount, 116 | ) 117 | 118 | @classmethod 119 | def from_coin(cls, coin: Coin, puzzle: Program) -> "CoinWrapper": 120 | return cls( 121 | coin.parent_coin_info, 122 | coin.puzzle_hash, 123 | coin.amount, 124 | puzzle, 125 | ) 126 | 127 | def create_standard_spend(self, priv: PrivateKey, conditions: List[List]): 128 | delegated_puzzle_solution = Program.to((1, conditions)) 129 | solution = Program.to([[], delegated_puzzle_solution, []]) 130 | 131 | coin_spend_object = CoinSpend( 132 | self.as_coin(), 133 | self.puzzle(), 134 | solution, 135 | ) 136 | 137 | # Create a signature for each of these. We'll aggregate them at the end. 138 | signature: G2Element = AugSchemeMPL.sign( 139 | calculate_synthetic_secret_key(priv, DEFAULT_HIDDEN_PUZZLE_HASH), 140 | (delegated_puzzle_solution.get_tree_hash() + self.name() + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA), 141 | ) 142 | 143 | return coin_spend_object, signature 144 | 145 | 146 | # We have two cases for coins: 147 | # - Wallet coins which contribute to the "wallet balance" of the user. 148 | # They enable a user to "spend money" and "take actions on the network" 149 | # that have monetary value. 150 | # 151 | # - Smart coins which either lock value or embody information and 152 | # services. These also contain a chia balance but are used for purposes 153 | # other than a fungible, liquid, spendable resource. They should not show 154 | # up in a "wallet" in the same way. We should use them by locking value 155 | # into wallet coins. We should ensure that value contained in a smart coin 156 | # coin is never destroyed. 157 | class SmartCoinWrapper: 158 | def __init__(self, genesis_challenge: bytes32, source: Program): 159 | """A wrapper for a smart coin carrying useful methods for interacting with chia.""" 160 | self.genesis_challenge = genesis_challenge 161 | self.source = source 162 | 163 | def puzzle(self) -> Program: 164 | """Give this smart coin's program""" 165 | return self.source 166 | 167 | def puzzle_hash(self) -> bytes32: 168 | """Give this smart coin's puzzle hash""" 169 | return self.source.get_tree_hash() 170 | 171 | def custom_coin(self, parent: Coin, amt: uint64) -> CoinWrapper: 172 | """Given a parent and an amount, create the Coin object representing this 173 | smart coin as it would exist post launch""" 174 | return CoinWrapper(parent.name(), self.puzzle_hash(), amt, self.source) 175 | 176 | 177 | # Used internally to accumulate a search for coins we can combine to the 178 | # target amount. 179 | # Result is the smallest set of coins whose sum of amounts is greater 180 | # than target_amount. 181 | class CoinPairSearch: 182 | def __init__(self, target_amount: uint64): 183 | self.target = target_amount 184 | self.total: uint64 = uint64(0) 185 | self.max_coins: List[Coin] = [] 186 | 187 | def get_result(self) -> Tuple[List[Coin], uint64]: 188 | return self.max_coins, self.total 189 | 190 | def insort(self, coin: Coin): 191 | for i in range(len(self.max_coins)): 192 | if self.max_coins[i].amount < coin.amount: 193 | self.max_coins.insert(i, coin) 194 | break 195 | else: 196 | self.max_coins.append(coin) 197 | 198 | def process_coin_for_combine_search(self, coin: Coin): 199 | self.total = uint64(self.total + coin.amount) 200 | if len(self.max_coins) == 0: 201 | self.max_coins.append(coin) 202 | else: 203 | self.insort(coin) 204 | while ( 205 | (len(self.max_coins) > 0) 206 | and (self.total - self.max_coins[-1].amount >= self.target) 207 | and ((self.total - self.max_coins[-1].amount > 0) or (len(self.max_coins) > 1)) 208 | ): 209 | self.total = uint64(self.total - self.max_coins[-1].amount) 210 | self.max_coins = self.max_coins[:-1] 211 | 212 | 213 | # A basic wallet that knows about standard coins. 214 | # We can use this to track our balance as an end user and keep track of 215 | # chia that is released by smart coins, if the smart coins interact 216 | # meaningfully with them, as many likely will. 217 | class Wallet: 218 | def __init__(self, parent: "Network", name: str, pk: G1Element, priv: PrivateKey): 219 | """Internal use constructor, use Network::make_wallet 220 | Fields: 221 | parent - The Network object that created this Wallet 222 | name - The textural name of the actor 223 | pk_ - The actor's public key 224 | sk_ - The actor's private key 225 | usable_coins - Standard coins spendable by this actor 226 | puzzle - A program for creating this actor's standard coin 227 | puzzle_hash - The puzzle hash for this actor's standard coin 228 | pk_to_sk_dict - a dictionary for retrieving the secret keys when presented with the corresponding public key 229 | """ 230 | self.parent = parent 231 | self.name = name 232 | self.pk_ = pk 233 | self.sk_ = priv 234 | self.usable_coins: Dict[bytes32, Coin] = {} 235 | self.puzzle: Program = puzzle_for_pk(self.pk()) 236 | self.puzzle_hash: bytes32 = self.puzzle.get_tree_hash() 237 | 238 | synth_sk: PrivateKey = calculate_synthetic_secret_key(self.sk_, DEFAULT_HIDDEN_PUZZLE_HASH) 239 | self.pk_to_sk_dict: Dict[str, PrivateKey] = { 240 | str(self.pk_): self.sk_, 241 | str(synth_sk.get_g1()): synth_sk, 242 | } 243 | 244 | def __repr__(self) -> str: 245 | return f"" 246 | 247 | # Make this coin available to the user it goes with. 248 | def add_coin(self, coin: Coin): 249 | self.usable_coins[coin.name()] = coin 250 | 251 | def pk_to_sk(self, pk: G1Element) -> PrivateKey: 252 | assert str(pk) in self.pk_to_sk_dict 253 | return self.pk_to_sk_dict[str(pk)] 254 | 255 | def compute_combine_action( 256 | self, amt: uint64, actions: List, usable_coins: Dict[bytes32, Coin] 257 | ) -> Optional[List[Coin]]: 258 | # No one coin is enough, try to find a best fit pair, otherwise combine the two 259 | # maximum coins. 260 | searcher = CoinPairSearch(amt) 261 | 262 | # Process coins for this round. 263 | for k, c in usable_coins.items(): 264 | searcher.process_coin_for_combine_search(c) 265 | 266 | max_coins, total = searcher.get_result() 267 | 268 | if total >= amt: 269 | return max_coins 270 | else: 271 | return None 272 | 273 | # Given some coins, combine them, causing the network to make us 274 | # recompute our balance in return. 275 | # 276 | # This is a metaphor for the meaning of "solution" in the coin "puzzle program" context: 277 | # 278 | # The arguments to the coin's puzzle program, which is the first kind 279 | # of object called a 'solution' in this case refers to the arguments 280 | # needed to cause the program to emit blockchain compatible opcodes. 281 | # It's a "puzzle" in the sense that a ctf RE exercise is a "puzzle". 282 | # "solving" it means producing input that causes the unknown program to 283 | # do something desirable. 284 | # 285 | # Spending multiple coins: 286 | # There are two running lists that need to be kept when spending multiple coins: 287 | # 288 | # - A list of signatures. Each coin has its own signature requirements, but standard 289 | # coins are signed like this: 290 | # 291 | # AugSchemeMPL.sign( 292 | # calculate_synthetic_secret_key(self.sk_,DEFAULT_HIDDEN_PUZZLE_HASH), 293 | # (delegated_puzzle_solution.get_tree_hash() + c.name() + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA) 294 | # ) 295 | # 296 | # where c.name is the coin's "name" (in the code) or coinID (in the 297 | # chialisp docs). delegated_puzzle_solution is a clvm program that 298 | # produces the conditions we want to give the puzzle program (the first 299 | # kind of 'solution'), which will add the basic ones needed by owned 300 | # standard coins. 301 | # 302 | # In most cases, we'll give a tuple like (1, [some, python, [data, 303 | # here]]) for these arguments, because '1' is the quote function 'q' in 304 | # clvm. One could write this program with any valid clvm code though. 305 | # The important thing is that it's runnable code, not literal data as 306 | # one might expect. 307 | # 308 | # - A list of CoinSpend objects. 309 | # Theese consist of (with c : Coin): 310 | # 311 | # CoinSpend( 312 | # c, 313 | # c.puzzle(), 314 | # solution, 315 | # ) 316 | # 317 | # Where solution is a second formulation of a 'solution' to a puzzle (the third form 318 | # of 'solution' we'll use in this documentation is the CoinSpend object.). This is 319 | # related to the program arguments above, but prefixes and suffixes an empty list on 320 | # them (admittedly i'm cargo culting this part): 321 | # 322 | # solution = Program.to([[], delegated_puzzle_solution, []]) 323 | # 324 | # So you do whatever you want with a bunch of coins at once and now you have two lists: 325 | # 1) A list of G1Element objects yielded by AugSchemeMPL.sign 326 | # 2) A list of CoinSpend objects. 327 | # 328 | # Now to spend them at once: 329 | # 330 | # signature = AugSchemeMPL.aggregate(signatures) 331 | # spend_bundle = SpendBundle(coin_spends, signature) 332 | # 333 | async def combine_coins(self, coins: List[CoinWrapper]) -> Optional[SpendResult]: 334 | # Overall structure: 335 | # Create len-1 spends that just assert that the final coin is created with full value. 336 | # Create 1 spend for the final coin that asserts the other spends occurred and 337 | # Creates the new coin. 338 | 339 | beginning_balance: uint64 = self.balance() 340 | beginning_coins: int = len(self.usable_coins) 341 | 342 | # We need the final coin to know what the announced coin name will be. 343 | final_coin = CoinWrapper( 344 | coins[-1].name(), 345 | self.puzzle_hash, 346 | uint64(sum(map(lambda x: x.amount, coins))), 347 | self.puzzle, 348 | ) 349 | 350 | destroyed_coin_spends: List[CoinSpend] = [] 351 | 352 | # Each coin wants agg_sig_me so we aggregate them at the end. 353 | signatures: List[G2Element] = [] 354 | 355 | for c in coins[:-1]: 356 | announce_conditions: List[List] = [ 357 | # Each coin expects the final coin creation announcement 358 | [ 359 | ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT, 360 | std_hash(coins[-1].name() + final_coin.name()), 361 | ] 362 | ] 363 | 364 | coin_spend, signature = c.create_standard_spend(self.sk_, announce_conditions) 365 | destroyed_coin_spends.append(coin_spend) 366 | signatures.append(signature) 367 | 368 | final_coin_creation: List[List] = [ 369 | [ConditionOpcode.CREATE_COIN_ANNOUNCEMENT, final_coin.name()], 370 | [ConditionOpcode.CREATE_COIN, self.puzzle_hash, final_coin.amount], 371 | ] 372 | 373 | coin_spend, signature = coins[-1].create_standard_spend(self.sk_, final_coin_creation) 374 | destroyed_coin_spends.append(coin_spend) 375 | signatures.append(signature) 376 | 377 | signature = AugSchemeMPL.aggregate(signatures) 378 | spend_bundle = SpendBundle(destroyed_coin_spends, signature) 379 | 380 | pushed: Dict[str, Union[str, List[Coin]]] = await self.parent.push_tx(spend_bundle) 381 | 382 | # We should have the same amount of money. 383 | assert beginning_balance == self.balance() 384 | # We should have shredded n-1 coins and replaced one. 385 | assert len(self.usable_coins) == beginning_coins - (len(coins) - 1) 386 | 387 | return SpendResult(pushed) 388 | 389 | # Find a coin containing amt we can use as a parent. 390 | # Synthesize a coin with sufficient funds if possible. 391 | async def choose_coin(self, amt) -> Optional[CoinWrapper]: 392 | """Given an amount requirement, find a coin that contains at least that much chia""" 393 | start_balance: uint64 = self.balance() 394 | coins_to_spend: Optional[List[Coin]] = self.compute_combine_action(amt, [], dict(self.usable_coins)) 395 | 396 | # Couldn't find a working combination. 397 | if coins_to_spend is None: 398 | return None 399 | 400 | if len(coins_to_spend) == 1: 401 | only_coin: Coin = coins_to_spend[0] 402 | return CoinWrapper( 403 | only_coin.parent_coin_info, 404 | only_coin.puzzle_hash, 405 | only_coin.amount, 406 | self.puzzle, 407 | ) 408 | 409 | # We receive a timeline of actions to take (indicating that we have a plan) 410 | # Do the first action and start over. 411 | result: Optional[SpendResult] = await self.combine_coins( 412 | list( 413 | map( 414 | lambda x: CoinWrapper(x.parent_coin_info, x.puzzle_hash, x.amount, self.puzzle), 415 | coins_to_spend, 416 | ) 417 | ) 418 | ) 419 | 420 | if result is None: 421 | return None 422 | 423 | assert self.balance() == start_balance 424 | return await self.choose_coin(amt) 425 | 426 | # Create a new smart coin based on a parent coin and return the coin to the user. 427 | # TODO: 428 | # - allow use of more than one coin to launch smart coin 429 | # - ensure input chia = output chia. it'd be dumb to just allow somebody 430 | # to lose their chia without telling them. 431 | async def launch_smart_coin(self, source: Program, **kwargs) -> Optional[CoinWrapper]: 432 | """Create a new smart coin based on a parent coin and return the smart coin's living 433 | coin to the user or None if the spend failed.""" 434 | amt = uint64(1) 435 | found_coin: Optional[CoinWrapper] = None 436 | 437 | if "amt" in kwargs: 438 | amt = kwargs["amt"] 439 | 440 | if "launcher" in kwargs: 441 | found_coin = kwargs["launcher"] 442 | else: 443 | found_coin = await self.choose_coin(amt) 444 | 445 | if found_coin is None: 446 | raise ValueError(f"could not find available coin containing {amt} mojo") 447 | 448 | # Create a puzzle based on the incoming smart coin 449 | cw = SmartCoinWrapper(DEFAULT_CONSTANTS.GENESIS_CHALLENGE, source) 450 | condition_args: List[List] = [ 451 | [ConditionOpcode.CREATE_COIN, cw.puzzle_hash(), amt], 452 | ] 453 | if amt < found_coin.amount: 454 | condition_args.append([ConditionOpcode.CREATE_COIN, self.puzzle_hash, found_coin.amount - amt]) 455 | 456 | delegated_puzzle_solution = Program.to((1, condition_args)) 457 | solution = Program.to([[], delegated_puzzle_solution, []]) 458 | 459 | # Sign the (delegated_puzzle_hash + coin_name) with synthetic secret key 460 | signature: G2Element = AugSchemeMPL.sign( 461 | calculate_synthetic_secret_key(self.sk_, DEFAULT_HIDDEN_PUZZLE_HASH), 462 | ( 463 | delegated_puzzle_solution.get_tree_hash() 464 | + found_coin.name() 465 | + DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA 466 | ), 467 | ) 468 | 469 | spend_bundle = SpendBundle( 470 | [ 471 | CoinSpend( 472 | found_coin.as_coin(), # Coin to spend 473 | self.puzzle, # Puzzle used for found_coin 474 | solution, # The solution to the puzzle locking found_coin 475 | ) 476 | ], 477 | signature, 478 | ) 479 | 480 | pushed: Dict[str, Union[str, List[Coin]]] = await self.parent.push_tx(spend_bundle) 481 | if "error" not in pushed: 482 | return cw.custom_coin(found_coin, amt) 483 | else: 484 | return None 485 | 486 | # Give chia 487 | async def give_chia(self, target: "Wallet", amt: uint64) -> Optional[CoinWrapper]: 488 | return await self.launch_smart_coin(target.puzzle, amt=amt) 489 | 490 | # Called each cycle before coins are re-established from the simulator. 491 | def _clear_coins(self): 492 | self.usable_coins = {} 493 | 494 | # Public key of wallet 495 | def pk(self) -> G1Element: 496 | """Return actor's public key""" 497 | return self.pk_ 498 | 499 | # Balance of wallet 500 | def balance(self) -> uint64: 501 | """Return the actor's balance in standard coins as we understand it""" 502 | return uint64(sum(map(lambda x: x.amount, self.usable_coins.values()))) 503 | 504 | # Spend a coin, probably a smart coin. 505 | # Allows the user to specify the arguments for the puzzle solution. 506 | # Automatically takes care of signing, etc. 507 | # Result is an object representing the actions taken when the block 508 | # with this transaction was farmed. 509 | async def spend_coin(self, coin: CoinWrapper, pushtx: bool = True, **kwargs) -> Union[SpendResult, SpendBundle]: 510 | """Given a coin object, invoke it on the blockchain, either as a standard 511 | coin if no arguments are given or with custom arguments in args=""" 512 | amt = uint64(1) 513 | if "amt" in kwargs: 514 | amt = kwargs["amt"] 515 | 516 | delegated_puzzle_solution: Optional[Program] = None 517 | if "args" not in kwargs: 518 | target_puzzle_hash: bytes32 = self.puzzle_hash 519 | # Allow the user to 'give this much chia' to another user. 520 | if "to" in kwargs: 521 | target_puzzle_hash = kwargs["to"].puzzle_hash 522 | 523 | # Automatic arguments from the user's intention. 524 | if "custom_conditions" not in kwargs: 525 | solution_list: List[List] = [[ConditionOpcode.CREATE_COIN, target_puzzle_hash, amt]] 526 | else: 527 | solution_list = kwargs["custom_conditions"] 528 | if "remain" in kwargs: 529 | remainer: Union[SmartCoinWrapper, Wallet] = kwargs["remain"] 530 | remain_amt = uint64(coin.amount - amt) 531 | if isinstance(remainer, SmartCoinWrapper): 532 | solution_list.append( 533 | [ 534 | ConditionOpcode.CREATE_COIN, 535 | remainer.puzzle_hash(), 536 | remain_amt, 537 | ] 538 | ) 539 | elif isinstance(remainer, Wallet): 540 | solution_list.append([ConditionOpcode.CREATE_COIN, remainer.puzzle_hash, remain_amt]) 541 | else: 542 | raise ValueError("remainer is not a wallet or a smart coin") 543 | 544 | delegated_puzzle_solution = Program.to((1, solution_list)) 545 | # Solution is the solution for the old coin. 546 | solution = Program.to([[], delegated_puzzle_solution, []]) 547 | else: 548 | delegated_puzzle_solution = Program.to(kwargs["args"]) 549 | solution = delegated_puzzle_solution 550 | 551 | solution_for_coin = CoinSpend( 552 | coin.as_coin(), 553 | coin.puzzle(), 554 | solution, 555 | ) 556 | 557 | # The reason this use of sign_coin_spends exists is that it correctly handles 558 | # the signing for non-standard coins. I don't fully understand the difference but 559 | # this definitely does the right thing. 560 | try: 561 | spend_bundle: SpendBundle = await sign_coin_spends( 562 | [solution_for_coin], 563 | self.pk_to_sk, 564 | DEFAULT_CONSTANTS.AGG_SIG_ME_ADDITIONAL_DATA, 565 | DEFAULT_CONSTANTS.MAX_BLOCK_COST_CLVM, 566 | ) 567 | except ValueError: 568 | spend_bundle = SpendBundle( 569 | [solution_for_coin], 570 | G2Element(), 571 | ) 572 | 573 | if pushtx: 574 | pushed: Dict[str, Union[str, List[Coin]]] = await self.parent.push_tx(spend_bundle) 575 | return SpendResult(pushed) 576 | else: 577 | return spend_bundle 578 | 579 | 580 | # A user oriented (domain specific) view of the chia network. 581 | class Network: 582 | """An object that owns a simulation, responsible for managing Wallet actors, 583 | time and initialization.""" 584 | 585 | time: datetime.timedelta 586 | sim: SpendSim 587 | sim_client: SimClient 588 | wallets: Dict[str, Wallet] 589 | nobody: Wallet 590 | 591 | @classmethod 592 | async def create(cls) -> "Network": 593 | self = cls() 594 | self.time = datetime.timedelta(days=18750, seconds=61201) # Past the initial transaction freeze 595 | self.sim = await SpendSim.create() 596 | self.sim_client = SimClient(self.sim) 597 | self.wallets = {} 598 | self.nobody = self.make_wallet("nobody") 599 | self.wallets[str(self.nobody.pk())] = self.nobody 600 | return self 601 | 602 | async def close(self): 603 | await self.sim.close() 604 | 605 | # Have the system farm one block with a specific beneficiary (nobody if not specified). 606 | async def farm_block(self, **kwargs) -> Tuple[List[Coin], List[Coin]]: 607 | """Given a farmer, farm a block with that actor as the beneficiary of the farm 608 | reward. 609 | Used for causing chia balance to exist so the system can do things. 610 | """ 611 | farmer: Wallet = self.nobody 612 | if "farmer" in kwargs: 613 | farmer = kwargs["farmer"] 614 | 615 | farm_duration = datetime.timedelta(block_time) 616 | farmed: Tuple[List[Coin], List[Coin]] = await self.sim.farm_block(farmer.puzzle_hash) 617 | 618 | for k, w in self.wallets.items(): 619 | w._clear_coins() 620 | 621 | for kw, w in self.wallets.items(): 622 | coin_records: List[CoinRecord] = await self.sim_client.get_coin_records_by_puzzle_hash(w.puzzle_hash) 623 | for coin_record in coin_records: 624 | if coin_record.spent is False: 625 | w.add_coin(CoinWrapper.from_coin(coin_record.coin, w.puzzle)) 626 | 627 | self.time += farm_duration 628 | return farmed 629 | 630 | def _alloc_key(self) -> Tuple[G1Element, PrivateKey]: 631 | key_idx: int = len(self.wallets) 632 | pk: G1Element = public_key_for_index(key_idx) 633 | priv: PrivateKey = private_key_for_index(key_idx) 634 | return pk, priv 635 | 636 | # Allow the user to create a wallet identity to whom standard coins may be targeted. 637 | # This results in the creation of a wallet that tracks balance and standard coins. 638 | # Public and private key from here are used in signing. 639 | def make_wallet(self, name: str) -> Wallet: 640 | """Create a wallet for an actor. This causes the actor's chia balance in standard 641 | coin to be tracked during the simulation. Wallets have some domain specific methods 642 | that behave in similar ways to other blockchains.""" 643 | pk, priv = self._alloc_key() 644 | w = Wallet(self, name, pk, priv) 645 | self.wallets[str(w.pk())] = w 646 | return w 647 | 648 | # Skip real time by farming blocks until the target duration is achieved. 649 | async def skip_time(self, target_duration: str, **kwargs): 650 | """Skip a duration of simulated time, causing blocks to be farmed. If a farmer 651 | is specified, they win each block""" 652 | target_time = self.time + datetime.timedelta(pytimeparse.parse(target_duration) / duration_div) 653 | while target_time > self.get_timestamp(): 654 | await self.farm_block(**kwargs) 655 | self.sim.pass_time(uint64(20)) 656 | 657 | # Or possibly aggregate farm_block results. 658 | return None 659 | 660 | def get_timestamp(self) -> datetime.timedelta: 661 | """Return the current simualtion time in seconds.""" 662 | return datetime.timedelta(seconds=self.sim.timestamp) 663 | 664 | # Given a spend bundle, farm a block and analyze the result. 665 | async def push_tx(self, bundle: SpendBundle) -> Dict[str, Union[str, List[Coin]]]: 666 | """Given a spend bundle, try to farm a block containing it. If the spend bundle 667 | didn't validate, then a result containing an 'error' key is returned. The reward 668 | for the block goes to Network::nobody""" 669 | 670 | status, error = await self.sim_client.push_tx(bundle) 671 | if error: 672 | return {"error": str(error)} 673 | 674 | # Common case that we want to farm this right away. 675 | additions, removals = await self.farm_block() 676 | return { 677 | "additions": additions, 678 | "removals": removals, 679 | } 680 | 681 | 682 | async def setup_oracle() -> Tuple[Network, Wallet, Wallet, Wallet]: 683 | network: Network = await Network.create() 684 | oracle: Wallet = network.make_wallet("oracle") 685 | alice: Wallet = network.make_wallet("alice") 686 | bob: Wallet = network.make_wallet("bob") 687 | await network.farm_block() 688 | await network.farm_block(farmer=oracle) 689 | return network, oracle, alice, bob 690 | 691 | 692 | async def setup_two_wallet_node() -> Tuple[Network, Wallet, Wallet]: 693 | network: Network = await Network.create() 694 | alice: Wallet = network.make_wallet("alice") 695 | bob: Wallet = network.make_wallet("bob") 696 | await network.farm_block() 697 | await network.farm_block(farmer=alice) 698 | await network.farm_block(farmer=bob) 699 | return network, alice, bob 700 | 701 | 702 | async def setup_node_only(): 703 | node = await Network.create() 704 | await node.farm_block() 705 | return node 706 | 707 | 708 | def load_clsp_relative(filename: str, search_paths: List[Path] = [Path("include/")]): 709 | base = Path().parent.resolve() 710 | source = base / filename 711 | target = base / f"{filename}.hex" 712 | searches = [base / s for s in search_paths] 713 | compile_clvm(source, target, searches) 714 | clvm = target.read_text() 715 | clvm_blob = bytes.fromhex(clvm) 716 | 717 | sp = SerializedProgram.from_bytes(clvm_blob) 718 | return Program.from_bytes(bytes(sp)) 719 | --------------------------------------------------------------------------------