Smart Contract
HeroesOfTheFlow
A.1dc37ab51a54d83f.HeroesOfTheFlow
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