diff --git a/chia/rpc/wallet_rpc_api.py b/chia/rpc/wallet_rpc_api.py index 768ac1bc04..abb648ad4a 100644 --- a/chia/rpc/wallet_rpc_api.py +++ b/chia/rpc/wallet_rpc_api.py @@ -1462,7 +1462,17 @@ class WalletRpcApi: nft_coin_id = bytes32.from_hexstr(nft_coin_id) nft_coin_info = nft_wallet.get_nft_coin_by_id(nft_coin_id) fee = uint64(request.get("fee", 0)) - spend_bundle = await nft_wallet.transfer_nft(nft_coin_info, puzzle_hash, fee=fee) + txs = await nft_wallet.generate_signed_transaction( + [nft_coin_info.coin.amount], + [puzzle_hash], + coins=[nft_coin_info.coin], + fee=fee, + ) + spend_bundle: Optional[SpendBundle] = None + for tx in txs: + if tx.spend_bundle is not None: + spend_bundle = tx.spend_bundle + await self.service.wallet_state_manager.add_pending_transaction(tx) return {"wallet_id": wallet_id, "success": True, "spend_bundle": spend_bundle} except Exception as e: log.exception(f"Failed to transfer NFT: {e}") diff --git a/chia/wallet/nft_wallet/nft_wallet.py b/chia/wallet/nft_wallet/nft_wallet.py index a4bab9085f..7aa3403652 100644 --- a/chia/wallet/nft_wallet/nft_wallet.py +++ b/chia/wallet/nft_wallet/nft_wallet.py @@ -5,7 +5,6 @@ from secrets import token_bytes from typing import Any, Dict, List, Optional, Set, Tuple, Type, TypeVar from blspy import AugSchemeMPL, G1Element, G2Element -from clvm.casts import int_to_bytes from chia.protocols.wallet_protocol import CoinState from chia.server.outbound_message import NodeType @@ -590,37 +589,6 @@ class NFTWallet: self.wallet_state_manager.state_changed("nft_coin_updated", self.wallet_info.id) return nft_tx_record.spend_bundle - async def transfer_nft( - self, - nft_coin_info: NFTCoinInfo, - puzzle_hash: bytes32, - fee: uint64 = uint64(0), - ) -> Optional[SpendBundle]: - self.log.info("Attempt to transfer a new NFT") - coin = nft_coin_info.coin - self.log.debug("Transferring NFT coin %r to puzhash: %s", nft_coin_info.coin, puzzle_hash) - - amount = coin.amount - unft = UncurriedNFT.uncurry(nft_coin_info.full_puzzle) - puzzle_hash_to_sign = unft.p2_puzzle.get_tree_hash() - if unft.supports_did: - self.log.debug("Transferring NFT with ownership layer") - inner_solution = create_ownership_layer_transfer_solution(int_to_bytes(0), int_to_bytes(0), [], puzzle_hash) - else: - condition_list = [make_create_coin_condition(puzzle_hash, amount, [puzzle_hash])] - inner_solution = Program.to([solution_for_conditions(condition_list)]) - self.log.debug("Solution for new coin: %r", disassemble(inner_solution)) - nft_tx_record = await self._make_nft_transaction( - nft_coin_info, - inner_solution, - [puzzle_hash_to_sign], - 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 - def get_current_nfts(self) -> List[NFTCoinInfo]: return self.nft_wallet_info.my_nft_coins @@ -744,6 +712,9 @@ class NFTWallet: coin_announcements_to_consume: Optional[Set[Announcement]] = None, puzzle_announcements_to_consume: Optional[Set[Announcement]] = None, ignore_max_send_amount: bool = False, + new_owner: Optional[bytes32] = None, + new_did_inner_hash: Optional[bytes32] = None, + trade_prices_list: Optional[Program] = None, ) -> List[TransactionRecord]: if memos is None: memos = [[] for _ in range(len(puzzle_hashes))] @@ -820,12 +791,17 @@ class NFTWallet: coins: Set[Coin] = None, coin_announcements_to_consume: Optional[Set[Announcement]] = None, puzzle_announcements_to_consume: Optional[Set[Announcement]] = None, + new_owner: Optional[bytes32] = None, + new_did_inner_hash: Optional[bytes32] = None, + trade_prices_list: Optional[Program] = None, ) -> Tuple[SpendBundle, Optional[TransactionRecord]]: - if coins is None: + if coins is None or len(coins) > 1: # Make sure the user is specifying which specific NFT coin to use - raise ValueError("NFT spends require a selected coin") + raise ValueError("NFT spends require a single selected coin") + elif len(payments) > 1: + raise ValueError("NFTs can only be sent to one party") else: - nft_coins = [c for c in self.nft_wallet_info.my_nft_coins if c.coin in coins] + nft_coin = [c for c in self.nft_wallet_info.my_nft_coins if c.coin in coins][0] if coin_announcements_to_consume is not None: coin_announcements_bytes: Optional[Set[bytes32]] = {a.name() for a in coin_announcements_to_consume} @@ -841,40 +817,32 @@ class NFTWallet: for payment in payments: primaries.append({"puzzlehash": payment.puzzle_hash, "amount": payment.amount, "memos": payment.memos}) - chia_tx = None - coin_spends = [] - first = True - for coin_info in nft_coins: - if first: - first = False - if fee > 0: - chia_tx = await self.create_tandem_xch_tx(fee) - innersol = self.standard_wallet.make_solution( - primaries=primaries, - coin_announcements_to_assert=coin_announcements_bytes, - puzzle_announcements_to_assert=puzzle_announcements_bytes, - ) - else: - innersol = self.standard_wallet.make_solution( - primaries=primaries, - coin_announcements_to_assert=coin_announcements_bytes, - puzzle_announcements_to_assert=puzzle_announcements_bytes, - ) - else: - # What announcements do we need? - innersol = self.standard_wallet.make_solution( - primaries=[], - ) + if fee > 0: + announcement_to_make = nft_coin.coin.name() + chia_tx = await self.create_tandem_xch_tx(fee, Announcement(nft_coin.coin.name(), announcement_to_make)) + else: + announcement_to_make = None + chia_tx = None - nft_layer_solution = Program.to([innersol, coin_info.coin.amount]) - assert isinstance(coin_info.lineage_proof, LineageProof) - singleton_solution = Program.to( - [coin_info.lineage_proof.to_program(), coin_info.coin.amount, nft_layer_solution] - ) - coin_spend = CoinSpend(coin_info.coin, coin_info.full_puzzle, singleton_solution) - coin_spends.append(coin_spend) + innersol: Program = self.standard_wallet.make_solution( + primaries=primaries, + coin_announcements=None if announcement_to_make is None else set((announcement_to_make,)), + coin_announcements_to_assert=coin_announcements_bytes, + puzzle_announcements_to_assert=puzzle_announcements_bytes, + ) - nft_spend_bundle = SpendBundle(coin_spends, G2Element()) + if UncurriedNFT.uncurry(nft_coin.full_puzzle).supports_did: + magic_condition = Program.to([-10, new_owner, trade_prices_list, new_did_inner_hash]) + # TODO: This line is a hack, make_solution should allow us to pass extra conditions to it + w_added_magic_condition = Program.to([[], (1, magic_condition.cons(innersol.at("rfr"))), []]) + innersol = Program.to([w_added_magic_condition]) + + nft_layer_solution = Program.to([innersol]) + assert isinstance(nft_coin.lineage_proof, LineageProof) + singleton_solution = Program.to([nft_coin.lineage_proof.to_program(), nft_coin.coin.amount, nft_layer_solution]) + coin_spend = CoinSpend(nft_coin.coin, nft_coin.full_puzzle, singleton_solution) + + nft_spend_bundle = SpendBundle([coin_spend], G2Element()) chia_spend_bundle = SpendBundle([], G2Element()) if chia_tx is not None and chia_tx.spend_bundle is not None: chia_spend_bundle = chia_tx.spend_bundle diff --git a/tests/wallet/nft_wallet/test_nft_wallet.py b/tests/wallet/nft_wallet/test_nft_wallet.py index 065b139fb2..ca2099eb09 100644 --- a/tests/wallet/nft_wallet/test_nft_wallet.py +++ b/tests/wallet/nft_wallet/test_nft_wallet.py @@ -102,9 +102,13 @@ async def test_nft_wallet_creation_automatically(two_wallet_nodes: Any, trusted: coins = nft_wallet_0.nft_wallet_info.my_nft_coins assert len(coins) == 1, "nft not generated" - sb = await nft_wallet_0.transfer_nft(coins[0], ph1) - assert sb is not None - await time_out_assert_not_none(15, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + txs = await nft_wallet_0.generate_signed_transaction([coins[0].coin.amount], [ph1], coins=set([coins[0].coin])) + assert len(txs) == 1 + assert txs[0].spend_bundle is not None + await wallet_node_0.wallet_state_manager.add_pending_transaction(txs[0]) + await time_out_assert_not_none( + 15, full_node_api.full_node.mempool_manager.get_spendbundle, txs[0].spend_bundle.name() + ) for i in range(1, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph1)) await time_out_assert(15, len, 2, wallet_node_1.wallet_state_manager.wallets) @@ -215,13 +219,14 @@ async def test_nft_wallet_creation_and_transfer(two_wallet_nodes: Any, trusted: nft_wallet_1 = await NFTWallet.create_new_nft_wallet( wallet_node_1.wallet_state_manager, wallet_1, name="NFT WALLET 2" ) - sb = await nft_wallet_0.transfer_nft(coins[1], ph1) - - assert sb is not None - # ensure hints are generated - assert compute_memos(sb) - - await time_out_assert_not_none(15, full_node_api.full_node.mempool_manager.get_spendbundle, sb.name()) + txs = await nft_wallet_0.generate_signed_transaction([coins[1].coin.amount], [ph1], coins=set([coins[1].coin])) + assert len(txs) == 1 + assert txs[0].spend_bundle is not None + await wallet_node_0.wallet_state_manager.add_pending_transaction(txs[0]) + await time_out_assert_not_none( + 15, full_node_api.full_node.mempool_manager.get_spendbundle, txs[0].spend_bundle.name() + ) + assert compute_memos(txs[0].spend_bundle) for i in range(1, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph1)) @@ -232,12 +237,14 @@ async def test_nft_wallet_creation_and_transfer(two_wallet_nodes: Any, trusted: await time_out_assert(15, wallet_1.get_pending_change_balance, 0) # Send it back to original owner - nsb = await nft_wallet_1.transfer_nft(coins[0], ph) - assert nsb is not None - - await time_out_assert_not_none(15, full_node_api.full_node.mempool_manager.get_spendbundle, nsb.name()) - # ensure hints are generated - assert compute_memos(nsb) + txs = await nft_wallet_1.generate_signed_transaction([coins[0].coin.amount], [ph], coins=set([coins[0].coin])) + assert len(txs) == 1 + assert txs[0].spend_bundle is not None + await wallet_node_1.wallet_state_manager.add_pending_transaction(txs[0]) + await time_out_assert_not_none( + 15, full_node_api.full_node.mempool_manager.get_spendbundle, txs[0].spend_bundle.name() + ) + assert compute_memos(txs[0].spend_bundle) for i in range(1, num_blocks): await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph1)) @@ -539,8 +546,6 @@ async def test_nft_wallet_rpc_update_metadata(two_wallet_nodes: Any, trusted: An ) @pytest.mark.asyncio async def test_nft_with_did_wallet_creation(two_wallet_nodes: Any, trusted: Any) -> None: - from chia.wallet.did_wallet.did_info import DID_HRP - num_blocks = 3 full_nodes, wallets = two_wallet_nodes full_node_api: FullNodeSimulator = full_nodes[0]