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