Smart Contract

AeraPanels

A.30cf5dcf6ea8d379.AeraPanels

Deployed

1w ago
Feb 15, 2026, 07:54:52 PM UTC

Dependents

1 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3import MetadataViews from 0x1d7e57aa55817448
4import ViewResolver from 0x1d7e57aa55817448
5import AeraNFT from 0x30cf5dcf6ea8d379
6import AeraRewards from 0x30cf5dcf6ea8d379
7import FindFurnace from 0x097bafa4e0b48eef 
8import FindViews from 0x097bafa4e0b48eef
9
10access(all)
11contract AeraPanels: NonFungibleToken{ 
12
13    access(all) entitlement Owner
14    access(all)
15    var totalSupply: UInt64
16
17    access(all)
18    event ContractInitialized()
19
20    access(all)
21    event Withdraw(id: UInt64, from: Address?)
22
23    access(all)
24    event Deposit(id: UInt64, to: Address?)
25
26    access(all)
27    event Minted(id: UInt64, address: Address, panel_id: UInt64, edition: UInt64)
28
29    // we cannot have address here as it will always be nil
30    access(all)
31    event Burned(id: UInt64, panel_id: UInt64, edition: UInt64)
32
33    access(all)
34    event PanelMetadataRegistered(chapter_id: UInt64, slot_id: UInt64, panel_id: UInt64, detail:{ String: String})
35
36    access(all)
37    event PanelStaked(id: UInt64, chapter_id: UInt64, slot_id: UInt64, panel_id: UInt64, owner: Address?)
38
39    access(all)
40    event PanelUnstaked(id: UInt64, chapter_id: UInt64, slot_id: UInt64, panel_id: UInt64, owner: Address?)
41
42    access(all)
43    event ChapterMetadataRegistered(chapter_id: UInt64, required_slot_ids: [UInt64], reward_ids: [UInt64])
44
45    access(all)
46    event Completed(chapter_id: UInt64, address: Address, required_slot_ids: [UInt64], burned_panel_ids: [UInt64], burned_panel_uuids: [UInt64], burned_panel_editions: [UInt64], rewardTemplateIds: [UInt64])
47
48    access(all)
49    event RewardSent(id: UInt64, name: String, thumbnail: String, address: Address, player_id: UInt64?, chapter_index: UInt64?, chapter_id: UInt64?)
50
51    access(all)
52    let CollectionStoragePath: StoragePath
53
54    access(all)
55    let CollectionPublicPath: PublicPath
56
57    access(account)
58    let royalties: [MetadataViews.Royalty]
59
60    access(all)
61    let chapterTemplates:{ UInt64: ChapterTemplate}
62
63    access(all)
64    let panelTemplates:{ UInt64: PanelMintDetail}
65
66    access(all)
67    var currentSerial: UInt64
68
69    access(all)
70    struct CompletionStatus{ 
71        access(all)
72        let message: String
73
74        access(all)
75        let complete: Bool
76
77        access(all)
78        let slot_id_to_id:{ UInt64: UInt64}
79
80        access(all)
81        let extra:{ String: AnyStruct}
82
83        init(message: String, complete: Bool, slot_id_to_id:{ UInt64: UInt64}){ 
84            self.message = message
85            self.complete = complete
86            self.slot_id_to_id = slot_id_to_id
87            self.extra ={} 
88        }
89    }
90
91    access(all)
92    struct ChapterTemplate{ 
93        access(all)
94        let chapter_id: UInt64
95
96        access(all)
97        let chapter_index: UInt64
98
99        access(all)
100        let required_slot_ids: [UInt64]
101
102        access(all)
103        let reward_ids: [UInt64]
104
105        access(all)
106        let extra:{ String: AnyStruct}
107
108        init(chapter_id: UInt64, required_slot_ids: [UInt64], reward_ids: [UInt64], chapter_index: UInt64){ 
109            self.chapter_id = chapter_id
110            self.required_slot_ids = required_slot_ids
111            self.reward_ids = reward_ids
112            self.extra ={} 
113            self.chapter_index = chapter_index
114        }
115    }
116
117    access(account)
118    fun addChapterTemplate(_ chapter: ChapterTemplate){ 
119        self.chapterTemplates[chapter.chapter_id] = chapter
120        emit ChapterMetadataRegistered(chapter_id: chapter.chapter_id, required_slot_ids: chapter.required_slot_ids, reward_ids: chapter.reward_ids)
121    }
122
123    access(all)
124    struct PanelMintDetail{ 
125        access(all)
126        let panel_template: PanelTemplate
127
128        access(all)
129        let mint_count: UInt64?
130
131        access(all)
132        var total_supply: UInt64
133
134        access(all)
135        let extra:{ String: AnyStruct}
136
137        init(panel_template: PanelTemplate, mint_count: UInt64?, total_supply: UInt64){ 
138            self.panel_template = panel_template
139            self.mint_count = mint_count
140            self.total_supply = total_supply
141            self.extra ={} 
142        }
143
144        access(contract)
145        fun mint(_ edition: UInt64): PanelMintDetail{ 
146            pre{ 
147                self.mint_count == nil || self.mint_count! >= edition:
148                "Cannot mint panel with edition : ".concat(edition.toString())
149            }
150            self.total_supply = self.total_supply + 1
151            return self
152        }
153
154        access(all)
155        fun getStringDetail():{ String: String}{ 
156            let map:{ String: String} ={} 
157            map["mint_count"] = self.mint_count?.toString()
158            map["total_supply"] = self.total_supply.toString()
159            map["panel_title"] = self.panel_template.panel_title
160            map["rarity"] = self.panel_template.rarity
161            map["image_blurred_hash"] = self.panel_template.image_blurred_hash
162            map["image_clear_hash"] = self.panel_template.image_clear_hash
163            map["thumbnail_hash"] = self.panel_template.thumbnail_hash
164            map["license_id"] = self.panel_template.license_id.toString()
165            map["player_id"] = self.panel_template.player_id.toString()
166            map["slot_id"] = self.panel_template.slot_id.toString()
167            map["panel_id"] = self.panel_template.panel_id.toString()
168            map["description"] = self.panel_template.description
169            map["video_hash"] = self.panel_template.video_hash
170            return map
171        }
172    }
173
174    access(all)
175    struct PanelTemplate{ 
176        access(all)
177        let chapter_id: UInt64
178
179        access(all)
180        let license_id: UInt64
181
182        access(all)
183        let player_id: UInt64
184
185        access(all)
186        let slot_id: UInt64
187
188        access(all)
189        let panel_id: UInt64
190
191        access(all)
192        let panel_title: String
193
194        access(all)
195        let description: String
196
197        access(all)
198        let rarity: String
199
200        access(all)
201        let image_blurred_hash: String
202
203        access(all)
204        let image_clear_hash: String
205
206        access(all)
207        let thumbnail_hash: String
208
209        access(all)
210        let reveal_thumbnail_hash: String
211
212        access(all)
213        let video_hash: String
214
215        access(all)
216        let extra:{ String: AnyStruct}
217
218        // player info should be same across the chapter.
219        init(chapter_id: UInt64, license_id: UInt64, player_id: UInt64, slot_id: UInt64, panel_id: UInt64, panel_title: String, description: String, rarity: String, image_blurred_hash: String, image_clear_hash: String, thumbnail_hash: String, reveal_thumbnail_hash: String, video_hash: String){ 
220            self.chapter_id = chapter_id
221            self.license_id = license_id
222            self.player_id = player_id
223            self.slot_id = slot_id
224            self.panel_id = panel_id
225            self.panel_title = panel_title
226            self.description = description
227            self.rarity = rarity
228            self.image_blurred_hash = image_blurred_hash
229            self.image_clear_hash = image_clear_hash
230            self.thumbnail_hash = thumbnail_hash
231            self.reveal_thumbnail_hash = reveal_thumbnail_hash
232            self.video_hash = video_hash
233            self.extra ={} 
234        }
235
236        // This should never be nil unless it is set up with error
237        access(all)
238        fun getPlayer(): AeraNFT.Player{ 
239            return AeraNFT.getPlayer(self.player_id)!
240        }
241
242        access(all)
243        fun getLicense(): AeraNFT.License?{ 
244            return AeraNFT.getLicense(self.license_id)
245        }
246    }
247
248    access(account)
249    fun addPanelTemplate(panel: PanelTemplate, mint_count: UInt64?){ 
250        let mintDetail = PanelMintDetail(panel_template: panel, mint_count: mint_count, total_supply: 0)
251        self.panelTemplates[panel.panel_id] = mintDetail
252        emit PanelMetadataRegistered(chapter_id: panel.chapter_id, slot_id: panel.slot_id, panel_id: panel.panel_id, detail: mintDetail.getStringDetail())
253    }
254
255    access(all)
256    resource NFT: NonFungibleToken.NFT, ViewResolver.Resolver{ 
257        access(all)
258        let id: UInt64
259
260        // Question : Do we want things to be mutable here ?
261        access(all)
262        let panel_id: UInt64
263
264        access(all)
265        let serial: UInt64
266
267        access(all)
268        let edition: UInt64
269
270        access(all)
271        let max_edition: UInt64?
272
273        access(contract)
274        var activated: Bool
275
276        access(all)
277        var nounce: UInt64
278
279        // Question : Do we want things to be mutable here ?
280        access(all)
281        let tag:{ String: String}
282
283        access(all)
284        let scalar:{ String: UFix64}
285
286        access(self)
287        let extra:{ String: AnyStruct}
288
289        init(panel_id: UInt64, serial: UInt64, edition: UInt64, max_edition: UInt64?){ 
290            self.panel_id = panel_id
291            self.nounce = 0
292            self.id = self.uuid
293            self.serial = serial
294            self.edition = edition
295            self.max_edition = max_edition
296            self.activated = false
297            self.tag ={} 
298            self.scalar ={} 
299            self.extra ={} 
300        }
301
302        access(contract)
303        fun setActivated(_ bool: Bool){ 
304            let panel = self.getPanel()
305            var panicMessage = ""
306            if bool{ 
307                panicMessage = "This panel NFT is already staked. You cannot stake this again. ID : ".concat(self.id.toString())
308                emit PanelStaked(id: self.id, chapter_id: panel.chapter_id, slot_id: panel.slot_id, panel_id: panel.panel_id, owner: self.owner?.address)
309            } else{ 
310                panicMessage = "This panel NFT is not staked. ID : ".concat(self.id.toString())
311                emit PanelUnstaked(id: self.id, chapter_id: panel.chapter_id, slot_id: panel.slot_id, panel_id: panel.panel_id, owner: self.owner?.address)
312            }
313            if self.activated == bool{ 
314                panic(panicMessage)
315            }
316            self.activated = bool
317        }
318
319        access(all)
320        fun getPanel(): AeraPanels.PanelTemplate{ 
321            return (AeraPanels.panelTemplates[self.panel_id]!).panel_template
322        }
323
324        access(all)
325        view fun getViews(): [Type]{ 
326            let views: [Type] = [Type<MetadataViews.Display>(), Type<MetadataViews.Medias>(), Type<MetadataViews.Royalties>(), Type<MetadataViews.ExternalURL>(), Type<MetadataViews.NFTCollectionData>(), Type<MetadataViews.NFTCollectionDisplay>(), Type<MetadataViews.Traits>(), Type<MetadataViews.Serial>(), Type<MetadataViews.Editions>(), Type<MetadataViews.Rarity>(), Type<AeraNFT.License>(),Type<AeraRewards.RewardClaimedData>()]
327            return views
328        }
329
330        access(all)
331        fun resolveView(_ view: Type): AnyStruct?{ 
332            let panel = self.getPanel()
333            let thumbnail = MetadataViews.IPFSFile(cid: panel.thumbnail_hash, path: nil)
334            var revealData:{ String: String} ={} 
335            let revealViews: [Type] = [Type<AeraRewards.RewardClaimedData>()]
336            if revealViews.contains(view){ 
337                revealData ={ "id": self.id.toString(), "name": panel.panel_title, "chapterId": panel.chapter_id.toString(), "license_id": panel.license_id.toString(), "playerId": panel.player_id.toString(), "slotId": panel.slot_id.toString(), "panelId": panel.panel_id.toString(), "panelTitle": panel.panel_title, "description": panel.description, "image": panel.reveal_thumbnail_hash, "maxSerial": (self.max_edition!).toString(), "serial": self.serial.toString(), "edition": self.edition.toString(), "rarity": panel.rarity, "video_hash": panel.video_hash}
338            }
339            switch view{ 
340            case Type<MetadataViews.Display>():
341                let name = panel.panel_title
342                // Question : Any preferred description on panel NFT?
343                let description = panel.description
344                return MetadataViews.Display(name: name, description: description, thumbnail: thumbnail)
345            case Type<MetadataViews.ExternalURL>():
346                if let addr = self.owner?.address{ 
347                    return MetadataViews.ExternalURL("https://aera.onefootball.com/collectibles/".concat(addr.toString()).concat("/panels/").concat(self.id.toString()))
348                }
349                return MetadataViews.ExternalURL("https://aera.onefootball.com/marketplace/")
350            case Type<MetadataViews.Royalties>():
351                var address = AeraPanels.account.address
352                if address == 0x46625f59708ec2f8{ 
353                    //testnet merchant address
354                    address = 0x4ff956c78244911b
355                } else if address == 0x30cf5dcf6ea8d379{ 
356                    //mainnet merchant address
357                    address = 0xa9277dcbec7769df
358                }
359                let ducReceiver = getAccount(address).capabilities.get<&{FungibleToken.Receiver}>(/public/dapperUtilityCoinReceiver)
360                return MetadataViews.Royalties([MetadataViews.Royalty(receiver: ducReceiver!, cut: 0.06, description: "onefootball largest of 6% or 0.65")])
361            case Type<MetadataViews.Medias>():
362                // animation
363                let file = MetadataViews.IPFSFile(cid: panel.video_hash, path: nil)
364                let media = MetadataViews.Media(file: file, mediaType: "video/mp4")
365
366                // thumbnail
367                let thumbnailMedia = MetadataViews.Media(file: thumbnail, mediaType: "image/png")
368
369                // clear or blurred panel
370                var panelImageIpfsFile = MetadataViews.IPFSFile(cid: panel.image_clear_hash, path: nil)
371                if !self.activated{ 
372                    panelImageIpfsFile = MetadataViews.IPFSFile(cid: panel.image_blurred_hash, path: nil)
373                }
374                let panelImage = MetadataViews.Media(file: panelImageIpfsFile, mediaType: "image/png")
375
376                // pack opening asset
377                let packOpeningIpfsFile = MetadataViews.IPFSFile(cid: panel.reveal_thumbnail_hash, path: nil)
378                let packOpeningImage = MetadataViews.Media(file: packOpeningIpfsFile, mediaType: "image/png")
379                return MetadataViews.Medias([thumbnailMedia, panelImage, media, packOpeningImage])
380            case Type<MetadataViews.NFTCollectionDisplay>():
381                let externalURL = MetadataViews.ExternalURL("http://aera.onefootbal.com")
382                let squareImage = MetadataViews.Media(file: MetadataViews.IPFSFile(cid: "bafkreiameqwyluog75u7zg3dmf56b5mbed7cdgv6uslkph6nvmdf2aipmy", path: nil), mediaType: "image/jpg")
383                let bannerImage = MetadataViews.Media(file: MetadataViews.IPFSFile(cid: "bafybeiayhvr2sm4lco3tbsa74blynlnzhhzrjouteyqaq43giuyiln4xb4", path: nil), mediaType: "image/png")
384                let socialMap:{ String: MetadataViews.ExternalURL} ={ "twitter": MetadataViews.ExternalURL("https://twitter.com/aera_football"), "discord": MetadataViews.ExternalURL("https://discord.gg/aera"), "instagram": MetadataViews.ExternalURL("https://www.instagram.com/aera_football/")}
385                return MetadataViews.NFTCollectionDisplay(name: "Footballers Journey Panel", description: "Aera by OneFootball", externalURL: externalURL, squareImage: squareImage, bannerImage: bannerImage, socials: socialMap)
386            case Type<MetadataViews.NFTCollectionData>():
387                return MetadataViews.NFTCollectionData(storagePath: AeraPanels.CollectionStoragePath, publicPath: AeraPanels.CollectionPublicPath, publicCollection: Type<&Collection>(), publicLinkedType: Type<&Collection>(), createEmptyCollectionFunction: fun (): @{NonFungibleToken.Collection}{ 
388                    return <-AeraPanels.createEmptyCollection(nftType: Type<@AeraPanels.Collection>())
389                })
390            case Type<MetadataViews.Traits>():
391                let chapter = AeraPanels.chapterTemplates[panel.chapter_id] ?? panic("Chapter ID does not exist : ".concat(panel.chapter_id.toString()))
392                let player = panel.getPlayer()
393                let ts: [MetadataViews.Trait] = [MetadataViews.Trait(name: "chapter_id", value: panel.chapter_id, displayType: "number", rarity: nil), MetadataViews.Trait(name: "chapter_index", value: chapter.chapter_index, displayType: "number", rarity: nil), MetadataViews.Trait(name: "slot", value: panel.slot_id, displayType: "number", rarity: nil), MetadataViews.Trait(name: "panel_id", value: panel.panel_id, displayType: "number", rarity: nil), MetadataViews.Trait(name: "panel_description", value: panel.description, displayType: "string", rarity: nil),																																																																																																																																													  
394                // Add new
395                MetadataViews.Trait(name: "player_id", value: player.id, displayType: "number", rarity: nil), MetadataViews.Trait(name: "player_jersey_name", value: player.jerseyname, displayType: "string", rarity: nil), MetadataViews.Trait(name: "player_position", value: player.position, displayType: "string", rarity: nil), MetadataViews.Trait(name: "player_number", value: player.number, displayType: "number", rarity: nil), MetadataViews.Trait(name: "player_nationality", value: player.nationality, displayType: "string", rarity: nil), MetadataViews.Trait(name: "player_birthday", value: player.birthday, displayType: "date", rarity: nil)]
396                if let l = panel.getLicense(){ 
397                    ts.append(MetadataViews.Trait(name: "copyright", value: l.copyright, displayType: "string", rarity: nil))
398                }
399                return MetadataViews.Traits(ts)
400            case Type<MetadataViews.Serial>():
401                return MetadataViews.Serial(self.serial)
402            case Type<MetadataViews.Editions>():
403                return MetadataViews.Editions([MetadataViews.Edition(name: "set", number: self.edition, max: self.max_edition)])
404            case Type<AeraRewards.RewardClaimedData>():
405                return AeraRewards.RewardClaimedData(revealData)
406            case Type<MetadataViews.Rarity>():
407                var rarity: UFix64 = 0.0
408                switch panel.rarity{ 
409                case "Common":
410                    rarity = 1.0
411                case "Rare":
412                    rarity = 2.0
413                }
414                return MetadataViews.Rarity(score: rarity, max: nil, description: panel.rarity)
415            case Type<AeraNFT.License>():
416                if let license = panel.getLicense(){ 
417                    return license
418                }
419                return nil
420            case Type<AeraPanels.PanelTemplate>():
421                return (AeraPanels.panelTemplates[self.panel_id]!).panel_template
422            }
423            return nil
424        }
425
426        access(account)
427        fun increaseNounce(){ 
428            self.nounce = self.nounce + 1
429        }
430
431        access(contract)
432        fun setLastOwner(){ 
433            self.extra["lastOwner"] = (self.owner!).address
434        }
435
436        access(all)
437        fun getLastOwner(): Address?{ 
438            if let owner = self.extra["lastOwner"]{ 
439                return owner as? Address
440            }
441            return nil
442        }
443
444        access(all)
445        fun createEmptyCollection(): @{NonFungibleToken.Collection}{ 
446            return <-create Collection()
447        }
448    }
449
450    access(all)
451    resource interface CollectionPublic{ 
452        access(all)
453        fun getPanelIdMap():{ UInt64: [UInt64]}
454
455        access(all)
456        fun hasNFT(_ id: UInt64): Bool
457    }
458
459    access(all)
460    resource Collection: NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.Collection, NonFungibleToken.CollectionPublic, ViewResolver.ResolverCollection, CollectionPublic{ 
461        // dictionary of NFT conforming tokens
462        // NFT is a resource type with an `UInt64` ID field
463        access(all)
464        var ownedNFTs: @{UInt64:{ NonFungibleToken.NFT}}
465
466        // panel Id : UUID of Panel NFT
467        access(self)
468        var panelIdMap:{ UInt64: [UInt64]}
469
470        init(){ 
471            self.ownedNFTs <-{} 
472            self.panelIdMap ={} 
473        }
474
475        access(all)
476        fun hasNFT(_ id: UInt64): Bool{ 
477            return self.ownedNFTs.containsKey(id)
478        }
479
480        access(all)
481        fun getPanelIdMap():{ UInt64: [UInt64]}{ 
482            return self.panelIdMap
483        }
484
485        // withdraw removes an NFT from the collection and moves it to the caller
486        access(NonFungibleToken.Withdraw)
487        fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT}{ 
488            pre{ 
489                self.ownedNFTs.containsKey(withdrawID):
490                "missing NFT. ID : ".concat(withdrawID.toString())
491            }
492            let ref = self.borrowPanelNFT(id: withdrawID)
493            ref.setLastOwner()
494            let token <- self.ownedNFTs.remove(key: withdrawID)!
495            emit Withdraw(id: token.id, from: self.owner?.address)
496            let typedToken <- token as! @AeraPanels.NFT
497            let index = (self.panelIdMap[typedToken.panel_id]!).firstIndex(of: typedToken.uuid)!
498            (self.panelIdMap[typedToken.panel_id]!).remove(at: index)
499            return <-typedToken
500        }
501
502        // deposit takes a NFT and adds it to the collections dictionary
503        // and adds the ID to the id array
504        access(all)
505        fun deposit(token: @{NonFungibleToken.NFT}): Void{ 
506            let token <- token as! @NFT
507            assert(!token.activated || token.getLastOwner() == nil || token.getLastOwner()! == (self.owner!).address, message: "This panel is staked. Cannot be deposited. ID ".concat(token.id.toString()))
508            let id: UInt64 = token.id
509            let array = self.panelIdMap[token.panel_id] ?? []
510            array.append(token.uuid)
511            self.panelIdMap[token.panel_id] = array
512            token.increaseNounce()
513            // add the new token to the dictionary which removes the old one
514            let oldToken <- self.ownedNFTs[id] <- token
515            emit Deposit(id: id, to: self.owner?.address)
516            destroy oldToken
517        }
518
519        // getIDs returns an array of the IDs that are in the collection
520        access(all)
521        view fun getIDs(): [UInt64]{ 
522            return self.ownedNFTs.keys
523        }
524
525        // borrowNFT gets a reference to an NFT in the collection
526        // so that the caller can read its metadata and call its methods
527        access(all)
528        view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}?{ 
529            pre{ 
530                self.ownedNFTs.containsKey(id):
531                "Cannot borrow reference to Panel NFT ID : ".concat(id.toString())
532            }
533            return (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)!
534        }
535
536        access(all)
537        fun borrowPanelNFT(id: UInt64): &AeraPanels.NFT{ 
538            pre{ 
539                self.ownedNFTs.containsKey(id):
540                "Cannot borrow reference to Panel NFT ID : ".concat(id.toString())
541            }
542            let nft = (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)!
543            return nft as! &AeraPanels.NFT
544        }
545
546        access(all)
547        view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}?{ 
548            pre{ 
549                self.ownedNFTs.containsKey(id):
550                "Cannot borrow reference to Panel NFT ID : ".concat(id.toString())
551            }
552            let nft = (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)!
553            let aeraPanelNFTs = nft as! &NFT
554            return aeraPanelNFTs as &{ViewResolver.Resolver}
555        }
556
557        access(Owner)
558        fun stake(chapterId: UInt64, nftIds: [UInt64]){ 
559            let chapter = AeraPanels.chapterTemplates[chapterId] ?? panic("Chapter ID does not exist : ".concat(chapterId.toString()))
560            for id in nftIds{ 
561                let nft = self.borrowPanelNFT(id: id)
562                let panel = nft.getPanel()
563                assert(panel.chapter_id == chapterId, message: "NFT ID : ".concat(id.toString()).concat("is not under Chapter ID ").concat(chapterId.toString()))
564
565                // check if the other NFT with same Panel ID is activated. If they are, unstake them. Only 1 can be activated in a user storage at a time
566                let panelNFTs = self.panelIdMap[nft.panel_id]!
567                for p in panelNFTs{ 
568                    if p == id{ 
569                        continue
570                    }
571                    let p = self.borrowPanelNFT(id: p)
572                    if p.activated{ 
573                        p.setActivated(false)
574                    }
575                }
576                nft.setActivated(true)
577            }
578        }
579
580        access(Owner)
581        fun unstake(nftIds: [UInt64]){ 
582            for id in nftIds{ 
583                let nft = self.borrowPanelNFT(id: id)
584                nft.setActivated(false)
585            }
586        }
587
588        // pass in the chapter id,
589        // get information for verification from chapter struct
590        // store nft reference for sorting and emit better event
591        // burn the nfts and emit events with reward id
592        access(all)
593        fun queryActivateStatus(chapterId: UInt64, nftIds: [UInt64]): CompletionStatus{ 
594            let checked:{ UInt64: UInt64} ={} 
595            let chapter = AeraPanels.chapterTemplates[chapterId] ?? panic("Chapter ID does not exist : ".concat(chapterId.toString()))
596            let required_slot_ids = chapter.required_slot_ids
597            for id in nftIds{ 
598                let nft = self.borrowPanelNFT(id: id)
599                let panel = nft.getPanel()
600                if panel.chapter_id == chapterId{ 
601                    if required_slot_ids.contains(panel.slot_id){ 
602                        if checked[panel.slot_id] != nil{ 
603                            return CompletionStatus(message: "You are trying to burn 2 or more panels of the same template. IDs : ".concat(nft.id.toString()).concat(" , ").concat((checked[panel.slot_id]!).toString()), complete: false, slot_id_to_id:{} )
604                        }
605                        checked[panel.slot_id] = nft.id
606                    }
607                }
608
609                // check if the panel is staked. If it's not, panic
610                if !nft.activated{ 
611                    return CompletionStatus(message: "Cannot burn unstaked panel for reward. Please stake it. ID : ".concat(id.toString()), complete: false, slot_id_to_id:{} )
612                }
613            }
614            if required_slot_ids.length != checked.length{ 
615                return CompletionStatus(message: "Please ensure you passed in all the needed panels for claiming the reward", complete: false, slot_id_to_id:{} )
616            }
617            return CompletionStatus(message: "Completed", complete: true, slot_id_to_id: checked)
618        }
619
620        access(Owner)
621        fun activate(chapterId: UInt64, nfts: [FindViews.AuthNFTPointer], receiver: &{NonFungibleToken.Receiver}){ 
622
623            // create a id map to pointers and get the ids
624            let nftIds: [UInt64] = []
625            let mappedNFTs:{ UInt64: FindViews.AuthNFTPointer} ={} 
626            for p in nfts{ 
627                mappedNFTs[p.id] = p
628                nftIds.append(p.id)
629            }
630            let completeStatus = self.queryActivateStatus(chapterId: chapterId, nftIds: nftIds)
631            if !completeStatus.complete{ 
632                panic(completeStatus.message)
633            }
634            let burned_panel_ids: [UInt64] = []
635            let burned_panel_uuids: [UInt64] = []
636            let burned_panel_editions: [UInt64] = []
637            let chapter = AeraPanels.chapterTemplates[chapterId] ?? panic("Chapter ID does not exist : ".concat(chapterId.toString()))
638            let required_slot_ids = chapter.required_slot_ids
639            var rewardData:{ UInt64:{ String: String}} ={} 
640            var chapter_reward_ids = ""
641            for i, rewardId in chapter.reward_ids{ 
642                if i > 0{ 
643                    chapter_reward_ids = chapter_reward_ids.concat(",")
644                }
645                chapter_reward_ids = chapter_reward_ids.concat(rewardId.toString())
646            }
647            for slot_id in required_slot_ids{ 
648                let nftId = completeStatus.slot_id_to_id[slot_id]!
649                let pointer = mappedNFTs[nftId]!
650                let vr = pointer.getViewResolver()
651                if let view = vr.resolveView(Type<AeraRewards.RewardClaimedData>()){ 
652                    if let v = view as? AeraRewards.RewardClaimedData{ 
653                        rewardData[pointer.id] = v.data
654                    }
655                }
656                let edition = (MetadataViews.getEditions(vr)!).infoList[0].number
657                let panel = vr.resolveView(Type<AeraPanels.PanelTemplate>())! as! AeraPanels.PanelTemplate
658                burned_panel_ids.append(panel.panel_id)
659                burned_panel_uuids.append(pointer.id)
660                burned_panel_editions.append(edition)
661                FindFurnace.burn(pointer: pointer, context:{ "tenant": "onefootball", "rewards": chapter_reward_ids})
662            }
663            // emit Completed(chapter_id: chapterId, address: self.owner!.address, required_slot_ids: required_slot_ids, burned_panel_ids: burned_panel_ids, burned_panel_uuids: burned_panel_uuids, burned_panel_editions: burned_panel_editions, rewardTemplateIds: chapter.reward_ids)
664            for reward_template_id in chapter.reward_ids{ 
665                AeraRewards.mintNFT(recipient: receiver, rewardTemplateId: reward_template_id, rewardFields: rewardData)
666                // let reward=AeraRewards.getReward(reward_template_id)
667                // emit RewardSent(id: rewardId, name: reward.reward_name, thumbnail: reward.thumbnail_hash, address: self.owner!.address, player_id:reward.detail_id["player_id"], chapter_index:reward.detail_id["chapter_index"], chapter_id: reward.detail_id["chapter_id"])
668            }
669        }
670
671        access(all)
672        view fun getSupportedNFTTypes():{ Type: Bool}{ 
673            panic("implement me")
674        }
675
676        access(all)
677        view fun isSupportedNFTType(type: Type): Bool{ 
678            panic("implement me")
679        }
680
681        access(all)
682        fun createEmptyCollection(): @{NonFungibleToken.Collection}{ 
683            return <-create Collection()
684        }
685    }
686
687    // public function that anyone can call to create a new empty collection
688    access(all)
689    fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection}{ 
690        return <-create Collection()
691    }
692
693    // mintNFT mints a new NFT with a new ID
694    // and deposit it in the recipients collection using their collection reference
695    //The distinction between sending in a reference and sending in a capability is that when you send in a reference it cannot be stored. So it can only be used in this method
696    //while a capability can be stored and used later. So in this case using a reference is the right choice, but it needs to be owned so that you can have a good event
697    access(account)
698    fun mintNFT(recipient: &{NonFungibleToken.Receiver}, edition: UInt64, panelTemplateId: UInt64){ 
699        pre{ 
700            recipient.owner != nil:
701            "Recipients NFT collection is not owned"
702            self.panelTemplates.containsKey(panelTemplateId):
703            "Panel template does not exist. ID : ".concat(panelTemplateId.toString())
704        }
705        AeraPanels.totalSupply = AeraPanels.totalSupply + 1
706        AeraPanels.currentSerial = AeraPanels.currentSerial + 1
707        let panelMintDetail = (self.panelTemplates[panelTemplateId]!).mint(edition)
708        // create a new NFT
709        var newNFT <- create NFT(panel_id: panelTemplateId, serial: AeraPanels.currentSerial, edition: edition, max_edition: panelMintDetail.mint_count)
710
711        //Always emit events on state changes! always contain human readable and machine readable information
712        emit Minted(id: newNFT.id, address: (recipient.owner!).address, panel_id: panelMintDetail.panel_template.panel_id, edition: edition)
713        // deposit it in the recipient's account using their reference
714        recipient.deposit(token: <-newNFT)
715    }
716
717    access(account)
718    fun addRoyaltycut(_ cutInfo: MetadataViews.Royalty){ 
719        var cutInfos = self.royalties
720        cutInfos.append(cutInfo)
721        // for validation only
722        let royalties = MetadataViews.Royalties(cutInfos)
723        self.royalties.append(cutInfo)
724    }
725
726    access(all) view fun getContractViews(resourceType: Type?): [Type] {
727        return [
728        Type<MetadataViews.NFTCollectionData>(),
729        Type<MetadataViews.NFTCollectionDisplay>()
730        ]
731    }
732
733    access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
734        switch viewType {
735        case Type<MetadataViews.NFTCollectionDisplay>():
736            let externalURL = MetadataViews.ExternalURL("http://aera.onefootbal.com")
737            let squareImage = MetadataViews.Media(file: MetadataViews.IPFSFile(cid: "bafkreiameqwyluog75u7zg3dmf56b5mbed7cdgv6uslkph6nvmdf2aipmy", path: nil), mediaType: "image/jpg")
738            let bannerImage = MetadataViews.Media(file: MetadataViews.IPFSFile(cid: "bafybeiayhvr2sm4lco3tbsa74blynlnzhhzrjouteyqaq43giuyiln4xb4", path: nil), mediaType: "image/png")
739            let socialMap:{ String: MetadataViews.ExternalURL} ={ "twitter": MetadataViews.ExternalURL("https://twitter.com/aera_football"), "discord": MetadataViews.ExternalURL("https://discord.gg/aera"), "instagram": MetadataViews.ExternalURL("https://www.instagram.com/aera_football/")}
740            return MetadataViews.NFTCollectionDisplay(name: "Footballers Journey Panel", description: "Aera by OneFootball", externalURL: externalURL, squareImage: squareImage, bannerImage: bannerImage, socials: socialMap)
741        case Type<MetadataViews.NFTCollectionData>():
742            return MetadataViews.NFTCollectionData(storagePath: AeraPanels.CollectionStoragePath, publicPath: AeraPanels.CollectionPublicPath, publicCollection: Type<&Collection>(), publicLinkedType: Type<&Collection>(), createEmptyCollectionFunction: fun (): @{NonFungibleToken.Collection}{ 
743                return <-AeraPanels.createEmptyCollection(nftType: Type<@AeraPanels.Collection>())
744            })
745        }
746        return nil
747    }
748
749
750
751    init(){ 
752        // Initialize the total supply
753        self.totalSupply = 0
754        self.currentSerial = 0
755        self.chapterTemplates ={} 
756        self.panelTemplates ={} 
757
758        // Set Royalty cuts in a transaction
759        self.royalties = []
760
761        // Set the named paths
762        self.CollectionStoragePath = /storage/aeraPanelNFT
763        self.CollectionPublicPath = /public/aeraPanelNFT
764        self.account.storage.save<@{NonFungibleToken.Collection}>(<-AeraPanels.createEmptyCollection(nftType: Type<@AeraPanels.Collection>()), to: AeraPanels.CollectionStoragePath)
765        var capability_1 = self.account.capabilities.storage.issue<&AeraPanels.Collection>(AeraPanels.CollectionStoragePath)
766        self.account.capabilities.publish(capability_1, at: AeraPanels.CollectionPublicPath)
767        var capability_2 = self.account.capabilities.storage.issue<&AeraPanels.Collection>(AeraPanels.CollectionStoragePath)
768        emit ContractInitialized()
769    }
770}
771