Skip to content

Commit

Permalink
Add a dynamic launcher that allows the creator to update the metadata…
Browse files Browse the repository at this point in the history
… 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.
  • Loading branch information
greimela committed Mar 27, 2024
1 parent 1b5f9d1 commit b13f731
Show file tree
Hide file tree
Showing 5 changed files with 268 additions and 26 deletions.
103 changes: 103 additions & 0 deletions 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)
)
)
)
@@ -0,0 +1 @@
ff02ffff01ff02ffff03ff822fffffff01ff04ffff04ff38ffff04ff825fffff808080ffff04ffff04ff34ffff04ff0bffff01ff01808080ffff04ffff04ff28ffff04ffff0bffff02ff36ffff04ff02ffff04ff825fffffff04ff0bffff01ff018080808080ffff02ff3effff04ff02ffff04ffff04ffff02ff2effff04ff02ffff04ff05ffff04ffff02ff2effff04ff02ffff04ff17ffff04ffff02ff2effff04ff02ffff04ff81bfffff04ff820bffffff04ffff02ff2effff04ff02ffff04ff82017fffff04ffff0bff3cff8205ff80ffff04ffff0bff3cff8202ff80ffff04ffff02ff3effff04ff02ffff04ffff04ff05ffff04ffff02ff36ffff04ff02ffff04ff825fffffff04ff0bffff01ff018080808080ff0b8080ff80808080ff80808080808080ffff04ffff0bff3cff8080ffff04ffff0bff3cff81bf80ff8080808080808080ffff04ffff0bff3cff5f80ffff04ffff02ffff03ff82bfffffff0182bfffffff012f80ff0180ffff04ffff0bff3cff1780ff8080808080808080ffff04ffff02ff3effff04ff02ffff04ffff04ff05ffff04ffff02ff36ffff04ff02ffff04ff825fffffff04ff0bffff01ff018080808080ff0b8080ff80808080ff808080808080ffff01ff01ff808080ff8080808080ff808080ffff04ffff02ffff03ff82bfffffff01ff04ff10ffff04ff8217ffffff04ff82bfffff80808080ffff01ff04ff32ff808080ff0180ff8080808080ffff01ff04ffff04ff10ffff04ff8217ffffff01ff01808080ff808080ff0180ffff04ffff01ffffff32ff3d46ffff0233ff0401ffffff0101ff0220ffffff02ffff03ff05ffff01ff02ff26ffff04ff02ffff04ff0dffff04ffff0bff2affff0bff3cff2c80ffff0bff2affff0bff2affff0bff3cff2280ff0980ffff0bff2aff0bffff0bff3cff8080808080ff8080808080ffff010b80ff0180ff02ffff03ffff22ffff09ffff0dff0580ff3a80ffff09ffff0dff0b80ff3a80ffff15ff17ffff0181ff8080ffff01ff0bff05ff0bff1780ffff01ff088080ff0180ffff0bff2affff0bff3cff2480ffff0bff2affff0bff2affff0bff3cff2280ff0580ffff0bff2affff02ff26ffff04ff02ffff04ff07ffff04ffff0bff3cff3c80ff8080808080ffff0bff3cff8080808080ff02ffff03ffff07ff0580ffff01ff0bffff0102ffff02ff3effff04ff02ffff04ff09ff80808080ffff02ff3effff04ff02ffff04ff0dff8080808080ffff01ff0bffff0101ff058080ff0180ff018080
54 changes: 37 additions & 17 deletions secure_the_mint/secure_the_mint.py
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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(),
Expand All @@ -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,
Expand All @@ -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(
Expand All @@ -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
Expand All @@ -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)
)
Expand Down Expand Up @@ -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] = {}
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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)

Expand Down
4 changes: 4 additions & 0 deletions 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

0 comments on commit b13f731

Please sign in to comment.