Smart Contract
ChainmonstersRewards
A.93615d25d14fa337.ChainmonstersRewards
1// This is the Kickstarter/Presale NFT contract of Chainmonsters.
2// Based on the "current" NonFungibleToken standard on Flow.
3// Does not include that much functionality as the only purpose it to mint and store the Presale NFTs.
4
5import NonFungibleToken from 0x1d7e57aa55817448
6import FungibleToken from 0xf233dcee88fe0abe
7import MetadataViews from 0x1d7e57aa55817448
8
9pub contract ChainmonstersRewards: NonFungibleToken {
10
11 pub var totalSupply: UInt64
12
13 pub event ContractInitialized()
14 pub event Withdraw(id: UInt64, from: Address?)
15 pub event Deposit(id: UInt64, to: Address?)
16
17 pub event RewardCreated(id: UInt32, metadata: String, season: UInt32)
18 pub event NFTMinted(NFTID: UInt64, rewardID: UInt32, serialNumber: UInt32)
19 pub event NewSeasonStarted(newCurrentSeason: UInt32)
20
21 pub event ItemConsumed(itemID: UInt64, playerId: String)
22 pub event ItemClaimed(itemID: UInt64, playerId: String, uid: String)
23 pub event ItemMigrated(itemID: UInt64, rewardID: UInt32, serialNumber: UInt32, playerId: String, imxWallet: String)
24
25 pub var nextRewardID: UInt32
26
27 // Variable size dictionary of Reward structs
28 access(self) var rewardDatas: {UInt32: Reward}
29 access(self) var rewardSupplies: {UInt32: UInt32}
30 access(self) var rewardSeasons: {UInt32 : UInt32}
31
32 // a mapping of Reward IDs that indicates what serial/mint number
33 // have been minted for this specific Reward yet
34 pub var numberMintedPerReward: {UInt32: UInt32}
35
36 // the season a reward belongs to
37 // A season is a concept where rewards are obtainable in-game for a limited time
38 // After a season is over the rewards can no longer be minted and thus create
39 // scarcity and drive the player-driven economy over time.
40 pub var currentSeason: UInt32
41
42
43
44 // A reward is a struct that keeps all the metadata information from an NFT in place.
45 // There are 19 different rewards and all need an NFT-Interface.
46 // Depending on the Reward-Type there are different ways to use and interact with future contracts.
47 // E.g. the "Alpha Access" NFT needs to be claimed in order to gain game access with your account.
48 // This process is destroying/moving the NFT to another contract.
49 pub struct Reward {
50
51 // The unique ID for the Reward
52 pub let rewardID: UInt32
53
54 // the game-season this reward belongs to
55 // Kickstarter NFTs are Pre-Season and equal 0
56 pub let season: UInt32
57
58 // The metadata for the rewards is restricted to the name since
59 // all other data is inside the token itself already
60 // visual stuff and descriptions need to be retrieved via API
61 pub let metadata: String
62
63 init(metadata: String) {
64 pre {
65 metadata.length != 0: "New Reward metadata cannot be empty"
66 }
67 self.rewardID = ChainmonstersRewards.nextRewardID
68 self.metadata = metadata
69 self.season = ChainmonstersRewards.currentSeason;
70
71 // Increment the ID so that it isn't used again
72 ChainmonstersRewards.nextRewardID = ChainmonstersRewards.nextRewardID + UInt32(1)
73
74 emit RewardCreated(id: self.rewardID, metadata: metadata, season: self.season)
75 }
76 }
77
78 pub struct NFTData {
79
80 // The ID of the Reward that the NFT references
81 pub let rewardID: UInt32
82
83 // The token mint number
84 // Otherwise known as the serial number
85 pub let serialNumber: UInt32
86
87 init(rewardID: UInt32, serialNumber: UInt32) {
88 self.rewardID = rewardID
89 self.serialNumber = serialNumber
90 }
91
92 }
93
94
95 pub resource NFT: NonFungibleToken.INFT, MetadataViews.Resolver {
96
97 // Global unique NFT ID
98 pub let id: UInt64
99
100 pub let data: NFTData
101
102 init(serialNumber: UInt32, rewardID: UInt32) {
103 // Increment the global NFT IDs
104 ChainmonstersRewards.totalSupply = ChainmonstersRewards.totalSupply + UInt64(1)
105
106 self.id = ChainmonstersRewards.totalSupply
107
108 self.data = NFTData(rewardID: rewardID, serialNumber: serialNumber)
109
110 emit NFTMinted(NFTID: self.id, rewardID: rewardID, serialNumber: self.data.serialNumber)
111 }
112
113 pub fun getViews(): [Type] {
114 return [
115 Type<MetadataViews.Display>(),
116 Type<MetadataViews.Edition>(),
117 Type<MetadataViews.NFTCollectionDisplay>(),
118 Type<MetadataViews.ExternalURL>(),
119 Type<MetadataViews.NFTCollectionData>(),
120 Type<MetadataViews.Royalties>(),
121 Type<MetadataViews.Serial>()
122 ]
123 }
124
125 pub fun resolveView(_ view: Type): AnyStruct? {
126 let externalRewardMetadata = ChainmonstersRewards.getExternalRewardMetadata(rewardID: self.data.rewardID)
127
128 let name = externalRewardMetadata != nil ? externalRewardMetadata!["name"] ?? "Chainmonsters Reward #".concat(self.data.rewardID.toString()) : "Chainmonsters Reward #".concat(self.data.rewardID.toString())
129 let description = externalRewardMetadata != nil ? externalRewardMetadata!["description"] ?? "A Chainmonsters Reward" : "A Chainmonsters Reward"
130
131 switch view {
132 case Type<MetadataViews.Display>():
133 return MetadataViews.Display(
134 name: name,
135 description: description,
136 thumbnail: MetadataViews.HTTPFile(
137 url: "https://chainmonsters.com/images/rewards/".concat(self.data.rewardID.toString()).concat(".png")
138 )
139 )
140 case Type<MetadataViews.Edition>():
141 return MetadataViews.Edition(
142 name: name,
143 number: UInt64(self.data.serialNumber),
144 max: UInt64(ChainmonstersRewards.getNumRewardsMinted(rewardID: self.data.rewardID)!)
145 )
146 case Type<MetadataViews.NFTCollectionDisplay>():
147 return MetadataViews.NFTCollectionDisplay(
148 name: "Chainmonsters Rewards",
149 description: "Chainmonsters is a massive multiplayer online RPG where you catch, battle, trade, explore, and combine different types of monsters and abilities to create strong chain reactions! No game subscription required. Explore the vast lands of Ancora together with your friends on Steam, iOS and Android!",
150 externalURL: MetadataViews.ExternalURL("https://chainmonsters.com"),
151 squareImage: MetadataViews.Media(
152 file: MetadataViews.HTTPFile(
153 url: "https://chainmonsters.com/images/chipleaf.png"
154 ),
155 mediaType: "image/png"
156 ),
157 bannerImage: MetadataViews.Media(
158 file: MetadataViews.HTTPFile(
159 url: "https://chainmonsters.com/images/bg.jpg"
160 ),
161 mediaType: "image/jpeg"
162 ),
163 socials: {
164 "twitter": MetadataViews.ExternalURL("https://twitter.com/chainmonsters"),
165 "discord": MetadataViews.ExternalURL("https://discord.gg/chainmonsters")
166 }
167 )
168 case Type<MetadataViews.ExternalURL>():
169 return MetadataViews.ExternalURL("https://chainmonsters.com/rewards/".concat(self.data.rewardID.toString()))
170 case Type<MetadataViews.NFTCollectionData>():
171 return MetadataViews.NFTCollectionData(
172 storagePath: /storage/ChainmonstersRewardCollection,
173 publicPath: /public/ChainmonstersRewardCollection,
174 providerPath: /private/ChainmonstersRewardsCollectionProvider,
175 publicCollection: Type<&ChainmonstersRewards.Collection{ChainmonstersRewards.ChainmonstersRewardCollectionPublic}>(),
176 publicLinkedType: Type<&ChainmonstersRewards.Collection{ChainmonstersRewards.ChainmonstersRewardCollectionPublic,NonFungibleToken.CollectionPublic,NonFungibleToken.Receiver,MetadataViews.ResolverCollection}>(),
177 providerLinkedType: Type<&ChainmonstersRewards.Collection{ChainmonstersRewards.ChainmonstersRewardCollectionPublic,NonFungibleToken.CollectionPublic,NonFungibleToken.Provider,MetadataViews.ResolverCollection}>(),
178 createEmptyCollectionFunction: (fun (): @ChainmonstersRewards.Collection {
179 return <- (ChainmonstersRewards.createEmptyCollection() as! @ChainmonstersRewards.Collection)
180 })
181 )
182 case Type<MetadataViews.Royalties>():
183 return MetadataViews.Royalties([
184 MetadataViews.Royalty(
185 receiver: ChainmonstersRewards.account.getCapability<&{FungibleToken.Receiver}>(MetadataViews.getRoyaltyReceiverPublicPath()),
186 cut: 0.05,
187 description: "Chainmonsters Platform Cut"
188 )
189 ])
190 case Type<MetadataViews.Serial>():
191 return MetadataViews.Serial(self.id)
192 }
193
194 return nil
195 }
196 }
197
198 // This is the interface that users can cast their Reward Collection as
199 // to allow others to deposit Rewards into their Collection. It also allows for reading
200 // the IDs of Rewards in the Collection.
201 pub resource interface ChainmonstersRewardCollectionPublic {
202 pub fun deposit(token: @NonFungibleToken.NFT)
203 pub fun batchDeposit(tokens: @NonFungibleToken.Collection)
204 pub fun getIDs(): [UInt64]
205 pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT
206 pub fun borrowReward(id: UInt64): &ChainmonstersRewards.NFT? {
207 // If the result isn't nil, the id of the returned reference
208 // should be the same as the argument to the function
209 post {
210 (result == nil) || (result?.id == id):
211 "Cannot borrow Reward reference: The ID of the returned reference is incorrect"
212 }
213 }
214 }
215
216
217 pub resource Collection: ChainmonstersRewardCollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection {
218 // dictionary of NFT conforming tokens
219 // NFT is a resource type with an `UInt64` ID field
220 pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT}
221
222 init () {
223 self.ownedNFTs <- {}
224 }
225
226 // withdraw removes an NFT-Reward from the collection and moves it to the caller
227 pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT {
228
229 // Remove the nft from the Collection
230 let token <- self.ownedNFTs.remove(key: withdrawID)
231 ?? panic("Cannot withdraw: Reward does not exist in the collection")
232
233 emit Withdraw(id: token.id, from: self.owner?.address)
234
235 // Return the withdrawn token
236 return <-token
237 }
238
239
240 // batchWithdraw withdraws multiple tokens and returns them as a Collection
241 //
242 // Parameters: ids: An array of IDs to withdraw
243 //
244 // Returns: @NonFungibleToken.Collection: A collection that contains
245 // the withdrawn rewards
246 //
247 pub fun batchWithdraw(ids: [UInt64]): @NonFungibleToken.Collection {
248 // Create a new empty Collection
249 var batchCollection <- create Collection()
250
251 // Iterate through the ids and withdraw them from the Collection
252 for id in ids {
253 batchCollection.deposit(token: <-self.withdraw(withdrawID: id))
254 }
255
256 // Return the withdrawn tokens
257 return <-batchCollection
258 }
259
260 // deposit takes a NFT and adds it to the collections dictionary
261 // and adds the ID to the id array
262 pub fun deposit(token: @NonFungibleToken.NFT) {
263 let token <- token as! @ChainmonstersRewards.NFT
264
265 let id: UInt64 = token.id
266
267 // add the new token to the dictionary which removes the old one
268 let oldToken <- self.ownedNFTs[id] <- token
269
270 emit Deposit(id: id, to: self.owner?.address)
271
272 destroy oldToken
273 }
274
275 // batchDeposit takes a Collection object as an argument
276 // and deposits each contained NFT into this Collection
277 pub fun batchDeposit(tokens: @NonFungibleToken.Collection) {
278
279 // Get an array of the IDs to be deposited
280 let keys = tokens.getIDs()
281
282 // Iterate through the keys in the collection and deposit each one
283 for key in keys {
284 self.deposit(token: <-tokens.withdraw(withdrawID: key))
285 }
286
287 // Destroy the empty Collection
288 destroy tokens
289 }
290
291 // getIDs returns an array of the IDs that are in the collection
292 pub fun getIDs(): [UInt64] {
293 return self.ownedNFTs.keys
294 }
295
296 // borrowNFT gets a reference to an NFT in the collection
297 // so that the caller can read its metadata and call its methods
298 pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT {
299 return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)!
300 }
301
302 // borrowMReward returns a borrowed reference to a Reward
303 // so that the caller can read data and call methods from it.
304 //
305 // Parameters: id: The ID of the NFT to get the reference for
306 //
307 // Returns: A reference to the NFT
308 pub fun borrowReward(id: UInt64): &ChainmonstersRewards.NFT? {
309 if self.ownedNFTs[id] != nil {
310 let ref = &self.ownedNFTs[id] as auth &NonFungibleToken.NFT?
311 return ref as! &ChainmonstersRewards.NFT?
312 } else {
313 return nil
314 }
315 }
316
317 pub fun borrowViewResolver(id: UInt64): &AnyResource{MetadataViews.Resolver} {
318 let nft = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
319 let rewardNFT = nft as! &ChainmonstersRewards.NFT
320 return rewardNFT
321 }
322
323 destroy() {
324 destroy self.ownedNFTs
325 }
326 }
327
328
329
330
331 // Resource that an admin or something similar would own to be
332 // able to mint new NFTs
333 //
334 pub resource Admin {
335
336
337 // creates a new Reward struct and stores it in the Rewards dictionary
338 // Parameters: metadata: the name of the reward
339 pub fun createReward(metadata: String, totalSupply: UInt32): UInt32 {
340 // Create the new Reward
341 var newReward = Reward(metadata: metadata)
342 let newID = newReward.rewardID;
343
344 // Kickstarter rewards are created with a fixed total supply.
345 // Future season rewards are not technically limited by a total supply
346 // but rather the time limitations in which a player can earn those.
347 // Once a season is over the total supply for those rewards is fixed since
348 // they can no longer be minted.
349
350 ChainmonstersRewards.rewardSupplies[newID] = totalSupply
351 ChainmonstersRewards.numberMintedPerReward[newID] = 0
352 ChainmonstersRewards.rewardSeasons[newID] = newReward.season
353
354 ChainmonstersRewards.rewardDatas[newID] = newReward
355
356 return newID
357 }
358
359 // consuming an NFT (item) to be converted to in-game economy
360 pub fun consumeItem(token: @NonFungibleToken.NFT, playerId: String) {
361 let token <- token as! @ChainmonstersRewards.NFT
362
363 let id: UInt64 = token.id
364
365 emit ItemConsumed(itemID: id, playerId: playerId)
366
367 destroy token
368 }
369
370 // removing an NFT (item) from the rewards collection to be migrated to the new IMX contracts
371 pub fun migrateItem(token: @NonFungibleToken.NFT, playerId: String, imxWallet: String) {
372 let token <- token as! @ChainmonstersRewards.NFT
373
374 let id: UInt64 = token.id
375
376 emit ItemMigrated(
377 itemID: id,
378 rewardID: token.data.rewardID,
379 serialNumber: token.data.serialNumber,
380 playerId: playerId,
381 imxWallet: imxWallet
382 )
383
384 destroy token
385 }
386
387 // claiming an NFT item from e.g. Season Pass or Store
388 // rewardID - reward to be claimed
389 // uid - unique identifier from system
390 pub fun claimItem(rewardID: UInt32, playerId: String, uid: String): @NFT {
391 let nft <- self.mintReward(rewardID: rewardID)
392
393 emit ItemClaimed(itemID: nft.id, playerId: playerId, uid: uid)
394
395 return <- nft
396 }
397
398
399 // mintReward mints a new NFT-Reward with a new ID
400 //
401 pub fun mintReward(rewardID: UInt32): @NFT {
402 pre {
403
404 // check if the reward is still in "season"
405 ChainmonstersRewards.rewardSeasons[rewardID] == ChainmonstersRewards.currentSeason
406 // check if total supply allows additional NFTs || ignore if there is no hard cap specified == 0
407 ChainmonstersRewards.numberMintedPerReward[rewardID] != ChainmonstersRewards.rewardSupplies[rewardID] || ChainmonstersRewards.rewardSupplies[rewardID] == UInt32(0)
408
409 }
410
411 // Gets the number of NFTs that have been minted for this Reward
412 // to use as this NFT's serial number
413 let numInReward = ChainmonstersRewards.numberMintedPerReward[rewardID]!
414
415 // Mint the new NFT
416 let newReward: @NFT <- create NFT(serialNumber: numInReward + UInt32(1),
417 rewardID: rewardID)
418
419 // Increment the count of NFTs minted for this Reward
420 ChainmonstersRewards.numberMintedPerReward[rewardID] = numInReward + UInt32(1)
421
422 return <-newReward
423 }
424
425 // batchMintReward mints an arbitrary quantity of Rewards
426 //
427 pub fun batchMintReward(rewardID: UInt32, quantity: UInt64): @Collection {
428 let newCollection <- create Collection()
429
430 var i: UInt64 = 0
431 while i < quantity {
432 newCollection.deposit(token: <-self.mintReward(rewardID: rewardID))
433 i = i + UInt64(1)
434 }
435
436 return <-newCollection
437 }
438
439 pub fun borrowReward(rewardID: UInt32): &Reward {
440 pre {
441 ChainmonstersRewards.rewardDatas[rewardID] != nil: "Cannot borrow Reward: The Reward doesn't exist"
442 }
443
444 // Get a reference to the Set and return it
445 // use `&` to indicate the reference to the object and type
446 return (&ChainmonstersRewards.rewardDatas[rewardID] as &Reward?)!
447 }
448
449
450 // ends the current season by incrementing the season number
451 // Rewards minted after this will use the new season number.
452 pub fun startNewSeason(): UInt32 {
453 ChainmonstersRewards.currentSeason = ChainmonstersRewards.currentSeason + UInt32(1)
454
455 emit NewSeasonStarted(newCurrentSeason: ChainmonstersRewards.currentSeason)
456
457 return ChainmonstersRewards.currentSeason
458 }
459
460
461 // createNewAdmin creates a new Admin resource
462 //
463 pub fun createNewAdmin(): @Admin {
464 return <-create Admin()
465 }
466 }
467
468
469
470 // -----------------------------------------------------------------------
471 // ChainmonstersRewards contract-level function definitions
472 // -----------------------------------------------------------------------
473
474 // public function that anyone can call to create a new empty collection
475 // This is required to receive Rewards in transactions.
476 pub fun createEmptyCollection(): @NonFungibleToken.Collection {
477 return <- create ChainmonstersRewards.Collection()
478 }
479
480 // returns all the rewards setup in this contract
481 pub fun getAllRewards(): [ChainmonstersRewards.Reward] {
482 return ChainmonstersRewards.rewardDatas.values
483 }
484
485 // returns returns all the metadata associated with a specific Reward
486 pub fun getRewardMetaData(rewardID: UInt32): String? {
487 return self.rewardDatas[rewardID]?.metadata
488 }
489
490
491 // returns the season this specified reward belongs to
492 pub fun getRewardSeason(rewardID: UInt32): UInt32? {
493 return ChainmonstersRewards.rewardDatas[rewardID]?.season
494 }
495
496 // returns the maximum supply of a reward
497 pub fun getRewardMaxSupply(rewardID: UInt32): UInt32? {
498 return ChainmonstersRewards.rewardSupplies[rewardID]
499 }
500
501 // isRewardLocked returns a boolean that indicates if a Reward
502 // can no longer be minted.
503 //
504 // Parameters: rewardID: The id of the Set that is being searched
505 //
506 //
507 // Returns: Boolean indicating if the reward is locked or not
508 pub fun isRewardLocked(rewardID: UInt32): Bool? {
509 // Don't force a revert if the reward is invalid
510 if (ChainmonstersRewards.rewardSupplies[rewardID] == ChainmonstersRewards.numberMintedPerReward[rewardID]) {
511
512 return true
513 } else {
514
515 // If the Reward wasn't found , return nil
516 return nil
517 }
518 }
519
520 // returns the number of Rewards that have been minted already
521 pub fun getNumRewardsMinted(rewardID: UInt32): UInt32? {
522 let amount = ChainmonstersRewards.numberMintedPerReward[rewardID]
523
524 return amount
525 }
526
527 // Get reward metadata from the contract owner storage, can be upgraded
528 pub fun getExternalSeasonMetadata(seasonID: UInt32): {String: String}? {
529 let data = self.account.getCapability<&[{ String: String }]>(/public/ChainmonstersSeasonsMetadata).borrow()
530
531 if (data == nil) {
532 return nil
533 }
534
535 return data![seasonID]
536 }
537
538 // Get reward metadata from the contract owner storage, can be upgraded
539 pub fun getExternalRewardMetadata(rewardID: UInt32): {String: String}? {
540 let data = self.account.getCapability<&[{ String: String }]>(/public/ChainmonstersRewardsMetadata).borrow()
541
542 if (data == nil) {
543 return nil
544 }
545
546 return data![rewardID]
547 }
548
549
550
551 init() {
552 // Initialize contract fields
553 self.rewardDatas = {}
554 self.nextRewardID = 1
555 self.totalSupply = 0
556 self.rewardSupplies = {}
557 self.numberMintedPerReward = {}
558 self.currentSeason = 0
559 self.rewardSeasons = {}
560
561 // Put a new Collection in storage
562 self.account.save<@Collection>(<- create Collection(), to: /storage/ChainmonstersRewardCollection)
563
564 // Create a public capability for the Collection
565 self.account.link<&{ChainmonstersRewardCollectionPublic}>(/public/ChainmonstersRewardCollection, target: /storage/ChainmonstersRewardCollection)
566
567 // Put the Minter in storage
568 self.account.save<@Admin>(<- create Admin(), to: /storage/ChainmonstersAdmin)
569
570 emit ContractInitialized()
571 }
572}
573