Monorepo for Aesthetic.Computer
aesthetic.computer
1"""
2KidLisp Keeps FA2 v6 - Aesthetic Computer NFT Contract (FINAL PRODUCTION)
3
4This contract is the v6 production release with revenue enabled.
5
6v6 CHANGES from v5:
7- Royalties immutable after keep (edit_metadata preserves royalties)
8
9v5 features (preserved):
10- Default fee: 2.5 XTZ (revenue enabled by default)
11- Immutable content_hash: edit_metadata preserves content_hash (prevents orphaned bigmap entries)
12- Improved error messages with context
13
14v4 features (preserved):
15- 10% Royalty Support: Automatic royalties on secondary sales (objkt.com compatible)
16- Emergency Pause: Admin can halt minting in emergencies
17- Admin Transfer: Customer service tool for edge cases
18
19v3 features (preserved):
20- edit_metadata: Token owner can edit
21- token_creators: Tracks original creator for each token
22- Pre-encoded metadata bytes (TzKT/objkt compatible)
23- Fee system with withdraw capability
24- Burn and re-mint functionality
25
26Key feature: Metadata bytes stored directly WITHOUT sp.pack(),
27ensuring compatibility with TzKT, objkt, and other Tezos indexers.
28"""
29
30import smartpy as sp
31from smartpy.templates import fa2_lib as fa2
32
33main = fa2.main
34
35
36@sp.module
37def keeps_module():
38 import main
39
40 # Order of inheritance: [Admin], [<policy>], <base class>, [<other mixins>].
41 class KidLispKeepsFA2v6(
42 main.Admin,
43 main.Nft,
44 main.OnchainviewBalanceOf,
45 ):
46 """
47 FA2 NFT contract for KidLisp Keeps (v6 - FINAL PRODUCTION).
48
49 v6 changes from v5:
50 - Royalties immutable during metadata edits
51
52 v5 features: default fee 2.5 XTZ, immutable content_hash
53 v4 features: royalties, emergency pause, admin transfer
54 v3 features: attribution tracking + creator bigmap support
55 """
56
57 def __init__(self, admin_address, contract_metadata, ledger, token_metadata):
58 # Initialize on-chain balance view
59 main.OnchainviewBalanceOf.__init__(self)
60
61 # Initialize the NFT base class
62 main.Nft.__init__(self, contract_metadata, ledger, token_metadata)
63
64 # Initialize administrative permissions
65 main.Admin.__init__(self, admin_address)
66
67 # Additional storage for metadata locking
68 self.data.metadata_locked = sp.cast(
69 sp.big_map(),
70 sp.big_map[sp.nat, sp.bool]
71 )
72
73 # Track content hashes to prevent duplicate mints
74 # Maps content_hash (bytes) -> token_id (nat)
75 self.data.content_hashes = sp.cast(
76 sp.big_map(),
77 sp.big_map[sp.bytes, sp.nat]
78 )
79
80 # Track original creator for each token (v3)
81 # Maps token_id -> creator address (the first minter)
82 self.data.token_creators = sp.cast(
83 sp.big_map(),
84 sp.big_map[sp.nat, sp.address]
85 )
86
87 # Contract-level metadata lock flag
88 self.data.contract_metadata_locked = False
89
90 # Mint fee configuration (admin-adjustable)
91 # v5/v6: Default fee set to 2.5 XTZ for revenue activation
92 self.data.keep_fee = sp.mutez(2500000)
93
94 # v4: Emergency pause flag
95 # When true, minting and metadata edits are disabled
96 self.data.paused = False
97
98 # v4: Default royalty configuration
99 # Basis points: 1000 = 10%, 2500 = 25% (max)
100 # Applied to all new mints unless overridden
101 self.data.default_royalty_bps = 1000 # 10% default
102
103 @sp.entrypoint
104 def keep(self, params):
105 """
106 Mint a new Keep token with minimal on-chain metadata.
107
108 Full metadata lives in IPFS (via metadata_uri). On-chain stores only
109 the essential fields needed for display and deduplication.
110
111 Two modes:
112 1. Admin calling: mints to specified owner (for server-side minting)
113 2. User calling: mints to sender, requires fee payment (default 2.5 XTZ)
114
115 All bytes parameters should be raw hex-encoded UTF-8 strings.
116 """
117 sp.cast(params, sp.record(
118 name=sp.bytes,
119 symbol=sp.bytes,
120 description=sp.bytes,
121 artifactUri=sp.bytes,
122 displayUri=sp.bytes,
123 thumbnailUri=sp.bytes,
124 decimals=sp.bytes,
125 creators=sp.bytes,
126 royalties=sp.bytes,
127 content_hash=sp.bytes,
128 metadata_uri=sp.bytes,
129 owner=sp.address
130 ))
131
132 # Check if contract is paused
133 assert not self.data.paused, "MINTING_PAUSED"
134
135 # Determine minting mode and owner
136 is_admin = self.is_administrator_()
137
138 # Non-admin callers must pay the fee and can only mint to themselves
139 if not is_admin:
140 assert sp.amount >= self.data.keep_fee, "INSUFFICIENT_FEE"
141 # User must mint to themselves (ensures they are firstMinter)
142 assert params.owner == sp.sender, "MUST_MINT_TO_SELF"
143
144 # Check for duplicate content hash
145 assert not self.data.content_hashes.contains(params.content_hash), "DUPLICATE_CONTENT_HASH"
146
147 # Get next token ID from the library's counter
148 token_id = self.data.next_token_id
149
150 # Minimal on-chain token_info — full metadata in IPFS via ""
151 token_info = sp.cast({
152 "name": params.name,
153 "symbol": params.symbol,
154 "description": params.description,
155 "artifactUri": params.artifactUri,
156 "displayUri": params.displayUri,
157 "thumbnailUri": params.thumbnailUri,
158 "decimals": params.decimals,
159 "creators": params.creators,
160 "royalties": params.royalties,
161 "content_hash": params.content_hash,
162 "": params.metadata_uri
163 }, sp.map[sp.string, sp.bytes])
164
165 # Store token metadata
166 self.data.token_metadata[token_id] = sp.record(
167 token_id=token_id,
168 token_info=token_info
169 )
170
171 # Assign token to owner
172 self.data.ledger[token_id] = params.owner
173
174 # Initialize as not locked
175 self.data.metadata_locked[token_id] = False
176
177 # Store content hash to prevent duplicates
178 self.data.content_hashes[params.content_hash] = token_id
179
180 # Track the original creator
181 self.data.token_creators[token_id] = params.owner
182
183 # Increment token counter
184 self.data.next_token_id = token_id + 1
185
186 @sp.entrypoint
187 def mint(self, batch):
188 """
189 Disable generic FA2 mint path.
190 Keeps must be minted through `keep` so fee/dedup rules always apply.
191 """
192 sp.cast(
193 batch,
194 sp.list[
195 sp.record(
196 to_=sp.address,
197 metadata=sp.map[sp.string, sp.bytes],
198 ).layout(("to_", "metadata"))
199 ],
200 )
201 assert False, "MINT_DISABLED_USE_KEEP"
202
203 @sp.entrypoint
204 def burn(self, batch):
205 """
206 Disable generic FA2 burn path.
207 Keeps must be burned through `burn_keep` so all indexes are cleaned.
208 """
209 sp.cast(
210 batch,
211 sp.list[
212 sp.record(
213 from_=sp.address,
214 token_id=sp.nat,
215 amount=sp.nat,
216 ).layout(("from_", ("token_id", "amount")))
217 ],
218 )
219 assert False, "BURN_DISABLED_USE_BURN_KEEP"
220
221 @sp.entrypoint
222 def edit_metadata(self, params):
223 """
224 Update metadata for an existing token.
225
226 Authorization:
227 - Token owner only (current holder)
228
229 Respects pause flag (cannot edit when paused).
230 content_hash and royalties are immutable — always preserved
231 from original mint.
232 """
233 sp.cast(params, sp.record(
234 token_id=sp.nat,
235 token_info=sp.map[sp.string, sp.bytes]
236 ))
237
238 # Check if contract is paused
239 assert not self.data.paused, "EDITING_PAUSED"
240
241 assert self.data.token_metadata.contains(params.token_id), "FA2_TOKEN_UNDEFINED"
242
243 # Check authorization: owner only
244 is_owner = self.data.ledger.get(params.token_id, default=sp.address("tz1burnburnburnburnburnburnburjAYjjX")) == sp.sender
245
246 assert is_owner, "NOT_TOKEN_OWNER"
247
248 # Check if locked
249 is_locked = self.data.metadata_locked.get(params.token_id, default=False)
250 assert not is_locked, "METADATA_LOCKED"
251
252 # Preserve immutable content_hash + royalties from original metadata
253 existing_info = self.data.token_metadata[params.token_id].token_info
254 original_hash = existing_info.get("content_hash", default=sp.bytes("0x"))
255 original_royalties = existing_info.get("royalties", default=sp.bytes("0x"))
256
257 # Update metadata
258 self.data.token_metadata[params.token_id] = sp.record(
259 token_id=params.token_id,
260 token_info=params.token_info
261 )
262
263 # Re-inject immutable fields (cannot be changed or removed via edit)
264 self.data.token_metadata[params.token_id].token_info["content_hash"] = original_hash
265 self.data.token_metadata[params.token_id].token_info["royalties"] = original_royalties
266
267 @sp.entrypoint
268 def lock_metadata(self, token_id):
269 """Permanently lock metadata for a token (admin or owner only)."""
270 sp.cast(token_id, sp.nat)
271
272 assert self.data.token_metadata.contains(token_id), "FA2_TOKEN_UNDEFINED"
273
274 # Check authorization: admin or owner
275 is_admin = self.is_administrator_()
276 is_owner = self.data.ledger.get(token_id, default=sp.address("tz1burnburnburnburnburnburnburjAYjjX")) == sp.sender
277
278 assert is_admin or is_owner, "NOT_AUTHORIZED"
279
280 self.data.metadata_locked[token_id] = True
281
282 @sp.entrypoint
283 def set_contract_metadata(self, params):
284 """Update contract-level metadata (admin only, if not locked)."""
285 sp.cast(params, sp.list[sp.record(key=sp.string, value=sp.bytes)])
286
287 assert self.is_administrator_(), "FA2_NOT_ADMIN"
288 assert not self.data.contract_metadata_locked, "CONTRACT_METADATA_LOCKED"
289
290 for item in params:
291 self.data.metadata[item.key] = item.value
292
293 @sp.entrypoint
294 def lock_contract_metadata(self):
295 """Permanently lock contract-level metadata (admin only)."""
296 assert self.is_administrator_(), "FA2_NOT_ADMIN"
297 self.data.contract_metadata_locked = True
298
299 @sp.entrypoint
300 def set_keep_fee(self, new_fee):
301 """
302 Set the keep fee required for minting.
303 Admin only. Fee is in mutez (1 tez = 1,000,000 mutez).
304 """
305 sp.cast(new_fee, sp.mutez)
306 assert self.is_administrator_(), "FA2_NOT_ADMIN"
307 self.data.keep_fee = new_fee
308
309 @sp.entrypoint
310 def withdraw_fees(self, destination):
311 """
312 Withdraw accumulated fees from the contract.
313 Admin only. Sends entire contract balance to destination.
314 """
315 sp.cast(destination, sp.address)
316 assert self.is_administrator_(), "FA2_NOT_ADMIN"
317 sp.send(destination, sp.balance)
318
319 @sp.entrypoint
320 def burn_keep(self, token_id):
321 """
322 Burn a token and remove its content_hash.
323 This allows re-minting the same piece name.
324 Owner only.
325 """
326 sp.cast(token_id, sp.nat)
327
328 assert self.data.token_metadata.contains(token_id), "FA2_TOKEN_UNDEFINED"
329 current_owner = self.data.ledger.get(
330 token_id,
331 default=sp.address("tz1burnburnburnburnburnburnburjAYjjX")
332 )
333 assert current_owner == sp.sender, "NOT_TOKEN_OWNER"
334
335 # Get content_hash before burning
336 token_info = self.data.token_metadata[token_id].token_info
337 content_hash = token_info.get("content_hash", default=sp.bytes("0x"))
338
339 # Remove from registries
340 if self.data.content_hashes.contains(content_hash):
341 del self.data.content_hashes[content_hash]
342
343 if self.data.ledger.contains(token_id):
344 del self.data.ledger[token_id]
345
346 del self.data.token_metadata[token_id]
347
348 if self.data.metadata_locked.contains(token_id):
349 del self.data.metadata_locked[token_id]
350
351 if self.data.token_creators.contains(token_id):
352 del self.data.token_creators[token_id]
353
354 # =====================================================================
355 # v4 ENTRYPOINTS (preserved in v5)
356 # =====================================================================
357
358 @sp.entrypoint
359 def pause(self):
360 """
361 Emergency pause - stops minting and metadata edits.
362 Admin only.
363
364 Use cases:
365 - Security vulnerability discovered
366 - IPFS infrastructure issues
367 - Spam attack detected
368 - Contract bug found
369
370 Note: Does NOT affect transfers (preserves FA2 composability)
371 """
372 assert self.is_administrator_(), "FA2_NOT_ADMIN"
373 self.data.paused = True
374
375 @sp.entrypoint
376 def unpause(self):
377 """
378 Resume normal operations after emergency pause.
379 Admin only.
380 """
381 assert self.is_administrator_(), "FA2_NOT_ADMIN"
382 self.data.paused = False
383
384 @sp.entrypoint
385 def set_default_royalty(self, bps):
386 """
387 Set default royalty percentage for new mints.
388 Admin only.
389
390 Args:
391 bps: Basis points (100 = 1%, 1000 = 10%, 2500 = 25% max)
392
393 Example:
394 set_default_royalty(1000) # 10% royalty
395 """
396 sp.cast(bps, sp.nat)
397 assert self.is_administrator_(), "FA2_NOT_ADMIN"
398 assert bps <= 2500, "MAX_ROYALTY_25_PERCENT"
399 self.data.default_royalty_bps = bps
400
401 @sp.entrypoint
402 def admin_transfer(self, params):
403 """Admin emergency transfer"""
404 sp.cast(params, sp.record(
405 token_id=sp.nat,
406 from_=sp.address,
407 to_=sp.address
408 ))
409
410 assert self.is_administrator_(), "FA2_NOT_ADMIN"
411 assert self.data.token_metadata.contains(params.token_id), "FA2_TOKEN_UNDEFINED"
412
413 current_owner = self.data.ledger.get(params.token_id, default=sp.address("tz1burnburnburnburnburnburnburjAYjjX"))
414 assert current_owner == params.from_, "INVALID_CURRENT_OWNER"
415
416 self.data.ledger[params.token_id] = params.to_
417
418
419def _get_balance(fa2_contract, args):
420 """Utility function to call the contract's get_balance view."""
421 return sp.View(fa2_contract, "get_balance")(args)
422
423
424def _total_supply(fa2_contract, args):
425 """Utility function to call the contract's total_supply view."""
426 return sp.View(fa2_contract, "total_supply")(args)
427
428
429@sp.add_test()
430def test():
431 """Minimal test to compile v6 contract."""
432 scenario = sp.test_scenario("KeepsFA2v6")
433 scenario.h1("KidLisp Keeps FA2 v6 - Final Production Contract")
434
435 # Define test account
436 admin = sp.test_account("Admin")
437
438 # Create empty initial state
439 ledger = {}
440 token_metadata = []
441
442 # Deploy contract
443 contract = keeps_module.KidLispKeepsFA2v6(
444 admin.address,
445 sp.big_map(),
446 ledger,
447 token_metadata
448 )
449
450 scenario += contract
451
452 scenario.p("v6: royalties immutable after keep")
453 scenario.p("v5: Default fee = 2.5 XTZ, revenue enabled")
454 scenario.p("v4 features: royalties, pause, admin transfer")
455 scenario.p("v3 features: creator tracking")