Smart Contract
MFLPlayer
A.8ebcbfd516b1da27.MFLPlayer
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