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),
|
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])
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue