Merge pull request #11899 from Chia-Network/mh.nft_tranferring_rework

NFT transferring rework
This commit is contained in:
William Allen
2022-06-14 15:51:48 -05:00
committed by GitHub
3 changed files with 69 additions and 86 deletions
+11 -1
View File
@@ -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}")
+35 -67
View File
@@ -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
+23 -18
View File
@@ -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]