Smart Contract

MFLPlayer

A.8ebcbfd516b1da27.MFLPlayer

Valid From

120,475,423

Deployed

2w ago
Feb 11, 2026, 05:29:26 PM UTC

Dependents

3443 imports
1import NonFungibleToken from 0x1d7e57aa55817448
2import ViewResolver from 0x1d7e57aa55817448
3import FungibleToken from 0xf233dcee88fe0abe
4import MetadataViews from 0x1d7e57aa55817448
5import MFLViews from 0x8ebcbfd516b1da27
6import MFLAdmin from 0x8ebcbfd516b1da27
7
8/**
9  This contract is based on the NonFungibleToken standard on Flow.
10  It allows an admin to mint players (NFTs). A player has metadata
11  that can be updated by an admin.
12**/
13
14access(all)
15contract MFLPlayer: NonFungibleToken {
16
17	// Entitlements
18	access(all)
19	entitlement PlayerAdminAction
20
21	// Events
22	access(all)
23	event ContractInitialized()
24
25	access(all)
26	event Withdraw(id: UInt64, from: Address?)
27
28	access(all)
29	event Deposit(id: UInt64, to: Address?)
30
31	access(all)
32	event Minted(id: UInt64)
33
34	access(all)
35	event Updated(id: UInt64)
36
37	// Named Paths
38	access(all)
39	let CollectionStoragePath: StoragePath
40
41	access(all)
42	let CollectionPublicPath: PublicPath
43
44	access(all)
45	let PlayerAdminStoragePath: StoragePath
46
47	// The total number of Players that have been minted
48	access(all)
49	var totalSupply: UInt64
50
51	// All players datas are stored in this dictionary
52	access(self)
53	let playersDatas: {UInt64: PlayerData}
54
55	// Data stored in playersdatas. Updatable by an admin
56	access(all)
57	struct PlayerData {
58		access(all)
59		let id: UInt64
60
61		access(contract)
62		let metadata: {String: AnyStruct}
63
64		access(all)
65		let season: UInt32
66
67		access(all)
68		let image: {MetadataViews.File}
69
70		init(id: UInt64, metadata: {String: AnyStruct}, season: UInt32, image: {MetadataViews.File}) {
71			self.id = id
72			self.metadata = metadata
73			self.season = season
74			self.image = image
75		}
76	}
77
78	// The resource that represents the Player NFT
79	access(all)
80	resource NFT: NonFungibleToken.NFT, ViewResolver.Resolver {
81
82		// The unique ID for the Player
83		access(all)
84		let id: UInt64
85
86		access(all)
87		let season: UInt32
88
89		access(all)
90		let image: {MetadataViews.File}
91
92		init(id: UInt64, season: UInt32, image: {MetadataViews.File}) {
93			self.id = id
94			// Increment the totalSupply so that id it isn't used again
95			MFLPlayer.totalSupply = MFLPlayer.totalSupply + 1 as UInt64
96			self.season = season
97			self.image = image
98			emit Minted(id: self.id)
99		}
100
101		// Get all supported views for this NFT
102		access(all)
103		view fun getViews(): [Type] {
104			return [
105				Type<MetadataViews.Display>(),
106				Type<MetadataViews.Royalties>(),
107				Type<MetadataViews.NFTCollectionDisplay>(),
108				Type<MetadataViews.NFTCollectionData>(),
109				Type<MetadataViews.ExternalURL>(),
110				Type<MetadataViews.Traits>(),
111				Type<MetadataViews.Serial>(),
112				Type<MFLViews.PlayerDataViewV1>()
113			]
114		}
115
116		// Resolve a specific view
117		access(all)
118		fun resolveView(_ view: Type): AnyStruct? {
119			let playerData = MFLPlayer.getPlayerData(id: self.id)!
120			return MFLPlayer.resolveViewFromData(view, playerData: playerData)
121		}
122
123		access(all)
124		fun createEmptyCollection(): @{NonFungibleToken.Collection} {
125			return <-MFLPlayer.createEmptyCollection(nftType: Type<@MFLPlayer.Collection>())
126		}
127	}
128
129	access(all)
130	fun resolveViewFromData(_ view: Type, playerData: PlayerData): AnyStruct? {
131		let playerImageUrl = MFLAdmin.imageHostUrl()
132			.concat("/players/v2/").concat(playerData.id.toString())
133			.concat("/card.png?co=").concat((playerData.metadata["overall"] as! UInt32?)!.toString())
134		let playerImage = MetadataViews.HTTPFile(url: playerImageUrl)
135
136		switch view {
137			case Type<MetadataViews.Display>():
138				return MetadataViews.Display(
139					name: playerData.metadata["name"] as! String? ?? "",
140					description: "Before purchasing this MFL Player, make sure to check the player's in-game profile for the latest information: https://app.playmfl.com/players/"
141						.concat(playerData.id.toString()),
142					thumbnail: playerImage
143				)
144			case Type<MetadataViews.Royalties>():
145				let royalties: [MetadataViews.Royalty] = []
146				let royaltyReceiverCap = getAccount(MFLAdmin.royaltyAddress()).capabilities.get<&{FungibleToken.Receiver}>(/public/GenericFTReceiver)
147				royalties.append(MetadataViews.Royalty(receiver: royaltyReceiverCap!, cut: 0.05, description: "Creator Royalty"))
148				return MetadataViews.Royalties(royalties)
149			case Type<MetadataViews.NFTCollectionDisplay>():
150				return MFLPlayer.resolveContractView(resourceType: Type<@MFLPlayer.NFT>(), viewType: Type<MetadataViews.NFTCollectionDisplay>())
151			case Type<MetadataViews.NFTCollectionData>():
152				return MFLPlayer.resolveContractView(resourceType: Type<@MFLPlayer.NFT>(), viewType: Type<MetadataViews.NFTCollectionData>())
153			case Type<MetadataViews.ExternalURL>():
154				return MetadataViews.ExternalURL("https://playmfl.com")
155			case Type<MetadataViews.Traits>():
156				let traits: [MetadataViews.Trait] = []
157				traits.append(MetadataViews.Trait(name: "name", value: playerData.metadata["name"] as! String?, displayType: "String", rarity: nil))
158				let nationalitiesOptional = playerData.metadata["nationalities"] as! [String]?
159				var nationalitiesString: String = ""
160				if let nationalities = nationalitiesOptional {
161					for nationality in nationalities {
162						if nationalitiesString.length > 0 {
163							nationalitiesString = nationalitiesString.concat(", ")
164						}
165						nationalitiesString = nationalitiesString.concat(nationality)
166					}
167				}
168				traits.append(MetadataViews.Trait(name: "nationalities", value: nationalitiesString, displayType: "String", rarity: nil))
169				var positionsString: String = ""
170				if let positions = playerData.metadata["positions"] as! [String]? {
171					for position in positions {
172						if positionsString.length > 0 {
173							positionsString = positionsString.concat(", ")
174						}
175						positionsString = positionsString.concat(position)
176					}
177				}
178				traits.append(MetadataViews.Trait(name: "positions", value: positionsString, displayType: "String", rarity: nil))
179				traits.append(MetadataViews.Trait(name: "preferredFoot", value: playerData.metadata["preferredFoot"] as! String?, displayType: "String", rarity: nil))
180				traits.append(MetadataViews.Trait(name: "ageAtMint", value: playerData.metadata["ageAtMint"] as! UInt32?, displayType: "Number", rarity: nil))
181				traits.append(MetadataViews.Trait(name: "height", value: playerData.metadata["height"] as! UInt32?, displayType: "Number", rarity: nil))
182				traits.append(MetadataViews.Trait(name: "overall", value: playerData.metadata["overall"] as! UInt32?, displayType: "Number", rarity: nil))
183				traits.append(MetadataViews.Trait(name: "pace", value: playerData.metadata["pace"] as! UInt32?, displayType: "Number", rarity: nil))
184				traits.append(MetadataViews.Trait(name: "shooting", value: playerData.metadata["shooting"] as! UInt32?, displayType: "Number", rarity: nil))
185				traits.append(MetadataViews.Trait(name: "passing", value: playerData.metadata["passing"] as! UInt32?, displayType: "Number", rarity: nil))
186				traits.append(MetadataViews.Trait(name: "dribbling", value: playerData.metadata["dribbling"] as! UInt32?, displayType: "Number", rarity: nil))
187				traits.append(MetadataViews.Trait(name: "defense", value: playerData.metadata["defense"] as! UInt32?, displayType: "Number", rarity: nil))
188				traits.append(MetadataViews.Trait(name: "physical", value: playerData.metadata["physical"] as! UInt32?, displayType: "Number", rarity: nil))
189				traits.append(MetadataViews.Trait(name: "goalkeeping", value: playerData.metadata["goalkeeping"] as! UInt32?, displayType: "Number", rarity: nil))
190				return MetadataViews.Traits(traits)
191			case Type<MetadataViews.Serial>():
192				return MetadataViews.Serial(playerData.id)
193			case Type<MFLViews.PlayerDataViewV1>():
194				return MFLViews.PlayerDataViewV1(id: playerData.id, metadata: playerData.metadata, season: playerData.season, thumbnail: playerImage)
195		}
196		return nil
197	}
198
199	// A collection of Player NFTs owned by an account
200	access(all)
201	resource Collection: NonFungibleToken.Collection, ViewResolver.ResolverCollection {
202
203		// Dictionary of NFT conforming tokens
204		access(all)
205		var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
206
207		init() {
208			self.ownedNFTs <-{}
209		}
210
211		// Removes an NFT from the collection and moves it to the caller
212		access(NonFungibleToken.Withdraw)
213		fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
214			let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT")
215
216			emit Withdraw(id: token.id, from: self.owner?.address)
217
218			return <-token
219		}
220
221		// Withdraws multiple Players and returns them as a Collection
222		access(NonFungibleToken.Withdraw)
223		fun batchWithdraw(ids: [UInt64]): @{NonFungibleToken.Collection} {
224			var batchCollection <- create Collection()
225
226			// Iterate through the ids and withdraw them from the Collection
227			for id in ids {
228				batchCollection.deposit(token: <-self.withdraw(withdrawID: id))
229			}
230
231			return <-batchCollection
232		}
233
234		// Takes a NFT and adds it to the collections dictionary and adds the ID to the id array
235		access(all)
236		fun deposit(token: @{NonFungibleToken.NFT}) {
237			let token <- token as! @MFLPlayer.NFT
238			let id: UInt64 = token.id
239
240			// Add the new token to the dictionary which removes the old one
241			let oldToken <- self.ownedNFTs[id] <- token
242
243			emit Deposit(id: id, to: self.owner?.address)
244
245			destroy oldToken
246		}
247
248		// Returns an array of the IDs that are in the collection
249		access(all)
250		view fun getIDs(): [UInt64] {
251			return self.ownedNFTs.keys
252		}
253
254		access(all)
255		view fun getLength(): Int {
256			return self.ownedNFTs.length
257		}
258
259		// Gets a reference to an NFT in the collection so that the caller can read its metadata and call its methods
260		access(all)
261		view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
262			return (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)
263		}
264
265		access(all)
266		view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? {
267			if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? {
268				return nft as &{ViewResolver.Resolver}
269			}
270            return nil
271		}
272
273		access(all)
274		view fun getSupportedNFTTypes(): {Type: Bool} {
275			let supportedTypes: {Type: Bool} = {}
276            supportedTypes[Type<@MFLPlayer.NFT>()] = true
277            return supportedTypes
278		}
279
280		access(all)
281		view fun isSupportedNFTType(type: Type): Bool {
282			return type == Type<@MFLPlayer.NFT>()
283		}
284
285		access(all)
286		fun createEmptyCollection(): @{NonFungibleToken.Collection} {
287			return <-MFLPlayer.createEmptyCollection(nftType: Type<@MFLPlayer.NFT>())
288		}
289
290		access(contract)
291		view fun emitNFTUpdated(_ id: UInt64) {
292            MFLPlayer.emitNFTUpdated((&self.ownedNFTs[id] as auth(NonFungibleToken.Update) &{NonFungibleToken.NFT}?)!)
293		}
294	}
295
296	// Public function that anyone can call to create a new empty collection
297	access(all)
298	fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {
299		return <-create Collection()
300	}
301
302	// Get data for a specific player ID
303	access(all)
304	view fun getPlayerData(id: UInt64): PlayerData? {
305		return self.playersDatas[id]
306	}
307
308	access(all)
309	view fun getContractViews(resourceType: Type?): [Type] {
310		return [
311			Type<MetadataViews.NFTCollectionData>(),
312			Type<MetadataViews.NFTCollectionDisplay>()
313		]
314	}
315
316	access(all)
317	fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
318		switch viewType {
319			case Type<MetadataViews.NFTCollectionData>():
320				let collectionData = MetadataViews.NFTCollectionData(
321					storagePath: self.CollectionStoragePath,
322					publicPath: self.CollectionPublicPath,
323					publicCollection: Type<&MFLPlayer.Collection>(),
324					publicLinkedType: Type<&MFLPlayer.Collection>(),
325					createEmptyCollectionFunction: (fun(): @{NonFungibleToken.Collection} {
326						return <-MFLPlayer.createEmptyCollection(nftType: Type<@MFLPlayer.NFT>())
327					})
328				)
329				return collectionData
330			case Type<MetadataViews.NFTCollectionDisplay>():
331				return MetadataViews.NFTCollectionDisplay(
332					name: "MFL Player Collection",
333					description: "Build your own football club, make strategic decisions, and live the thrill of real competition. Join a universe where the stakes–and your rivals–are real.",
334					externalURL: MetadataViews.ExternalURL("https://playmfl.com"),
335					squareImage: MetadataViews.Media(
336						file: MetadataViews.HTTPFile(url: "https://app.playmfl.com/img/collections/players/thumbnail.png"),
337						mediaType: "image/png"
338					),
339					bannerImage: MetadataViews.Media(
340						file: MetadataViews.HTTPFile(url: "https://app.playmfl.com/img/collections/players/banner.png"),
341						mediaType: "image/png"
342					),
343					socials: {
344						"twitter": MetadataViews.ExternalURL("https://twitter.com/playMFL"),
345						"discord": MetadataViews.ExternalURL("https://discord.gg/pEDTR4wSPr"),
346						"linkedin": MetadataViews.ExternalURL("https://www.linkedin.com/company/playmfl"),
347						"medium": MetadataViews.ExternalURL("https://medium.com/playmfl")
348					}
349				)
350		}
351		return nil
352	}
353
354	// Deprecated: Only here for backward compatibility.
355	access(all)
356	resource interface PlayerAdminClaim {}
357
358	access(all)
359	resource PlayerAdmin: PlayerAdminClaim {
360		access(all)
361		let name: String
362
363		init() {
364			self.name = "PlayerAdminClaim"
365		}
366
367		// Mint a new Player and returns it
368		access(PlayerAdminAction)
369		fun mintPlayer(id: UInt64, metadata: {String: AnyStruct}, season: UInt32, image: {MetadataViews.File}): @MFLPlayer.NFT {
370			pre {
371				MFLPlayer.getPlayerData(id: id) == nil:
372					"Player already exists"
373			}
374			let newPlayerNFT <- create MFLPlayer.NFT(id: id, season: season, image: image)
375			MFLPlayer.playersDatas[newPlayerNFT.id] = MFLPlayer.PlayerData(id: newPlayerNFT.id, metadata: metadata, season: season, image: image)
376			return <-newPlayerNFT
377		}
378
379		// Update Player Metadata
380		access(PlayerAdminAction)
381		fun updatePlayerMetadata(id: UInt64, metadata: {String: AnyStruct}, collectionRefOptional: &MFLPlayer.Collection?) {
382			let playerData = MFLPlayer.playersDatas[id] ?? panic("Data not found")
383			let updatedPlayerData = MFLPlayer.PlayerData(id: playerData.id, metadata: metadata, season: playerData.season, image: playerData.image)
384			MFLPlayer.playersDatas[id] = updatedPlayerData
385			emit Updated(id: id)
386			if let collectionRef = collectionRefOptional {
387			    collectionRef.emitNFTUpdated(id)
388			}
389		}
390	}
391
392	init() {
393		// Set our named paths
394		self.CollectionStoragePath = /storage/MFLPlayerCollection
395		self.CollectionPublicPath = /public/MFLPlayerCollection
396		self.PlayerAdminStoragePath = /storage/MFLPlayerAdmin
397
398		// Initialize contract fields
399		self.totalSupply = 0
400		self.playersDatas = {}
401
402		// Put a new Collection in storage
403		self.account.storage.save<@Collection>(<-create Collection(), to: self.CollectionStoragePath)
404		// Create a public capability for the Collection
405		var capability_1 = self.account.capabilities.storage.issue<&MFLPlayer.Collection>(self.CollectionStoragePath)
406		self.account.capabilities.publish(capability_1, at: self.CollectionPublicPath)
407
408		// Create a PlayerAdmin resource and save it to storage
409		self.account.storage.save(<-create PlayerAdmin(), to: self.PlayerAdminStoragePath)
410		emit ContractInitialized()
411	}
412}
413