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