mirror of
https://github.com/Chia-Network/chia-blockchain.git
synced 2026-09-05 02:24:21 -05:00
Merge pull request #11899 from Chia-Network/mh.nft_tranferring_rework
NFT transferring rework
This commit is contained in:
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user