Smart Contract
FlovatarDustCollectible
A.921ea449dffec68a.FlovatarDustCollectible
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3import FlowToken from 0x1654653399040a61
4import FlovatarDustCollectibleTemplate from 0x921ea449dffec68a
5import FlovatarDustCollectibleAccessory from 0x921ea449dffec68a
6import MetadataViews from 0x1d7e57aa55817448
7import FlovatarDustToken from 0x921ea449dffec68a
8import ViewResolver from 0x1d7e57aa55817448
9
10/*
11
12 The contract that defines the Dust Collectible NFT and a Collection to manage them
13
14
15This contract contains also the Admin resource that can be used to manage and generate the Dust Collectible Templates.
16
17 */
18
19access(all)
20contract FlovatarDustCollectible: NonFungibleToken{
21 access(all)
22 let CollectionStoragePath: StoragePath
23
24 access(all)
25 let CollectionPublicPath: PublicPath
26
27 access(all)
28 let AdminStoragePath: StoragePath
29
30 // These will be used in the Marketplace to pay out
31 // royalties to the creator and to the marketplace
32 access(account)
33 var royaltyCut: UFix64
34
35 access(account)
36 var marketplaceCut: UFix64
37
38 // Here we keep track of all the Flovatar unique combinations and names
39 // that people will generate to make sure that there are no duplicates
40 access(all)
41 var totalSupply: UInt64
42
43 access(contract)
44 let mintedCombinations:{ String: Bool}
45
46 access(contract)
47 let mintedNames:{ String: Bool}
48
49 access(all)
50 event ContractInitialized()
51
52 access(all)
53 event Withdraw(id: UInt64, from: Address?)
54
55 access(all)
56 event Deposit(id: UInt64, to: Address?)
57
58 access(all)
59 event Created(id: UInt64, mint: UInt64, series: UInt64, address: Address)
60
61 access(all)
62 event Updated(id: UInt64)
63
64 access(all)
65 event Destroyed(id: UInt64)
66
67 access(all)
68 event NameSet(id: UInt64, name: String)
69
70 access(all)
71 event PositionChanged(id: UInt64, position: String)
72
73 access(all)
74 event StoryAdded(id: UInt64, story: String)
75
76 access(all)
77 struct Royalties{
78 access(all)
79 let royalty: [Royalty]
80
81 init(royalty: [Royalty]){
82 self.royalty = royalty
83 }
84 }
85
86 access(all)
87 enum RoyaltyType: UInt8{
88 access(all)
89 case fixed
90
91 access(all)
92 case percentage
93 }
94
95 access(all)
96 struct Royalty{
97 access(all)
98 let wallet: Capability<&{FungibleToken.Receiver}>
99
100 access(all)
101 let cut: UFix64
102
103 //can be percentage
104 access(all)
105 let type: RoyaltyType
106
107 init(wallet: Capability<&{FungibleToken.Receiver}>, cut: UFix64, type: RoyaltyType){
108 if !wallet.check(){}
109 //panic("Capability not valid!")
110 self.wallet = wallet
111 self.cut = cut
112 self.type = type
113 }
114 }
115
116 access(all) entitlement PrivateEnt
117
118 // The public interface can show metadata and the content for the Flovatar.
119 // In addition to it, it provides methods to access the additional optional
120 // components (accessory, hat, eyeglasses, background) for everyone.
121 access(all)
122 resource interface Public{
123 access(all)
124 let id: UInt64
125
126 access(all)
127 let mint: UInt64
128
129 access(all)
130 let series: UInt64
131
132 access(all)
133 let combination: String
134
135 access(all)
136 let creatorAddress: Address
137
138 access(all)
139 let createdAt: UFix64
140
141 access(contract)
142 let royalties: Royalties
143
144 // these three are added because I think they will be in the standard. At least Dieter thinks it will be needed
145 access(contract)
146 var name: String
147
148 access(all)
149 let description: String
150
151 access(all)
152 let schema: String?
153
154 access(all)
155 fun getName(): String
156
157 access(all)
158 fun getSvg(): String
159
160 access(all)
161 fun getRoyalties(): Royalties
162
163 access(all)
164 fun getBio():{ String: String}
165
166 access(all)
167 fun getMetadata():{ String: String}
168
169 access(all)
170 fun getLayers():{ UInt32: UInt64?}
171
172 access(all)
173 fun getAccessories(): [UInt64]
174
175 access(all)
176 fun getSeries(): FlovatarDustCollectibleTemplate.CollectibleSeriesData?
177 }
178
179 //The private interface can update the Accessory, Hat, Eyeglasses and Background
180 //for the Flovatar and is accessible only to the owner of the NFT
181 access(all)
182 resource interface Private{
183 access(FlovatarDustCollectible.PrivateEnt)
184 fun setName(name: String, vault: @{FungibleToken.Vault}): String
185
186 access(FlovatarDustCollectible.PrivateEnt)
187 fun addStory(text: String, vault: @{FungibleToken.Vault}): String
188
189 access(FlovatarDustCollectible.PrivateEnt)
190 fun setPosition(latitude: Fix64, longitude: Fix64, vault: @{FungibleToken.Vault}): String
191
192 access(FlovatarDustCollectible.PrivateEnt)
193 fun setAccessory(layer: UInt32, accessory: @FlovatarDustCollectibleAccessory.NFT): @FlovatarDustCollectibleAccessory.NFT?
194
195 access(FlovatarDustCollectible.PrivateEnt)
196 fun removeAccessory(layer: UInt32): @FlovatarDustCollectibleAccessory.NFT?
197 }
198
199 //The NFT resource that implements both Private and Public interfaces
200 access(all)
201 resource NFT: NonFungibleToken.NFT, Public, Private {
202 access(all)
203 let id: UInt64
204
205 access(all)
206 let mint: UInt64
207
208 access(all)
209 let series: UInt64
210
211 access(all)
212 let combination: String
213
214 access(all)
215 let creatorAddress: Address
216
217 access(all)
218 let createdAt: UFix64
219
220 access(contract)
221 let royalties: Royalties
222
223 access(contract)
224 var name: String
225
226 access(all)
227 let description: String
228
229 access(all)
230 let schema: String?
231
232 access(self)
233 let bio:{ String: String}
234
235 access(self)
236 let metadata:{ String: String}
237
238 access(self)
239 let layers:{ UInt32: UInt64?}
240
241 access(self)
242 let accessories: @{UInt32: FlovatarDustCollectibleAccessory.NFT}
243
244 init(series: UInt64, layers:{ UInt32: UInt64?}, creatorAddress: Address, royalties: Royalties){
245 FlovatarDustCollectible.totalSupply = FlovatarDustCollectible.totalSupply + UInt64(1)
246
247 FlovatarDustCollectibleTemplate.increaseTotalMintedCollectibles(series: series)
248 let coreLayers:{ UInt32: UInt64} = FlovatarDustCollectible.getCoreLayers(series: series, layers: layers)
249 self.id = FlovatarDustCollectible.totalSupply
250 self.mint = FlovatarDustCollectibleTemplate.getTotalMintedCollectibles(series: series)!
251 self.series = series
252 self.combination = FlovatarDustCollectible.getCombinationString(series: series, layers: coreLayers)
253 self.creatorAddress = creatorAddress
254 self.createdAt = getCurrentBlock().timestamp
255 self.royalties = royalties
256 self.schema = nil
257 self.name = ""
258 self.description = ""
259 self.bio ={}
260 self.metadata ={}
261 self.layers = layers
262 self.accessories <-{}
263 }
264
265 access(all)
266 fun getID(): UInt64{
267 return self.id
268 }
269
270 access(all)
271 fun getMetadata():{ String: String}{
272 return self.metadata
273 }
274
275 access(all)
276 fun getRoyalties(): Royalties{
277 return self.royalties
278 }
279
280 access(all)
281 fun getBio():{ String: String}{
282 return self.bio
283 }
284
285 access(all)
286 fun getName(): String{
287 return self.name
288 }
289
290 access(all)
291 fun getSeries(): FlovatarDustCollectibleTemplate.CollectibleSeriesData?{
292 return FlovatarDustCollectibleTemplate.getCollectibleSeries(id: self.series)
293 }
294
295 // This will allow to change the Name of the Flovatar only once.
296 // It checks for the current name is empty, otherwise it will throw an error.
297 // $DUST vault must contain 100 tokens that will be burned in the process
298 access(FlovatarDustCollectible.PrivateEnt)
299 fun setName(name: String, vault: @{FungibleToken.Vault}): String{
300 pre{
301 // TODO: Make sure that the text of the name is sanitized
302 //and that bad words are not accepted?
303 name.length > 2:
304 "The name is too short"
305 name.length < 32:
306 "The name is too long"
307 self.name == "":
308 "The name has already been set"
309 vault.balance == 100.0:
310 "The amount of $DUST is not correct"
311 vault.isInstance(Type<@FlovatarDustToken.Vault>()):
312 "Vault not of the right Token Type"
313 }
314
315 // Makes sure that the name is available and not taken already
316 if FlovatarDustCollectible.checkNameAvailable(name: name) == false{
317 panic("This name has already been taken")
318 }
319 destroy vault
320 self.name = name
321
322 // Adds the name to the array to remember it
323 FlovatarDustCollectible.addMintedName(name: name)
324 emit NameSet(id: self.id, name: name)
325 return self.name
326 }
327
328 // This will allow to add a text Story to the Flovatar Bio.
329 // The String will be concatenated each time.
330 // There is a limit of 300 characters per story but there is no limit in the full concatenated story length
331 // $DUST vault must contain 50 tokens that will be burned in the process
332 access(FlovatarDustCollectible.PrivateEnt)
333 fun addStory(text: String, vault: @{FungibleToken.Vault}): String{
334 pre{
335 // TODO: Make sure that the text of the name is sanitized
336 //and that bad words are not accepted?
337 text.length > 0:
338 "The text is too short"
339 text.length <= 300:
340 "The text is too long"
341 vault.balance == 50.0:
342 "The amount of $DUST is not correct"
343 vault.isInstance(Type<@FlovatarDustToken.Vault>()):
344 "Vault not of the right Token Type"
345 }
346 destroy vault
347 let currentStory: String = self.bio["story"] ?? ""
348 let story: String = currentStory.concat(" ").concat(text)
349 self.bio.insert(key: "story", story)
350 emit StoryAdded(id: self.id, story: story)
351 return story
352 }
353
354 // This will allow to set the GPS location of a Flovatar
355 // It can be run multiple times and each time it will override the previous state
356 // $DUST vault must contain 10 tokens that will be burned in the process
357 access(FlovatarDustCollectible.PrivateEnt)
358 fun setPosition(latitude: Fix64, longitude: Fix64, vault: @{FungibleToken.Vault}): String{
359 pre{
360 latitude >= -90.0:
361 "The latitude is out of range"
362 latitude <= 90.0:
363 "The latitude is out of range"
364 longitude >= -180.0:
365 "The longitude is out of range"
366 longitude <= 180.0:
367 "The longitude is out of range"
368 vault.balance == 10.0:
369 "The amount of $DUST is not correct"
370 vault.isInstance(Type<@FlovatarDustToken.Vault>()):
371 "Vault not of the right Token Type"
372 }
373 destroy vault
374 let position: String = latitude.toString().concat(",").concat(longitude.toString())
375 self.bio.insert(key: "position", position)
376 emit PositionChanged(id: self.id, position: position)
377 return position
378 }
379
380 access(all)
381 fun getLayers():{ UInt32: UInt64?}{
382 return self.layers
383 }
384
385 access(all)
386 fun getAccessories(): [UInt64]{
387 let accessoriesIds: [UInt64] = []
388 for k in self.accessories.keys{
389 let accessoryId = self.accessories[k]?.id
390 if accessoryId != nil{
391 accessoriesIds.append(accessoryId!)
392 }
393 }
394 return accessoriesIds
395 }
396
397 // This will allow to change the Accessory of the Flovatar any time.
398 // It checks for the right category and series before executing.
399 access(FlovatarDustCollectible.PrivateEnt)
400 fun setAccessory(layer: UInt32, accessory: @FlovatarDustCollectibleAccessory.NFT): @FlovatarDustCollectibleAccessory.NFT?{
401 if(accessory.getSeries() != self.series) {
402 panic("The accessory belongs to a different series")
403 }
404 if FlovatarDustCollectibleTemplate.isCollectibleLayerAccessory(layer: layer, series: self.series){
405 emit Updated(id: self.id)
406 self.layers[layer] = accessory.templateId
407 let oldAccessory <- self.accessories[layer] <- accessory
408 return <-oldAccessory
409 }
410 panic("The Layer is out of range or it's not an accessory")
411 }
412
413 // This will allow to remove the Accessory of the Flovatar any time.
414 access(FlovatarDustCollectible.PrivateEnt)
415 fun removeAccessory(layer: UInt32): @FlovatarDustCollectibleAccessory.NFT?{
416 if FlovatarDustCollectibleTemplate.isCollectibleLayerAccessory(layer: layer, series: self.series){
417 emit Updated(id: self.id)
418 self.layers[layer] = nil
419 let accessory <- self.accessories[layer] <- nil
420 return <-accessory
421 }
422 panic("The Layer is out of range or it's not an accessory")
423 }
424
425 // This function will return the full SVG of the Flovatar. It will take the
426 // optional components (Accessory, Hat, Eyeglasses and Background) from their
427 // original Template resources, while all the other unmutable components are
428 // taken from the Metadata directly.
429 access(all)
430 fun getSvg(): String{
431 let series = FlovatarDustCollectibleTemplate.getCollectibleSeries(id: self.series)
432 let layersArr: [String] = []
433 for k in (series!).layers.keys{
434 layersArr.append("")
435 }
436 var svg: String = (series!).svgPrefix
437 for k in self.layers.keys{
438 if self.layers[k] != nil{
439 let layer = self.layers[k]!
440 if layer != nil{
441 let tempSvg = FlovatarDustCollectibleTemplate.getCollectibleTemplateSvg(id: layer!)
442 //svg = svg.concat(tempSvg!)
443 layersArr[k - UInt32(1)] = tempSvg!
444 }
445 }
446 }
447 for tempLayer in layersArr{
448 svg = svg.concat(tempLayer)
449 }
450 svg = svg.concat((series!).svgSuffix)
451 return svg
452 }
453
454 access(all)
455 view fun getViews(): [Type]{
456 return [
457 Type<MetadataViews.NFTCollectionData>(),
458 Type<MetadataViews.NFTCollectionDisplay>(),
459 Type<MetadataViews.Display>(),
460 Type<MetadataViews.Royalties>(),
461 Type<MetadataViews.Edition>(),
462 Type<MetadataViews.ExternalURL>(),
463 Type<MetadataViews.Serial>(),
464 Type<MetadataViews.Traits>(),
465 Type<MetadataViews.EVMBridgedMetadata>()
466 ]
467 }
468
469 access(all)
470 fun resolveView(_ view: Type): AnyStruct?{
471 if view == Type<MetadataViews.ExternalURL>(){
472 return MetadataViews.ExternalURL("https://flovatar.com/collectibles/".concat(self.id.toString()))
473 }
474 if view == Type<MetadataViews.Royalties>(){
475 let royalties: [MetadataViews.Royalty] = []
476 var count: Int = 0
477 for royalty in self.royalties.royalty{
478 royalties.append(MetadataViews.Royalty(receiver: royalty.wallet, cut: royalty.cut, description: "Flovatar Royalty ".concat(count.toString())))
479 count = count + Int(1)
480 }
481 return MetadataViews.Royalties(royalties)
482 }
483 if view == Type<MetadataViews.Serial>(){
484 return MetadataViews.Serial(self.id)
485 }
486 if view == Type<MetadataViews.Editions>(){
487 let series = self.getSeries()
488 var maxMint: UInt64 = (series!).maxMintable
489 if maxMint == UInt64(0){
490 maxMint = UInt64(999999)
491 }
492 let editionInfo = MetadataViews.Edition(name: "Flovatar Stardust Collectible Series ".concat(self.series.toString()), number: self.mint, max: maxMint)
493 let editionList: [MetadataViews.Edition] = [editionInfo]
494 return MetadataViews.Editions(editionList)
495 }
496 if view == Type<MetadataViews.NFTCollectionDisplay>(){
497 let mediaSquare = MetadataViews.Media(file: MetadataViews.HTTPFile(url: "https://images.flovatar.com/logo.svg"), mediaType: "image/svg+xml")
498 let mediaBanner = MetadataViews.Media(file: MetadataViews.HTTPFile(url: "https://images.flovatar.com/logo-horizontal.svg"), mediaType: "image/svg+xml")
499 return MetadataViews.NFTCollectionDisplay(name: "Flovatar Stardust Collectible", description: "The Flovatar Stardust Collectibles are the next generation of composable and customizable NFTs that populate the Flovatar Universe and can be minted exclusively by using the $DUST token.", externalURL: MetadataViews.ExternalURL("https://flovatar.com"), squareImage: mediaSquare, bannerImage: mediaBanner, socials:{ "discord": MetadataViews.ExternalURL("https://discord.gg/flovatar"), "twitter": MetadataViews.ExternalURL("https://twitter.com/flovatar"), "instagram": MetadataViews.ExternalURL("https://instagram.com/flovatar_nft"), "tiktok": MetadataViews.ExternalURL("https://www.tiktok.com/@flovatar")})
500 }
501 if view == Type<MetadataViews.Display>(){
502 return MetadataViews.Display(name: self.name == "" ? "Stardust Collectible #".concat(self.id.toString()) : self.name, description: self.description, thumbnail: MetadataViews.HTTPFile(url: "https://images.flovatar.com/collectible/svg/".concat(self.id.toString()).concat(".svg")))
503 }
504 if view == Type<MetadataViews.Traits>(){
505 let traits: [MetadataViews.Trait] = []
506 let series = self.getSeries()
507 for k in self.layers.keys{
508 if self.layers[k] != nil{
509 let layer = (series!).layers[k]!
510 if self.layers[k] != nil{
511 let layerSelf = self.layers[k]!
512 if layer != nil{
513 let template = FlovatarDustCollectibleTemplate.getCollectibleTemplate(id: layerSelf!)
514 let trait = MetadataViews.Trait(name: (layer!).name, value: (template!).name, displayType: "String", rarity: MetadataViews.Rarity(score: nil, max: nil, description: (template!).rarity))
515 traits.append(trait)
516 }
517 }
518 }
519 }
520 return MetadataViews.Traits(traits)
521 }
522 if view == Type<MetadataViews.NFTCollectionData>(){
523 return MetadataViews.NFTCollectionData(storagePath: FlovatarDustCollectible.CollectionStoragePath, publicPath: FlovatarDustCollectible.CollectionPublicPath, publicCollection: Type<&FlovatarDustCollectible.Collection>(), publicLinkedType: Type<&FlovatarDustCollectible.Collection>(), createEmptyCollectionFunction: fun (): @{NonFungibleToken.Collection}{
524 return <-FlovatarDustCollectible.createEmptyCollection(nftType: Type<@FlovatarDustCollectible.Collection>())
525 })
526 }
527 if view == Type<MetadataViews.EVMBridgedMetadata>(){
528 let contractLevel = FlovatarDustCollectible.resolveContractView(
529 resourceType: nil,
530 viewType: Type<MetadataViews.EVMBridgedMetadata>()
531 ) as! MetadataViews.EVMBridgedMetadata?
532 ?? panic("Could not resolve contract-level EVMBridgedMetadata")
533
534 return MetadataViews.EVMBridgedMetadata(
535 name: contractLevel.name,
536 symbol: contractLevel.symbol,
537 uri: MetadataViews.URI(
538 baseURI: "https://flovatar.com/collectibles/json/",
539 value: self.id.toString()
540 )
541 )
542 }
543 return nil
544 }
545
546 access(all)
547 fun createEmptyCollection(): @{NonFungibleToken.Collection}{
548 return <-create Collection()
549 }
550 }
551
552 // Standard NFT collectionPublic interface that can also borrowFlovatar as the correct type
553 access(all)
554 resource interface CollectionPublic{
555 access(all)
556 fun deposit(token: @{NonFungibleToken.NFT})
557
558 access(all)
559 fun getIDs(): [UInt64]
560
561 access(all)
562 view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}?
563
564 access(all)
565 fun borrowDustCollectible(id: UInt64): &FlovatarDustCollectible.NFT?{
566 // If the result isn't nil, the id of the returned reference
567 // should be the same as the argument to the function
568 post{
569 result == nil || result?.id == id:
570 "Cannot borrow Flovatar Dust Collectible reference: The ID of the returned reference is incorrect"
571 }
572 }
573 }
574
575 // Main Collection to manage all the Flovatar NFT
576 access(all)
577 resource Collection: CollectionPublic, NonFungibleToken.Collection {
578 // dictionary of NFT conforming tokens
579 // NFT is a resource type with an `UInt64` ID field
580 access(all)
581 var ownedNFTs: @{UInt64:{ NonFungibleToken.NFT}}
582
583 init(){
584 self.ownedNFTs <-{}
585 }
586
587 access(all) view fun getLength(): Int {
588 return self.ownedNFTs.length
589 }
590
591 // withdraw removes an NFT from the collection and moves it to the caller
592 access(NonFungibleToken.Withdraw)
593 fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT}{
594 let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT")
595 emit Withdraw(id: token.id, from: self.owner?.address)
596 return <-token
597 }
598
599 // deposit takes a NFT and adds it to the collections dictionary
600 // and adds the ID to the id array
601 access(all)
602 fun deposit(token: @{NonFungibleToken.NFT}){
603 let token <- token as! @FlovatarDustCollectible.NFT
604 let id: UInt64 = token.id
605
606 // add the new token to the dictionary which removes the old one
607 let oldToken <- self.ownedNFTs[id] <- token
608 emit Deposit(id: id, to: self.owner?.address)
609 destroy oldToken
610 }
611
612 // getIDs returns an array of the IDs that are in the collection
613 access(all)
614 view fun getIDs(): [UInt64]{
615 return self.ownedNFTs.keys
616 }
617
618 // borrowNFT gets a reference to an NFT in the collection
619 // so that the caller can read its metadata and call its methods
620 access(all)
621 view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}?{
622 return (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)
623 }
624
625 // borrowFlovatar returns a borrowed reference to a Flovatar
626 // so that the caller can read data and call methods from it.
627 access(all)
628 fun borrowDustCollectible(id: UInt64): &FlovatarDustCollectible.NFT?{
629 if self.ownedNFTs[id] != nil {
630 let ref = (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)!
631 let collectibleNFT = ref as! &FlovatarDustCollectible.NFT
632 return collectibleNFT
633 } else {
634 return nil
635 }
636 }
637
638 /*
639 // borrowFlovatarPrivate returns a borrowed reference to a Flovatar using the Private interface
640 // so that the caller can read data and call methods from it, like setting the optional components.
641 */
642 access(FlovatarDustCollectible.PrivateEnt)
643 fun borrowDustCollectiblePrivate(id: UInt64): auth(FlovatarDustCollectible.PrivateEnt) &FlovatarDustCollectible.NFT?{
644 if self.ownedNFTs[id] != nil{
645 let ref = (&self.ownedNFTs[id] as auth(FlovatarDustCollectible.PrivateEnt) &{NonFungibleToken.NFT}?)!
646 return ref as! auth(FlovatarDustCollectible.PrivateEnt) &FlovatarDustCollectible.NFT
647 } else{
648 return nil
649 }
650 }
651
652 access(all)
653 view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}?{
654 if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? {
655 return nft as &{ViewResolver.Resolver}
656 }
657 return nil
658 }
659
660 /// getSupportedNFTTypes returns a list of NFT types that this receiver accepts
661 access(all)
662 view fun getSupportedNFTTypes(): {Type: Bool} {
663 let supportedTypes: {Type: Bool} = {}
664 supportedTypes[Type<@FlovatarDustCollectible.NFT>()] = true
665 return supportedTypes
666 }
667
668 /// Returns whether or not the given type is accepted by the collection
669 /// A collection that can accept any type should just return true by default
670 access(all)
671 view fun isSupportedNFTType(type: Type): Bool {
672 return type == Type<@FlovatarDustCollectible.NFT>()
673 }
674
675 access(all)
676 fun createEmptyCollection(): @{NonFungibleToken.Collection}{
677 return <-FlovatarDustCollectible.createEmptyCollection(nftType: Type<@FlovatarDustCollectible.NFT>())
678 }
679 }
680
681 // public function that anyone can call to create a new empty collection
682 access(all)
683 fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection}{
684 return <-create Collection()
685 }
686
687
688
689 access(all)
690 view fun getContractViews(resourceType: Type?): [Type] {
691 return [
692 Type<MetadataViews.NFTCollectionData>(),
693 Type<MetadataViews.NFTCollectionDisplay>(),
694 Type<MetadataViews.EVMBridgedMetadata>()
695 ]
696 }
697
698
699 access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
700 switch viewType {
701 case Type<MetadataViews.NFTCollectionData>():
702 let collectionData = MetadataViews.NFTCollectionData(
703 storagePath: self.CollectionStoragePath,
704 publicPath: self.CollectionPublicPath,
705 publicCollection: Type<&FlovatarDustCollectible.Collection>(),
706 publicLinkedType: Type<&FlovatarDustCollectible.Collection>(),
707 createEmptyCollectionFunction: (fun(): @{NonFungibleToken.Collection} {
708 return <-FlovatarDustCollectible.createEmptyCollection(nftType: Type<@FlovatarDustCollectible.NFT>())
709 })
710 )
711 return collectionData
712 case Type<MetadataViews.NFTCollectionDisplay>():
713 let media = MetadataViews.Media(
714 file: MetadataViews.HTTPFile(
715 url: "https://images.flovatar.com/logo.svg"
716 ),
717 mediaType: "image/svg+xml"
718 )
719 let mediaBanner = MetadataViews.Media(
720 file: MetadataViews.HTTPFile(
721 url: "https://images.flovatar.com/logo-horizontal.svg"
722 ),
723 mediaType: "image/svg+xml"
724 )
725 return MetadataViews.NFTCollectionDisplay(
726 name: "Flovatar Dust Collectible Collection",
727 description: "Flovatar is pioneering a new way to unleash community creativity in Web3 by allowing users to be co-creators of their prized NFTs, instead of just being passive collectors.",
728 externalURL: MetadataViews.ExternalURL("https://flovatar.com"),
729 squareImage: media,
730 bannerImage: mediaBanner,
731 socials: {
732 "twitter": MetadataViews.ExternalURL("https://x.com/flovatar"),
733 "discord": MetadataViews.ExternalURL("https://discord.gg/flovatar"),
734 "instagram": MetadataViews.ExternalURL("https://instagram.com/flovatar_nft"),
735 "tiktok": MetadataViews.ExternalURL("https://www.tiktok.com/@flovatar")
736 }
737 )
738 case Type<MetadataViews.EVMBridgedMetadata>():
739 // Implementing this view gives the project control over how the bridged NFT is represented as an ERC721
740 // when bridged to EVM on Flow via the public infrastructure bridge.
741
742 // Compose the contract-level URI. In this case, the contract metadata is located on some HTTP host,
743 // but it could be IPFS, S3, a data URL containing the JSON directly, etc.
744 return MetadataViews.EVMBridgedMetadata(
745 name: "FlovatarDustCollectible",
746 symbol: "XMPL",
747 uri: MetadataViews.URI(
748 baseURI: nil, // setting baseURI as nil sets the given value as the uri field value
749 value: "https://flovatar.com"
750 )
751 )
752 }
753 return nil
754 }
755
756
757
758
759 // This struct is used to send a data representation of the Flovatar Dust Collectibles
760 // when retrieved using the contract helper methods outside the collection.
761 access(all)
762 struct FlovatarDustCollectibleData{
763 access(all)
764 let id: UInt64
765
766 access(all)
767 let mint: UInt64
768
769 access(all)
770 let series: UInt64
771
772 access(all)
773 let name: String
774
775 access(all)
776 let svg: String?
777
778 access(all)
779 let combination: String
780
781 access(all)
782 let creatorAddress: Address
783
784 access(all)
785 let layers:{ UInt32: UInt64?}
786
787 access(all)
788 let bio:{ String: String}
789
790 access(all)
791 let metadata:{ String: String}
792
793 init(id: UInt64, mint: UInt64, series: UInt64, name: String, svg: String?, combination: String, creatorAddress: Address, layers:{ UInt32: UInt64?}, bio:{ String: String}, metadata:{ String: String}){
794 self.id = id
795 self.mint = mint
796 self.series = series
797 self.name = name
798 self.svg = svg
799 self.combination = combination
800 self.creatorAddress = creatorAddress
801 self.layers = layers
802 self.bio = bio
803 self.metadata = metadata
804 }
805 }
806
807 // This function will look for a specific Flovatar on a user account and return a FlovatarData if found
808 access(all)
809 fun getCollectible(address: Address, collectibleId: UInt64): FlovatarDustCollectibleData?{
810 let account = getAccount(address)
811
812 if let collectibleCollection = account.capabilities.borrow<&FlovatarDustCollectible.Collection>(FlovatarDustCollectible.CollectionPublicPath){
813 if let collectible = collectibleCollection.borrowDustCollectible(id: collectibleId){
814 return FlovatarDustCollectibleData(id: collectibleId, mint: (collectible!).mint, series: (collectible!).series, name: (collectible!).getName(), svg: (collectible!).getSvg(), combination: (collectible!).combination, creatorAddress: (collectible!).creatorAddress, layers: (collectible!).getLayers(), bio: (collectible!).getBio(), metadata: (collectible!).getMetadata())
815 }
816 }
817
818 return nil
819 }
820
821 // This function will return all Flovatars on a user account and return an array of FlovatarData
822 access(all)
823 fun getCollectibles(address: Address): [FlovatarDustCollectibleData]{
824 var dustCollectibleData: [FlovatarDustCollectibleData] = []
825
826 let account = getAccount(address)
827 if let collectibleCollection = account.capabilities.borrow<&FlovatarDustCollectible.Collection>(FlovatarDustCollectible.CollectionPublicPath){
828 for id in collectibleCollection.getIDs(){
829 if let collectible = collectibleCollection.borrowDustCollectible(id: id){
830 dustCollectibleData.append(FlovatarDustCollectibleData(id: id, mint: (collectible!).mint, series: (collectible!).series, name: (collectible!).getName(), svg: nil, combination: (collectible!).combination, creatorAddress: (collectible!).creatorAddress, layers: (collectible!).getLayers(), bio: (collectible!).getBio(), metadata: (collectible!).getMetadata()))
831 }
832 }
833 }
834
835 return dustCollectibleData
836 }
837
838 // This returns all the previously minted combinations, so that duplicates won't be allowed
839 access(all)
840 fun getMintedCombinations(): [String]{
841 return FlovatarDustCollectible.mintedCombinations.keys
842 }
843
844 // This returns all the previously minted names, so that duplicates won't be allowed
845 access(all)
846 fun getMintedNames(): [String]{
847 return FlovatarDustCollectible.mintedNames.keys
848 }
849
850 // This function will add a minted combination to the array
851 access(account)
852 fun addMintedCombination(combination: String){
853 FlovatarDustCollectible.mintedCombinations.insert(key: combination, true)
854 }
855
856 // This function will add a new name to the array
857 access(account)
858 fun addMintedName(name: String){
859 FlovatarDustCollectible.mintedNames.insert(key: name, true)
860 }
861
862 access(all)
863 fun getCoreLayers(series: UInt64, layers:{ UInt32: UInt64?}):{ UInt32: UInt64}{
864 let coreLayers:{ UInt32: UInt64} ={}
865 for k in layers.keys{
866 if !FlovatarDustCollectibleTemplate.isCollectibleLayerAccessory(layer: k, series: series){
867 let templateId = layers[k]!
868 let template = FlovatarDustCollectibleTemplate.getCollectibleTemplate(id: templateId!)!
869 if template.series != series{
870 panic("Template belonging to the wrong Dust Collectible Series")
871 }
872 if template.layer != k{
873 panic("Template belonging to the wrong Layer")
874 }
875 coreLayers[k] = templateId!
876 }
877 }
878 return coreLayers
879 }
880
881 // This helper function will generate a string from a list of components,
882 // to be used as a sort of barcode to keep the inventory of the minted
883 // Flovatars and to avoid duplicates
884 access(all)
885 fun getCombinationString(series: UInt64, layers:{ UInt32: UInt64}): String{
886 var combination = "S".concat(series.toString())
887 var i: UInt32 = UInt32(2)
888 while i < UInt32(7){
889 if layers[i] != nil{
890 let layerId = layers[i]!
891 combination = combination.concat("-L").concat(i.toString()).concat("_").concat(layerId.toString())
892 }
893 i = i + UInt32(1)
894 }
895 //Disabling because is not ordered and will generate duplicates
896 //for k in layers.keys {
897 // if(layers[k] != nil){
898 // let layerId = layers[k]!
899 // combination = combination.concat("-L").concat(k.toString()).concat("_").concat(layerId.toString())
900 // }
901 //}
902 return combination
903 }
904
905 // This function will get a list of component IDs and will check if the
906 // generated string is unique or if someone already used it before.
907 access(all)
908 fun checkCombinationAvailable(series: UInt64, layers:{ UInt32: UInt64}): Bool{
909 let combinationString = FlovatarDustCollectible.getCombinationString(series: series, layers: layers)
910 return !FlovatarDustCollectible.mintedCombinations.containsKey(combinationString)
911 }
912
913 // This will check if a specific Name has already been taken
914 // and assigned to some Flovatar
915 access(all)
916 fun checkNameAvailable(name: String): Bool{
917 return name.length > 2 && name.length < 20 && !FlovatarDustCollectible.mintedNames.containsKey(name)
918 }
919
920 // This is a public function that anyone can call to generate a new Flovatar Dust Collectible
921 // A list of components resources needs to be passed to executed.
922 // It will check first for uniqueness of the combination + name and will then
923 // generate the Flovatar and burn all the passed components.
924 // The Spark NFT will entitle to use any common basic component (body, hair, etc.)
925 // In order to use special rare components a boost of the same rarity will be needed
926 // for each component used
927 access(all)
928 fun createDustCollectible(series: UInt64, layers: [UInt32], templateIds: [UInt64?], address: Address, vault: @{FungibleToken.Vault}): @FlovatarDustCollectible.NFT{
929 pre{
930 vault.isInstance(Type<@FlovatarDustToken.Vault>()):
931 "Vault not of the right Token Type"
932 }
933 let seriesData = FlovatarDustCollectibleTemplate.getCollectibleSeries(id: series)
934 if seriesData == nil{
935 panic("Dust Collectible Series not found!")
936 }
937 if (seriesData!).layers.length != layers.length{
938 panic("The amount of layers is not matching!")
939 }
940 if templateIds.length != layers.length{
941 panic("The amount of layers and templates is not matching!")
942 }
943 let mintedCollectibles = FlovatarDustCollectibleTemplate.getTotalMintedCollectibles(series: series)
944 if mintedCollectibles != nil{
945 if mintedCollectibles! >= (seriesData!).maxMintable{
946 panic("Reached the maximum mint number for this Series!")
947 }
948 }
949 let templates: [FlovatarDustCollectibleTemplate.CollectibleTemplateData] = []
950 var totalPrice: UFix64 = 0.0
951 let coreLayers:{ UInt32: UInt64} ={}
952 let fullLayers:{ UInt32: UInt64?} ={}
953 var i: UInt32 = UInt32(0)
954 while i < UInt32(layers.length){
955 let layerId: UInt32 = layers[i]!
956 let templateId: UInt64? = templateIds[i] ?? nil
957 if !FlovatarDustCollectibleTemplate.isCollectibleLayerAccessory(layer: layerId, series: series){
958 if templateId == nil{
959 panic("Core Layer missing ".concat(layerId.toString()).concat(" - ").concat(i.toString()).concat("/").concat(layers.length.toString()))
960 }
961 let template = FlovatarDustCollectibleTemplate.getCollectibleTemplate(id: templateId!)!
962 if template.series != series{
963 panic("Template belonging to the wrong Dust Collectible Series")
964 }
965 if template.layer != layerId{
966 panic("Template belonging to the wrong Layer")
967 }
968 let totalMintedComponents: UInt64 = FlovatarDustCollectibleTemplate.getTotalMintedComponents(id: template.id)!
969 // Makes sure that the original minting limit set for each Template has not been reached
970 if totalMintedComponents >= template.maxMintableComponents{
971 panic("Reached maximum mintable count for this trait")
972 }
973 coreLayers[layerId] = template.id
974 fullLayers[layerId] = template.id
975 templates.append(template)
976 totalPrice = totalPrice + FlovatarDustCollectibleTemplate.getTemplateCurrentPrice(id: template.id)!
977
978
979 FlovatarDustCollectibleTemplate.increaseTotalMintedComponents(id: template.id)
980 FlovatarDustCollectibleTemplate.increaseTemplatesCurrentPrice(id: template.id)
981 FlovatarDustCollectibleTemplate.setLastComponentMintedAt(id: template.id, value: getCurrentBlock().timestamp)
982
983 } else{
984 fullLayers[layerId] = nil
985 }
986 i = i + UInt32(1)
987 }
988 if totalPrice > vault.balance{
989 panic("Not enough tokens provided")
990 }
991
992 // Generates the combination string to check for uniqueness.
993 // This is like a barcode that defines exactly which components were used
994 // to create the Flovatar
995 let combinationString = FlovatarDustCollectible.getCombinationString(series: series, layers: coreLayers)
996
997 // Makes sure that the combination is available and not taken already
998 if FlovatarDustCollectible.mintedCombinations.containsKey(combinationString) == true{
999 panic("This combination has already been taken")
1000 }
1001 let royalties: [Royalty] = []
1002 let creatorAccount = getAccount(address)
1003 royalties.append(Royalty(wallet: creatorAccount.capabilities.get<&FlowToken.Vault>(/public/flowTokenReceiver), cut: FlovatarDustCollectible.getRoyaltyCut(), type: RoyaltyType.percentage))
1004 royalties.append(Royalty(wallet: self.account.capabilities.get<&FlowToken.Vault>(/public/flowTokenReceiver), cut: FlovatarDustCollectible.getMarketplaceCut(), type: RoyaltyType.percentage))
1005
1006 // Mint the new Flovatar NFT by passing the metadata to it
1007 var newNFT <- create NFT(series: series, layers: fullLayers, creatorAddress: address, royalties: Royalties(royalty: royalties))
1008
1009 // Adds the combination to the arrays to remember it
1010 FlovatarDustCollectible.addMintedCombination(combination: combinationString)
1011
1012 // Emits the Created event to notify about its existence
1013 emit Created(id: newNFT.id, mint: newNFT.mint, series: newNFT.series, address: address)
1014 destroy vault
1015 return <-newNFT
1016 }
1017
1018 // These functions will return the current Royalty cuts for
1019 // both the Creator and the Marketplace.
1020 access(all)
1021 fun getRoyaltyCut(): UFix64{
1022 return self.royaltyCut
1023 }
1024
1025 access(all)
1026 fun getMarketplaceCut(): UFix64{
1027 return self.marketplaceCut
1028 }
1029
1030 // Only Admins will be able to call the set functions to
1031 // manage Royalties and Marketplace cuts.
1032 access(account)
1033 fun setRoyaltyCut(value: UFix64){
1034 self.royaltyCut = value
1035 }
1036
1037 access(account)
1038 fun setMarketplaceCut(value: UFix64){
1039 self.marketplaceCut = value
1040 }
1041
1042 // This is the main Admin resource that will allow the owner
1043 // to generate new Templates, Components and Packs
1044 access(all)
1045 resource Admin{
1046
1047
1048 //This will create a new FlovatarComponentTemplate that
1049 // contains all the SVG and basic informations to represent
1050 // a specific part of the Flovatar (body, hair, eyes, mouth, etc.)
1051 // More info in the FlovatarComponentTemplate.cdc file
1052 access(all)
1053 fun createCollectibleSeries(name: String, description: String, svgPrefix: String, svgSuffix: String, priceIncrease: UFix64, layers:{ UInt32: FlovatarDustCollectibleTemplate.Layer}, colors:{ UInt32: String}, metadata:{ String: String}, maxMintable: UInt64): @FlovatarDustCollectibleTemplate.CollectibleSeries{
1054 return <-FlovatarDustCollectibleTemplate.createCollectibleSeries(name: name, description: description, svgPrefix: svgPrefix, svgSuffix: svgSuffix, priceIncrease: priceIncrease, layers: layers, colors: colors, metadata: metadata, maxMintable: maxMintable)
1055 }
1056
1057 //This will create a new FlovatarComponentTemplate that
1058 // contains all the SVG and basic informations to represent
1059 // a specific part of the Flovatar (body, hair, eyes, mouth, etc.)
1060 // More info in the FlovatarComponentTemplate.cdc file
1061 access(all)
1062 fun createCollectibleTemplate(name: String, description: String, series: UInt64, layer: UInt32, metadata:{ String: String}, rarity: String, basePrice: UFix64, svg: String, maxMintableComponents: UInt64): @FlovatarDustCollectibleTemplate.CollectibleTemplate{
1063 return <-FlovatarDustCollectibleTemplate.createCollectibleTemplate(name: name, description: description, series: series, layer: layer, metadata: metadata, rarity: rarity, basePrice: basePrice, svg: svg, maxMintableComponents: maxMintableComponents)
1064 }
1065
1066 //This will mint a new Component based from a selected Template
1067 access(all)
1068 fun createCollectible(templateId: UInt64): @FlovatarDustCollectibleAccessory.NFT{
1069 return <-FlovatarDustCollectibleAccessory.createCollectibleAccessoryInternal(templateId: templateId)
1070 }
1071
1072 //This will mint Components in batch and return a Collection instead of the single NFT
1073 access(all)
1074 fun batchCreateCollectibles(templateId: UInt64, quantity: UInt64): @FlovatarDustCollectibleAccessory.Collection{
1075 return <-FlovatarDustCollectibleAccessory.batchCreateCollectibleAccessory(templateId: templateId, quantity: quantity)
1076 }
1077
1078 // With this function you can generate a new Admin resource
1079 // and pass it to another user if needed
1080 access(all)
1081 fun createNewAdmin(): @Admin{
1082 return <-create Admin()
1083 }
1084
1085 // Helper functions to update the Royalty cut
1086 access(all)
1087 fun setRoyaltyCut(value: UFix64){
1088 FlovatarDustCollectible.setRoyaltyCut(value: value)
1089 }
1090
1091 // Helper functions to update the Marketplace cut
1092 access(all)
1093 fun setMarketplaceCut(value: UFix64){
1094 FlovatarDustCollectible.setMarketplaceCut(value: value)
1095 }
1096
1097 }
1098
1099 init(){
1100 self.CollectionPublicPath = /public/FlovatarDustCollectibleCollection
1101 self.CollectionStoragePath = /storage/FlovatarDustCollectibleCollection
1102 self.AdminStoragePath = /storage/FlovatarDustCollectibleAdmin
1103
1104 // Initialize the total supply
1105 self.totalSupply = UInt64(0)
1106 self.mintedCombinations ={}
1107 self.mintedNames ={}
1108
1109 // Set the default Royalty and Marketplace cuts
1110 self.royaltyCut = 0.01
1111 self.marketplaceCut = 0.05
1112 self.account.storage.save<@{NonFungibleToken.Collection}>(<-FlovatarDustCollectible.createEmptyCollection(nftType: Type<@FlovatarDustCollectible.Collection>()), to: FlovatarDustCollectible.CollectionStoragePath)
1113 var capability_1 = self.account.capabilities.storage.issue<&{FlovatarDustCollectible.CollectionPublic}>(FlovatarDustCollectible.CollectionStoragePath)
1114 self.account.capabilities.publish(capability_1, at: FlovatarDustCollectible.CollectionPublicPath)
1115
1116 // Put the Admin resource in storage
1117 self.account.storage.save<@Admin>(<-create Admin(), to: self.AdminStoragePath)
1118 emit ContractInitialized()
1119 }
1120}
1121