Smart Contract
Testies
A.ce9dd43888d99574.Testies
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