Create royalty payments in fewer coins (#13858)
* Create royalty payments in fewer coins * flake8 * mypy
This commit is contained in:
parent
72e83181cb
commit
ba6a5a3b63
3 changed files with 100 additions and 34 deletions
|
@ -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])
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue