Smart Contract
AeraRewards
A.30cf5dcf6ea8d379.AeraRewards
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3import MetadataViews from 0x1d7e57aa55817448
4import ViewResolver from 0x1d7e57aa55817448
5import AeraNFT from 0x30cf5dcf6ea8d379
6
7
8/*
9Aera Reward is the NFT for fulfilling all the required panels for a chapter
10consists of thumbnail and a DVM
11*/
12
13access(all)
14contract AeraRewards: NonFungibleToken{
15 access(all)
16 var totalSupply: UInt64
17
18 access(all)
19 event ContractInitialized()
20
21 access(all)
22 event Withdraw(id: UInt64, from: Address?)
23
24 access(all)
25 event Deposit(id: UInt64, to: Address?)
26
27 access(all)
28 event Minted(id: UInt64, address: Address, reward_template_id: UInt64, edition: UInt64)
29
30 // we cannot have address here as it will always be nil
31 access(all)
32 event Burned(id: UInt64, reward_template_id: UInt64, edition: UInt64)
33
34 access(all)
35 event RewardMetadataRegistered(reward_template_id: UInt64, reward_name: String, thumbnail: String, video: String, max_quantity: UInt64?)
36
37 access(all)
38 event RewardClaimed(id: UInt64, address: Address, rewardTemplateId: UInt64, rewardName: String, description: String, thumbnailHash: String, edition: UInt64, type: String, soulBounded: Bool, rarity: String, rewardFields:{ UInt64:{ String: String}})
39
40 access(all)
41 let CollectionStoragePath: StoragePath
42
43 access(all)
44 let CollectionPublicPath: PublicPath
45
46 access(account)
47 let royalties: [MetadataViews.Royalty]
48
49 access(all)
50 let rewardTemplates:{ UInt64: MintDetail}
51
52 access(all)
53 var currentSerial: UInt64
54
55 access(all)
56 struct RewardClaimedData{
57 access(all)
58 let data:{ String: String}
59
60 init(_ data:{ String: String}){
61 self.data = data
62 }
63 }
64
65 access(all)
66 struct MintDetail{
67 access(all)
68 let reward_template: RewardTemplate
69
70 access(all)
71 var current_edition: UInt64
72
73 access(all)
74 var total_supply: UInt64
75
76 access(all)
77 let max_quantity: UInt64?
78
79 init(reward_template: RewardTemplate, current_edition: UInt64, total_supply: UInt64, max_quantity: UInt64?){
80 self.reward_template = reward_template
81 self.current_edition = current_edition
82 self.total_supply = total_supply
83 self.max_quantity = max_quantity
84 }
85
86 access(account)
87 fun mint(): MintDetail{
88 pre{
89 self.max_quantity == nil || self.max_quantity! >= self.current_edition + UInt64(1):
90 "Cannot mint reward with edition greater than max quantity"
91 }
92 self.current_edition = self.current_edition + 1
93 self.total_supply = self.total_supply + 1
94 return self
95 }
96
97 access(account)
98 fun burn(){
99 self.total_supply = self.total_supply - 1
100 }
101 }
102
103 access(all)
104 struct Media{
105 access(all)
106 let name: String
107
108 access(all)
109 let media_type: String
110
111 access(all)
112 let ipfs_hash: String
113
114 access(all)
115 let extra:{ String: AnyStruct}
116
117 init(name: String, media_type: String, ipfs_hash: String){
118 self.name = name
119 self.media_type = media_type
120 self.ipfs_hash = ipfs_hash
121 self.extra ={}
122 }
123 }
124
125 access(all)
126 struct RewardTemplate{
127 access(all)
128 let reward_template_id: UInt64
129
130 access(all)
131 let reward_name: String
132
133 access(all)
134 let description: String
135
136 access(all)
137 let thumbnail_hash: String
138
139 access(all)
140 let video_hash: String
141
142 access(all)
143 let video_file: String
144
145 access(all)
146 let video_type: String
147
148 // for struct's use only
149 access(all)
150 var edition: UInt64
151
152 access(all)
153 let files: [Media]
154
155 access(all)
156 let type: String
157
158 access(all)
159 let detail_id:{ String: UInt64}
160
161 access(all)
162 let traits: [MetadataViews.Trait]
163
164 access(all)
165 let soulBounded: Bool
166
167 access(all)
168 let rarity: String
169
170 access(all)
171 let extra:{ String: AnyStruct}
172
173 init(reward_template_id: UInt64, reward_name: String, description: String, thumbnail_hash: String, video_hash: String, video_file: String, video_type: String, files: [Media], type: String, detail_id:{ String: UInt64}, traits: [MetadataViews.Trait], soulBounded: Bool, rarity: String){
174 pre{
175 video_hash == "" && video_file != "" || video_file == "" && video_hash != "":
176 "Requires either video_file or video_hash to be empty string"
177 }
178 self.reward_template_id = reward_template_id
179 self.reward_name = reward_name
180 self.description = description
181 self.thumbnail_hash = thumbnail_hash
182 self.video_hash = video_hash
183 self.video_file = video_file
184 self.video_type = video_type
185 self.extra ={}
186 self.edition = 0
187 self.type = type
188 self.detail_id = detail_id
189 self.traits = traits
190 self.files = files
191 self.soulBounded = soulBounded
192 self.rarity = rarity
193 }
194
195 access(all)
196 fun getPlayer(): AeraNFT.Player?{
197 if let p = self.detail_id["player_id"]{
198 return AeraNFT.getPlayer(p)
199 }
200 return nil
201 }
202
203 access(all)
204 fun getLicense(): AeraNFT.License?{
205 if let id = self.detail_id["license_id"]{
206 return AeraNFT.getLicense(id)
207 }
208 return nil
209 }
210
211 access(all)
212 fun getFiles(): [MetadataViews.Media]{
213 var m: [MetadataViews.Media] = []
214 for f in self.files{
215 m.append(MetadataViews.Media(file: MetadataViews.IPFSFile(cid: f.ipfs_hash, path: nil), mediaType: f.media_type))
216 }
217 return m
218 }
219
220 access(all)
221 fun getFileAsTraits(): [MetadataViews.Trait]{
222 var t: [MetadataViews.Trait] = []
223 for f in self.files{
224 t.append(MetadataViews.Trait(name: f.name, value: "ipfs://".concat(f.ipfs_hash), displayType: "string", rarity: nil))
225 }
226 return t
227 }
228
229 access(all)
230 fun getVideoAsFile():{ MetadataViews.File}{
231 if self.video_hash != ""{
232 return MetadataViews.IPFSFile(cid: self.video_hash, path: nil)
233 }
234 return MetadataViews.HTTPFile(url: self.video_file)
235 }
236 }
237
238 access(account)
239 fun addRewardTemplate(reward: RewardTemplate, maxQuantity: UInt64?){
240 let mintDetail = MintDetail(reward_template: reward, current_edition: 0, total_supply: 0, max_quantity: maxQuantity)
241 self.rewardTemplates[reward.reward_template_id] = mintDetail
242 emit RewardMetadataRegistered(reward_template_id: reward.reward_template_id, reward_name: reward.reward_name, thumbnail: "ipfs://".concat(reward.thumbnail_hash), video: reward.getVideoAsFile().uri(), max_quantity: maxQuantity)
243 }
244
245 access(all)
246 fun getReward(_ id: UInt64): RewardTemplate{
247 return (AeraRewards.rewardTemplates[id]!).reward_template
248 }
249
250 access(all)
251 resource NFT: NonFungibleToken.NFT, ViewResolver.Resolver{
252 access(all)
253 let id: UInt64
254
255 access(all)
256 let reward_template_id: UInt64
257
258 access(all)
259 let serial: UInt64
260
261 access(all)
262 let edition: UInt64
263
264 access(all)
265 let max_edition: UInt64?
266
267 access(all)
268 var nounce: UInt64
269
270 access(all)
271 let extra:{ String: AnyStruct}
272
273 init(reward_template_id: UInt64, serial: UInt64, edition: UInt64, max_edition: UInt64?){
274 self.reward_template_id = reward_template_id
275 self.nounce = 0
276 self.id = self.uuid
277 self.serial = serial
278 self.edition = edition
279 self.max_edition = max_edition
280 self.extra ={ }
281 }
282
283 access(all)
284 fun getReward(): AeraRewards.RewardTemplate{
285 return (AeraRewards.rewardTemplates[self.reward_template_id]!).reward_template
286 }
287
288 access(all)
289 fun getMintedAt(): UFix64?{
290 if let minted = self.extra["mintedAt"]{
291 let res = minted as! UFix64
292 return res
293 }
294 return nil
295 }
296
297 access(all)
298 view fun getViews(): [Type]{
299 let views: [Type] = [Type<MetadataViews.Display>(), Type<MetadataViews.Medias>(), Type<MetadataViews.Royalties>(), Type<MetadataViews.ExternalURL>(), Type<MetadataViews.NFTCollectionData>(), Type<MetadataViews.NFTCollectionDisplay>(), Type<MetadataViews.Serial>(), Type<MetadataViews.Editions>(), Type<MetadataViews.Traits>(), Type<MetadataViews.Rarity>(), Type<AeraNFT.License>(), Type<RewardTemplate>()]
300 let reward = (AeraRewards.rewardTemplates[self.reward_template_id]!).reward_template
301 return views
302 }
303
304 access(all)
305 fun resolveView(_ view: Type): AnyStruct?{
306 let reward = self.getReward()
307 let thumbnail = MetadataViews.IPFSFile(cid: reward.thumbnail_hash, path: nil)
308 let ipfsVideo = reward.getVideoAsFile()
309
310 // mind the mediaType here, to be confirmed
311 let thumbnailMedia = MetadataViews.Media(file: thumbnail, mediaType: "image/png")
312 let media = MetadataViews.Media(file: ipfsVideo, mediaType: reward.video_type)
313 switch view{
314 case Type<MetadataViews.Display>():
315 let name = reward.reward_name
316 // Question : Any preferred description on rewrad NFT?
317 let description = reward.description
318 let thumbnail = thumbnail
319 return MetadataViews.Display(name: name, description: description, thumbnail: thumbnail)
320 case Type<MetadataViews.ExternalURL>():
321 if let addr = self.owner?.address{
322 return MetadataViews.ExternalURL("https://aera.onefootball.com/collectibles/".concat(addr.toString()).concat("/rewards/").concat(self.id.toString()))
323 }
324 return MetadataViews.ExternalURL("https://aera.onefootball.com/collectibles/")
325 case Type<MetadataViews.Royalties>():
326 // return MetadataViews.Royalties(AeraRewards.royalties)
327 var address = AeraRewards.account.address
328 if address == 0x46625f59708ec2f8{
329 //testnet merchant address
330 address = 0x4ff956c78244911b
331 } else if address == 0x30cf5dcf6ea8d379{
332 //mainnet merchant address
333 address = 0xa9277dcbec7769df
334 }
335 let ducReceiver = getAccount(address).capabilities.get<&{FungibleToken.Receiver}>(/public/dapperUtilityCoinReceiver)
336 return MetadataViews.Royalties([MetadataViews.Royalty(receiver: ducReceiver!, cut: 0.06, description: "onefootball largest of 6% or 0.65")])
337 case Type<MetadataViews.Medias>():
338 let m = [thumbnailMedia, media]
339 let r = self.getReward()
340 m.appendAll(r.getFiles())
341 return MetadataViews.Medias(m)
342 case Type<MetadataViews.NFTCollectionDisplay>():
343 let externalURL = MetadataViews.ExternalURL("http://aera.onefootbal.com")
344 let squareImage = MetadataViews.Media(file: MetadataViews.IPFSFile(cid: "bafkreiameqwyluog75u7zg3dmf56b5mbed7cdgv6uslkph6nvmdf2aipmy", path: nil), mediaType: "image/jpg")
345 let bannerImage = MetadataViews.Media(file: MetadataViews.IPFSFile(cid: "bafybeiayhvr2sm4lco3tbsa74blynlnzhhzrjouteyqaq43giuyiln4xb4", path: nil), mediaType: "image/png")
346 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/")}
347 return MetadataViews.NFTCollectionDisplay(name: "Aera Rewards", description: "Aera by OneFootball", externalURL: externalURL, squareImage: squareImage, bannerImage: bannerImage, socials: socialMap)
348 case Type<MetadataViews.NFTCollectionData>():
349 return MetadataViews.NFTCollectionData(storagePath: AeraRewards.CollectionStoragePath, publicPath: AeraRewards.CollectionPublicPath, publicCollection: Type<&Collection>(), publicLinkedType: Type<&Collection>(), createEmptyCollectionFunction: fun (): @{NonFungibleToken.Collection}{
350 return <-AeraRewards.createEmptyCollection(nftType: Type<@AeraRewards.Collection>())
351 })
352 case Type<MetadataViews.Serial>():
353 return MetadataViews.Serial(self.serial)
354 case Type<MetadataViews.Editions>():
355 return MetadataViews.Editions([MetadataViews.Edition(name: "set", number: self.edition, max: self.max_edition)])
356 case Type<MetadataViews.Traits>():
357 let ts: [MetadataViews.Trait] = [MetadataViews.Trait(name: "reward_template_id", value: self.reward_template_id, displayType: "number", rarity: nil)]
358 for detail in reward.detail_id.keys{
359 ts.append(MetadataViews.Trait(name: detail, value: reward.detail_id[detail], displayType: "number", rarity: nil))
360 }
361 if let l = reward.getLicense(){
362 ts.append(MetadataViews.Trait(name: "copyright", value: l.copyright, displayType: "string", rarity: nil))
363 }
364 if let player = reward.getPlayer(){
365 ts.appendAll([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)])
366 }
367 if let m = self.getMintedAt(){
368 ts.append(MetadataViews.Trait(name: "minted_at", value: m, displayType: "date", rarity: nil))
369 }
370 let r = self.getReward()
371 ts.appendAll(r.getFileAsTraits())
372 ts.appendAll(r.traits)
373 return MetadataViews.Traits(ts)
374 case Type<AeraNFT.License>():
375 if let license = reward.getLicense(){
376 return license
377 }
378 return nil
379 case Type<AeraRewards.RewardTemplate>():
380 return (AeraRewards.rewardTemplates[self.reward_template_id]!).reward_template
381 case Type<MetadataViews.Rarity>():
382 let reward = (AeraRewards.rewardTemplates[self.reward_template_id]!).reward_template
383 return MetadataViews.Rarity(score: nil, max: nil, description: reward.rarity)
384 }
385 return nil
386 }
387
388 access(account)
389 fun increaseNounce(){
390 self.nounce = self.nounce + 1
391 }
392
393 access(all)
394 fun createEmptyCollection(): @{NonFungibleToken.Collection}{
395 return <-create Collection()
396 }
397 }
398
399 access(all)
400 resource Collection: NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.Collection, NonFungibleToken.CollectionPublic, ViewResolver.ResolverCollection{
401 // dictionary of NFT conforming tokens
402 // NFT is a resource type with an `UInt64` ID field
403 access(all)
404 var ownedNFTs: @{UInt64:{ NonFungibleToken.NFT}}
405
406 init(){
407 self.ownedNFTs <-{}
408 }
409
410 access(all)
411 fun hasNFT(_ id: UInt64): Bool{
412 return self.ownedNFTs.containsKey(id)
413 }
414
415 // withdraw removes an NFT from the collection and moves it to the caller
416 access(NonFungibleToken.Withdraw)
417 fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT}{
418 let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT")
419 emit Withdraw(id: token.id, from: self.owner?.address)
420 return <-token
421 }
422
423 // deposit takes a NFT and adds it to the collections dictionary
424 // and adds the ID to the id array
425 access(all)
426 fun deposit(token: @{NonFungibleToken.NFT}): Void{
427 let token <- token as! @NFT
428 let id: UInt64 = token.id
429 token.increaseNounce()
430 // add the new token to the dictionary which removes the old one
431 let oldToken <- self.ownedNFTs[id] <- token
432 emit Deposit(id: id, to: self.owner?.address)
433 destroy oldToken
434 }
435
436 // getIDs returns an array of the IDs that are in the collection
437 access(all)
438 view fun getIDs(): [UInt64]{
439 return self.ownedNFTs.keys
440 }
441
442 // borrowNFT gets a reference to an NFT in the collection
443 // so that the caller can read its metadata and call its methods
444 access(all)
445 view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}?{
446 pre{
447 self.ownedNFTs.containsKey(id):
448 "Cannot borrow reference to Reward NFT ID : ".concat(id.toString())
449 }
450 return (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)!
451 }
452
453 access(all)
454 fun borrowRewardNFT(id: UInt64): &AeraRewards.NFT{
455 pre{
456 self.ownedNFTs.containsKey(id):
457 "Cannot borrow reference to Reward NFT ID : ".concat(id.toString())
458 }
459 let nft = (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)!
460 return nft as! &AeraRewards.NFT
461 }
462
463 access(all)
464 view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}?{
465 pre{
466 self.ownedNFTs.containsKey(id):
467 "Cannot borrow reference to Reward NFT ID : ".concat(id.toString())
468 }
469 let nft = (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)!
470 let aerarewardNFT = nft as! &NFT
471 return aerarewardNFT as &{ViewResolver.Resolver}
472 }
473
474 access(all)
475 view fun getSupportedNFTTypes():{ Type: Bool}{
476 panic("implement me")
477 }
478
479 access(all)
480 view fun isSupportedNFTType(type: Type): Bool{
481 panic("implement me")
482 }
483
484 access(all)
485 fun createEmptyCollection(): @{NonFungibleToken.Collection}{
486 return <-create Collection()
487 }
488 }
489
490 // public function that anyone can call to create a new empty collection
491 access(all)
492 fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection}{
493 return <-create Collection()
494 }
495
496 // mintNFT mints a new NFT with a new ID
497 // and deposit it in the recipients collection using their collection reference
498 //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
499 //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
500 access(account)
501 fun mintNFT(recipient: &{NonFungibleToken.Receiver}, rewardTemplateId: UInt64, rewardFields:{ UInt64:{ String: String}}): UInt64{
502 pre{
503 recipient.owner != nil:
504 "Recipients NFT collection is not owned"
505 self.rewardTemplates.containsKey(rewardTemplateId):
506 "Reward template does not exist. ID : ".concat(rewardTemplateId.toString())
507 }
508 AeraRewards.totalSupply = AeraRewards.totalSupply + 1
509 AeraRewards.currentSerial = AeraRewards.currentSerial + 1
510 let rewardMintDetail = (self.rewardTemplates[rewardTemplateId]!).mint()
511 // create a new NFT
512 var newNFT <- create NFT(reward_template_id: rewardTemplateId, serial: AeraRewards.currentSerial, edition: rewardMintDetail.current_edition, max_edition: rewardMintDetail.max_quantity)
513 let t = rewardMintDetail.reward_template
514 //Always emit events on state changes! always contain human readable and machine readable information
515 emit Minted(id: newNFT.id, address: (recipient.owner!).address, reward_template_id: rewardTemplateId, edition: rewardMintDetail.current_edition)
516 emit RewardClaimed(id: newNFT.id, address: (recipient.owner!).address, rewardTemplateId: t.reward_template_id, rewardName: t.reward_name, description: t.description, thumbnailHash: t.thumbnail_hash, edition: rewardMintDetail.current_edition, type: t.type, soulBounded: t.soulBounded, rarity: t.rarity, rewardFields: rewardFields)
517
518 // deposit it in the recipient's account using their reference
519 let id = newNFT.id
520 recipient.deposit(token: <-newNFT)
521 return id
522 }
523
524 access(account)
525 fun addRoyaltycut(_ cutInfo: MetadataViews.Royalty){
526 var cutInfos = self.royalties
527 cutInfos.append(cutInfo)
528 // for validation only
529 let royalties = MetadataViews.Royalties(cutInfos)
530 self.royalties.append(cutInfo)
531 }
532
533 access(all) view fun getContractViews(resourceType: Type?): [Type] {
534 return [
535 Type<MetadataViews.NFTCollectionData>(),
536 Type<MetadataViews.NFTCollectionDisplay>()
537 ]
538 }
539
540 access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
541 switch viewType {
542
543 case Type<MetadataViews.NFTCollectionDisplay>():
544 let externalURL = MetadataViews.ExternalURL("http://aera.onefootbal.com")
545 let squareImage = MetadataViews.Media(file: MetadataViews.IPFSFile(cid: "bafkreiameqwyluog75u7zg3dmf56b5mbed7cdgv6uslkph6nvmdf2aipmy", path: nil), mediaType: "image/jpg")
546 let bannerImage = MetadataViews.Media(file: MetadataViews.IPFSFile(cid: "bafybeiayhvr2sm4lco3tbsa74blynlnzhhzrjouteyqaq43giuyiln4xb4", path: nil), mediaType: "image/png")
547 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/")}
548 return MetadataViews.NFTCollectionDisplay(name: "Aera Rewards", description: "Aera by OneFootball", externalURL: externalURL, squareImage: squareImage, bannerImage: bannerImage, socials: socialMap)
549 case Type<MetadataViews.NFTCollectionData>():
550 return MetadataViews.NFTCollectionData(storagePath: AeraRewards.CollectionStoragePath, publicPath: AeraRewards.CollectionPublicPath, publicCollection: Type<&Collection>(), publicLinkedType: Type<&Collection>(), createEmptyCollectionFunction: fun (): @{NonFungibleToken.Collection}{
551 return <-AeraRewards.createEmptyCollection(nftType: Type<@AeraRewards.Collection>())
552 })
553
554 }
555 return nil
556 }
557
558
559
560 init(){
561 // Initialize the total supply
562 self.totalSupply = 0
563 self.currentSerial = 0
564 self.rewardTemplates ={}
565
566 // Set Royalty cuts in a transaction
567 self.royalties = []
568
569 // Set the named paths
570 self.CollectionStoragePath = /storage/aeraRewardsNFT
571 self.CollectionPublicPath = /public/aeraRewardsNFT
572 self.account.storage.save<@{NonFungibleToken.Collection}>(<-AeraRewards.createEmptyCollection(nftType: Type<@AeraRewards.Collection>()), to: AeraRewards.CollectionStoragePath)
573 var capability_1 = self.account.capabilities.storage.issue<&AeraRewards.Collection>(AeraRewards.CollectionStoragePath)
574 self.account.capabilities.publish(capability_1, at: AeraRewards.CollectionPublicPath)
575 var capability_2 = self.account.capabilities.storage.issue<&AeraRewards.Collection>(AeraRewards.CollectionStoragePath)
576 emit ContractInitialized()
577 }
578}
579
580