Smart Contract

SemesterZeroV3

A.ce9dd43888d99574.SemesterZeroV3

Valid From

134,390,733

Deployed

1w ago
Feb 15, 2026, 11:57:55 PM UTC

Dependents

48 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3import MetadataViews from 0x1d7e57aa55817448
4import ViewResolver from 0x1d7e57aa55817448
5
6/// SemesterZeroV3 - Production-ready Flunks: Semester Zero NFT Collection
7/// 
8/// FEATURES:
9/// ✅ Pin evolution: Base → Silver → Gold → Special
10/// ✅ Patch evolution: Base → Retro → Punk → Nerdy
11/// ✅ GUM cost tracking for each evolution tier (stored in Admin, updatable)
12/// ✅ Location-based NFTs (Paradise Motel, Crystal Springs, etc.)
13/// ✅ Custom metadata at mint with evolution/reveal capabilities
14/// ✅ Traits revealed and locked during first evolution
15/// ✅ Full MetadataViews support for marketplaces
16/// ✅ Royalties (10% to Flunks)
17/// ✅ Admin minting and evolution controls
18/// ✅ NFT burning capability
19/// ✅ Clean codebase - NO legacy baggage
20///
21/// V3.1: Fixed duplicate traits - November 2025
22access(all) contract SemesterZeroV3: NonFungibleToken, ViewResolver {
23    
24    // ========================================
25    // PATHS
26    // ========================================
27    
28    access(all) let CollectionStoragePath: StoragePath
29    access(all) let CollectionPublicPath: PublicPath
30    access(all) let AdminStoragePath: StoragePath
31    
32    // ========================================
33    // EVENTS
34    // ========================================
35    
36    access(all) event ContractInitialized()
37    access(all) event NFTMinted(nftID: UInt64, recipient: Address, nftType: String, location: String, serialNumber: UInt64, timestamp: UFix64)
38    access(all) event NFTEvolved(nftID: UInt64, owner: Address, oldTier: String, newTier: String, timestamp: UFix64)
39    access(all) event NFTBurned(nftID: UInt64, owner: Address, timestamp: UFix64)
40    access(all) event Withdraw(id: UInt64, from: Address?)
41    access(all) event Deposit(id: UInt64, to: Address?)
42    access(all) event EvolutionCostsUpdated(nftType: String, costs: {String: UFix64})
43    
44    // ========================================
45    // STATE VARIABLES
46    // ========================================
47    
48    access(all) var totalSupply: UInt64
49    
50    // ========================================
51    // NFT RESOURCE
52    // ========================================
53    
54    access(all) resource NFT: NonFungibleToken.NFT {
55        access(all) let id: UInt64
56        access(all) let nftType: String
57        access(all) let location: String
58        access(all) let recipient: Address
59        access(all) let mintedAt: UFix64
60        access(all) let serialNumber: UInt64
61        access(all) var metadata: {String: String}
62        access(all) var evolutionTier: String
63        access(all) var traitsLocked: Bool
64        
65        init(
66            id: UInt64,
67            recipient: Address,
68            serialNumber: UInt64,
69            nftType: String,
70            location: String,
71            initialMetadata: {String: String}
72        ) {
73            self.id = id
74            self.nftType = nftType
75            self.location = location
76            self.recipient = recipient
77            self.mintedAt = getCurrentBlock().timestamp
78            self.serialNumber = serialNumber
79            self.metadata = initialMetadata
80            self.evolutionTier = "Base"
81            self.traitsLocked = false
82        }
83        
84        access(contract) fun evolve(newMetadata: {String: String}, newTier: String) {
85            self.metadata = newMetadata
86            self.evolutionTier = newTier
87            
88            if !self.traitsLocked {
89                self.traitsLocked = true
90            }
91        }
92        
93        access(all) view fun getViews(): [Type] {
94            return [
95                Type<MetadataViews.Display>(),
96                Type<MetadataViews.NFTCollectionData>(),
97                Type<MetadataViews.NFTCollectionDisplay>(),
98                Type<MetadataViews.Royalties>(),
99                Type<MetadataViews.ExternalURL>(),
100                Type<MetadataViews.Serial>(),
101                Type<MetadataViews.Traits>()
102            ]
103        }
104        
105        access(all) fun resolveView(_ view: Type): AnyStruct? {
106            switch view {
107                case Type<MetadataViews.Display>():
108                    return MetadataViews.Display(
109                        name: self.metadata["name"] ?? "Semester Zero NFT",
110                        description: self.metadata["description"] ?? "A collectible from Flunks: Semester Zero",
111                        thumbnail: MetadataViews.HTTPFile(url: self.metadata["image"] ?? "")
112                    )
113                
114                case Type<MetadataViews.NFTCollectionData>():
115                    return SemesterZeroV3.resolveContractView(resourceType: nil, viewType: Type<MetadataViews.NFTCollectionData>())
116                
117                case Type<MetadataViews.NFTCollectionDisplay>():
118                    return SemesterZeroV3.resolveContractView(resourceType: nil, viewType: Type<MetadataViews.NFTCollectionDisplay>())
119                
120                case Type<MetadataViews.Royalties>():
121                    let royaltyCap = SemesterZeroV3.getRoyaltyReceiverCapability()
122                    if !royaltyCap.check() {
123                        return MetadataViews.Royalties([])
124                    }
125                    return MetadataViews.Royalties([
126                        MetadataViews.Royalty(
127                            receiver: royaltyCap,
128                            cut: 0.10,
129                            description: "Flunks: Semester Zero creator royalty"
130                        )
131                    ])
132
133                case Type<MetadataViews.ExternalURL>():
134                    return MetadataViews.ExternalURL("https://flunks.net")
135                
136                case Type<MetadataViews.Serial>():
137                    return MetadataViews.Serial(self.serialNumber)
138                
139                case Type<MetadataViews.Traits>():
140                    let traits: [MetadataViews.Trait] = []
141                    
142                    // Keys to exclude from metadata loop (we add them from resource fields)
143                    let excludeKeys: [String] = ["name", "description", "image", "nftType", "location", "evolutionTier", "mintedAt"]
144                    
145                    // Add metadata traits (excluding display fields and core traits)
146                    for key in self.metadata.keys {
147                        var skip = false
148                        for excludeKey in excludeKeys {
149                            if key == excludeKey {
150                                skip = true
151                                break
152                            }
153                        }
154                        if !skip {
155                            traits.append(MetadataViews.Trait(
156                                name: key,
157                                value: self.metadata[key]!,
158                                displayType: "String",
159                                rarity: nil
160                            ))
161                        }
162                    }
163                    
164                    // Add core traits from resource fields (single source of truth)
165                    traits.append(MetadataViews.Trait(name: "nftType", value: self.nftType, displayType: "String", rarity: nil))
166                    traits.append(MetadataViews.Trait(name: "location", value: self.location, displayType: "String", rarity: nil))
167                    traits.append(MetadataViews.Trait(name: "evolutionTier", value: self.evolutionTier, displayType: "String", rarity: nil))
168                    // traitsLocked hidden from display
169                    
170                    return MetadataViews.Traits(traits)
171            }
172            return nil
173        }
174        
175        access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
176            return <-SemesterZeroV3.createEmptyCollection(nftType: Type<@SemesterZeroV3.NFT>())
177        }
178    }
179    
180    // ========================================
181    // NFT COLLECTION
182    // ========================================
183    
184    access(all) resource Collection: NonFungibleToken.Collection, ViewResolver.ResolverCollection {
185        access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
186        
187        init() {
188            self.ownedNFTs <- {}
189        }
190        
191        access(all) view fun getLength(): Int {
192            return self.ownedNFTs.length
193        }
194        
195        access(all) view fun getIDs(): [UInt64] {
196            return self.ownedNFTs.keys
197        }
198        
199        access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
200            return &self.ownedNFTs[id]
201        }
202        
203        access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
204            let token <- self.ownedNFTs.remove(key: withdrawID)
205                ?? panic("NFT not found in collection")
206            let nft <- token as! @SemesterZeroV3.NFT
207            emit Withdraw(id: nft.id, from: self.owner?.address)
208            return <-nft
209        }
210        
211        access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
212            let nft <- token as! @SemesterZeroV3.NFT
213            let id = nft.id
214            let oldToken <- self.ownedNFTs[id] <- nft
215            destroy oldToken
216            emit Deposit(id: id, to: self.owner?.address)
217        }
218        
219        access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
220            return {Type<@SemesterZeroV3.NFT>(): true}
221        }
222        
223        access(all) view fun isSupportedNFTType(type: Type): Bool {
224            return type == Type<@SemesterZeroV3.NFT>()
225        }
226        
227        access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
228            return <-SemesterZeroV3.createEmptyCollection(nftType: Type<@SemesterZeroV3.NFT>())
229        }
230        
231        access(all) view fun borrowSemesterZeroNFT(id: UInt64): &NFT? {
232            if self.ownedNFTs[id] != nil {
233                let ref = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}?
234                return ref as! &NFT?
235            }
236            return nil
237        }
238        
239        access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? {
240            if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? {
241                return nft as &{ViewResolver.Resolver}
242            }
243            return nil
244        }
245    }
246    
247    // ========================================
248    // ADMIN RESOURCE
249    // ========================================
250    
251    access(all) resource Admin {
252        
253        access(all) var evolutionCosts: {String: UFix64}
254        
255        init() {
256            self.evolutionCosts = {
257                "Pin_Silver": 250.0,
258                "Pin_Gold": 500.0,
259                "Pin_Special": 750.0,
260                "Patch_Retro": 500.0,
261                "Patch_Punk": 250.0,
262                "Patch_Nerdy": 250.0
263            }
264        }
265        
266        access(all) fun mintNFT(
267            recipientAddress: Address,
268            nftType: String,
269            location: String,
270            metadata: {String: String}
271        ) {
272            pre {
273                nftType == "Pin" || nftType == "Patch": "NFT type must be 'Pin' or 'Patch'"
274                metadata["name"] != nil: "Metadata must include 'name'"
275                metadata["description"] != nil: "Metadata must include 'description'"
276                metadata["image"] != nil: "Metadata must include 'image'"
277            }
278            
279            let recipientCap = getAccount(recipientAddress)
280                .capabilities.get<&SemesterZeroV3.Collection>(SemesterZeroV3.CollectionPublicPath)
281            
282            assert(recipientCap.check(), message: "Recipient does not have SemesterZeroV3 collection set up")
283            
284            let recipient = recipientCap.borrow()!
285            
286            let nftID = SemesterZeroV3.totalSupply
287            let serialNumber = SemesterZeroV3.totalSupply + 1
288            
289            var fullMetadata = metadata
290            fullMetadata["nftType"] = nftType
291            fullMetadata["location"] = location
292            fullMetadata["serialNumber"] = serialNumber.toString()
293            fullMetadata["evolutionTier"] = "Base"
294            fullMetadata["collection"] = "Flunks: Semester Zero"
295            fullMetadata["mintedAt"] = getCurrentBlock().timestamp.toString()
296            
297            let nft <- create NFT(
298                id: nftID,
299                recipient: recipientAddress,
300                serialNumber: serialNumber,
301                nftType: nftType,
302                location: location,
303                initialMetadata: fullMetadata
304            )
305            
306            SemesterZeroV3.totalSupply = SemesterZeroV3.totalSupply + 1
307            
308            recipient.deposit(token: <-nft)
309            
310            emit NFTMinted(
311                nftID: nftID,
312                recipient: recipientAddress,
313                nftType: nftType,
314                location: location,
315                serialNumber: serialNumber,
316                timestamp: getCurrentBlock().timestamp
317            )
318        }
319        
320        access(all) fun evolveNFT(
321            userAddress: Address,
322            nftID: UInt64,
323            newTier: String,
324            newMetadata: {String: String}
325        ) {
326            pre {
327                newTier == "Silver" || newTier == "Gold" || newTier == "Special" || 
328                newTier == "Retro" || newTier == "Punk" || newTier == "Nerdy": 
329                    "Invalid evolution tier. Must be: Silver, Gold, Special, Retro, Punk, or Nerdy"
330                newMetadata["image"] != nil: "New metadata must include updated 'image'"
331            }
332            
333            let collectionRef = getAccount(userAddress)
334                .capabilities.get<&SemesterZeroV3.Collection>(SemesterZeroV3.CollectionPublicPath)
335                .borrow()
336                ?? panic("User does not have SemesterZeroV3 collection")
337            
338            let nftRef = collectionRef.borrowSemesterZeroNFT(id: nftID)
339                ?? panic("NFT not found in collection")
340            
341            let oldTier = nftRef.evolutionTier
342            
343            var fullMetadata = newMetadata
344            fullMetadata["evolutionTier"] = newTier
345            fullMetadata["evolvedAt"] = getCurrentBlock().timestamp.toString()
346            
347            nftRef.evolve(newMetadata: fullMetadata, newTier: newTier)
348            
349            emit NFTEvolved(
350                nftID: nftID,
351                owner: userAddress,
352                oldTier: oldTier,
353                newTier: newTier,
354                timestamp: getCurrentBlock().timestamp
355            )
356        }
357        
358        access(all) fun updatePinCosts(silverCost: UFix64, goldCost: UFix64, specialCost: UFix64) {
359            self.evolutionCosts["Pin_Silver"] = silverCost
360            self.evolutionCosts["Pin_Gold"] = goldCost
361            self.evolutionCosts["Pin_Special"] = specialCost
362            
363            emit EvolutionCostsUpdated(nftType: "Pin", costs: {
364                "Silver": silverCost,
365                "Gold": goldCost,
366                "Special": specialCost
367            })
368        }
369        
370        access(all) fun updatePatchCosts(retroCost: UFix64, punkCost: UFix64, nerdyCost: UFix64) {
371            self.evolutionCosts["Patch_Retro"] = retroCost
372            self.evolutionCosts["Patch_Punk"] = punkCost
373            self.evolutionCosts["Patch_Nerdy"] = nerdyCost
374            
375            emit EvolutionCostsUpdated(nftType: "Patch", costs: {
376                "Retro": retroCost,
377                "Punk": punkCost,
378                "Nerdy": nerdyCost
379            })
380        }
381        
382        access(all) fun getEvolutionCosts(): {String: UFix64} {
383            return self.evolutionCosts
384        }
385        
386        access(all) fun getEvolutionCost(nftType: String, tier: String): UFix64? {
387            let key = nftType.concat("_").concat(tier)
388            return self.evolutionCosts[key]
389        }
390        
391        access(all) fun burnNFT(
392            collection: auth(NonFungibleToken.Withdraw) &Collection,
393            nftID: UInt64
394        ) {
395            assert(collection.ownedNFTs[nftID] != nil, message: "NFT does not exist in collection")
396            
397            let nft <- collection.withdraw(withdrawID: nftID)
398            let ownerAddress = collection.owner!.address
399            
400            emit NFTBurned(
401                nftID: nftID,
402                owner: ownerAddress,
403                timestamp: getCurrentBlock().timestamp
404            )
405            
406            destroy nft
407        }
408    }
409    
410    // ========================================
411    // PUBLIC CONTRACT FUNCTIONS
412    // ========================================
413    
414    access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {
415        return <- create Collection()
416    }
417    
418    access(all) fun getRoyaltyReceiverCapability(): Capability<&{FungibleToken.Receiver}> {
419        return getAccount(0xbfffec679fff3a94)
420            .capabilities
421            .get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
422    }
423    
424    access(all) fun getStats(): {String: AnyStruct} {
425        return {
426            "totalSupply": SemesterZeroV3.totalSupply,
427            "contractName": "SemesterZeroV3",
428            "version": "3.1.0",
429            "collectionPath": SemesterZeroV3.CollectionPublicPath.toString()
430        }
431    }
432    
433    // ========================================
434    // VIEW RESOLVER (Marketplace Compatibility)
435    // ========================================
436    
437    access(all) view fun getContractViews(resourceType: Type?): [Type] {
438        return [
439            Type<MetadataViews.NFTCollectionData>(),
440            Type<MetadataViews.NFTCollectionDisplay>()
441        ]
442    }
443    
444    access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
445        switch viewType {
446            case Type<MetadataViews.NFTCollectionData>():
447                return MetadataViews.NFTCollectionData(
448                    storagePath: SemesterZeroV3.CollectionStoragePath,
449                    publicPath: SemesterZeroV3.CollectionPublicPath,
450                    publicCollection: Type<&SemesterZeroV3.Collection>(),
451                    publicLinkedType: Type<&SemesterZeroV3.Collection>(),
452                    createEmptyCollectionFunction: (fun(): @{NonFungibleToken.Collection} {
453                        return <-SemesterZeroV3.createEmptyCollection(nftType: Type<@SemesterZeroV3.NFT>())
454                    })
455                )
456            
457            case Type<MetadataViews.NFTCollectionDisplay>():
458                let squareMedia = MetadataViews.Media(
459                    file: MetadataViews.HTTPFile(
460                        url: "https://storage.googleapis.com/flunks_public/images/semesterzero.png"
461                    ),
462                    mediaType: "image/png"
463                )
464                let bannerMedia = MetadataViews.Media(
465                    file: MetadataViews.HTTPFile(
466                        url: "https://storage.googleapis.com/flunks_public/images/banner.png"
467                    ),
468                    mediaType: "image/png"
469                )
470                return MetadataViews.NFTCollectionDisplay(
471                    name: "Flunks: Semester Zero",
472                    description: "Collectible Pins and Patches from Flunks: Semester Zero. Earn them by exploring locations, completing challenges, and participating in events. Evolve your NFTs with GUM to unlock new tiers and reveal hidden traits!",
473                    externalURL: MetadataViews.ExternalURL("https://flunks.net"),
474                    squareImage: squareMedia,
475                    bannerImage: bannerMedia,
476                    socials: {
477                        "twitter": MetadataViews.ExternalURL("https://twitter.com/FlunksNFT"),
478                        "discord": MetadataViews.ExternalURL("https://discord.gg/flunks"),
479                        "website": MetadataViews.ExternalURL("https://flunks.net")
480                    }
481                )
482        }
483        return nil
484    }
485    
486    // ========================================
487    // INITIALIZATION
488    // ========================================
489    
490    init() {
491        self.CollectionStoragePath = /storage/SemesterZeroV3Collection
492        self.CollectionPublicPath = /public/SemesterZeroV3Collection
493        self.AdminStoragePath = /storage/SemesterZeroV3Admin
494        
495        self.totalSupply = 0
496        
497        self.account.storage.save(<-create Admin(), to: self.AdminStoragePath)
498        
499        emit ContractInitialized()
500    }
501}
502