Smart Contract
FlovatarComponent
A.921ea449dffec68a.FlovatarComponent
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3import FlowToken from 0x1654653399040a61
4import FlovatarComponentTemplate from 0x921ea449dffec68a
5import MetadataViews from 0x1d7e57aa55817448
6import ViewResolver from 0x1d7e57aa55817448
7
8/*
9
10 This contract defines the Flovatar Component NFT and the Collection to manage them.
11 Components are like the building blocks (lego bricks) of the final Flovatar (body, mouth, hair, eyes, etc.) and they can be traded as normal NFTs.
12 Components are linked to a specific Template that will ultimately contain the SVG and all the other metadata
13
14 */
15
16access(all)
17contract FlovatarComponent: NonFungibleToken{
18 access(all)
19 let CollectionStoragePath: StoragePath
20
21 access(all)
22 let CollectionPublicPath: PublicPath
23
24 // Counter for all the Components ever minted
25 access(all)
26 var totalSupply: UInt64
27
28 // Standard events that will be emitted
29 access(all)
30 event ContractInitialized()
31
32 access(all)
33 event Withdraw(id: UInt64, from: Address?)
34
35 access(all)
36 event Deposit(id: UInt64, to: Address?)
37
38 access(all)
39 event Created(id: UInt64, templateId: UInt64, mint: UInt64)
40
41 access(all)
42 event Destroyed(id: UInt64, templateId: UInt64)
43
44 access(all) entitlement PrivateEnt
45
46 // The public interface provides all the basic informations about
47 // the Component and also the Template ID associated with it.
48 access(all)
49 resource interface Public{
50 access(all)
51 let id: UInt64
52
53 access(all)
54 let templateId: UInt64
55
56 access(all)
57 let mint: UInt64
58
59 access(all)
60 fun getTemplate(): FlovatarComponentTemplate.ComponentTemplateData
61
62 access(all)
63 fun getSvg(): String
64
65 access(all)
66 fun getCategory(): String
67
68 access(all)
69 fun getSeries(): UInt32
70
71 access(all)
72 fun getRarity(): String
73
74 access(all)
75 fun isBooster(rarity: String): Bool
76
77 access(all)
78 fun checkCategorySeries(category: String, series: UInt32): Bool
79
80 //these three are added because I think they will be in the standard. At least Dieter thinks it will be needed
81 access(all)
82 let name: String
83
84 access(all)
85 let description: String
86
87 access(all)
88 let schema: String?
89 }
90
91 // The NFT resource that implements the Public interface as well
92 access(all)
93 resource NFT: NonFungibleToken.NFT, Public{
94 access(all)
95 let id: UInt64
96
97 access(all)
98 let templateId: UInt64
99
100 access(all)
101 let mint: UInt64
102
103 access(all)
104 let name: String
105
106 access(all)
107 let description: String
108
109 access(all)
110 let schema: String?
111
112 // Initiates the NFT from a Template ID.
113 init(templateId: UInt64){
114 FlovatarComponent.totalSupply = FlovatarComponent.totalSupply + UInt64(1)
115 let componentTemplate = FlovatarComponentTemplate.getComponentTemplate(id: templateId)!
116 self.id = FlovatarComponent.totalSupply
117 self.templateId = templateId
118 self.mint = FlovatarComponentTemplate.getTotalMintedComponents(id: templateId)! + UInt64(1)
119 self.name = componentTemplate.name
120 self.description = componentTemplate.description
121 self.schema = nil
122
123
124 // Increments the counter and stores the timestamp
125 FlovatarComponentTemplate.setTotalMintedComponents(id: templateId, value: self.mint)
126 FlovatarComponentTemplate.setLastComponentMintedAt(id: templateId, value: getCurrentBlock().timestamp)
127 }
128
129 access(all)
130 fun getID(): UInt64{
131 return self.id
132 }
133
134 // Returns the Template associated to the current Component
135 access(all)
136 fun getTemplate(): FlovatarComponentTemplate.ComponentTemplateData{
137 return FlovatarComponentTemplate.getComponentTemplate(id: self.templateId)!
138 }
139
140 // Gets the SVG from the parent Template
141 access(all)
142 fun getSvg(): String{
143 return self.getTemplate().svg!
144 }
145
146 // Gets the category from the parent Template
147 access(all)
148 fun getCategory(): String{
149 return self.getTemplate().category
150 }
151
152 // Gets the series number from the parent Template
153 access(all)
154 fun getSeries(): UInt32{
155 return self.getTemplate().series
156 }
157
158 // Gets the rarity from the parent Template
159 access(all)
160 fun getRarity(): String{
161 return self.getTemplate().rarity
162 }
163
164 // Check the boost and rarity from the parent Template
165 access(all)
166 fun isBooster(rarity: String): Bool{
167 let template = self.getTemplate()
168 return template.category == "boost" && template.rarity == rarity
169 }
170
171 //Check the category and series from the parent Template
172 access(all)
173 fun checkCategorySeries(category: String, series: UInt32): Bool{
174 let template = self.getTemplate()
175 return template.category == category && template.series == series
176 }
177
178 // Emit a Destroyed event when it will be burned to create a Flovatar
179 // This will help to keep track of how many Components are still
180 // available on the market.
181 access(all)
182 view fun getViews(): [Type]{
183 return [
184 Type<MetadataViews.NFTCollectionData>(),
185 Type<MetadataViews.NFTCollectionDisplay>(),
186 Type<MetadataViews.Display>(),
187 Type<MetadataViews.Royalties>(),
188 Type<MetadataViews.Edition>(),
189 Type<MetadataViews.ExternalURL>(),
190 Type<MetadataViews.Serial>(),
191 Type<MetadataViews.Traits>(),
192 Type<MetadataViews.EVMBridgedMetadata>()
193 ]
194 }
195
196 access(all)
197 fun resolveView(_ view: Type): AnyStruct?{
198 if view == Type<MetadataViews.ExternalURL>(){
199 let address = self.owner?.address
200 let url = address == nil ? "https://flovatar.com/builder/" : "https://flovatar.com/components/".concat(self.id.toString()).concat("/").concat((address!).toString())
201 return MetadataViews.ExternalURL("https://flovatar.com/builder/")
202 }
203 if view == Type<MetadataViews.Royalties>(){
204 let royalties: [MetadataViews.Royalty] = []
205 royalties.append(MetadataViews.Royalty(receiver: FlovatarComponent.account.capabilities.get<&FlowToken.Vault>(/public/flowTokenReceiver), cut: 0.05, description: "Flovatar Royalty"))
206 return MetadataViews.Royalties(royalties)
207 }
208 if view == Type<MetadataViews.Serial>(){
209 return MetadataViews.Serial(self.id)
210 }
211 if view == Type<MetadataViews.Editions>(){
212 let componentTemplate: FlovatarComponentTemplate.ComponentTemplateData = self.getTemplate()
213 let editionInfo = MetadataViews.Edition(name: "Flovatar Component", number: self.mint, max: componentTemplate.maxMintableComponents)
214 let editionList: [MetadataViews.Edition] = [editionInfo]
215 return MetadataViews.Editions(editionList)
216 }
217 if view == Type<MetadataViews.NFTCollectionDisplay>(){
218 let mediaSquare = MetadataViews.Media(file: MetadataViews.HTTPFile(url: "https://images.flovatar.com/logo.svg"), mediaType: "image/svg+xml")
219 let mediaBanner = MetadataViews.Media(file: MetadataViews.HTTPFile(url: "https://images.flovatar.com/logo-horizontal.svg"), mediaType: "image/svg+xml")
220 return MetadataViews.NFTCollectionDisplay(name: "Flovatar Component", 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.", 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")})
221 }
222 if view == Type<MetadataViews.Display>(){
223 return MetadataViews.Display(name: self.name, description: self.description, thumbnail: MetadataViews.HTTPFile(url: "https://flovatar.com/api/image/template/".concat(self.templateId.toString())))
224 }
225 if view == Type<MetadataViews.Traits>(){
226 let traits: [MetadataViews.Trait] = []
227 let template = self.getTemplate()
228 let trait = MetadataViews.Trait(name: template.category, value: template.name, displayType: "String", rarity: MetadataViews.Rarity(score: nil, max: nil, description: template.rarity))
229 traits.append(trait)
230 let setTrait = MetadataViews.Trait(name: "set", value: template.category, displayType: "String", rarity: nil)
231 traits.append(setTrait)
232 return MetadataViews.Traits(traits)
233 }
234 if view == Type<MetadataViews.Rarity>(){
235 let template = self.getTemplate()
236 return MetadataViews.Rarity(score: nil, max: nil, description: template.rarity)
237 }
238 if view == Type<MetadataViews.NFTCollectionData>(){
239 return MetadataViews.NFTCollectionData(storagePath: FlovatarComponent.CollectionStoragePath, publicPath: FlovatarComponent.CollectionPublicPath, publicCollection: Type<&FlovatarComponent.Collection>(), publicLinkedType: Type<&FlovatarComponent.Collection>(), createEmptyCollectionFunction: fun (): @{NonFungibleToken.Collection}{
240 return <-FlovatarComponent.createEmptyCollection(nftType: Type<@FlovatarComponent.Collection>())
241 })
242 }
243
244 if view == Type<MetadataViews.EVMBridgedMetadata>(){
245 let contractLevel = FlovatarComponent.resolveContractView(
246 resourceType: nil,
247 viewType: Type<MetadataViews.EVMBridgedMetadata>()
248 ) as! MetadataViews.EVMBridgedMetadata?
249 ?? panic("Could not resolve contract-level EVMBridgedMetadata")
250
251 return MetadataViews.EVMBridgedMetadata(
252 name: contractLevel.name,
253 symbol: contractLevel.symbol,
254 uri: MetadataViews.URI(
255 baseURI: "https://flovatar.com/components/json/",
256 value: self.id.toString()
257 )
258 )
259 }
260 return nil
261 }
262
263 access(all)
264 fun createEmptyCollection(): @{NonFungibleToken.Collection}{
265 return <-create Collection()
266 }
267 }
268
269 // Standard NFT collectionPublic interface that can also borrowComponent as the correct type
270 access(all)
271 resource interface CollectionPublic{
272 access(all)
273 fun deposit(token: @{NonFungibleToken.NFT})
274
275 access(all)
276 fun getIDs(): [UInt64]
277
278 access(all)
279 view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}?
280
281 access(all)
282 fun borrowComponent(id: UInt64): &FlovatarComponent.NFT?{
283 // If the result isn't nil, the id of the returned reference
284 // should be the same as the argument to the function
285 post{
286 result == nil || result?.id == id:
287 "Cannot borrow Component reference: The ID of the returned reference is incorrect"
288 }
289 }
290
291 }
292
293 // Main Collection to manage all the Components NFT
294 access(all)
295 resource Collection: CollectionPublic, NonFungibleToken.Collection {
296 // dictionary of NFT conforming tokens
297 // NFT is a resource type with an `UInt64` ID field
298 access(all)
299 var ownedNFTs: @{UInt64:{ NonFungibleToken.NFT}}
300
301 init(){
302 self.ownedNFTs <-{}
303 }
304
305 access(all) view fun getLength(): Int {
306 return self.ownedNFTs.length
307 }
308
309 // withdraw removes an NFT from the collection and moves it to the caller
310 access(NonFungibleToken.Withdraw)
311 fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT}{
312 let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT")
313 emit Withdraw(id: token.id, from: self.owner?.address)
314 return <-token
315 }
316
317 // deposit takes a NFT and adds it to the collections dictionary
318 // and adds the ID to the id array
319 access(all)
320 fun deposit(token: @{NonFungibleToken.NFT}){
321 let token <- token as! @FlovatarComponent.NFT
322 let id: UInt64 = token.id
323
324 // add the new token to the dictionary which removes the old one
325 let oldToken <- self.ownedNFTs[id] <- token
326 emit Deposit(id: id, to: self.owner?.address)
327 destroy oldToken
328 }
329
330 // getIDs returns an array of the IDs that are in the collection
331 access(all)
332 view fun getIDs(): [UInt64]{
333 return self.ownedNFTs.keys
334 }
335
336 // borrowNFT gets a reference to an NFT in the collection
337 // so that the caller can read its metadata and call its methods
338 access(all)
339 view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}?{
340 return (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)
341 }
342
343 access(all)
344 fun borrowComponent(id: UInt64): &FlovatarComponent.NFT?{
345 if self.ownedNFTs[id] != nil {
346 let ref = (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)!
347 let componentNFT = ref as! &FlovatarComponent.NFT
348 return componentNFT
349 } else {
350 return nil
351 }
352 }
353
354
355 access(all)
356 view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}?{
357 if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? {
358 return nft as &{ViewResolver.Resolver}
359 }
360 return nil
361 }
362
363
364
365 /// getSupportedNFTTypes returns a list of NFT types that this receiver accepts
366 access(all)
367 view fun getSupportedNFTTypes(): {Type: Bool} {
368 let supportedTypes: {Type: Bool} = {}
369 supportedTypes[Type<@FlovatarComponent.NFT>()] = true
370 return supportedTypes
371 }
372
373 /// Returns whether or not the given type is accepted by the collection
374 /// A collection that can accept any type should just return true by default
375 access(all)
376 view fun isSupportedNFTType(type: Type): Bool {
377 return type == Type<@FlovatarComponent.NFT>()
378 }
379
380 access(all)
381 fun createEmptyCollection(): @{NonFungibleToken.Collection}{
382 return <-FlovatarComponent.createEmptyCollection(nftType: Type<@FlovatarComponent.NFT>())
383 }
384 }
385
386
387
388
389
390 access(all)
391 view fun getContractViews(resourceType: Type?): [Type] {
392 return [
393 Type<MetadataViews.NFTCollectionData>(),
394 Type<MetadataViews.NFTCollectionDisplay>(),
395 Type<MetadataViews.EVMBridgedMetadata>()
396 ]
397 }
398
399
400 access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
401 switch viewType {
402 case Type<MetadataViews.NFTCollectionData>():
403 let collectionData = MetadataViews.NFTCollectionData(
404 storagePath: self.CollectionStoragePath,
405 publicPath: self.CollectionPublicPath,
406 publicCollection: Type<&FlovatarComponent.Collection>(),
407 publicLinkedType: Type<&FlovatarComponent.Collection>(),
408 createEmptyCollectionFunction: (fun(): @{NonFungibleToken.Collection} {
409 return <-FlovatarComponent.createEmptyCollection(nftType: Type<@FlovatarComponent.NFT>())
410 })
411 )
412 return collectionData
413 case Type<MetadataViews.NFTCollectionDisplay>():
414 let media = MetadataViews.Media(
415 file: MetadataViews.HTTPFile(
416 url: "https://images.flovatar.com/logo.svg"
417 ),
418 mediaType: "image/svg+xml"
419 )
420 let mediaBanner = MetadataViews.Media(
421 file: MetadataViews.HTTPFile(
422 url: "https://images.flovatar.com/logo-horizontal.svg"
423 ),
424 mediaType: "image/svg+xml"
425 )
426 return MetadataViews.NFTCollectionDisplay(
427 name: "Flovatar Component Collection",
428 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.",
429 externalURL: MetadataViews.ExternalURL("https://flovatar.com"),
430 squareImage: media,
431 bannerImage: mediaBanner,
432 socials: {
433 "twitter": MetadataViews.ExternalURL("https://x.com/flovatar"),
434 "discord": MetadataViews.ExternalURL("https://discord.gg/flovatar"),
435 "instagram": MetadataViews.ExternalURL("https://instagram.com/flovatar_nft"),
436 "tiktok": MetadataViews.ExternalURL("https://www.tiktok.com/@flovatar")
437 }
438 )
439 case Type<MetadataViews.EVMBridgedMetadata>():
440 // Implementing this view gives the project control over how the bridged NFT is represented as an ERC721
441 // when bridged to EVM on Flow via the public infrastructure bridge.
442
443 // Compose the contract-level URI. In this case, the contract metadata is located on some HTTP host,
444 // but it could be IPFS, S3, a data URL containing the JSON directly, etc.
445 return MetadataViews.EVMBridgedMetadata(
446 name: "Flovatar Component",
447 symbol: "FLVC",
448 uri: MetadataViews.URI(
449 baseURI: nil, // setting baseURI as nil sets the given value as the uri field value
450 value: "https://flovatar.com"
451 )
452 )
453 }
454 return nil
455 }
456
457 // public function that anyone can call to create a new empty collection
458 access(all)
459 fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection}{
460 return <-create Collection()
461 }
462
463 // This struct is used to send a data representation of the Components
464 // when retrieved using the contract helper methods outside the collection.
465 access(all)
466 struct ComponentData{
467 access(all)
468 let id: UInt64
469
470 access(all)
471 let templateId: UInt64
472
473 access(all)
474 let mint: UInt64
475
476 access(all)
477 let name: String
478
479 access(all)
480 let description: String
481
482 access(all)
483 let category: String
484
485 access(all)
486 let rarity: String
487
488 access(all)
489 let color: String
490
491 init(id: UInt64, templateId: UInt64, mint: UInt64){
492 self.id = id
493 self.templateId = templateId
494 self.mint = mint
495 let componentTemplate = FlovatarComponentTemplate.getComponentTemplate(id: templateId)!
496 self.name = componentTemplate.name
497 self.description = componentTemplate.description
498 self.category = componentTemplate.category
499 self.rarity = componentTemplate.rarity
500 self.color = componentTemplate.color
501 }
502 }
503
504 // Get the SVG of a specific Component from an account and the ID
505 access(all)
506 fun getSvgForComponent(address: Address, componentId: UInt64): String?{
507 let account = getAccount(address)
508 if let componentCollection = account.capabilities.borrow<&FlovatarComponent.Collection>(FlovatarComponent.CollectionPublicPath){
509 return (componentCollection.borrowComponent(id: componentId)!).getSvg()
510 }
511
512 return nil
513 }
514
515 // Get a specific Component from an account and the ID as ComponentData
516 access(all)
517 fun getComponent(address: Address, componentId: UInt64): ComponentData?{
518 let account = getAccount(address)
519 if let componentCollection = account.capabilities.borrow<&FlovatarComponent.Collection>(FlovatarComponent.CollectionPublicPath){
520 if !componentCollection.isInstance(Type<@FlovatarComponent.Collection>()){
521 panic("The Collection is not from the correct Type")
522 }
523 if let component = componentCollection.borrowComponent(id: componentId){
524 return ComponentData(id: componentId, templateId: (component!).templateId, mint: (component!).mint)
525 }
526 }
527
528 return nil
529 }
530
531 // Get an array of all the components in a specific account as ComponentData
532 access(all)
533 fun getComponents(address: Address): [ComponentData]{
534 var componentData: [ComponentData] = []
535 let account = getAccount(address)
536 if let componentCollection = account.capabilities.borrow<&FlovatarComponent.Collection>(FlovatarComponent.CollectionPublicPath){
537 if !componentCollection.isInstance(Type<@FlovatarComponent.Collection>()){
538 panic("The Collection is not from the correct Type")
539 }
540 for id in componentCollection.getIDs(){
541 var component = componentCollection.borrowComponent(id: id)
542 componentData.append(ComponentData(id: id, templateId: (component!).templateId, mint: (component!).mint))
543 }
544 }
545
546 return componentData
547 }
548
549 // This method can only be called from another contract in the same account.
550 // In FlovatarComponent case it is called from the Flovatar Admin that is used
551 // to administer the components.
552 // The only parameter is the parent Template ID and it will return a Component NFT resource
553 access(account)
554 fun createComponent(templateId: UInt64): @FlovatarComponent.NFT{
555 let componentTemplate: FlovatarComponentTemplate.ComponentTemplateData = FlovatarComponentTemplate.getComponentTemplate(id: templateId)!
556 let totalMintedComponents: UInt64 = FlovatarComponentTemplate.getTotalMintedComponents(id: templateId)!
557
558 // Makes sure that the original minting limit set for each Template has not been reached
559 if totalMintedComponents >= componentTemplate.maxMintableComponents{
560 panic("Reached maximum mintable components for this type")
561 }
562 var newNFT <- create NFT(templateId: templateId)
563 emit Created(id: newNFT.id, templateId: templateId, mint: newNFT.mint)
564 return <-newNFT
565 }
566
567 // This function will batch create multiple Components and pass them back as a Collection
568 access(account)
569 fun batchCreateComponents(templateId: UInt64, quantity: UInt64): @Collection{
570 let newCollection <- create Collection()
571 var i: UInt64 = 0
572 while i < quantity{
573 newCollection.deposit(token: <-self.createComponent(templateId: templateId))
574 i = i + UInt64(1)
575 }
576 return <-newCollection
577 }
578
579 init(){
580 self.CollectionPublicPath = /public/FlovatarComponentCollection
581 self.CollectionStoragePath = /storage/FlovatarComponentCollection
582
583 // Initialize the total supply
584 self.totalSupply = UInt64(0)
585 self.account.storage.save<@{NonFungibleToken.Collection}>(<-FlovatarComponent.createEmptyCollection(nftType: Type<@FlovatarComponent.Collection>()), to: FlovatarComponent.CollectionStoragePath)
586 var capability_1 = self.account.capabilities.storage.issue<&{FlovatarComponent.CollectionPublic}>(FlovatarComponent.CollectionStoragePath)
587 self.account.capabilities.publish(capability_1, at: FlovatarComponent.CollectionPublicPath)
588 emit ContractInitialized()
589 }
590}
591