Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 455 lines 16 kB view raw
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")