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), abs(amount),
Offer.ph(), Offer.ph(),
primaries=[ primaries=[
AmountWithPuzzlehash({"amount": p.amount, "puzzlehash": Offer.ph(), "memos": []}) AmountWithPuzzlehash(
for _, p in payments {
"amount": uint64(sum(p.amount for _, p in payments)),
"puzzlehash": Offer.ph(),
"memos": [],
}
)
], ],
fee=fee, fee=fee,
coins=offered_coins_by_asset[asset], coins=offered_coins_by_asset[asset],
@ -918,8 +923,8 @@ class NFTWallet:
else: else:
payments = royalty_payments[asset] payments = royalty_payments[asset]
txs = await wallet.generate_signed_transaction( txs = await wallet.generate_signed_transaction(
[abs(amount), *(p.amount for _, p in payments)], [abs(amount), sum(p.amount for _, p in payments)],
[Offer.ph()] * (len(payments) + 1), [Offer.ph(), Offer.ph()],
fee=fee_left_to_pay, fee=fee_left_to_pay,
coins=offered_coins_by_asset[asset], coins=offered_coins_by_asset[asset],
puzzle_announcements_to_consume=announcements_to_assert, puzzle_announcements_to_consume=announcements_to_assert,
@ -929,29 +934,65 @@ class NFTWallet:
# Then, adding in the spends for the royalty offer mod # Then, adding in the spends for the royalty offer mod
if asset in fungible_asset_dict: if asset in fungible_asset_dict:
coin_spends: List[CoinSpend] = [] # Create a coin_spend for the royalty payout from OFFER MOD
for launcher_id, payment in payments:
# Create a coin_spend for the royalty payout from OFFER MOD # We cannot create coins with the same puzzle hash and amount
# ((nft_launcher_id . ((ROYALTY_ADDRESS, royalty_amount, memos)))) # So if there's multiple NFTs with the same royalty puzhash/percentage, we must create multiple
inner_royalty_sol = Program.to([(launcher_id, [payment.as_condition_args()])]) # 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 if asset is None: # xch offer
offer_puzzle = OFFER_MOD offer_puzzle = OFFER_MOD
royalty_ph = OFFER_MOD_HASH royalty_ph = OFFER_MOD_HASH
else: else:
offer_puzzle = construct_puzzle(driver_dict[asset], OFFER_MOD) offer_puzzle = construct_puzzle(driver_dict[asset], OFFER_MOD)
royalty_ph = offer_puzzle.get_tree_hash() royalty_ph = offer_puzzle.get_tree_hash()
royalty_coin: Coin if royalty_coin is None:
for tx in txs: for tx in txs:
if tx.spend_bundle is not None: if tx.spend_bundle is not None:
for coin in tx.spend_bundle.additions(): for coin in tx.spend_bundle.additions():
if coin.amount == payment.amount and coin.puzzle_hash == royalty_ph: royalty_payment_amount: int = sum(p.amount for _, p in payments)
royalty_coin = coin if coin.amount == royalty_payment_amount and coin.puzzle_hash == royalty_ph:
parent_spend = next( royalty_coin = coin
cs parent_spend = next(
for cs in tx.spend_bundle.coin_spends cs
if cs.coin.name() == royalty_coin.parent_coin_info for cs in tx.spend_bundle.coin_spends
) if cs.coin.name() == royalty_coin.parent_coin_info
break )
break
else:
continue
break
assert royalty_coin is not None
assert parent_spend is not None
if asset is None: # If XCH if asset is None: # If XCH
royalty_sol = inner_royalty_sol royalty_sol = inner_royalty_sol
else: else:
@ -974,8 +1015,17 @@ class NFTWallet:
} }
) )
royalty_sol = solve_puzzle(driver_dict[asset], solver, OFFER_MOD, inner_royalty_sol) 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 # 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]) 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_puzzle: UncurriedPuzzle = uncurry_puzzle(parent_spend.puzzle_reveal.to_program())
parent_solution: Program = parent_spend.solution.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) puzzle_driver = match_puzzle(parent_puzzle)
if puzzle_driver is not None: if puzzle_driver is not None:
@ -164,17 +164,27 @@ class Offer:
inner_puzzle: Optional[Program] = get_inner_puzzle(puzzle_driver, parent_puzzle) inner_puzzle: Optional[Program] = get_inner_puzzle(puzzle_driver, parent_puzzle)
inner_solution: Optional[Program] = get_inner_solution(puzzle_driver, parent_solution) inner_solution: Optional[Program] = get_inner_solution(puzzle_driver, parent_solution)
assert inner_puzzle is not None and inner_solution is not None 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) 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(): for condition in conditions.as_iter():
if condition.first() == 51 and condition.rest().first() in [OFFER_MOD_HASH, OFFER_MOD_OLD_HASH]: if condition.first() == 51 and condition.rest().first() in [OFFER_MOD_HASH, OFFER_MOD_OLD_HASH]:
matching_spend_additions.extend( expected_num_matches += 1
[a for a in additions if a.amount == condition.rest().rest().first().as_int()] offered_amounts.append(condition.rest().rest().first().as_int())
)
if len(matching_spend_additions) == 1: # Start by filtering additions that match the amount
coins_for_this_spend.append(matching_spend_additions[0]) 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: 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 a
for a in matching_spend_additions for a in matching_spend_additions
if a.puzzle_hash if a.puzzle_hash
@ -187,14 +197,20 @@ class Offer:
), ),
] ]
] ]
if len(additions_w_amount_and_puzhash) == 1: if len(matching_spend_additions) == expected_num_matches:
coins_for_this_spend.append(additions_w_amount_and_puzhash[0]) coins_for_this_spend.extend(matching_spend_additions)
else:
raise ValueError("Could not properly guess offered coins from parent spend")
else: else:
# It's much easier if the asset is bare XCH
asset_id = None asset_id = None
coins_for_this_spend.extend( coins_for_this_spend.extend(
[a for a in additions if a.puzzle_hash in [OFFER_MOD_HASH, OFFER_MOD_OLD_HASH]] [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 != []: if coins_for_this_spend != []:
offered_coins.setdefault(asset_id, []) offered_coins.setdefault(asset_id, [])
offered_coins[asset_id].extend(coins_for_this_spend) 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_puzhash_taker = ph_taker
royalty_basis_pts_maker = uint16(200) royalty_basis_pts_maker = uint16(200)
royalty_basis_pts_taker_1 = uint16(500) 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( 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 wallet_node_maker.wallet_state_manager, wallet_maker, name="NFT WALLET DID 1", did_id=did_id_maker