Smart Contract

ChainmonstersRewards

A.93615d25d14fa337.ChainmonstersRewards

Deployed

2d ago
Feb 25, 2026, 02:34:14 PM UTC

Dependents

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