From b13f731896bd20ea768c587cb31a1014cc481fef Mon Sep 17 00:00:00 2001 From: Andreas Greimel Date: Wed, 27 Mar 2024 16:07:27 +0100 Subject: [PATCH] Add a dynamic launcher that allows the creator to update the metadata just before mint. The update has to be signed by the creators private key, to make sure they intend it. This can be used to dynamically mint NFTs while limiting the number of NFTs in a collection and not require a DID spend for every NFT. --- .../secure_the_mint_dynamic_launcher.clsp | 103 ++++++++++++++ .../secure_the_mint_dynamic_launcher.clsp.hex | 1 + secure_the_mint/secure_the_mint.py | 54 ++++--- tests/secure_the_mint/metadata_updated.csv | 4 + ...ure_the_bag.py => test_secure_the_mint.py} | 132 ++++++++++++++++-- 5 files changed, 268 insertions(+), 26 deletions(-) create mode 100644 secure_the_mint/puzzles/secure_the_mint_dynamic_launcher.clsp create mode 100644 secure_the_mint/puzzles/secure_the_mint_dynamic_launcher.clsp.hex create mode 100644 tests/secure_the_mint/metadata_updated.csv rename tests/secure_the_mint/{test_secure_the_bag.py => test_secure_the_mint.py} (76%) diff --git a/secure_the_mint/puzzles/secure_the_mint_dynamic_launcher.clsp b/secure_the_mint/puzzles/secure_the_mint_dynamic_launcher.clsp new file mode 100644 index 0000000..710d9ba --- /dev/null +++ b/secure_the_mint/puzzles/secure_the_mint_dynamic_launcher.clsp @@ -0,0 +1,103 @@ +(mod (SINGLETON_MOD_HASH SINGLETON_LAUNCHER_PUZHASH + NFT_STATE_LAYER_MOD_HASH METADATA_HASH METADATA_UPDATER_PUZZLE_HASH + NFT_OWNERSHIP_LAYER_MOD_HASH + NFT_OWNERSHIP_TRANSFER_PROGRAM_MOD_HASH ROYALTY_ADDRESS TRADE_PRICE_PERCENTAGE + P2_PUZZLE_HASH + CREATOR_PUBLIC_KEY + mode ; 1 for mint, 0 for melt + my_id + updated_metadata_hash) + (include condition_codes.clib) + (include curry-and-treehash.clib) + + (defun-inline nft_ownership_transfer_program_puzzle_hash (NFT_OWNERSHIP_TRANSFER_PROGRAM_MOD_HASH SINGLETON_STRUCT ROYALTY_ADDRESS TRADE_PRICE_PERCENTAGE) + (puzzle-hash-of-curried-function NFT_OWNERSHIP_TRANSFER_PROGRAM_MOD_HASH + (sha256 ONE TRADE_PRICE_PERCENTAGE) + (sha256 ONE ROYALTY_ADDRESS) + (sha256tree SINGLETON_STRUCT) + ) + ) + + (defun-inline nft_ownership_layer_puzzle_hash (NFT_OWNERSHIP_LAYER_MOD_HASH CURRENT_OWNER TRANSFER_PROGRAM_HASH inner_puzzle_hash) + (puzzle-hash-of-curried-function NFT_OWNERSHIP_LAYER_MOD_HASH + inner_puzzle_hash + TRANSFER_PROGRAM_HASH + (sha256 ONE CURRENT_OWNER) + (sha256 ONE NFT_OWNERSHIP_LAYER_MOD_HASH) + ) + ) + + (defun-inline nft_state_layer_puzzle_hash (NFT_STATE_LAYER_MOD_HASH METADATA_HASH METADATA_UPDATER_PUZZLE_HASH inner_puzzle_hash) + (puzzle-hash-of-curried-function NFT_STATE_LAYER_MOD_HASH + inner_puzzle_hash + (sha256 ONE METADATA_UPDATER_PUZZLE_HASH) + METADATA_HASH + (sha256 ONE NFT_STATE_LAYER_MOD_HASH) + ) + ) + + (defun-inline calculate_singleton_puzzle_hash (SINGLETON_STRUCT inner_puzzle_hash) + (puzzle-hash-of-curried-function (f SINGLETON_STRUCT) + inner_puzzle_hash + (sha256tree SINGLETON_STRUCT) + ) + ) + + (defun-inline calculate_full_puzzle_hash + (SINGLETON_STRUCT + NFT_STATE_LAYER_MOD_HASH METADATA_HASH METADATA_UPDATER_PUZZLE_HASH + NFT_OWNERSHIP_LAYER_MOD_HASH + NFT_OWNERSHIP_TRANSFER_PROGRAM_MOD_HASH ROYALTY_ADDRESS TRADE_PRICE_PERCENTAGE + inner_puzzle_hash + ) + (calculate_singleton_puzzle_hash + SINGLETON_STRUCT + (nft_state_layer_puzzle_hash + NFT_STATE_LAYER_MOD_HASH + METADATA_HASH + METADATA_UPDATER_PUZZLE_HASH + (nft_ownership_layer_puzzle_hash + NFT_OWNERSHIP_LAYER_MOD_HASH + () + (nft_ownership_transfer_program_puzzle_hash + NFT_OWNERSHIP_TRANSFER_PROGRAM_MOD_HASH + SINGLETON_STRUCT + ROYALTY_ADDRESS + TRADE_PRICE_PERCENTAGE + ) + inner_puzzle_hash + ) + ) + ) + ) + + + (if mode + (list ; mint + (list ASSERT_MY_COIN_ID my_id) + (list CREATE_COIN SINGLETON_LAUNCHER_PUZHASH 1) + (list ASSERT_COIN_ANNOUNCEMENT + (sha256 + (calculate_coin_id my_id SINGLETON_LAUNCHER_PUZHASH 1) + (sha256tree + (list + (calculate_full_puzzle_hash + (c SINGLETON_MOD_HASH (c (calculate_coin_id my_id SINGLETON_LAUNCHER_PUZHASH 1) SINGLETON_LAUNCHER_PUZHASH)) + NFT_STATE_LAYER_MOD_HASH (if updated_metadata_hash updated_metadata_hash METADATA_HASH) METADATA_UPDATER_PUZZLE_HASH + NFT_OWNERSHIP_LAYER_MOD_HASH + NFT_OWNERSHIP_TRANSFER_PROGRAM_MOD_HASH ROYALTY_ADDRESS TRADE_PRICE_PERCENTAGE + P2_PUZZLE_HASH + ) + 1 + () + ) + ) + ) + ) + (if updated_metadata_hash (list AGG_SIG_ME CREATOR_PUBLIC_KEY updated_metadata_hash) (list REMARK)) + ) + (list ; melt + (list AGG_SIG_ME CREATOR_PUBLIC_KEY 1) + ) + ) +) diff --git a/secure_the_mint/puzzles/secure_the_mint_dynamic_launcher.clsp.hex b/secure_the_mint/puzzles/secure_the_mint_dynamic_launcher.clsp.hex new file mode 100644 index 0000000..45b302c --- /dev/null +++ b/secure_the_mint/puzzles/secure_the_mint_dynamic_launcher.clsp.hex @@ -0,0 +1 @@ +ff02ffff01ff02ffff03ff822fffffff01ff04ffff04ff38ffff04ff825fffff808080ffff04ffff04ff34ffff04ff0bffff01ff01808080ffff04ffff04ff28ffff04ffff0bffff02ff36ffff04ff02ffff04ff825fffffff04ff0bffff01ff018080808080ffff02ff3effff04ff02ffff04ffff04ffff02ff2effff04ff02ffff04ff05ffff04ffff02ff2effff04ff02ffff04ff17ffff04ffff02ff2effff04ff02ffff04ff81bfffff04ff820bffffff04ffff02ff2effff04ff02ffff04ff82017fffff04ffff0bff3cff8205ff80ffff04ffff0bff3cff8202ff80ffff04ffff02ff3effff04ff02ffff04ffff04ff05ffff04ffff02ff36ffff04ff02ffff04ff825fffffff04ff0bffff01ff018080808080ff0b8080ff80808080ff80808080808080ffff04ffff0bff3cff8080ffff04ffff0bff3cff81bf80ff8080808080808080ffff04ffff0bff3cff5f80ffff04ffff02ffff03ff82bfffffff0182bfffffff012f80ff0180ffff04ffff0bff3cff1780ff8080808080808080ffff04ffff02ff3effff04ff02ffff04ffff04ff05ffff04ffff02ff36ffff04ff02ffff04ff825fffffff04ff0bffff01ff018080808080ff0b8080ff80808080ff808080808080ffff01ff01ff808080ff8080808080ff808080ffff04ffff02ffff03ff82bfffffff01ff04ff10ffff04ff8217ffffff04ff82bfffff80808080ffff01ff04ff32ff808080ff0180ff8080808080ffff01ff04ffff04ff10ffff04ff8217ffffff01ff01808080ff808080ff0180ffff04ffff01ffffff32ff3d46ffff0233ff0401ffffff0101ff0220ffffff02ffff03ff05ffff01ff02ff26ffff04ff02ffff04ff0dffff04ffff0bff2affff0bff3cff2c80ffff0bff2affff0bff2affff0bff3cff2280ff0980ffff0bff2aff0bffff0bff3cff8080808080ff8080808080ffff010b80ff0180ff02ffff03ffff22ffff09ffff0dff0580ff3a80ffff09ffff0dff0b80ff3a80ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ffff0bff2affff0bff3cff2480ffff0bff2affff0bff2affff0bff3cff2280ff0580ffff0bff2affff02ff26ffff04ff02ffff04ff07ffff04ffff0bff3cff3c80ff8080808080ffff0bff3cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080 diff --git a/secure_the_mint/secure_the_mint.py b/secure_the_mint/secure_the_mint.py index 9113243..4363e72 100644 --- a/secure_the_mint/secure_the_mint.py +++ b/secure_the_mint/secure_the_mint.py @@ -44,8 +44,8 @@ package_or_requirement="secure_the_mint.puzzles", recompile=True, ) -SECURE_P2_DELEGATE = load_clvm_maybe_recompile( - "secure_the_mint_p2_delegated_puzzle.clsp", +DYNAMIC_PRE_LAUNCHER_MOD = load_clvm_maybe_recompile( + "secure_the_mint_dynamic_launcher.clsp", package_or_requirement="secure_the_mint.puzzles", recompile=True, ) @@ -115,10 +115,15 @@ def __init__( self.royalty_puzzle_hash = royalty_puzzle_hash self.requested_payments = requested_payments - def get_nft_puzzle(self, launcher_coin: Coin, p2_puzzle: Program) -> Program: + def get_nft_puzzle( + self, + launcher_coin: Coin, + p2_puzzle: Program, + updated_metadata: Optional[Program] = None, + ) -> Program: return nft_puzzles.create_full_puzzle( launcher_coin.name(), - self.metadata, + updated_metadata or self.metadata, NFT_METADATA_UPDATER.get_tree_hash(), create_ownership_layer_puzzle( launcher_coin.name(), @@ -129,13 +134,22 @@ def get_nft_puzzle(self, launcher_coin: Coin, p2_puzzle: Program) -> Program: ), ) - def to_coin_spends(self, pre_launcher_parent_id: bytes32) -> List[CoinSpend]: + def to_coin_spends( + self, + pre_launcher_parent_id: bytes32, + updated_metadata: Optional[Program] = None, + ) -> List[CoinSpend]: amount = uint64(1) pre_launcher_coin = Coin( pre_launcher_parent_id, self.pre_launcher_puzzle.get_tree_hash(), amount ) mode = 1 # 1 for mint, 0 for melt - pre_launcher_solution = Program.to([mode, pre_launcher_coin.name()]) + pre_launcher_solution = Program.to( + ( + [mode, pre_launcher_coin.name()] + + ([updated_metadata.get_tree_hash()] if updated_metadata else []) + ) + ) pre_launcher_spend = CoinSpend( pre_launcher_coin, self.pre_launcher_puzzle, @@ -144,7 +158,9 @@ def to_coin_spends(self, pre_launcher_parent_id: bytes32) -> List[CoinSpend]: launcher_coin = Coin( pre_launcher_coin.name(), SINGLETON_LAUNCHER_PUZZLE_HASH, amount ) - eve_puzzle = self.get_nft_puzzle(launcher_coin, self.eve_p2_puzzle) + eve_puzzle = self.get_nft_puzzle( + launcher_coin, self.eve_p2_puzzle, updated_metadata + ) launcher_solution = Program.to([eve_puzzle.get_tree_hash(), amount, []]) launcher_spend = CoinSpend( @@ -170,11 +186,13 @@ def to_coin_spends(self, pre_launcher_parent_id: bytes32) -> List[CoinSpend]: def to_offer( self, pre_launcher_parent_id: bytes32, + updated_metadata: Optional[Program] = None, + creator_signature: Optional[G2Element] = None, ) -> Offer: if self.requested_payments is None: raise Exception("This target does not request a payment") - coin_spends = self.to_coin_spends(pre_launcher_parent_id) + coin_spends = self.to_coin_spends(pre_launcher_parent_id, updated_metadata) launcher_coin = coin_spends[1].coin eve_coin = coin_spends[2].coin @@ -188,7 +206,7 @@ def to_offer( NotarizedPayment(puzzle_hash, amount, memos, eve_coin.name()) ) - bundle = SpendBundle(coin_spends, G2Element()) + bundle = SpendBundle(coin_spends, creator_signature or G2Element()) puzzle_info: Optional[PuzzleInfo] = match_puzzle( uncurry_puzzle(coin_spends[2].puzzle_reveal) ) @@ -296,8 +314,9 @@ def read_secure_the_bag_targets( target_puzzle_hash: bytes32, royalty_puzzle_hash: bytes32, royalty_percentage_times_100: uint16, - melt_public_key: Optional[bytes32] = None, + creator_public_key: Optional[bytes32] = None, requested_mojos: Optional[uint64] = None, + allow_update_on_mint: bool = False, ) -> Tuple[List[Target], Dict[bytes32, MintSpends]]: targets: List[Target] = [] mint_spends: Dict[bytes32, MintSpends] = {} @@ -336,16 +355,17 @@ def read_secure_the_bag_targets( [p.as_condition_args() for p in requested_payments[None]] ) trade_prices = Program.to( - [[p.amount, OFFER_MOD_HASH] for p in requested_payments[None]] - ) - p2_puzzle = OFFER_DELEGATE.curry( - OFFER_MOD_HASH, payments, trade_prices + [[requested_mojos, OFFER_MOD_HASH]] if requested_mojos > 0 else [] ) + p2_puzzle = OFFER_DELEGATE.curry(OFFER_MOD_HASH, payments, trade_prices) else: requested_payments = None p2_puzzle = DIRECT_DELEGATE.curry(target_puzzle_hash) - pre_launcher_puzzle = PRE_LAUNCHER_MOD.curry( + pre_launcher_mod = ( + DYNAMIC_PRE_LAUNCHER_MOD if allow_update_on_mint else PRE_LAUNCHER_MOD + ) + pre_launcher_puzzle = pre_launcher_mod.curry( SINGLETON_MOD_HASH, LAUNCHER_PUZZLE_HASH, NFT_STATE_LAYER_MOD_HASH, @@ -356,7 +376,7 @@ def read_secure_the_bag_targets( royalty_puzzle_hash, royalty_percentage_times_100, p2_puzzle.get_tree_hash(), - melt_public_key + creator_public_key, ) pre_launcher_target = Target(pre_launcher_puzzle.get_tree_hash(), uint64(1)) targets.append(pre_launcher_target) @@ -470,7 +490,7 @@ def cli( target_puzzle_hash, target_puzzle_hash, uint16(5 * 100), - requested_mojos=requested_mojos + requested_mojos=requested_mojos, ) root_puzzle_hash, parent_puzzle_lookup = secure_the_bag(targets, leaf_width) diff --git a/tests/secure_the_mint/metadata_updated.csv b/tests/secure_the_mint/metadata_updated.csv new file mode 100644 index 0000000..0376823 --- /dev/null +++ b/tests/secure_the_mint/metadata_updated.csv @@ -0,0 +1,4 @@ +hash,uris,meta_hash,meta_uris,license_hash,license_uris,edition_number,edition_total +1513fdebd3534ac65adb0c66c95f0ad26daf9179ee42fe6749e3fbbf91129c2a,https://picsum.photos/367/812,19cd52eb2b39627f6a6d1be24bf514f7a25004ab9f5809787846b2d212dd9a30,http://www.ford.com/,873e895e1224a52738c8a63ffc34a1ac134d93feac0fce77a862015ccabb4dfb,http://www.sanchez.net/,1,1 +326c3fa53129a7e09a225261a3f2eb1075afa276027daeb891169821e453b40d,https://dummyimage.com/81x681,6fdcc8e1c0d8ae548d87a2cef119f95dad674e35e8eeaeb142ac50c64d45a75b,https://lynch.biz/,060f7bb447554b03a6888912981088cec1c5b72c9dd6c50d74152a7a9d476abd,http://vincent-williams.com/,1,1 +dd59fa90fa0fac296160f2cac9b1ed8d653e846838676cef39a375ed404f299b,https://dummyimage.com/75x723,64819a3d7a205b8a8e8b986f7d283d1b01caa3b08fe6abd145ee3fee215fbba2,http://cannon-perkins.com/,bd05c1fa2c4f46e28c4ae98135040d019a8eeb6876a0ed45092e03cb045d9cb7,http://www.hicks-saunders.com/,1,1 diff --git a/tests/secure_the_mint/test_secure_the_bag.py b/tests/secure_the_mint/test_secure_the_mint.py similarity index 76% rename from tests/secure_the_mint/test_secure_the_bag.py rename to tests/secure_the_mint/test_secure_the_mint.py index b845f71..22e8c12 100644 --- a/tests/secure_the_mint/test_secure_the_bag.py +++ b/tests/secure_the_mint/test_secure_the_mint.py @@ -1,21 +1,20 @@ from __future__ import annotations from typing import Optional -from clvm_tools.binutils import disassemble import pytest -from blspy import G2Element from chia.types.blockchain_format.program import Program, INFINITE_COST from chia.types.blockchain_format.sized_bytes import bytes32 -from chia.types.spend_bundle import SpendBundle from chia.types.condition_opcodes import ConditionOpcode from chia.util.hash import std_hash from chia.util.ints import uint64, uint16 +from chia.wallet.nft_wallet.uncurry_nft import UncurriedNFT from chia.wallet.puzzles.singleton_top_layer_v1_1 import SINGLETON_LAUNCHER_HASH from chia.wallet.trading.offer import OFFER_MOD_HASH from clvm.casts import int_to_bytes +from clvm_tools.binutils import disassemble -from secure_the_mint.secure_the_bag import ( +from secure_the_mint.secure_the_mint import ( Target, batch_the_bag, parent_of_puzzle_hash, @@ -334,7 +333,7 @@ def test_parent_of_puzzle_hash() -> None: root_coin_name = std_hash( genesis_coin_name + root_puzzle_hash - + int_to_bytes(0) # root coin has amount 0 so it can be created from a DID + + int_to_bytes(0) # root coin has amount 0 so it can be created from a DID ) assert root_coin_name == expected_root_coin_name @@ -417,7 +416,6 @@ def test_read_secure_the_bag_targets(requested_mojos: Optional[int]) -> None: # ).to_json_dict() # ) - assert targets[0].puzzle_hash == pre_launcher_spend.coin.puzzle_hash assert targets[0].amount == pre_launcher_spend.coin.amount @@ -440,7 +438,9 @@ def test_read_secure_the_bag_targets(requested_mojos: Optional[int]) -> None: pre_launcher_conditions.rest().rest().first().first() == ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT ) - announcement_message = Program.to([eve_spend.coin.puzzle_hash, 1, []]).get_tree_hash() + announcement_message = Program.to( + [eve_spend.coin.puzzle_hash, 1, []] + ).get_tree_hash() assert pre_launcher_conditions.rest().rest().first().rest().first() == std_hash( launcher_spend.coin.name() + announcement_message ) @@ -456,9 +456,123 @@ def test_read_secure_the_bag_targets(requested_mojos: Optional[int]) -> None: assert offer.requested_payments[None][0].puzzle_hash == target_puzzle_hash # eve coin asserts offer condition - assert_puzzle_condition = ( - eve_spend_conditions.rest().rest().rest().first() + assert_puzzle_condition = eve_spend_conditions.rest().rest().rest().first() + + assert ( + assert_puzzle_condition.first() + == ConditionOpcode.ASSERT_PUZZLE_ANNOUNCEMENT + ) + + msg: bytes32 = Program.to( + ( + offer.requested_payments[None][0].nonce, + [p.as_condition_args() for p in offer.requested_payments[None]], + ) + ).get_tree_hash() + assert assert_puzzle_condition.rest().first() == std_hash(OFFER_MOD_HASH + msg) + + +def test_dynamic_read_secure_the_bag_targets() -> None: + requested_mojos = uint64(100000) + + target_puzzle_hash = bytes32.fromhex( + "4bc6435b409bcbabe53870dae0f03755f6aabb4594c5915ec983acf12a5d1fba" + ) + melt_public_key = bytes32.fromhex( + "4bc6435b409bcbabe53870dae0f03755f6aabb4594c5915ec983acf12a5d1fba" + ) + targets, mint_spends = read_secure_the_bag_targets( + "./tests/secure_the_mint/metadata.csv", + target_puzzle_hash, + target_puzzle_hash, + uint16(5 * 100), + melt_public_key, + requested_mojos, + allow_update_on_mint=True, + ) + + updated_targets, updated_mint_spends = read_secure_the_bag_targets( + "./tests/secure_the_mint/metadata_updated.csv", + target_puzzle_hash, + target_puzzle_hash, + uint16(5 * 100), + melt_public_key, + requested_mojos, + allow_update_on_mint=True, + ) + + assert len(targets) == 3 + assert len(mint_spends) == 3 + + pre_launcher_parent_id = bytes32.fromhex( + "f3153d27c1d14581971203f10082fa2db2fbc0fd786a9b210e43f227eca499b5" + ) + + mint_spend_0 = mint_spends.get(targets[0].puzzle_hash) + updated_metadata = updated_mint_spends[updated_targets[0].puzzle_hash].metadata + coin_spends_0 = mint_spend_0.to_coin_spends(pre_launcher_parent_id, updated_metadata) + pre_launcher_spend = coin_spends_0[0] + assert pre_launcher_spend.coin.parent_coin_info == pre_launcher_parent_id + assert pre_launcher_spend.coin.amount == 1 + if requested_mojos: + assert bytes32(pre_launcher_spend.coin.puzzle_hash) == bytes32.fromhex( + "b8d65b74b86cb863dc97aed08f65a7bff8666614fd02b08053a6bebc88ff6c79" + ) + else: + assert bytes32(pre_launcher_spend.coin.puzzle_hash) == bytes32.fromhex( + "36d16c1fee484220fb22dc45c1ebed3195ee577dcfdb61dd98f99579146cb4cf" ) + # + # print( + # SpendBundle( + # coin_spends=coin_spends_0, aggregated_signature=G2Element() + # ).to_json_dict() + # ) + + assert targets[0].puzzle_hash == pre_launcher_spend.coin.puzzle_hash + assert targets[0].amount == pre_launcher_spend.coin.amount + + launcher_spend = coin_spends_0[1] + assert launcher_spend.coin.parent_coin_info == pre_launcher_spend.coin.name() + assert launcher_spend.coin.amount == 1 + assert bytes32(launcher_spend.coin.puzzle_hash) == SINGLETON_LAUNCHER_HASH + + eve_spend = coin_spends_0[2] + assert eve_spend.coin.parent_coin_info == launcher_spend.coin.name() + assert eve_spend.coin.amount == 1 + + uncurried_nft = UncurriedNFT.uncurry(*eve_spend.puzzle_reveal.uncurry()) + assert uncurried_nft.metadata == updated_mint_spends[updated_targets[0].puzzle_hash].metadata + + _, pre_launcher_conditions = pre_launcher_spend.puzzle_reveal.run_with_cost( + INFINITE_COST, pre_launcher_spend.solution + ) + assert pre_launcher_conditions.first().first() == ConditionOpcode.ASSERT_MY_COIN_ID + + # pre launcher asserts launcher coin announcement + assert ( + pre_launcher_conditions.rest().rest().first().first() + == ConditionOpcode.ASSERT_COIN_ANNOUNCEMENT + ) + announcement_message = Program.to( + [eve_spend.coin.puzzle_hash, 1, []] + ).get_tree_hash() + assert pre_launcher_conditions.rest().rest().first().rest().first() == std_hash( + launcher_spend.coin.name() + announcement_message + ) + + _, eve_spend_conditions = eve_spend.puzzle_reveal.run_with_cost( + INFINITE_COST, eve_spend.solution + ) + + if requested_mojos: + offer = mint_spend_0.to_offer(pre_launcher_parent_id, updated_metadata) + assert len(offer.requested_payments[None]) == 1 + assert offer.requested_payments[None][0].amount == requested_mojos + assert offer.requested_payments[None][0].puzzle_hash == target_puzzle_hash + + # eve coin asserts offer condition + assert_puzzle_condition = eve_spend_conditions.rest().rest().rest().first() assert ( assert_puzzle_condition.first()