diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index 88dac2bd8a..0cc8037b3f 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -20,6 +20,7 @@ from chia.util.config import load_config from chia.util.default_root import DEFAULT_ROOT_PATH from chia.util.ints import uint16, uint32, uint64, uint128 from chia.wallet.did_wallet.did_info import DID_HRP +from chia.wallet.nft_wallet.nft_puzzles import NFT_METADATA_UPDATER_PUZZLE_HASH from chia.wallet.nft_wallet.nft_info import NFT_HRP, NFTInfo from chia.wallet.trade_record import TradeRecord from chia.wallet.trading.offer import Offer @@ -515,16 +516,50 @@ async def take_offer(args: dict, wallet_client: WalletRpcClient, fingerprint: in print("Please enter a valid offer file or hex blob") return - offered, requested, _ = offer.summary() + offered, requested, driver_dict = offer.summary() cat_name_resolver = wallet_client.cat_asset_id_to_name print("Summary:") print(" OFFERED:") await print_offer_summary(cat_name_resolver, offered) print(" REQUESTED:") await print_offer_summary(cat_name_resolver, requested) + + print() + + nft_coin_id: Optional[bytes32] = nft_coin_id_from_offer(driver_dict) + nft_royalty_percentage: int = ( + 0 if nft_coin_id is None else await get_nft_royalty_percentage(nft_coin_id, wallet_client) + ) + nft_total_amount_requested_str: Optional[str] = None + if nft_royalty_percentage > 0: + print("NFT Royalty Fee:") + nft_royalty_asset_id, nft_royalty_amount, nft_total_amount_requested = calculate_nft_royalty_amount( + offered, requested, nft_coin_id, nft_royalty_percentage + ) + nft_royalty_currency: str = "Unknown CAT" + if nft_royalty_asset_id == "xch": + nft_royalty_currency = "XCH" + else: + result = await cat_name_resolver(bytes32.fromhex(nft_royalty_asset_id)) + if result is not None: + nft_royalty_currency = result[1] + + nft_royalty_divisor = units["chia"] if nft_royalty_asset_id == "xch" else units["cat"] + nft_total_amount_requested_str = ( + f"{Decimal(nft_total_amount_requested) / nft_royalty_divisor} {nft_royalty_currency}" + ) + print( + f" {Decimal(nft_royalty_amount) / nft_royalty_divisor} {nft_royalty_currency} " + f"({nft_royalty_amount} mojos)" + ) + print(f"Included Fees: {Decimal(offer.bundle.fees()) / units['chia']}") + if nft_total_amount_requested_str is not None: + print(f"Total Amount Requested: {nft_total_amount_requested_str}") + if not examine_only: + print() confirmation = input("Would you like to take this offer? (y/n): ") if confirmation in ["y", "yes"]: trade_record = await wallet_client.take_offer(offer, fee=fee) @@ -929,3 +964,44 @@ async def get_nft_info(args: Dict, wallet_client: WalletRpcClient, fingerprint: print_nft_info(nft_info) except Exception as e: print(f"Failed to get NFT info: {e}") + + +async def get_nft_royalty_percentage(nft_coin_id: bytes32, wallet_client: WalletRpcClient) -> int: + info = NFTInfo.from_json_dict((await wallet_client.get_nft_info(nft_coin_id.hex()))["nft_info"]) + return info.royalty_percentage + + +def calculate_nft_royalty_amount( + offered: Dict[str, Any], requested: Dict[str, Any], nft_coin_id: bytes32, nft_royalty_percentage: int +) -> Tuple[str, int, int]: + nft_asset_id = nft_coin_id.hex() + amount_dict: Dict[str, Any] = requested if nft_asset_id in offered else offered + amounts: List[Tuple[str, int]] = list(amount_dict.items()) + + if len(amounts) != 1 or not isinstance(amounts[0][1], int): + raise ValueError("Royalty enabled NFTs only support offering/requesting one NFT for one currency") + + royalty_amount: uint64 = uint64(amounts[0][1] * nft_royalty_percentage / 10000) + royalty_asset_id = amounts[0][0] + total_amount_requested = (requested[royalty_asset_id] if amount_dict == requested else 0) + royalty_amount + return royalty_asset_id, royalty_amount, total_amount_requested + + +def driver_dict_asset_is_nft(driver_dict: Dict[str, Any], asset_id: str) -> bool: + asset_dict: Dict[str, Any] = driver_dict[asset_id] + if asset_dict.get("type") == "singleton": + updater_hash_hexstr: Optional[str] = asset_dict.get("also", {}).get("updater_hash") + try: + updater_hash = bytes32.from_hexstr(updater_hash_hexstr) + return updater_hash == NFT_METADATA_UPDATER_PUZZLE_HASH + except ValueError: + # Failed to construct bytes32 from updater_hash_hexstr + pass + return False + + +def nft_coin_id_from_offer(driver_dict: Dict[str, Any]) -> Optional[bytes32]: + nft_asset_id: Optional[str] = next( + (key for key in driver_dict.keys() if driver_dict_asset_is_nft(driver_dict, key)), None + ) + return bytes32.fromhex(nft_asset_id) if nft_asset_id is not None else None diff --git a/chia/wallet/nft_wallet/nft_puzzles.py b/chia/wallet/nft_wallet/nft_puzzles.py index c25ac6e836..01b6b7985a 100644 --- a/chia/wallet/nft_wallet/nft_puzzles.py +++ b/chia/wallet/nft_wallet/nft_puzzles.py @@ -20,6 +20,7 @@ LAUNCHER_PUZZLE_HASH = LAUNCHER_PUZZLE.get_tree_hash() SINGLETON_MOD_HASH = SINGLETON_TOP_LAYER_MOD.get_tree_hash() NFT_STATE_LAYER_MOD_HASH = NFT_STATE_LAYER_MOD.get_tree_hash() NFT_METADATA_UPDATER = load_clvm("nft_metadata_updater_default.clvm") +NFT_METADATA_UPDATER_PUZZLE_HASH = NFT_METADATA_UPDATER.get_tree_hash() NFT_OWNERSHIP_LAYER = load_clvm("nft_ownership_layer.clvm") NFT_TRANSFER_PROGRAM_DEFAULT = load_clvm("nft_ownership_transfer_program_one_way_claim_with_royalties.clvm") STANDARD_PUZZLE_MOD = load_clvm("p2_delegated_puzzle_or_hidden_puzzle.clvm")