From 14353ccd40b6832cb3cf5ba77c6eab60491b10ab Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Sat, 30 Apr 2022 15:29:14 -0400 Subject: [PATCH 1/4] Cleanup UncurriedNFT --- chia/wallet/nft_wallet/nft_puzzles.py | 8 +- chia/wallet/nft_wallet/nft_wallet.py | 40 +++---- chia/wallet/nft_wallet/uncurry_nft.py | 150 +++++++++++++++----------- chia/wallet/wallet_state_manager.py | 4 +- 4 files changed, 118 insertions(+), 84 deletions(-) diff --git a/chia/wallet/nft_wallet/nft_puzzles.py b/chia/wallet/nft_wallet/nft_puzzles.py index 9e6731567e6e..53a2c6f26ec0 100644 --- a/chia/wallet/nft_wallet/nft_puzzles.py +++ b/chia/wallet/nft_wallet/nft_puzzles.py @@ -131,7 +131,13 @@ def get_nft_info_from_puzzle(puzzle: Program, nft_coin: Coin) -> NFTInfo: :return: NFTInfo """ # TODO Update this method after the NFT code finalized - uncurried_nft: UncurriedNFT = UncurriedNFT.uncurry(puzzle, True) + uncurried_nft = UncurriedNFT.uncurry(puzzle, raise_exception=True) + # Even with raise_exception=True you can still get None back + if uncurried_nft is None: + # The code below would have raised anyways as None does not have a + # .data_uris attribute. + raise ValueError(f"UncurriedNFT.uncurry() call was unable to uncurry puzzle {puzzle} but also did not raise.") + data_uris = [] for uri in uncurried_nft.data_uris.as_python(): data_uris.append(str(uri, "utf-8")) diff --git a/chia/wallet/nft_wallet/nft_wallet.py b/chia/wallet/nft_wallet/nft_wallet.py index 0f3a59c1444d..a110ff117cdb 100644 --- a/chia/wallet/nft_wallet/nft_wallet.py +++ b/chia/wallet/nft_wallet/nft_wallet.py @@ -185,9 +185,19 @@ async def puzzle_solution_received(self, coin_spend: CoinSpend, in_transaction: coin_name = coin_spend.coin.name() puzzle: Program = Program.from_bytes(bytes(coin_spend.puzzle_reveal)) solution: Program = Program.from_bytes(bytes(coin_spend.solution)).rest().rest().first() - uncurried_nft: UncurriedNFT = UncurriedNFT.uncurry(puzzle) + uncurried_nft = UncurriedNFT.uncurry(puzzle) nft_transfer_program = None - if uncurried_nft.matched: + if uncurried_nft is None: + # The parent is not an NFT which means we need to scrub all of its children from our DB + child_coin_records = await self.wallet_state_manager.coin_store.get_coin_records_by_parent_id(coin_name) + if len(child_coin_records) > 0: + for record in child_coin_records: + if record.wallet_id == self.id(): + await self.wallet_state_manager.coin_store.delete_coin_record(record.coin.name()) + # await self.remove_lineage(record.coin.name()) + # We also need to make sure there's no record of the transaction + await self.wallet_state_manager.tx_store.delete_transaction_record(record.coin.name()) + else: # check if we already know this hash, if not then try to find reveal in solution for hash, reveal in self.nft_wallet_info.known_transfer_programs: if hash == bytes32(uncurried_nft.transfer_program_hash.as_atom()): @@ -244,16 +254,6 @@ async def puzzle_solution_received(self, coin_spend: CoinSpend, in_transaction: child_puzzle, in_transaction=in_transaction, ) - else: - # The parent is not an NFT which means we need to scrub all of its children from our DB - child_coin_records = await self.wallet_state_manager.coin_store.get_coin_records_by_parent_id(coin_name) - if len(child_coin_records) > 0: - for record in child_coin_records: - if record.wallet_id == self.id(): - await self.wallet_state_manager.coin_store.delete_coin_record(record.coin.name()) - # await self.remove_lineage(record.coin.name()) - # We also need to make sure there's no record of the transaction - await self.wallet_state_manager.tx_store.delete_transaction_record(record.coin.name()) async def add_coin( self, coin: Coin, lineage_proof: LineageProof, transfer_program: Program, puzzle: Program, in_transaction: bool @@ -515,13 +515,15 @@ async def receive_nft(self, sending_sb: SpendBundle, fee: uint64 = uint64(0)) -> trade_price_list_discovered = None nft_id = None for coin_spend in sending_sb.coin_spends: - uncurried_nft: UncurriedNFT = UncurriedNFT.uncurry(Program.from_bytes(bytes(coin_spend.puzzle_reveal))) - if uncurried_nft.matched: - inner_sol = Program.from_bytes(bytes(coin_spend.solution)).rest().rest().first() - trade_price_list_discovered = nft_puzzles.get_trade_prices_list_from_inner_solution(inner_sol) - nft_id = uncurried_nft.singleton_launcher_id.as_atom() - royalty_address: bytes32 = uncurried_nft.royalty_address.as_atom() - royalty_percentage: uint64 = uint64(uncurried_nft.trade_price_percentage.as_int()) + uncurried_nft = UncurriedNFT.uncurry(Program.from_bytes(bytes(coin_spend.puzzle_reveal))) + if uncurried_nft is None: + continue + + inner_sol = Program.from_bytes(bytes(coin_spend.solution)).rest().rest().first() + trade_price_list_discovered = nft_puzzles.get_trade_prices_list_from_inner_solution(inner_sol) + nft_id = uncurried_nft.singleton_launcher_id.as_atom() + royalty_address: bytes32 = uncurried_nft.royalty_address.as_atom() + royalty_percentage: uint64 = uint64(uncurried_nft.trade_price_percentage.as_int()) assert trade_price_list_discovered is not None assert nft_id is not None diff --git a/chia/wallet/nft_wallet/uncurry_nft.py b/chia/wallet/nft_wallet/uncurry_nft.py index 32fd805c6be0..b9a013feff6d 100644 --- a/chia/wallet/nft_wallet/uncurry_nft.py +++ b/chia/wallet/nft_wallet/uncurry_nft.py @@ -1,6 +1,7 @@ from __future__ import annotations -from dataclasses import dataclass, field +from dataclasses import dataclass +from typing import Optional, Type, TypeVar from chia.types.blockchain_format.program import Program from chia.wallet.puzzles.load_clvm import load_clvm @@ -9,6 +10,9 @@ NFT_MOD = load_clvm("nft_innerpuz.clvm") +_T_UncurriedNFT = TypeVar("_T_UncurriedNFT", bound="UncurriedNFT") + + @dataclass class UncurriedNFT: """ @@ -17,92 +21,114 @@ class UncurriedNFT: This is the only place you need to change after modified the Chialisp curried parameters. """ - matched: bool = field(init=False) - """If the puzzle is a NFT puzzle""" - - nft_mod_hash: Program = field(init=False) + nft_mod_hash: Program """NFT module hash""" - singleton_struct: Program = field(init=False) + singleton_struct: Program """ Singleton struct [singleton_mod_hash, singleton_launcher_id, launcher_puzhash] """ - singleton_mod_hash: Program = field(init=False) - singleton_launcher_id: Program = field(init=False) - launcher_puzhash: Program = field(init=False) + singleton_mod_hash: Program + singleton_launcher_id: Program + launcher_puzhash: Program - owner_did: Program = field(init=False) + owner_did: Program """Owner's DID""" - transfer_program_hash: Program = field(init=False) + transfer_program_hash: Program """Puzzle hash of the transfer program""" - transfer_program_curry_params: Program = field(init=False) + transfer_program_curry_params: Program """ Curried parameters of the transfer program [royalty_address, trade_price_percentage, settlement_mod_hash, cat_mod_hash] """ - royalty_address: Program = field(init=False) - trade_price_percentage: Program = field(init=False) - settlement_mod_hash: Program = field(init=False) - cat_mod_hash: Program = field(init=False) + royalty_address: Program + trade_price_percentage: Program + settlement_mod_hash: Program + cat_mod_hash: Program - metadata: Program = field(init=False) + metadata: Program """ NFT metadata [("u", data_uris), ("h", data_hash)] """ - data_uris: Program = field(init=False) - data_hash: Program = field(init=False) + data_uris: Program + data_hash: Program + + # TODO: If we make raise_exception=True result in no None return we could overload + # this to express that and avoid None handling in that case. + @classmethod + def uncurry(cls: Type[_T_UncurriedNFT], puzzle: Program, raise_exception: bool = False) -> Optional[_T_UncurriedNFT]: + """Try to uncurry a NFT puzzle - @staticmethod - def uncurry(puzzle: Program, raise_exception: bool = False) -> UncurriedNFT: - """ - Try to uncurry a NFT puzzle :param puzzle: Puzzle :param raise_exception: If want to raise an exception when the puzzle is invalid :return Uncurried NFT """ - nft = UncurriedNFT() try: mod, curried_args = puzzle.uncurry() - if mod == SINGLETON_TOP_LAYER_MOD: - mod, curried_args = curried_args.rest().first().uncurry() - if mod == NFT_MOD: - nft.matched = True - # Set nft parameters - ( - nft.nft_mod_hash, - nft.singleton_struct, - nft.owner_did, - nft.transfer_program_hash, - nft.transfer_program_curry_params, - nft.metadata, - ) = curried_args.as_iter() - # Set singleton - nft.singleton_mod_hash = nft.singleton_struct.first() - nft.singleton_launcher_id = nft.singleton_struct.rest().first() - nft.launcher_puzhash = nft.singleton_struct.rest().rest() - # Set transfer program parameters - ( - nft.royalty_address, - nft.trade_price_percentage, - nft.settlement_mod_hash, - nft.cat_mod_hash, - ) = nft.transfer_program_curry_params.as_iter() - # Set metadata - for kv_pair in nft.metadata.as_iter(): - if kv_pair.first().as_atom() == b"u": - nft.data_uris = kv_pair.rest() - if kv_pair.first().as_atom() == b"h": - nft.data_hash = kv_pair.rest() - return nft - nft.matched = False - return nft - except Exception: + if mod != SINGLETON_TOP_LAYER_MOD: + # TODO: shouldn't this raise if raise_exception? + return None + + mod, curried_args = curried_args.rest().first().uncurry() + if mod != NFT_MOD: + # TODO: shouldn't this raise if raise_exception? + return None + + # nft parameters + # TODO: Centralize the definition of this order with a class and construct + # an instance of it instead of using free variables. + ( + nft_mod_hash, + singleton_struct, + owner_did, + transfer_program_hash, + transfer_program_curry_params, + metadata, + ) = curried_args.as_iter() + + # singleton + singleton_mod_hash = singleton_struct.first() + singleton_launcher_id = singleton_struct.rest().first() + launcher_puzhash = singleton_struct.rest().rest() + + # transfer program parameters + ( + royalty_address, + trade_price_percentage, + settlement_mod_hash, + cat_mod_hash, + ) = transfer_program_curry_params.as_iter() + + # metadata + for kv_pair in metadata.as_iter(): + if kv_pair.first().as_atom() == b"u": + data_uris = kv_pair.rest() + if kv_pair.first().as_atom() == b"h": + data_hash = kv_pair.rest() + + return cls( + nft_mod_hash=nft_mod_hash, + singleton_struct=singleton_struct, + singleton_mod_hash=singleton_mod_hash, + singleton_launcher_id=singleton_launcher_id, + launcher_puzhash=launcher_puzhash, + owner_did=owner_did, + transfer_program_hash=transfer_program_hash, + transfer_program_curry_params=transfer_program_curry_params, + royalty_address=royalty_address, + trade_price_percentage=trade_price_percentage, + settlement_mod_hash=settlement_mod_hash, + cat_mod_hash=cat_mod_hash, + metadata=metadata, + data_uris=data_uris, + data_hash=data_hash, + ) + except Exception as e: if raise_exception: - raise ValueError(f"Cannot uncurry puzzle {puzzle}, it's not a NFT puzzle.") - else: - nft.matched = False - return nft + raise ValueError(f"Cannot uncurry puzzle {puzzle}, it's not an NFT puzzle.") from e + + return None diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 8be3707def00..6e138be515db 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -560,8 +560,8 @@ async def determine_coin_type( # Check if the coin is a NFT # hint # First spend where 1 mojo coin -> Singleton launcher -> NFT -> NFT - uncurried_nft: UncurriedNFT = UncurriedNFT.uncurry(Program.from_bytes(bytes(coin_spend.puzzle_reveal))) - if uncurried_nft.matched: + uncurried_nft = UncurriedNFT.uncurry(Program.from_bytes(bytes(coin_spend.puzzle_reveal))) + if uncurried_nft is not None: return await self.handle_nft(coin_spend) # Check if the coin is a DID From f6adbcb68bb235bca66aa552aacfd54d337b59a1 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Sat, 30 Apr 2022 15:46:15 -0400 Subject: [PATCH 2/4] black --- chia/wallet/nft_wallet/uncurry_nft.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/chia/wallet/nft_wallet/uncurry_nft.py b/chia/wallet/nft_wallet/uncurry_nft.py index b9a013feff6d..0c1809b86591 100644 --- a/chia/wallet/nft_wallet/uncurry_nft.py +++ b/chia/wallet/nft_wallet/uncurry_nft.py @@ -60,7 +60,11 @@ class UncurriedNFT: # TODO: If we make raise_exception=True result in no None return we could overload # this to express that and avoid None handling in that case. @classmethod - def uncurry(cls: Type[_T_UncurriedNFT], puzzle: Program, raise_exception: bool = False) -> Optional[_T_UncurriedNFT]: + def uncurry( + cls: Type[_T_UncurriedNFT], + puzzle: Program, + raise_exception: bool = False, + ) -> Optional[_T_UncurriedNFT]: """Try to uncurry a NFT puzzle :param puzzle: Puzzle From 3de1e3631a9dd49f15b0f52e47413a4aba8f3a00 Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Sat, 30 Apr 2022 16:00:03 -0400 Subject: [PATCH 3/4] just always raise --- chia/wallet/nft_wallet/nft_puzzles.py | 8 +- chia/wallet/nft_wallet/nft_wallet.py | 122 +++++++++++++------------- chia/wallet/nft_wallet/uncurry_nft.py | 23 ++--- chia/wallet/wallet_state_manager.py | 5 +- 4 files changed, 74 insertions(+), 84 deletions(-) diff --git a/chia/wallet/nft_wallet/nft_puzzles.py b/chia/wallet/nft_wallet/nft_puzzles.py index 53a2c6f26ec0..86b707e287ea 100644 --- a/chia/wallet/nft_wallet/nft_puzzles.py +++ b/chia/wallet/nft_wallet/nft_puzzles.py @@ -130,13 +130,7 @@ def get_nft_info_from_puzzle(puzzle: Program, nft_coin: Coin) -> NFTInfo: :param nft_coin: NFT coin :return: NFTInfo """ - # TODO Update this method after the NFT code finalized - uncurried_nft = UncurriedNFT.uncurry(puzzle, raise_exception=True) - # Even with raise_exception=True you can still get None back - if uncurried_nft is None: - # The code below would have raised anyways as None does not have a - # .data_uris attribute. - raise ValueError(f"UncurriedNFT.uncurry() call was unable to uncurry puzzle {puzzle} but also did not raise.") + uncurried_nft = UncurriedNFT.uncurry(puzzle) data_uris = [] for uri in uncurried_nft.data_uris.as_python(): diff --git a/chia/wallet/nft_wallet/nft_wallet.py b/chia/wallet/nft_wallet/nft_wallet.py index a110ff117cdb..f05513b508e9 100644 --- a/chia/wallet/nft_wallet/nft_wallet.py +++ b/chia/wallet/nft_wallet/nft_wallet.py @@ -185,9 +185,11 @@ async def puzzle_solution_received(self, coin_spend: CoinSpend, in_transaction: coin_name = coin_spend.coin.name() puzzle: Program = Program.from_bytes(bytes(coin_spend.puzzle_reveal)) solution: Program = Program.from_bytes(bytes(coin_spend.solution)).rest().rest().first() - uncurried_nft = UncurriedNFT.uncurry(puzzle) nft_transfer_program = None - if uncurried_nft is None: + + try: + uncurried_nft = UncurriedNFT.uncurry(puzzle) + except ValueError: # The parent is not an NFT which means we need to scrub all of its children from our DB child_coin_records = await self.wallet_state_manager.coin_store.get_coin_records_by_parent_id(coin_name) if len(child_coin_records) > 0: @@ -197,63 +199,64 @@ async def puzzle_solution_received(self, coin_spend: CoinSpend, in_transaction: # await self.remove_lineage(record.coin.name()) # We also need to make sure there's no record of the transaction await self.wallet_state_manager.tx_store.delete_transaction_record(record.coin.name()) - else: - # check if we already know this hash, if not then try to find reveal in solution - for hash, reveal in self.nft_wallet_info.known_transfer_programs: - if hash == bytes32(uncurried_nft.transfer_program_hash.as_atom()): - nft_transfer_program = reveal - if nft_transfer_program is None: - attempt = nft_puzzles.get_transfer_program_from_inner_solution(solution) - if attempt is not None: - nft_transfer_program = attempt - await self.add_transfer_program(nft_transfer_program, in_transaction=in_transaction) - - assert nft_transfer_program is not None - self.log.info(f"found the info for coin {coin_name}") - parent_coin = None - coin_record = await self.wallet_state_manager.coin_store.get_coin_record(coin_name) - if coin_record is None: - coin_states: Optional[List[CoinState]] = await self.wallet_state_manager.wallet_node.get_coin_state( - [coin_name] - ) - if coin_states is not None: - parent_coin = coin_states[0].coin - if coin_record is not None: - parent_coin = coin_record.coin - if parent_coin is None: - raise ValueError("Error in finding parent") - inner_puzzle: Program = nft_puzzles.create_nft_layer_puzzle_with_curry_params( - uncurried_nft.singleton_launcher_id.as_atom(), - uncurried_nft.owner_did.as_atom(), - uncurried_nft.transfer_program_hash.as_atom(), - uncurried_nft.metadata, - uncurried_nft.transfer_program_curry_params, - ) - child_coin: Optional[Coin] = None - for new_coin in coin_spend.additions(): - if new_coin.amount % 2 == 1: - child_coin = new_coin - break - assert child_coin is not None - - metadata = nft_puzzles.update_metadata(uncurried_nft.metadata, solution) - # TODO: add smarter check for -22 to see if curry_params changed and use this for metadata too - child_puzzle: Program = nft_puzzles.create_full_puzzle_with_curry_params( - uncurried_nft.singleton_launcher_id.as_atom(), - self.nft_wallet_info.my_did, - uncurried_nft.transfer_program_hash.as_atom(), - metadata, - uncurried_nft.transfer_program_curry_params, + return + + # check if we already know this hash, if not then try to find reveal in solution + for hash, reveal in self.nft_wallet_info.known_transfer_programs: + if hash == bytes32(uncurried_nft.transfer_program_hash.as_atom()): + nft_transfer_program = reveal + if nft_transfer_program is None: + attempt = nft_puzzles.get_transfer_program_from_inner_solution(solution) + if attempt is not None: + nft_transfer_program = attempt + await self.add_transfer_program(nft_transfer_program, in_transaction=in_transaction) + + assert nft_transfer_program is not None + self.log.info(f"found the info for coin {coin_name}") + parent_coin = None + coin_record = await self.wallet_state_manager.coin_store.get_coin_record(coin_name) + if coin_record is None: + coin_states: Optional[List[CoinState]] = await self.wallet_state_manager.wallet_node.get_coin_state( + [coin_name] ) + if coin_states is not None: + parent_coin = coin_states[0].coin + if coin_record is not None: + parent_coin = coin_record.coin + if parent_coin is None: + raise ValueError("Error in finding parent") + inner_puzzle: Program = nft_puzzles.create_nft_layer_puzzle_with_curry_params( + uncurried_nft.singleton_launcher_id.as_atom(), + uncurried_nft.owner_did.as_atom(), + uncurried_nft.transfer_program_hash.as_atom(), + uncurried_nft.metadata, + uncurried_nft.transfer_program_curry_params, + ) + child_coin: Optional[Coin] = None + for new_coin in coin_spend.additions(): + if new_coin.amount % 2 == 1: + child_coin = new_coin + break + assert child_coin is not None - assert child_puzzle.get_tree_hash() == child_coin.puzzle_hash - await self.add_coin( - child_coin, - LineageProof(parent_coin.parent_coin_info, inner_puzzle.get_tree_hash(), parent_coin.amount), - nft_transfer_program, - child_puzzle, - in_transaction=in_transaction, - ) + metadata = nft_puzzles.update_metadata(uncurried_nft.metadata, solution) + # TODO: add smarter check for -22 to see if curry_params changed and use this for metadata too + child_puzzle: Program = nft_puzzles.create_full_puzzle_with_curry_params( + uncurried_nft.singleton_launcher_id.as_atom(), + self.nft_wallet_info.my_did, + uncurried_nft.transfer_program_hash.as_atom(), + metadata, + uncurried_nft.transfer_program_curry_params, + ) + + assert child_puzzle.get_tree_hash() == child_coin.puzzle_hash + await self.add_coin( + child_coin, + LineageProof(parent_coin.parent_coin_info, inner_puzzle.get_tree_hash(), parent_coin.amount), + nft_transfer_program, + child_puzzle, + in_transaction=in_transaction, + ) async def add_coin( self, coin: Coin, lineage_proof: LineageProof, transfer_program: Program, puzzle: Program, in_transaction: bool @@ -515,8 +518,9 @@ async def receive_nft(self, sending_sb: SpendBundle, fee: uint64 = uint64(0)) -> trade_price_list_discovered = None nft_id = None for coin_spend in sending_sb.coin_spends: - uncurried_nft = UncurriedNFT.uncurry(Program.from_bytes(bytes(coin_spend.puzzle_reveal))) - if uncurried_nft is None: + try: + uncurried_nft = UncurriedNFT.uncurry(Program.from_bytes(bytes(coin_spend.puzzle_reveal))) + except ValueError: continue inner_sol = Program.from_bytes(bytes(coin_spend.solution)).rest().rest().first() diff --git a/chia/wallet/nft_wallet/uncurry_nft.py b/chia/wallet/nft_wallet/uncurry_nft.py index 0c1809b86591..4cb877b7f2d4 100644 --- a/chia/wallet/nft_wallet/uncurry_nft.py +++ b/chia/wallet/nft_wallet/uncurry_nft.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Optional, Type, TypeVar +from typing import Type, TypeVar from chia.types.blockchain_format.program import Program from chia.wallet.puzzles.load_clvm import load_clvm @@ -57,30 +57,24 @@ class UncurriedNFT: data_uris: Program data_hash: Program - # TODO: If we make raise_exception=True result in no None return we could overload - # this to express that and avoid None handling in that case. @classmethod - def uncurry( - cls: Type[_T_UncurriedNFT], - puzzle: Program, - raise_exception: bool = False, - ) -> Optional[_T_UncurriedNFT]: + def uncurry(cls: Type[_T_UncurriedNFT], puzzle: Program) -> _T_UncurriedNFT: """Try to uncurry a NFT puzzle :param puzzle: Puzzle :param raise_exception: If want to raise an exception when the puzzle is invalid :return Uncurried NFT """ + exception = ValueError(f"Cannot uncurry puzzle {puzzle}, it's not an NFT puzzle.") + try: mod, curried_args = puzzle.uncurry() if mod != SINGLETON_TOP_LAYER_MOD: - # TODO: shouldn't this raise if raise_exception? - return None + raise exception mod, curried_args = curried_args.rest().first().uncurry() if mod != NFT_MOD: - # TODO: shouldn't this raise if raise_exception? - return None + raise exception # nft parameters # TODO: Centralize the definition of this order with a class and construct @@ -132,7 +126,4 @@ def uncurry( data_hash=data_hash, ) except Exception as e: - if raise_exception: - raise ValueError(f"Cannot uncurry puzzle {puzzle}, it's not an NFT puzzle.") from e - - return None + raise exception from e diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index 6e138be515db..d8c9b3688f1e 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -1,4 +1,5 @@ import asyncio +import contextlib import json import logging import multiprocessing @@ -560,8 +561,8 @@ async def determine_coin_type( # Check if the coin is a NFT # hint # First spend where 1 mojo coin -> Singleton launcher -> NFT -> NFT - uncurried_nft = UncurriedNFT.uncurry(Program.from_bytes(bytes(coin_spend.puzzle_reveal))) - if uncurried_nft is not None: + with contextlib.suppress(ValueError): + UncurriedNFT.uncurry(Program.from_bytes(bytes(coin_spend.puzzle_reveal))) return await self.handle_nft(coin_spend) # Check if the coin is a DID From 8af1e863b1f53427850d937b7f73c5ef9ea85d0f Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Sun, 1 May 2022 20:56:26 -0400 Subject: [PATCH 4/4] another pass --- chia/wallet/nft_wallet/nft_puzzles.py | 4 +- chia/wallet/nft_wallet/nft_wallet.py | 4 +- chia/wallet/nft_wallet/uncurry_nft.py | 101 ++++++++++++++------------ chia/wallet/wallet_state_manager.py | 6 +- 4 files changed, 64 insertions(+), 51 deletions(-) diff --git a/chia/wallet/nft_wallet/nft_puzzles.py b/chia/wallet/nft_wallet/nft_puzzles.py index 86b707e287ea..687ca978a954 100644 --- a/chia/wallet/nft_wallet/nft_puzzles.py +++ b/chia/wallet/nft_wallet/nft_puzzles.py @@ -130,8 +130,8 @@ def get_nft_info_from_puzzle(puzzle: Program, nft_coin: Coin) -> NFTInfo: :param nft_coin: NFT coin :return: NFTInfo """ - uncurried_nft = UncurriedNFT.uncurry(puzzle) - + # TODO Update this method after the NFT code finalized + uncurried_nft: UncurriedNFT = UncurriedNFT.uncurry(puzzle) data_uris = [] for uri in uncurried_nft.data_uris.as_python(): data_uris.append(str(uri, "utf-8")) diff --git a/chia/wallet/nft_wallet/nft_wallet.py b/chia/wallet/nft_wallet/nft_wallet.py index f05513b508e9..4483cccba9a1 100644 --- a/chia/wallet/nft_wallet/nft_wallet.py +++ b/chia/wallet/nft_wallet/nft_wallet.py @@ -189,7 +189,7 @@ async def puzzle_solution_received(self, coin_spend: CoinSpend, in_transaction: try: uncurried_nft = UncurriedNFT.uncurry(puzzle) - except ValueError: + except Exception: # The parent is not an NFT which means we need to scrub all of its children from our DB child_coin_records = await self.wallet_state_manager.coin_store.get_coin_records_by_parent_id(coin_name) if len(child_coin_records) > 0: @@ -520,7 +520,7 @@ async def receive_nft(self, sending_sb: SpendBundle, fee: uint64 = uint64(0)) -> for coin_spend in sending_sb.coin_spends: try: uncurried_nft = UncurriedNFT.uncurry(Program.from_bytes(bytes(coin_spend.puzzle_reveal))) - except ValueError: + except Exception: continue inner_sol = Program.from_bytes(bytes(coin_spend.solution)).rest().rest().first() diff --git a/chia/wallet/nft_wallet/uncurry_nft.py b/chia/wallet/nft_wallet/uncurry_nft.py index 4cb877b7f2d4..744e732a29db 100644 --- a/chia/wallet/nft_wallet/uncurry_nft.py +++ b/chia/wallet/nft_wallet/uncurry_nft.py @@ -65,20 +65,20 @@ def uncurry(cls: Type[_T_UncurriedNFT], puzzle: Program) -> _T_UncurriedNFT: :param raise_exception: If want to raise an exception when the puzzle is invalid :return Uncurried NFT """ - exception = ValueError(f"Cannot uncurry puzzle {puzzle}, it's not an NFT puzzle.") - + mod, curried_args = puzzle.uncurry() + if mod != SINGLETON_TOP_LAYER_MOD: + # TODO: proper message + raise ValueError(f"Cannot uncurry puzzle, mod not a single top layer: {puzzle}") + + mod, curried_args = curried_args.rest().first().uncurry() + if mod != NFT_MOD: + # TODO: proper message + raise ValueError(f"Cannot uncurry puzzle, mod not an NFT mode: {puzzle}") + + # nft parameters + # TODO: Centralize the definition of this order with a class and construct + # an instance of it instead of using free variables. try: - mod, curried_args = puzzle.uncurry() - if mod != SINGLETON_TOP_LAYER_MOD: - raise exception - - mod, curried_args = curried_args.rest().first().uncurry() - if mod != NFT_MOD: - raise exception - - # nft parameters - # TODO: Centralize the definition of this order with a class and construct - # an instance of it instead of using free variables. ( nft_mod_hash, singleton_struct, @@ -87,43 +87,54 @@ def uncurry(cls: Type[_T_UncurriedNFT], puzzle: Program) -> _T_UncurriedNFT: transfer_program_curry_params, metadata, ) = curried_args.as_iter() + except ValueError as e: + # TODO: proper message + raise ValueError(f"Cannot uncurry puzzle, incorrect number of arguemnts: {puzzle}") from e - # singleton - singleton_mod_hash = singleton_struct.first() - singleton_launcher_id = singleton_struct.rest().first() - launcher_puzhash = singleton_struct.rest().rest() + # singleton + singleton_mod_hash = singleton_struct.first() + singleton_launcher_id = singleton_struct.rest().first() + launcher_puzhash = singleton_struct.rest().rest() - # transfer program parameters + # transfer program parameters + try: ( royalty_address, trade_price_percentage, settlement_mod_hash, cat_mod_hash, ) = transfer_program_curry_params.as_iter() - - # metadata - for kv_pair in metadata.as_iter(): - if kv_pair.first().as_atom() == b"u": - data_uris = kv_pair.rest() - if kv_pair.first().as_atom() == b"h": - data_hash = kv_pair.rest() - - return cls( - nft_mod_hash=nft_mod_hash, - singleton_struct=singleton_struct, - singleton_mod_hash=singleton_mod_hash, - singleton_launcher_id=singleton_launcher_id, - launcher_puzhash=launcher_puzhash, - owner_did=owner_did, - transfer_program_hash=transfer_program_hash, - transfer_program_curry_params=transfer_program_curry_params, - royalty_address=royalty_address, - trade_price_percentage=trade_price_percentage, - settlement_mod_hash=settlement_mod_hash, - cat_mod_hash=cat_mod_hash, - metadata=metadata, - data_uris=data_uris, - data_hash=data_hash, - ) - except Exception as e: - raise exception from e + except ValueError as e: + # TODO: proper message + raise ValueError( + f"Cannot uncurry puzzle, incorrect number of transfer program curry parameters: {puzzle}", + ) from e + + # metadata + for kv_pair in metadata.as_iter(): + if kv_pair.first().as_atom() == b"u": + data_uris = kv_pair.rest() + if kv_pair.first().as_atom() == b"h": + data_hash = kv_pair.rest() + + # TODO: How should we handle the case that no data_uris or no data_hash + # is found? Is it ok if we find them multiple times and just + # ignore the first ones? + + return cls( + nft_mod_hash=nft_mod_hash, + singleton_struct=singleton_struct, + singleton_mod_hash=singleton_mod_hash, + singleton_launcher_id=singleton_launcher_id, + launcher_puzhash=launcher_puzhash, + owner_did=owner_did, + transfer_program_hash=transfer_program_hash, + transfer_program_curry_params=transfer_program_curry_params, + royalty_address=royalty_address, + trade_price_percentage=trade_price_percentage, + settlement_mod_hash=settlement_mod_hash, + cat_mod_hash=cat_mod_hash, + metadata=metadata, + data_uris=data_uris, + data_hash=data_hash, + ) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index d8c9b3688f1e..2bf713b1f1bc 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -1,5 +1,4 @@ import asyncio -import contextlib import json import logging import multiprocessing @@ -561,8 +560,11 @@ async def determine_coin_type( # Check if the coin is a NFT # hint # First spend where 1 mojo coin -> Singleton launcher -> NFT -> NFT - with contextlib.suppress(ValueError): + try: UncurriedNFT.uncurry(Program.from_bytes(bytes(coin_spend.puzzle_reveal))) + except Exception: + pass + else: return await self.handle_nft(coin_spend) # Check if the coin is a DID