diff --git a/chia-blockchain-gui b/chia-blockchain-gui index b5c6bbc649333..1cb1a8e7b7ab8 160000 --- a/chia-blockchain-gui +++ b/chia-blockchain-gui @@ -1 +1 @@ -Subproject commit b5c6bbc649333621c626b02799a037cc79d42a59 +Subproject commit 1cb1a8e7b7ab8025744a4205e2444b87621ccd1b diff --git a/chia/cmds/wallet.py b/chia/cmds/wallet.py index 770c7efff67fc..b8416a72d145c 100644 --- a/chia/cmds/wallet.py +++ b/chia/cmds/wallet.py @@ -487,11 +487,15 @@ def nft_cmd(): default=None, ) @click.option("-f", "--fingerprint", help="Set the fingerprint to specify which wallet to use", type=int) -def nft_wallet_create_cmd(wallet_rpc_port: Optional[int], fingerprint: int) -> None: +@click.option("-di", "--did-id", help="DID Id to use", type=str) +@click.option("-n", "--name", help="Set the NFT wallet name", type=str) +def nft_wallet_create_cmd( + wallet_rpc_port: Optional[int], fingerprint: int, did_id: Optional[str], name: Optional[str] +) -> None: import asyncio from .wallet_funcs import execute_with_wallet, create_nft_wallet - extra_params: Dict[str, Any] = {} + extra_params: Dict[str, Any] = {"did_id": did_id, "name": name} asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, create_nft_wallet)) @@ -524,6 +528,14 @@ def nft_wallet_create_cmd(wallet_rpc_port: Optional[int], fingerprint: int) -> N show_default=True, callback=validate_fee, ) +@click.option( + "-rp", + "--royalty-percentage-fraction", + help="NFT royalty percentage fraction in basis points. Example: 175 would represent 1.75%", + type=int, + default=0, + show_default=True, +) def nft_mint_cmd( wallet_rpc_port: Optional[int], fingerprint: int, @@ -539,6 +551,7 @@ def nft_mint_cmd( series_total: Optional[int], series_number: Optional[int], fee: str, + royalty_percentage_fraction: int, ) -> None: import asyncio from .wallet_funcs import execute_with_wallet, mint_nft @@ -566,6 +579,7 @@ def nft_mint_cmd( "series_total": series_total, "series_number": series_number, "fee": fee, + "royalty_percentage": royalty_percentage_fraction, } asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, mint_nft)) @@ -668,3 +682,44 @@ def nft_list_cmd(wallet_rpc_port: Optional[int], fingerprint: int, id: int) -> N extra_params = {"wallet_id": id} asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, list_nfts)) + + +@nft_cmd.command("set_did", short_help="Set a DID on an NFT") +@click.option( + "-wp", + "--wallet-rpc-port", + help="Set the port where the Wallet is hosting the RPC interface. See the rpc_port under wallet in config.yaml", + type=int, + default=None, +) +@click.option("-f", "--fingerprint", help="Set the fingerprint to specify which wallet to use", type=int) +@click.option("-i", "--id", help="Id of the NFT wallet to use", type=int, required=True) +@click.option("-di", "--did-id", help="DID Id to set on the NFT", type=str, required=True) +@click.option("-ni", "--nft-coin-id", help="Id of the NFT coin to set the DID on", type=str, required=True) +@click.option( + "-m", + "--fee", + help="Set the fees per transaction, in XCH.", + type=str, + default="0", + show_default=True, + callback=validate_fee, +) +def nft_set_did_cmd( + wallet_rpc_port: Optional[int], + fingerprint: int, + id: int, + did_id: str, + nft_coin_id: str, + fee: str, +) -> None: + import asyncio + from .wallet_funcs import execute_with_wallet, set_nft_did + + extra_params = { + "wallet_id": id, + "did_id": did_id, + "nft_coin_id": nft_coin_id, + "fee": fee, + } + asyncio.run(execute_with_wallet(wallet_rpc_port, fingerprint, extra_params, set_nft_did)) diff --git a/chia/cmds/wallet_funcs.py b/chia/cmds/wallet_funcs.py index d7de3fb5346e2..3fb51a9d486e2 100644 --- a/chia/cmds/wallet_funcs.py +++ b/chia/cmds/wallet_funcs.py @@ -682,8 +682,10 @@ async def get_did(args: Dict, wallet_client: WalletRpcClient, fingerprint: int) async def create_nft_wallet(args: Dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: + did_id = args["did_id"] + name = args["name"] try: - response = await wallet_client.create_new_nft_wallet(None) + response = await wallet_client.create_new_nft_wallet(did_id, name) wallet_id = response["wallet_id"] print(f"Successfully created an NFT wallet with id {wallet_id} on key {fingerprint}") except Exception as e: @@ -703,6 +705,7 @@ async def mint_nft(args: Dict, wallet_client: WalletRpcClient, fingerprint: int) series_total = args["series_total"] series_number = args["series_number"] fee = args["fee"] + royalty_percentage = args["royalty_percentage"] try: response = await wallet_client.mint_nft( wallet_id, @@ -717,6 +720,7 @@ async def mint_nft(args: Dict, wallet_client: WalletRpcClient, fingerprint: int) series_total, series_number, fee, + royalty_percentage, ) spend_bundle = response["spend_bundle"] print(f"NFT minted Successfully with spend bundle: {spend_bundle}") @@ -763,13 +767,17 @@ async def list_nfts(args: Dict, wallet_client: WalletRpcClient, fingerprint: int for n in nft_list: nft = NFTInfo.from_json_dict(n) + if nft.owner_pubkey is None: + owner_pubkey = None + else: + owner_pubkey = nft.owner_pubkey.hex() print() print(f"{'Launcher coin ID:'.ljust(26)} {nft.launcher_id}") print(f"{'Launcher puzhash:'.ljust(26)} {nft.launcher_puzhash}") print(f"{'Current NFT coin ID:'.ljust(26)} {nft.nft_coin_id}") print(f"{'On-chain data/info:'.ljust(26)} {nft.chain_info}") print(f"{'Owner DID:'.ljust(26)} {nft.owner_did}") - print(f"{'Owner pubkey:'.ljust(26)} {nft.owner_pubkey}") + print(f"{'Owner pubkey:'.ljust(26)} {owner_pubkey}") print(f"{'Royalty percentage:'.ljust(26)} {nft.royalty_percentage}") print(f"{'Royalty puzhash:'.ljust(26)} {nft.royalty_puzzle_hash}") print(f"{'NFT content hash:'.ljust(26)} {nft.data_hash.hex()}") @@ -797,3 +805,16 @@ async def list_nfts(args: Dict, wallet_client: WalletRpcClient, fingerprint: int print(f"No NFTs found for wallet with id {wallet_id} on key {fingerprint}") except Exception as e: print(f"Failed to list NFTs for wallet with id {wallet_id} on key {fingerprint}: {e}") + + +async def set_nft_did(args: Dict, wallet_client: WalletRpcClient, fingerprint: int) -> None: + wallet_id = args["wallet_id"] + did_id = args["did_id"] + nft_coin_id = args["nft_coin_id"] + fee = args["fee"] + try: + response = await wallet_client.set_nft_did(wallet_id, did_id, nft_coin_id, fee) + spend_bundle = response["spend_bundle"] + print(f"Transaction to set DID on NFT has been initiated with: {spend_bundle}") + except Exception as e: + print(f"Failed to set DID on NFT: {e}") diff --git a/chia/rpc/rpc_server.py b/chia/rpc/rpc_server.py index 9a7ff003b49ff..fbd5e50724330 100644 --- a/chia/rpc/rpc_server.py +++ b/chia/rpc/rpc_server.py @@ -14,6 +14,7 @@ from chia.types.peer_info import PeerInfo from chia.util.byte_types import hexstr_to_bytes from chia.util.ints import uint16 from chia.util.json_util import dict_to_json_str +from chia.util.network import select_port from chia.util.ws_message import create_payload, create_payload_dict, format_response, pong log = logging.getLogger(__name__) @@ -328,7 +329,13 @@ async def start_rpc_server( site = web.TCPSite(runner, self_hostname, int(rpc_port), ssl_context=rpc_server.ssl_context) await site.start() - rpc_port = runner.addresses[0][1] + + # + # On a dual-stack system, we want to get the (first) IPv4 port unless + # prefer_ipv6 is set in which case we use the IPv6 port + # + if rpc_port == 0: + rpc_port = select_port(root_path, runner.addresses) async def cleanup(): await rpc_server.stop() diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index e045f6a2b2eaf..e9832645e27bc 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -135,6 +135,7 @@ class WalletRpcApi: # NFT Wallet "/nft_mint_nft": self.nft_mint_nft, "/nft_get_nfts": self.nft_get_nfts, + "/nft_get_by_did": self.nft_get_by_did, "/nft_get_info": self.nft_get_info, "/nft_transfer_nft": self.nft_transfer_nft, "/nft_add_uri": self.nft_add_uri, @@ -1388,6 +1389,16 @@ class WalletRpcApi: nft_info_list.append(nft_puzzles.get_nft_info_from_puzzle(nft)) return {"wallet_id": wallet_id, "success": True, "nft_list": nft_info_list} + async def nft_get_by_did(self, request) -> Dict: + did_id: Optional[bytes32] = None + if "did_id" in request: + did_id = bytes32.from_hexstr(request["did_id"]) + assert self.service.wallet_state_manager is not None + for wallet in self.service.wallet_state_manager.wallets.values(): + if isinstance(wallet, NFTWallet) and wallet.get_did() == did_id: + return {"wallet_id": wallet.wallet_id, "success": True} + return {"error": f"Cannot find a NFT wallet DID = {did_id}", "success": False} + async def nft_transfer_nft(self, request): assert self.service.wallet_state_manager is not None wallet_id = uint32(request["wallet_id"]) diff --git a/chia/rpc/wallet_rpc_client.py b/chia/rpc/wallet_rpc_client.py index c86bd63ede8f9..fa826dab53307 100644 --- a/chia/rpc/wallet_rpc_client.py +++ b/chia/rpc/wallet_rpc_client.py @@ -650,3 +650,8 @@ class WalletRpcClient(RpcClient): request: Dict[str, Any] = {"wallet_id": wallet_id} response = await self.fetch("nft_get_nfts", request) return response + + async def set_nft_did(self, wallet_id, did_id, nft_coin_id, fee): + request: Dict[str, Any] = {"wallet_id": wallet_id, "did_id": did_id, "nft_coin_id": nft_coin_id, "fee": fee} + response = await self.fetch("nft_set_nft_did", request) + return response diff --git a/chia/server/server.py b/chia/server/server.py index 326085fc93d1f..f8c88976a99f8 100644 --- a/chia/server/server.py +++ b/chia/server/server.py @@ -30,7 +30,7 @@ from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.peer_info import PeerInfo from chia.util.errors import Err, ProtocolError from chia.util.ints import uint16 -from chia.util.network import is_in_network, is_localhost +from chia.util.network import is_in_network, is_localhost, select_port from chia.util.ssl_check import verify_ssl_certs_and_keys max_message_size = 50 * 1024 * 1024 # 50MB @@ -267,13 +267,19 @@ class ChiaServer: # this port from the socket itself and update self._port. self.site = web.TCPSite( self.runner, - host="0.0.0.0", + host="", # should listen to both IPv4 and IPv6 on a dual-stack system port=int(self._port), shutdown_timeout=3, ssl_context=ssl_context, ) await self.site.start() - self._port = self.runner.addresses[0][1] + # + # On a dual-stack system, we want to get the (first) IPv4 port unless + # prefer_ipv6 is set in which case we use the IPv6 port + # + if self._port == 0: + self._port = select_port(self.root_path, self.runner.addresses) + self.log.info(f"Started listening on port: {self._port}") async def incoming_connection(self, request): diff --git a/chia/util/network.py b/chia/util/network.py index 3bce97ac5fefb..bbf48a44210a6 100644 --- a/chia/util/network.py +++ b/chia/util/network.py @@ -1,9 +1,11 @@ import socket +from pathlib import Path from ipaddress import ip_address, IPv4Network, IPv6Network from typing import Iterable, List, Tuple, Union, Any, Optional, Dict from chia.server.outbound_message import NodeType from chia.types.blockchain_format.sized_bytes import bytes32 from chia.types.peer_info import PeerInfo +from chia.util.config import load_config from chia.util.ints import uint16 @@ -84,3 +86,21 @@ def is_trusted_inner(peer_host: str, peer_node_id: bytes32, trusted_peers: Dict, return False return True + + +def select_port(root_path: Path, addresses: List[Any]) -> uint16: + global_config = load_config(root_path, "config.yaml") + prefer_ipv6 = global_config.get("prefer_ipv6", False) + selected_port: uint16 + for address_string, port, *_ in addresses: + address = ip_address(address_string) + if address.version == 6 and prefer_ipv6: + selected_port = port + break + elif address.version == 4 and not prefer_ipv6: + selected_port = port + break + else: + selected_port = addresses[0][1] # no matches, just use the first one in the list + + return selected_port diff --git a/chia/wallet/nft_wallet/nft_puzzles.py b/chia/wallet/nft_wallet/nft_puzzles.py index e059d98dcb10e..d1f44349a7021 100644 --- a/chia/wallet/nft_wallet/nft_puzzles.py +++ b/chia/wallet/nft_wallet/nft_puzzles.py @@ -11,7 +11,11 @@ from chia.wallet.nft_wallet.nft_info import NFTCoinInfo, NFTInfo from chia.wallet.nft_wallet.uncurry_nft import UncurriedNFT from chia.wallet.puzzles.cat_loader import CAT_MOD from chia.wallet.puzzles.load_clvm import load_clvm -from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import solution_for_conditions +from chia.wallet.puzzles.p2_delegated_puzzle_or_hidden_puzzle import ( + DEFAULT_HIDDEN_PUZZLE_HASH, + calculate_synthetic_public_key, + solution_for_conditions, +) log = logging.getLogger(__name__) SINGLETON_TOP_LAYER_MOD = load_clvm("singleton_top_layer_v1_1.clvm") diff --git a/chia/wallet/nft_wallet/nft_wallet.py b/chia/wallet/nft_wallet/nft_wallet.py index ebde990c1c12c..820e7ec2eee00 100644 --- a/chia/wallet/nft_wallet/nft_wallet.py +++ b/chia/wallet/nft_wallet/nft_wallet.py @@ -64,7 +64,10 @@ class NFTWallet: nft_wallet_info: NFTWalletInfo standard_wallet: Wallet wallet_id: int - did_id: Optional[bytes32] + + @property + def did_id(self): + return self.nft_wallet_info.did_id @classmethod async def create_new_nft_wallet( @@ -72,7 +75,7 @@ class NFTWallet: wallet_state_manager: Any, wallet: Wallet, did_id: Optional[bytes32] = None, - name: str = "", + name: Optional[str] = None, in_transaction: bool = False, ) -> _T_NFTWallet: """ @@ -80,12 +83,14 @@ class NFTWallet: """ self = cls() self.standard_wallet = wallet + if name is None: + name = "NFT Wallet" self.log = logging.getLogger(name if name else __name__) self.wallet_state_manager = wallet_state_manager self.nft_wallet_info = NFTWalletInfo([], did_id) info_as_string = json.dumps(self.nft_wallet_info.to_json_dict()) wallet_info = await wallet_state_manager.user_store.create_wallet( - "NFT Wallet" if not name else name, + name, uint32(WalletType.NFT.value), info_as_string, in_transaction=in_transaction, @@ -99,12 +104,6 @@ class NFTWallet: await self.wallet_state_manager.add_new_wallet(self, self.wallet_info.id, in_transaction=in_transaction) self.log.debug("Generated a new NFT wallet: %s", self.__dict__) - if not did_id: - # default profile wallet - self.log.debug("Standard NFT wallet created") - self.did_id = None - else: - self.did_id = did_id return self @classmethod @@ -244,7 +243,8 @@ class NFTWallet: if new_coin.puzzle_hash == child_puzzle.get_tree_hash(): child_coin = new_coin break - + else: + raise ValueError(f"Rebuild NFT doesn't match the actual puzzle hash: {child_puzzle}") launcher_coin_states: List[CoinState] = await self.wallet_state_manager.wallet_node.get_coin_state( [singleton_id] ) @@ -412,8 +412,6 @@ class NFTWallet: bundles_to_agg = [tx_record.spend_bundle, launcher_sb] - if not target_puzzle_hash: - target_puzzle_hash = p2_inner_puzzle.get_tree_hash() record: Optional[DerivationRecord] = None # Create inner solution for eve spend if did_id is not None: @@ -514,7 +512,7 @@ class NFTWallet: additional_bundles: List[SpendBundle] = [], ) -> TransactionRecord: # Update NFT status - await self.update_coin_status(nft_coin_info.coin.name(), True) + coin = nft_coin_info.coin amount = coin.amount if not additional_bundles: @@ -578,6 +576,7 @@ class NFTWallet: inner_solution = Program.to([solution_for_conditions(condition_list)]) nft_tx_record = await self._make_nft_transaction(nft_coin_info, inner_solution, [puzzle_hash], fee) await self.standard_wallet.push_transaction(nft_tx_record) + await self.update_coin_status(nft_coin_info.coin.name(), True) self.wallet_state_manager.state_changed("nft_coin_updated", self.wallet_info.id) return nft_tx_record.spend_bundle @@ -608,6 +607,7 @@ class NFTWallet: fee, ) await self.standard_wallet.push_transaction(nft_tx_record) + await self.update_coin_status(nft_coin_info.coin.name(), True) self.wallet_state_manager.state_changed("nft_coin_transferred", self.wallet_info.id) return nft_tx_record.spend_bundle @@ -705,6 +705,7 @@ class NFTWallet: return await cls.create_new_nft_wallet( wallet_state_manager, wallet, + None, name, in_transaction, ) diff --git a/chia/wallet/wallet_state_manager.py b/chia/wallet/wallet_state_manager.py index f014a9fc195bb..7e7068f006673 100644 --- a/chia/wallet/wallet_state_manager.py +++ b/chia/wallet/wallet_state_manager.py @@ -716,19 +716,18 @@ class WalletStateManager: """ wallet_id = None wallet_type = None - self.log.debug("Handling NFT: %s", coin_spend) did_id = uncurried_nft.owner_did - for wallet_info in await self.get_all_wallet_info_entries(): - if wallet_info.type == WalletType.NFT: - nft_wallet_info: NFTWalletInfo = NFTWalletInfo.from_json_dict(json.loads(wallet_info.data)) - if nft_wallet_info.did_id == did_id: - self.log.debug( - "Checking NFT wallet %r and inner puzzle %s", - wallet_info.name, - uncurried_nft.inner_puzzle.get_tree_hash(), - ) - wallet_id = wallet_info.id - wallet_type = WalletType.NFT + self.log.debug("Handling NFT: %s, DID: %s", coin_spend, did_id) + for wallet_info in await self.get_all_wallet_info_entries(wallet_type=WalletType.NFT): + nft_wallet_info: NFTWalletInfo = NFTWalletInfo.from_json_dict(json.loads(wallet_info.data)) + if nft_wallet_info.did_id == did_id: + self.log.debug( + "Checking NFT wallet %r and inner puzzle %s", + wallet_info.name, + uncurried_nft.inner_puzzle.get_tree_hash(), + ) + wallet_id = wallet_info.id + wallet_type = WalletType.NFT if wallet_id is None: self.log.info( @@ -748,7 +747,6 @@ class WalletStateManager: self, coin_states: List[CoinState], peer: WSChiaConnection, fork_height: Optional[uint32] ) -> None: # TODO: add comment about what this method does - # Input states should already be sorted by cs_height, with reorgs at the beginning curr_h = -1 for c_state in coin_states: diff --git a/tests/wallet/nft_wallet/test_nft_wallet.py b/tests/wallet/nft_wallet/test_nft_wallet.py index 57c8b73a0f3ab..744d8c386e776 100644 --- a/tests/wallet/nft_wallet/test_nft_wallet.py +++ b/tests/wallet/nft_wallet/test_nft_wallet.py @@ -597,21 +597,36 @@ async def test_nft_with_did_wallet_creation(two_wallet_nodes: Any, trusted: Any) assert res.get("success") nft_wallet_p2_puzzle = res["wallet_id"] assert nft_wallet_p2_puzzle != nft_wallet_0_id + + res = await api_0.nft_get_by_did({"did_id": hex_did_id}) + assert nft_wallet_0_id == res["wallet_id"] await time_out_assert(10, wallet_0.get_unconfirmed_balance, 5999999999999) await time_out_assert(10, wallet_0.get_confirmed_balance, 5999999999999) # Create a NFT with DID + nft_ph: bytes32 = await wallet_0.get_new_puzzlehash() resp = await api_0.nft_mint_nft( { "wallet_id": nft_wallet_0_id, "hash": "0xD4584AD463139FA8C0D9F68F4B59F185", "uris": ["https://www.chia.net/img/branding/chia-logo.svg"], + "target_address": encode_puzzle_hash(nft_ph, "txch"), } ) assert resp.get("success") sb = resp["spend_bundle"] + # ensure hints are generated correctly + memos = compute_memos(sb) + assert memos + puzhashes = [] + for x in memos.values(): + puzhashes.extend(list(x)) + assert len(puzhashes) > 0 + matched = 0 + for puzhash in puzhashes: + if puzhash.hex() == nft_ph.hex(): + matched += 1 + assert matched > 0 - # ensure hints are generated - assert compute_memos(sb) await time_out_assert_not_none(5, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) for i in range(1, num_blocks):