Smart Contract
AeraPanels
A.30cf5dcf6ea8d379.AeraPanels
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