Smart Contract

Sportbit

A.ca5c31c0c03e11be.Sportbit

Deployed

1w ago
Feb 16, 2026, 10:42:32 PM UTC

Dependents

134 imports
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