Smart Contract

Testies

A.ce9dd43888d99574.Testies

Valid From

133,074,988

Deployed

1w ago
Feb 21, 2026, 06:14:24 PM UTC

Dependents

3 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3import MetadataViews from 0x1d7e57aa55817448
4import ViewResolver from 0x1d7e57aa55817448
5
6/// Testies - Simple leveling NFT test contract
7/// Tests proper metadata display on Flowty and level-up mechanism
8access(all) contract Testies: NonFungibleToken, ViewResolver {
9    
10    // ========================================
11    // PATHS
12    // ========================================
13    
14    access(all) let CollectionStoragePath: StoragePath
15    access(all) let CollectionPublicPath: PublicPath
16    access(all) let AdminStoragePath: StoragePath
17    
18    // ========================================
19    // STATE
20    // ========================================
21    
22    access(all) var totalSupply: UInt64
23    
24    // ========================================
25    // EVENTS
26    // ========================================
27    
28    access(all) event ContractInitialized()
29    access(all) event Withdraw(id: UInt64, from: Address?)
30    access(all) event Deposit(id: UInt64, to: Address?)
31    access(all) event NFTMinted(id: UInt64, recipient: Address)
32    access(all) event NFTLeveledUp(id: UInt64, newLevel: UInt64)
33    
34    // ========================================
35    // NFT RESOURCE
36    // ========================================
37    
38    access(all) resource NFT: NonFungibleToken.NFT, ViewResolver.Resolver {
39        access(all) let id: UInt64
40        access(all) let originalMinter: Address
41        access(all) let mintedAt: UFix64
42        
43        // Mutable level (can be upgraded!)
44        access(all) var level: UInt64
45        
46        // Base metadata (name, description stay same)
47        access(self) let baseMetadata: {String: String}
48        
49        init(id: UInt64, recipient: Address, name: String, description: String, baseImage: String, type: String, strength: String, evolvedType: String, evolvedStrength: String) {
50            self.id = id
51            self.originalMinter = recipient
52            self.mintedAt = getCurrentBlock().timestamp
53            self.level = 1
54            
55            self.baseMetadata = {
56                "name": name,
57                "description": description,
58                "baseImage": baseImage,
59                "type": type,
60                "strength": strength,
61                "evolvedType": evolvedType,
62                "evolvedStrength": evolvedStrength
63            }
64        }
65        
66        // Level up function - updates level and triggers metadata refresh
67        access(contract) fun levelUp() {
68            self.level = self.level + 1
69        }
70        
71        // Update metadata function - for evolving NFTs (admin only)
72        access(contract) fun updateMetadata(key: String, value: String) {
73            self.baseMetadata[key] = value
74        }
75        
76        // Dynamic image based on level
77        access(all) fun getImage(): String {
78            let baseImage = self.baseMetadata["baseImage"]!
79            // For level 1: use base image (e.g., "1-new.png")
80            // For level 2+: replace "-new" with "-evolve" (e.g., "1-evolve.png")
81            if self.level == 1 {
82                return baseImage
83            } else {
84                // Replace "-new" with "-evolve" for leveled up NFTs
85                if baseImage.contains("-new") {
86                    return baseImage.replaceAll(of: "-new", with: "-evolve")
87                }
88                return baseImage
89            }
90        }
91        
92        access(all) view fun getViews(): [Type] {
93            return [
94                Type<MetadataViews.Display>(),
95                Type<MetadataViews.NFTCollectionData>(),
96                Type<MetadataViews.NFTCollectionDisplay>(),
97                Type<MetadataViews.ExternalURL>(),
98                Type<MetadataViews.Serial>(),
99                Type<MetadataViews.Traits>(),
100                Type<MetadataViews.Royalties>(),
101                Type<MetadataViews.Editions>(),
102                Type<MetadataViews.Medias>(),
103                Type<MetadataViews.EVMBridgedMetadata>()
104            ]
105        }
106        
107        access(all) fun resolveView(_ view: Type): AnyStruct? {
108            switch view {
109                case Type<MetadataViews.Display>():
110                    return MetadataViews.Display(
111                        name: self.baseMetadata["name"]!,
112                        description: self.baseMetadata["description"]!.concat(" | Level ").concat(self.level.toString()),
113                        thumbnail: MetadataViews.HTTPFile(url: self.getImage())
114                    )
115                
116                case Type<MetadataViews.NFTCollectionData>():
117                    return Testies.resolveContractView(resourceType: Type<@Testies.NFT>(), viewType: Type<MetadataViews.NFTCollectionData>())
118                
119                case Type<MetadataViews.NFTCollectionDisplay>():
120                    return Testies.resolveContractView(resourceType: Type<@Testies.NFT>(), viewType: Type<MetadataViews.NFTCollectionDisplay>())
121                
122                case Type<MetadataViews.ExternalURL>():
123                    return MetadataViews.ExternalURL("https://flunks.net/testies")
124                
125                case Type<MetadataViews.Serial>():
126                    return MetadataViews.Serial(self.id)
127                
128                case Type<MetadataViews.Traits>():
129                    let traits: [MetadataViews.Trait] = []
130                    traits.append(MetadataViews.Trait(name: "Level", value: self.level, displayType: "Number", rarity: nil))
131                    traits.append(MetadataViews.Trait(name: "Minted At", value: self.mintedAt, displayType: "Date", rarity: nil))
132                    
133                    // Dynamic traits based on level
134                    if self.level == 1 {
135                        traits.append(MetadataViews.Trait(name: "Type", value: self.baseMetadata["type"]!, displayType: "String", rarity: nil))
136                        traits.append(MetadataViews.Trait(name: "Strength", value: self.baseMetadata["strength"]!, displayType: "String", rarity: nil))
137                    } else {
138                        traits.append(MetadataViews.Trait(name: "Type", value: self.baseMetadata["evolvedType"]!, displayType: "String", rarity: nil))
139                        traits.append(MetadataViews.Trait(name: "Strength", value: self.baseMetadata["evolvedStrength"]!, displayType: "String", rarity: nil))
140                    }
141                    
142                    return MetadataViews.Traits(traits)
143                
144                case Type<MetadataViews.Royalties>():
145                    // 10% royalty to contract deployer
146                    // Using generic receiver for multi-token support via FungibleTokenSwitchboard
147                    let royaltyReceiver = getAccount(Testies.account.address)
148                        .capabilities.get<&{FungibleToken.Receiver}>(MetadataViews.getRoyaltyReceiverPublicPath())
149                    
150                    return MetadataViews.Royalties([
151                        MetadataViews.Royalty(
152                            receiver: royaltyReceiver,
153                            cut: 0.10,
154                            description: "Testies creator royalty"
155                        )
156                    ])
157                
158                case Type<MetadataViews.Editions>():
159                    let editionInfo = MetadataViews.Edition(
160                        name: "Testies",
161                        number: self.id,
162                        max: nil  // No max supply
163                    )
164                    return MetadataViews.Editions([editionInfo])
165                
166                case Type<MetadataViews.Medias>():
167                    return MetadataViews.Medias([
168                        MetadataViews.Media(
169                            file: MetadataViews.HTTPFile(url: self.getImage()),
170                            mediaType: "image/png"
171                        )
172                    ])
173                
174                case Type<MetadataViews.EVMBridgedMetadata>():
175                    // EVM metadata for Flow <-> EVM bridging
176                    return MetadataViews.EVMBridgedMetadata(
177                        name: self.baseMetadata["name"]!,
178                        symbol: "TESTIES",
179                        uri: MetadataViews.URI(
180                            baseURI: "https://flunks.net/testies/metadata/",
181                            value: self.id.toString()
182                        )
183                    )
184            }
185            
186            return nil
187        }
188        
189        access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
190            return <- Testies.createEmptyCollection(nftType: Type<@Testies.NFT>())
191        }
192    }
193    
194    // ========================================
195    // COLLECTION RESOURCE
196    // ========================================
197    
198    access(all) resource Collection: NonFungibleToken.Collection {
199        access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
200        
201        init() {
202            self.ownedNFTs <- {}
203        }
204        
205        access(all) view fun getLength(): Int {
206            return self.ownedNFTs.length
207        }
208        
209        access(all) view fun getIDs(): [UInt64] {
210            return self.ownedNFTs.keys
211        }
212        
213        access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
214            return &self.ownedNFTs[id]
215        }
216        
217        access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
218            let token <- self.ownedNFTs.remove(key: withdrawID) 
219                ?? panic("NFT not found in collection")
220            emit Withdraw(id: token.id, from: self.owner?.address)
221            return <-token
222        }
223        
224        access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
225            let nft <- token as! @Testies.NFT
226            let id = nft.id
227            emit Deposit(id: id, to: self.owner?.address)
228            let oldToken <- self.ownedNFTs[id] <- nft
229            destroy oldToken
230        }
231        
232        access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
233            return {Type<@Testies.NFT>(): true}
234        }
235        
236        access(all) view fun isSupportedNFTType(type: Type): Bool {
237            return type == Type<@Testies.NFT>()
238        }
239        
240        access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
241            return <- Testies.createEmptyCollection(nftType: Type<@Testies.NFT>())
242        }
243        
244        // Helper to borrow NFT as Testies.NFT (for leveling up)
245        access(all) fun borrowTestiesNFT(id: UInt64): &Testies.NFT? {
246            if let nftRef = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? {
247                return nftRef as! &Testies.NFT
248            }
249            return nil
250        }
251    }
252    
253    // ========================================
254    // ADMIN RESOURCE
255    // ========================================
256    
257    access(all) resource Admin {
258        
259        // Mint new NFT
260        access(all) fun mintNFT(
261            recipient: Address,
262            name: String,
263            description: String,
264            baseImage: String,
265            type: String,
266            strength: String,
267            evolvedType: String,
268            evolvedStrength: String
269        ) {
270            let recipientAccount = getAccount(recipient)
271            let receiverCap = recipientAccount.capabilities
272                .get<&{NonFungibleToken.Receiver}>(Testies.CollectionPublicPath)
273            
274            if !receiverCap.check() {
275                panic("Recipient does not have Testies collection set up")
276            }
277            
278            let receiver = receiverCap.borrow()!
279            
280            let nft <- create NFT(
281                id: Testies.totalSupply,
282                recipient: recipient,
283                name: name,
284                description: description,
285                baseImage: baseImage,
286                type: type,
287                strength: strength,
288                evolvedType: evolvedType,
289                evolvedStrength: evolvedStrength
290            )
291            
292            let nftID = nft.id
293            receiver.deposit(token: <-nft)
294            
295            Testies.totalSupply = Testies.totalSupply + 1
296            emit NFTMinted(id: nftID, recipient: recipient)
297        }
298        
299        // Level up an NFT (for testing)
300        access(all) fun levelUpNFT(ownerAddress: Address, nftID: UInt64) {
301            let ownerAccount = getAccount(ownerAddress)
302            let collectionCap = ownerAccount.capabilities
303                .get<&Testies.Collection>(Testies.CollectionPublicPath)
304            
305            if !collectionCap.check() {
306                panic("Owner does not have Testies collection")
307            }
308            
309            let collection = collectionCap.borrow()!
310            let nftRef = collection.borrowTestiesNFT(id: nftID)
311                ?? panic("NFT not found")
312            
313            nftRef.levelUp()
314            emit NFTLeveledUp(id: nftID, newLevel: nftRef.level)
315        }
316        
317        // Update NFT metadata - for evolving NFTs
318        access(all) fun updateNFTMetadata(ownerAddress: Address, nftID: UInt64, key: String, value: String) {
319            let ownerAccount = getAccount(ownerAddress)
320            let collectionCap = ownerAccount.capabilities
321                .get<&Testies.Collection>(Testies.CollectionPublicPath)
322            
323            if !collectionCap.check() {
324                panic("Owner does not have Testies collection")
325            }
326            
327            let collection = collectionCap.borrow()!
328            let nftRef = collection.borrowTestiesNFT(id: nftID)
329                ?? panic("NFT not found")
330            
331            nftRef.updateMetadata(key: key, value: value)
332        }
333    }
334    
335    // ========================================
336    // CONTRACT FUNCTIONS
337    // ========================================
338    
339    access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {
340        return <- create Collection()
341    }
342    
343    access(all) view fun getContractViews(resourceType: Type?): [Type] {
344        return [
345            Type<MetadataViews.NFTCollectionData>(),
346            Type<MetadataViews.NFTCollectionDisplay>()
347        ]
348    }
349    
350    access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
351        switch viewType {
352            case Type<MetadataViews.NFTCollectionData>():
353                return MetadataViews.NFTCollectionData(
354                    storagePath: self.CollectionStoragePath,
355                    publicPath: self.CollectionPublicPath,
356                    publicCollection: Type<&Testies.Collection>(),
357                    publicLinkedType: Type<&Testies.Collection>(),
358                    createEmptyCollectionFunction: (fun(): @{NonFungibleToken.Collection} {
359                        return <- Testies.createEmptyCollection(nftType: Type<@Testies.NFT>())
360                    })
361                )
362            
363            case Type<MetadataViews.NFTCollectionDisplay>():
364                let squareImage = MetadataViews.Media(
365                    file: MetadataViews.HTTPFile(url: "https://storage.googleapis.com/flunks_public/images/1-evolve.png"),
366                    mediaType: "image/png"
367                )
368                let bannerImage = MetadataViews.Media(
369                    file: MetadataViews.HTTPFile(url: "https://storage.googleapis.com/flunks_public/images/1-banner.png"),
370                    mediaType: "image/png"
371                )
372                
373                return MetadataViews.NFTCollectionDisplay(
374                    name: "Testies NFT Collection",
375                    description: "Test collection for verifying NFT metadata display and level-up mechanics on Flowty and other marketplaces.",
376                    externalURL: MetadataViews.ExternalURL("https://flunks.net/testies"),
377                    squareImage: squareImage,
378                    bannerImage: bannerImage,
379                    socials: {
380                        "twitter": MetadataViews.ExternalURL("https://twitter.com/flunks")
381                    }
382                )
383        }
384        
385        return nil
386    }
387    
388    // ========================================
389    // INIT
390    // ========================================
391    
392    init() {
393        self.totalSupply = 0
394        
395        self.CollectionStoragePath = /storage/TestiesCollection
396        self.CollectionPublicPath = /public/TestiesCollection
397        self.AdminStoragePath = /storage/TestiesAdmin
398        
399        // Create admin resource and save it
400        self.account.storage.save(<-create Admin(), to: self.AdminStoragePath)
401        
402        emit ContractInitialized()
403    }
404}
405