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