Create royalty payments in fewer coins (#13858)

* Create royalty payments in fewer coins

* flake8

* mypy
This commit is contained in:
Matt Hauff 2022-11-10 02:04:52 -07:00 committed by GitHub
parent 72e83181cb
commit ba6a5a3b63
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 100 additions and 34 deletions

View file

@ -898,8 +898,13 @@ class NFTWallet:
abs(amount),
Offer.ph(),
primaries=[
AmountWithPuzzlehash({"amount": p.amount, "puzzlehash": Offer.ph(), "memos": []})
for _, p in payments
AmountWithPuzzlehash(
{
"amount": uint64(sum(p.amount for _, p in payments)),
"puzzlehash": Offer.ph(),
"memos": [],
}
)
],
fee=fee,
coins=offered_coins_by_asset[asset],
@ -918,8 +923,8 @@ class NFTWallet:
else:
payments = royalty_payments[asset]
txs = await wallet.generate_signed_transaction(
[abs(amount), *(p.amount for _, p in payments)],
[Offer.ph()] * (len(payments) + 1),
[abs(amount), sum(p.amount for _, p in payments)],
[Offer.ph(), Offer.ph()],
fee=fee_left_to_pay,
coins=offered_coins_by_asset[asset],
puzzle_announcements_to_consume=announcements_to_assert,
@ -929,29 +934,65 @@ class NFTWallet:
# Then, adding in the spends for the royalty offer mod
if asset in fungible_asset_dict:
coin_spends: List[CoinSpend] = []
for launcher_id, payment in payments:
# Create a coin_spend for the royalty payout from OFFER MOD
# ((nft_launcher_id . ((ROYALTY_ADDRESS, royalty_amount, memos))))
inner_royalty_sol = Program.to([(launcher_id, [payment.as_condition_args()])])
# Create a coin_spend for the royalty payout from OFFER MOD
# We cannot create coins with the same puzzle hash and amount
# So if there's multiple NFTs with the same royalty puzhash/percentage, we must create multiple
# generations of offer coins
royalty_coin: Optional[Coin] = None
parent_spend: Optional[CoinSpend] = None
while True:
duplicate_payments: List[Tuple[bytes32, Payment]] = []
deduped_payment_list: List[Tuple[bytes32, Payment]] = []
for launcher_id, payment in payments:
if payment in [p for _, p in deduped_payment_list]:
duplicate_payments.append((launcher_id, payment))
else:
deduped_payment_list.append((launcher_id, payment))
# ((nft_launcher_id . ((ROYALTY_ADDRESS, royalty_amount, memos) ...)))
inner_royalty_sol = Program.to(
[
(launcher_id, [payment.as_condition_args()])
for launcher_id, payment in deduped_payment_list
]
)
if duplicate_payments != []:
inner_royalty_sol = Program.to(
(
None,
[
Payment(
Offer.ph(), uint64(sum(p.amount for _, p in duplicate_payments)), []
).as_condition_args()
],
)
).cons(inner_royalty_sol)
if asset is None: # xch offer
offer_puzzle = OFFER_MOD
royalty_ph = OFFER_MOD_HASH
else:
offer_puzzle = construct_puzzle(driver_dict[asset], OFFER_MOD)
royalty_ph = offer_puzzle.get_tree_hash()
royalty_coin: Coin
for tx in txs:
if tx.spend_bundle is not None:
for coin in tx.spend_bundle.additions():
if coin.amount == payment.amount and coin.puzzle_hash == royalty_ph:
royalty_coin = coin
parent_spend = next(
cs
for cs in tx.spend_bundle.coin_spends
if cs.coin.name() == royalty_coin.parent_coin_info
)
break
if royalty_coin is None:
for tx in txs:
if tx.spend_bundle is not None:
for coin in tx.spend_bundle.additions():
royalty_payment_amount: int = sum(p.amount for _, p in payments)
if coin.amount == royalty_payment_amount and coin.puzzle_hash == royalty_ph:
royalty_coin = coin
parent_spend = next(
cs
for cs in tx.spend_bundle.coin_spends
if cs.coin.name() == royalty_coin.parent_coin_info
)
break
else:
continue
break
assert royalty_coin is not None
assert parent_spend is not None
if asset is None: # If XCH
royalty_sol = inner_royalty_sol
else:
@ -974,8 +1015,17 @@ class NFTWallet:
}
)
royalty_sol = solve_puzzle(driver_dict[asset], solver, OFFER_MOD, inner_royalty_sol)
coin_spends.append(CoinSpend(royalty_coin, offer_puzzle, royalty_sol))
additional_bundles.append(SpendBundle(coin_spends, G2Element()))
new_coin_spend = CoinSpend(royalty_coin, offer_puzzle, royalty_sol)
additional_bundles.append(SpendBundle([new_coin_spend], G2Element()))
if duplicate_payments != []:
payments = duplicate_payments
royalty_coin = next(c for c in new_coin_spend.additions() if c.puzzle_hash == royalty_ph)
parent_spend = new_coin_spend
continue
else:
break
# Finally, assemble the tx records properly
txs_bundle = SpendBundle.aggregate([tx.spend_bundle for tx in all_transactions if tx.spend_bundle is not None])

View file

@ -156,7 +156,7 @@ class Offer:
parent_puzzle: UncurriedPuzzle = uncurry_puzzle(parent_spend.puzzle_reveal.to_program())
parent_solution: Program = parent_spend.solution.to_program()
additions: List[Coin] = [a for a in parent_spend.additions() if a not in self.bundle.removals()]
additions: List[Coin] = parent_spend.additions()
puzzle_driver = match_puzzle(parent_puzzle)
if puzzle_driver is not None:
@ -164,17 +164,27 @@ class Offer:
inner_puzzle: Optional[Program] = get_inner_puzzle(puzzle_driver, parent_puzzle)
inner_solution: Optional[Program] = get_inner_solution(puzzle_driver, parent_solution)
assert inner_puzzle is not None and inner_solution is not None
# We're going to look at the conditions created by the inner puzzle
conditions: Program = inner_puzzle.run(inner_solution)
matching_spend_additions: List[Coin] = [] # coins that match offered amount and are sent to offer ph.
expected_num_matches: int = 0
offered_amounts: List[int] = []
for condition in conditions.as_iter():
if condition.first() == 51 and condition.rest().first() in [OFFER_MOD_HASH, OFFER_MOD_OLD_HASH]:
matching_spend_additions.extend(
[a for a in additions if a.amount == condition.rest().rest().first().as_int()]
)
if len(matching_spend_additions) == 1:
coins_for_this_spend.append(matching_spend_additions[0])
expected_num_matches += 1
offered_amounts.append(condition.rest().rest().first().as_int())
# Start by filtering additions that match the amount
matching_spend_additions = [a for a in additions if a.amount in offered_amounts]
if len(matching_spend_additions) == expected_num_matches:
coins_for_this_spend.extend(matching_spend_additions)
# We didn't quite get there so now lets narrow it down by puzzle hash
else:
additions_w_amount_and_puzhash: List[Coin] = [
# If we narrowed down too much, we can't trust the amounts so start over with all additions
if len(matching_spend_additions) < expected_num_matches:
matching_spend_additions = additions
matching_spend_additions = [
a
for a in matching_spend_additions
if a.puzzle_hash
@ -187,14 +197,20 @@ class Offer:
),
]
]
if len(additions_w_amount_and_puzhash) == 1:
coins_for_this_spend.append(additions_w_amount_and_puzhash[0])
if len(matching_spend_additions) == expected_num_matches:
coins_for_this_spend.extend(matching_spend_additions)
else:
raise ValueError("Could not properly guess offered coins from parent spend")
else:
# It's much easier if the asset is bare XCH
asset_id = None
coins_for_this_spend.extend(
[a for a in additions if a.puzzle_hash in [OFFER_MOD_HASH, OFFER_MOD_OLD_HASH]]
)
# We only care about unspent coins
coins_for_this_spend = [c for c in coins_for_this_spend if c not in self.bundle.removals()]
if coins_for_this_spend != []:
offered_coins.setdefault(asset_id, [])
offered_coins[asset_id].extend(coins_for_this_spend)

View file

@ -1313,7 +1313,7 @@ async def test_complex_nft_offer(two_wallet_nodes: Any, trusted: Any) -> None:
royalty_puzhash_taker = ph_taker
royalty_basis_pts_maker = uint16(200)
royalty_basis_pts_taker_1 = uint16(500)
royalty_basis_pts_taker_2 = uint16(100)
royalty_basis_pts_taker_2 = uint16(500)
nft_wallet_maker = await NFTWallet.create_new_nft_wallet(
wallet_node_maker.wallet_state_manager, wallet_maker, name="NFT WALLET DID 1", did_id=did_id_maker