Skip to content

Commit

Permalink
nft royalty edge cases (#14789)
Browse files Browse the repository at this point in the history
* nft royalty edge cases

* making tests faster

* Update tests/wallet/nft_wallet/test_nft_1_offers.py

Co-authored-by: Matt Hauff <[email protected]>

* flake

---------

Co-authored-by: Matt Hauff <[email protected]>
  • Loading branch information
trepca and Quexington authored Mar 15, 2023
1 parent 766c33c commit a6f4235
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 24 deletions.
18 changes: 15 additions & 3 deletions chia/wallet/nft_wallet/nft_wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,11 @@ async def generate_new_nft(
# For a DID enabled NFT wallet it cannot mint NFT0. Mint NFT1 instead.
did_id = self.did_id
amount = uint64(1)
# ensure percentage is uint16
try:
percentage = uint16(percentage)
except ValueError:
raise ValueError("Percentage must be lower than 655%")
coins = await self.standard_wallet.select_coins(uint64(amount + fee))
if coins is None:
return None
Expand Down Expand Up @@ -663,7 +668,6 @@ async def generate_signed_transaction(
payments.append(Payment(puzhash, amount, memos_with_hint))

payment_sum = sum([p.amount for p in payments])

unsigned_spend_bundle, chia_tx = await self.generate_unsigned_spendbundle(
payments,
fee,
Expand Down Expand Up @@ -869,16 +873,23 @@ async def make_nft1_offer(
for asset, amount in royalty_nft_asset_dict.items(): # royalty enabled NFTs
transfer_info = driver_dict[asset].also().also() # type: ignore
assert isinstance(transfer_info, PuzzleInfo)
royalty_percentage_raw = transfer_info["transfer_program"]["royalty_percentage"]
assert royalty_percentage_raw is not None
# clvm encodes large ints as bytes
if isinstance(royalty_percentage_raw, bytes):
royalty_percentage = int_from_bytes(royalty_percentage_raw)
else:
royalty_percentage = int(royalty_percentage_raw)
if amount > 0:
required_royalty_info.append(
(
asset,
bytes32(transfer_info["transfer_program"]["royalty_address"]),
uint16(transfer_info["transfer_program"]["royalty_percentage"]),
uint16(royalty_percentage),
)
)
else:
offered_royalty_percentages[asset] = uint16(transfer_info["transfer_program"]["royalty_percentage"])
offered_royalty_percentages[asset] = uint16(royalty_percentage)

royalty_payments: Dict[Optional[bytes32], List[Tuple[bytes32, Payment]]] = {}
for asset, amount in fungible_asset_dict.items(): # offered fungible items
Expand Down Expand Up @@ -950,6 +961,7 @@ async def make_nft1_offer(
additional_bundles: List[SpendBundle] = []
# standard pays the fee if possible
fee_left_to_pay: uint64 = uint64(0) if None in offer_dict and offer_dict[None] < 0 else fee

for asset, amount in offer_dict.items():
if amount < 0:
if asset is None:
Expand Down
4 changes: 1 addition & 3 deletions chia/wallet/trade_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import dataclasses
import logging
import time
import traceback
from typing import Any, Dict, List, Optional, Set, Tuple, Union

from typing_extensions import Literal
Expand Down Expand Up @@ -597,8 +596,7 @@ async def _create_offer_for_ids(
return True, offer, None

except Exception as e:
tb = traceback.format_exc()
self.log.error(f"Error with creating trade offer: {type(e)}{tb}")
self.log.exception("Error creating trade offer")
return False, None, str(e)

async def maybe_create_wallets_for_offer(self, offer: Offer) -> None:
Expand Down
81 changes: 63 additions & 18 deletions tests/wallet/nft_wallet/test_nft_1_offers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1272,7 +1272,14 @@ async def get_trade_and_status(trade_manager: Any, trade: Any) -> TradeStatus:
)
@pytest.mark.parametrize(
"forwards_compat,royalty_pts",
[(True, (200, 500, 500)), (False, (200, 500, 500)), (False, (0, 0, 0))],
[
(True, (200, 500, 500)),
(False, (200, 500, 500)),
(False, (0, 0, 0)), # test that we can have 0 royalty
(False, (65000, 65534, 65535)), # test that we can reach max royalty
(False, (10000, 10001, 10005)), # tests 100% royalty is not allowed
(False, (100000, 10001, 10005)), # 1000% shouldn't work
],
)
@pytest.mark.asyncio
# @pytest.mark.skip
Expand Down Expand Up @@ -1316,12 +1323,30 @@ async def test_complex_nft_offer(
for i in range(0, 2):
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_maker))
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_taker))
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_taker))
if royalty_pts[0] > 60000:
blocks_needed = 9
else:
blocks_needed = 3
if not forwards_compat:
for i in range(blocks_needed):
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_taker))
else:
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_taker))
await full_node_api.farm_new_transaction_block(FarmNewBlockProtocol(ph_token))
await full_node_api.wait_for_wallets_synced(wallet_nodes=[wallet_node_maker, wallet_node_taker], timeout=30)

funds_maker = sum([calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) for i in range(1, 3)])
funds_taker = sum([calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) for i in range(1, 4)])
if forwards_compat:
funds_taker = sum(
[calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i)) for i in range(1, 4)]
)
else:
funds_taker = sum(
[
calculate_pool_reward(uint32(i)) + calculate_base_farmer_reward(uint32(i))
for i in range(1, 3 + blocks_needed)
]
)

await time_out_assert(30, wallet_maker.get_unconfirmed_balance, funds_maker)
await time_out_assert(30, wallet_maker.get_confirmed_balance, funds_maker)
Expand Down Expand Up @@ -1391,7 +1416,7 @@ async def test_complex_nft_offer(
royalty_puzhash_maker = ph_maker
royalty_puzhash_taker = ph_taker
royalty_basis_pts_maker, royalty_basis_pts_taker_1, royalty_basis_pts_taker_2 = (
uint16(royalty_pts[0]),
royalty_pts[0],
uint16(royalty_pts[1]),
uint16(royalty_pts[2]),
)
Expand All @@ -1408,14 +1433,25 @@ async def test_complex_nft_offer(
("h", "0xD4584AD463139FA8C0D9F68F4B59F185"),
]
)
if royalty_basis_pts_maker > 65535:
with pytest.raises(ValueError):
await nft_wallet_maker.generate_new_nft(
metadata,
target_puzhash_maker,
royalty_puzhash_maker,
royalty_basis_pts_maker, # type: ignore
did_id_maker,
)
return
else:
sb_maker = await nft_wallet_maker.generate_new_nft(
metadata,
target_puzhash_maker,
royalty_puzhash_maker,
uint16(royalty_basis_pts_maker),
did_id_maker,
)

sb_maker = await nft_wallet_maker.generate_new_nft(
metadata,
target_puzhash_maker,
royalty_puzhash_maker,
royalty_basis_pts_maker,
did_id_maker,
)
sb_taker_1 = await nft_wallet_taker.generate_new_nft(
metadata,
target_puzhash_taker,
Expand Down Expand Up @@ -1508,20 +1544,29 @@ async def test_complex_nft_offer(
assert error is None
assert success
assert trade_make is not None

trade_take, tx_records = await trade_manager_taker.respond_to_offer(
old_maker_offer if forwards_compat else Offer.from_bytes(trade_make.offer),
wallet_node_taker.get_full_node_peer(),
fee=FEE,
)
if royalty_basis_pts_maker == 10000:
with pytest.raises(ValueError):
trade_take, tx_records = await trade_manager_taker.respond_to_offer(
old_maker_offer if forwards_compat else Offer.from_bytes(trade_make.offer),
wallet_node_taker.get_full_node_peer(),
fee=FEE,
)
# all done for this test
return
else:
trade_take, tx_records = await trade_manager_taker.respond_to_offer(
old_maker_offer if forwards_compat else Offer.from_bytes(trade_make.offer),
wallet_node_taker.get_full_node_peer(),
fee=FEE,
)
assert trade_take is not None
assert tx_records is not None
await full_node_api.process_transaction_records(records=tx_records)

# Now let's make sure the final wallet state is correct
maker_royalty_summary = NFTWallet.royalty_calculation(
{
nft_to_offer_asset_id_maker: (royalty_puzhash_maker, royalty_basis_pts_maker),
nft_to_offer_asset_id_maker: (royalty_puzhash_maker, uint16(royalty_basis_pts_maker)),
},
{
None: uint64(XCH_REQUESTED),
Expand Down

0 comments on commit a6f4235

Please sign in to comment.