Smart Contract

HeroesOfTheFlow

A.1dc37ab51a54d83f.HeroesOfTheFlow

Deployed

2d ago
Feb 26, 2026, 02:30:26 PM UTC

Dependents

0 imports
1/*
2    HeroesOfTheFlow.cdc
3
4    Author: Brian Min brian@flowverse.co
5*/
6
7import NonFungibleToken from 0x1d7e57aa55817448
8import MetadataViews from 0x1d7e57aa55817448
9import FungibleToken from 0xf233dcee88fe0abe
10import ViewResolver from 0x1d7e57aa55817448
11import FlowversePrimarySaleV2 from 0x9212a87501a8a6a2
12
13access(all) contract HeroesOfTheFlow: NonFungibleToken {
14    // Events
15    access(all) event EntityCreated(id: UInt64, metadata: {String:String})
16    access(all) event EntityUpdated(id: UInt64, metadata: {String:String})
17    access(all) event NFTMinted(nftID: UInt64, nftUUID: UInt64, entityID: UInt64, minterAddress: Address)
18
19    // Named Paths
20    access(all) let CollectionStoragePath: StoragePath
21    access(all) let CollectionPublicPath: PublicPath
22    access(all) let AdminStoragePath: StoragePath
23
24    access(self) var entityDatas: {UInt64: Entity}
25    access(self) var numMintedPerEntity: {UInt64: UInt64}
26
27    // Total number of HeroesOfTheFlow NFTs that have been minted
28    // Incremented ID used to create nfts
29    access(all) var totalSupply: UInt64
30
31    // Incremented ID used to create entities
32    access(all) var nextEntityID: UInt64
33
34    // Entity is a blueprint that holds metadata associated with an NFT
35    access(all) struct Entity {
36        // Unique ID for the entity
37        access(all) let entityID: UInt64
38
39        // Stores all the metadata about the entity as a string mapping
40        access(contract) var metadata: {String: String}
41
42        init(metadata: {String: String}) {
43            pre {
44                metadata.length != 0: "New Entity metadata cannot be empty"
45            }
46            self.entityID = HeroesOfTheFlow.nextEntityID
47            self.metadata = metadata
48        }
49
50        access(contract) fun removeMetadata(key: String) {
51            self.metadata.remove(key: key)
52        }
53
54        access(contract) fun setMetadata(key: String, value: String) {
55            self.metadata[key] = value
56        }
57
58        access(contract) fun replaceMetadata(_ metadata: {String: String}) {
59            self.metadata = metadata
60        }
61    }
62
63    // NFT Resource that represents the Entity instances
64    access(all) resource NFT: NonFungibleToken.NFT, ViewResolver.Resolver {
65        // Global unique NFT ID
66        access(all) let id: UInt64
67
68        // The ID of the Entity that the NFT references
69        access(all) let entityID: UInt64
70
71        // The minterAddress of the NFT
72        access(all) let minterAddress: Address
73
74        init(entityID: UInt64, minterAddress: Address) {
75            self.id = HeroesOfTheFlow.totalSupply
76            self.entityID = entityID
77            self.minterAddress = minterAddress
78
79            emit NFTMinted(nftID: self.id, nftUUID: self.uuid, entityID: entityID, minterAddress: self.minterAddress)
80        }
81
82        access(all) view fun checkSoulbound(): Bool {
83            return HeroesOfTheFlow.getEntityMetaDataByField(entityID: self.entityID, field: "soulbound") == "true"
84        }
85
86        access(all) view fun getViews(): [Type] {
87            let supportedViews = [
88                Type<MetadataViews.Display>(),
89                Type<MetadataViews.Royalties>(),
90                Type<MetadataViews.Edition>(),
91                Type<MetadataViews.Traits>(),
92                Type<MetadataViews.ExternalURL>(),
93                Type<MetadataViews.Rarity>()
94            ]
95            return supportedViews
96        }
97
98        access(all) fun resolveView(_ view: Type): AnyStruct? {
99            switch view {
100                case Type<MetadataViews.Display>():
101                    return MetadataViews.Display(
102                        name: HeroesOfTheFlow.getEntityMetaDataByField(entityID: self.entityID, field: "name") ?? "",
103                        description: HeroesOfTheFlow.getEntityMetaDataByField(entityID: self.entityID, field: "description") ?? "",
104                        thumbnail: MetadataViews.HTTPFile(
105                          url: HeroesOfTheFlow.getEntityMetaDataByField(entityID: self.entityID, field: "thumbnailURL") ?? ""
106                        )
107                    )
108                case Type<MetadataViews.Royalties>():
109                    let royalties : [MetadataViews.Royalty] = [
110                        MetadataViews.Royalty(
111                            receiver: getAccount(0xc5857663ca37efbf).capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)!,
112                            cut: 0.05,
113                            description: "Creator Royalty Fee")
114                    ]
115                    return MetadataViews.Royalties(royalties)
116                case Type<MetadataViews.Edition>():
117                    return MetadataViews.Edition(
118                        name: HeroesOfTheFlow.getEntityMetaDataByField(entityID: self.entityID, field: "name") ?? "",
119                        number: self.entityID,
120                        max: 12000
121                    )
122                case Type<MetadataViews.Traits>():
123                    let traits: [MetadataViews.Trait] = []
124                    if let background = HeroesOfTheFlow.getEntityMetaDataByField(entityID: self.entityID, field: "background") {
125                        traits.append(MetadataViews.Trait(
126                            name: "Background",
127                            value: background,
128                            displayType: nil,
129                            rarity: nil
130                        ))
131                    }
132                    if let rarity = HeroesOfTheFlow.getEntityMetaDataByField(entityID: self.entityID, field: "rarity") {
133                        traits.append(MetadataViews.Trait(
134                            name: "Rarity",
135                            value: rarity,
136                            displayType: nil,
137                            rarity: nil
138                        ))
139                    }
140                    if let minion = HeroesOfTheFlow.getEntityMetaDataByField(entityID: self.entityID, field: "minion") {
141                        traits.append(MetadataViews.Trait(
142                            name: "Minion",
143                            value: minion,
144                            displayType: nil,
145                            rarity: nil
146                        ))
147                    }
148                    return MetadataViews.Traits(traits)
149                case Type<MetadataViews.Rarity>():
150                    return MetadataViews.Rarity(
151                        score: nil,
152                        max: nil,
153                        description: HeroesOfTheFlow.getEntityMetaDataByField(entityID: self.entityID, field: "rarity") ?? "Rare"
154                    )
155                case Type<MetadataViews.ExternalURL>():
156                    let baseURL = "https://nft.flowverse.co/collections/HeroesOfTheFlow/"
157                    return MetadataViews.ExternalURL(baseURL.concat(self.owner!.address.toString()).concat("/".concat(self.id.toString())))
158            }
159            return nil
160        }
161
162        access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
163            return <- HeroesOfTheFlow.createEmptyCollection(nftType: Type<@HeroesOfTheFlow.NFT>())
164        }
165    }
166
167    access(self) fun mint(entityID: UInt64, minterAddress: Address): @NFT {
168        pre {
169            HeroesOfTheFlow.entityDatas[entityID] != nil: "Cannot mint: the entity doesn't exist."
170        }
171
172        // Gets the number of NFTs that have been minted for this Entity
173        let entityMintNumber = HeroesOfTheFlow.numMintedPerEntity[entityID]!
174
175        // Increment the global NFT ID
176        HeroesOfTheFlow.totalSupply = HeroesOfTheFlow.totalSupply + UInt64(1)
177
178        // Mint the new NFT
179        let newNFT: @NFT <- create NFT(entityID: entityID, minterAddress: minterAddress)
180
181        // Increment the number of copies minted for this NFT
182        HeroesOfTheFlow.numMintedPerEntity[entityID] = entityMintNumber + UInt64(1)
183        return <-newNFT
184    }
185
186    access(all) resource NFTMinter: FlowversePrimarySaleV2.IMinter {
187        init() {}
188        access(all) fun mint(entityID: UInt64, minterAddress: Address): @NFT {
189            return <-HeroesOfTheFlow.mint(entityID: entityID, minterAddress: minterAddress)
190        }
191    }
192
193    // Admin is a special authorization resource that 
194    // allows the owner to perform important functions to modify the 
195    // various aspects of the Entities, Sets, and NFTs
196    //
197    access(all) resource Admin {
198
199        // createEntity creates a new Entity struct 
200        // and stores it in the Entities dictionary in the HeroesOfTheFlow smart contract
201        access(all) fun createEntity(metadata: {String: String}): UInt64 {
202            // Create the new Entity
203            var newEntity = Entity(metadata: metadata)
204            let newID = newEntity.entityID
205
206            // Increment the ID so that it isn't used again
207            HeroesOfTheFlow.nextEntityID = HeroesOfTheFlow.nextEntityID + UInt64(1)
208
209            // Store it in the contract storage
210            HeroesOfTheFlow.entityDatas[newID] = newEntity
211
212            // Initialise numMintedPerEntity
213            HeroesOfTheFlow.numMintedPerEntity[newID] = UInt64(0)
214            
215            emit EntityCreated(id: newID, metadata: metadata)
216
217            return newID
218        }
219        
220        // updateEntity updates an existing Entity 
221        access(all) fun updateEntity(entityID: UInt64, metadata: {String: String}) {
222            let updatedEntity = HeroesOfTheFlow.entityDatas[entityID]!
223            updatedEntity.replaceMetadata(metadata)
224            HeroesOfTheFlow.entityDatas[entityID] = updatedEntity
225            
226            emit EntityUpdated(id: entityID, metadata: metadata)
227        }
228
229        access(all) fun setEntitySoulbound(entityID: UInt64, soulbound: Bool) {
230            assert(HeroesOfTheFlow.entityDatas[entityID] != nil, message: "Cannot set soulbound: the entity doesn't exist.")
231            if soulbound {
232                HeroesOfTheFlow.entityDatas[entityID]!.setMetadata(key: "soulbound", value: "true")
233            } else {
234                HeroesOfTheFlow.entityDatas[entityID]!.removeMetadata(key: "soulbound")
235            }
236        }
237
238        access(all) fun mint(entityID: UInt64, minterAddress: Address): @NFT {
239            return <-HeroesOfTheFlow.mint(entityID: entityID, minterAddress: minterAddress)
240        }
241
242        // createNFTMinter creates a new NFTMinter resource
243        access(all) fun createNFTMinter(): @NFTMinter {
244            return <-create NFTMinter()
245        }
246
247        // createNewAdmin creates a new Admin resource
248        access(all) fun createNewAdmin(): @Admin {
249            return <-create Admin()
250        }
251    }
252    
253    access(all) resource interface CollectionPublic {
254        access(all) fun deposit(token: @{NonFungibleToken.NFT})
255        access(all) view fun getIDs(): [UInt64]
256        access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}?
257        access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}?
258    }
259
260    access(all) resource Collection: CollectionPublic, NonFungibleToken.Collection {
261        // Dictionary of entity instances conforming tokens
262        // NFT is a resource type with a UInt64 ID field
263        access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
264
265        init () {
266            self.ownedNFTs <- {}
267        }
268
269        /// getSupportedNFTTypes returns a list of NFT types that this receiver accepts
270        access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
271            let supportedTypes: {Type: Bool} = {}
272            supportedTypes[Type<@HeroesOfTheFlow.NFT>()] = true
273            return supportedTypes
274        }
275
276        /// Returns whether or not the given type is accepted by the collection
277        /// A collection that can accept any type should just return true by default
278        access(all) view fun isSupportedNFTType(type: Type): Bool {
279           if type == Type<@HeroesOfTheFlow.NFT>() {
280            return true
281           } else {
282            return false
283           }
284        }
285
286        /// Gets the amount of NFTs stored in the collection
287        access(all) view fun getLength(): Int {
288            return self.ownedNFTs.keys.length
289        }
290
291        /// withdraw removes an NFT from the collection and moves it to the caller
292        access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
293            let token <- self.ownedNFTs.remove(key: withdrawID)
294                ?? panic("Could not withdraw an NFT with the provided ID from the collection")
295
296            return <-token
297        }
298
299        /// deposit takes a NFT and adds it to the collections dictionary
300        /// and adds the ID to the id array
301        access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
302            let token <- token as! @HeroesOfTheFlow.NFT
303            let id = token.id
304
305            // add the new token to the dictionary which removes the old one
306            let oldToken <- self.ownedNFTs[token.id] <- token
307
308            destroy oldToken
309        }
310
311        access(all) view fun getIDs(): [UInt64] {
312            return self.ownedNFTs.keys
313        }
314
315        access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
316            return (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)
317        }
318
319        /// Borrow the view resolver for the specified NFT ID
320        access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? {
321            if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? {
322                return nft as &{ViewResolver.Resolver}
323            }
324            return nil
325        }
326
327        /// createEmptyCollection creates an empty Collection of the same type
328        /// and returns it to the caller
329        /// @return A an empty collection of the same type
330        access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
331            return <-HeroesOfTheFlow.createEmptyCollection(nftType: Type<@HeroesOfTheFlow.NFT>())
332        }
333    }
334
335    // -----------------------------------------------------------------------
336    // HeroesOfTheFlow contract-level function definitions
337    // -----------------------------------------------------------------------
338
339    /// createEmptyCollection creates an empty Collection for the specified NFT type
340    /// and returns it to the caller so that they can own NFTs
341    access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {
342        return <- create Collection()
343    }
344
345    /// Function that returns all the Metadata Views implemented by a Non Fungible Token
346    ///
347    /// @return An array of Types defining the implemented views. This value will be used by
348    ///         developers to know which parameter to pass to the resolveView() method.
349    ///
350    access(all) view fun getContractViews(resourceType: Type?): [Type] {
351        return [
352            Type<MetadataViews.NFTCollectionData>(),
353            Type<MetadataViews.NFTCollectionDisplay>()
354        ]
355    }
356
357    /// Function that resolves a metadata view for this contract.
358    ///
359    /// @param view: The Type of the desired view.
360    /// @return A structure representing the requested view.
361    ///
362    access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
363        switch viewType {
364           case Type<MetadataViews.NFTCollectionData>():
365                return MetadataViews.NFTCollectionData(
366                    storagePath: self.CollectionStoragePath,
367                    publicPath: self.CollectionPublicPath,
368                    publicCollection: Type<&HeroesOfTheFlow.Collection>(),
369                    publicLinkedType: Type<&HeroesOfTheFlow.Collection>(),
370                    createEmptyCollectionFunction: (fun(): @{NonFungibleToken.Collection} {return <- HeroesOfTheFlow.createEmptyCollection(nftType: Type<@HeroesOfTheFlow.NFT>())}),
371                )
372            case Type<MetadataViews.NFTCollectionDisplay>():
373                return MetadataViews.NFTCollectionDisplay(
374                    name: "Heroes of the Flow",
375                    description: "Heroes of the Flow is a post-apocalyptic auto-battler set in the Rogues universe.",
376                    externalURL: MetadataViews.ExternalURL("https://twitter.com/heroesoftheflow"),
377                    squareImage: MetadataViews.Media(
378                        file: MetadataViews.HTTPFile(
379                            url: "https://flowverse.myfilebase.com/ipfs/QmU7a1eLvsmLda1VPe2ioikeWmhPwk5Xm7eV2iBUuirm55"
380                        ),
381                        mediaType: "image/jpg"
382                    ),
383                    bannerImage: MetadataViews.Media(
384                        file: MetadataViews.HTTPFile(
385                            url: "https://flowverse.myfilebase.com/ipfs/QmNMek1Q2i3MoGwz7bDVAU6mCMWByqpmEhk1TFLpNXEcEF"
386                        ),
387                        mediaType: "image/jpg"
388                    ),
389                    socials: {
390                        "twitter": MetadataViews.ExternalURL("https://twitter.com/heroesoftheflow")
391                    }
392                )
393        }
394        return nil
395    }
396
397    // getAllEntities returns all the entities available
398    access(all) view fun getAllEntities(): [HeroesOfTheFlow.Entity] {
399        return HeroesOfTheFlow.entityDatas.values
400    }
401
402    // getEntity returns an entity by ID
403    access(all) view fun getEntity(entityID: UInt64): HeroesOfTheFlow.Entity? {
404        return self.entityDatas[entityID]
405    }
406
407    // getEntityMetaData returns all the metadata associated with a specific Entity
408    access(all) view fun getEntityMetaData(entityID: UInt64): {String: String}? {
409        return self.entityDatas[entityID]?.metadata
410    }
411    
412    access(all) view fun getEntityMetaDataByField(entityID: UInt64, field: String): String? {
413        if let entity = HeroesOfTheFlow.entityDatas[entityID] {
414            return entity.metadata[field]
415        } else {
416            return nil
417        }
418    }
419
420    access(all) view fun getNumMintedPerEntity(): {UInt64: UInt64} {
421        return self.numMintedPerEntity
422    }
423
424    // -----------------------------------------------------------------------
425    // HeroesOfTheFlow initialization function
426    // -----------------------------------------------------------------------
427    //
428    init() {
429        self.AdminStoragePath = /storage/HeroesOfTheFlowAdmin
430        self.CollectionStoragePath = /storage/HeroesOfTheFlowCollection
431        self.CollectionPublicPath = /public/HeroesOfTheFlowCollection
432
433        // Initialize contract fields
434        self.entityDatas = {}
435        self.numMintedPerEntity = {}
436        self.nextEntityID = 1
437        self.totalSupply = 0
438
439        // Create and save a new Collection in storage
440        let collection <- create Collection()
441        self.account.storage.save(<-collection, to: self.CollectionStoragePath)
442
443        // Issue a public capability for the Collection
444        let collectionCap = self.account.capabilities.storage.issue<&HeroesOfTheFlow.Collection>(self.CollectionStoragePath)
445        self.account.capabilities.publish(collectionCap, at: self.CollectionPublicPath)
446
447        // Create and save Admin resource in storage
448        self.account.storage.save<@Admin>(<- create Admin(), to: self.AdminStoragePath)
449    }
450}
451