Smart Contract
DriverzNFT
A.f887ece39166906e.DriverzNFT
1
2import NonFungibleToken from 0x1d7e57aa55817448
3import MetadataViews from 0x1d7e57aa55817448
4import FungibleToken from 0xf233dcee88fe0abe
5import Crypto
6
7pub contract DriverzNFT: NonFungibleToken {
8
9 // Events
10 //
11 // This contract is initialized
12 pub event ContractInitialized()
13
14 // NFT is minted
15 pub event NFTMinted(
16 nftID: UInt64,
17 setID: UInt64,
18 templateID: UInt64,
19 displayName: String,
20 displayDescription: String,
21 displayURI: String,
22 creator: Address,
23 )
24
25 // NFT is withdrawn from a collection
26 pub event Withdraw(id: UInt64, from: Address?)
27
28 // NFT is deposited from a collection
29 pub event Deposit(id: UInt64, to: Address?)
30
31 // NFT is destroyed
32 pub event NFTDestroyed(id: UInt64)
33
34 // NFT template metadata is revealed
35 pub event NFTRevealed(
36 nftID: UInt64,
37 setID: UInt64,
38 templateID: UInt64,
39 displayName: String,
40 displayDescription: String,
41 displayURI: String,
42 metadata: {String: String},
43 )
44
45 // Set has been created
46 pub event SetCreated(setID: UInt64, metadata: SetMetadata)
47
48 // Template has been added
49 pub event TemplateAdded(setID: UInt64, templateID: UInt64, displayName: String, displayDescription: String, displayURI: String)
50
51 // Set has been marked Locked
52 pub event SetLocked(setID: UInt64, numTemplates: UInt64)
53
54 // Paths
55 //
56 pub let CollectionStoragePath: StoragePath
57 pub let CollectionPublicPath: PublicPath
58 pub let CollectionPrivatePath: PrivatePath
59 pub let AdminStoragePath: StoragePath
60
61 // Total NFT supply
62 pub var totalSupply: UInt64
63
64 pub fun externalURL(): MetadataViews.ExternalURL {
65 return MetadataViews.ExternalURL("https://driverz.world")
66 }
67
68 pub fun royaltyAddress(): Address {
69 return 0xa039bd7d55a96c0c
70 }
71
72 pub fun squareImageCID(): String {
73 return "QmV4FsnFiU7QY8ybwd5uuXwogVo9wcRExQLwedh7HU1mrU"
74 }
75
76 pub fun bannerImageCID(): String {
77 return "QmYn6vg1pCuKb6jWT3SDHuyX4NDyJB4wvcYarmsyppoGDS"
78 }
79
80 // Total number of sets
81 access(self) var totalSets: UInt64
82
83 // Dictionary mapping from set IDs to Set resources
84 access(self) var sets: @{UInt64: Set}
85
86 // Template metadata can have custom definitions but must have the
87 // following implemented in order to power all the features of the
88 // NFT contract.
89 pub struct interface TemplateMetadata {
90
91 // Hash representation of implementing structs.
92 pub fun hash(): [UInt8]
93
94 // Representative Display
95 pub fun display(): MetadataViews.Display
96
97 // Representative {string: string} serialization
98 pub fun repr(): {String: String}
99 }
100
101 pub struct DynamicTemplateMetadata: TemplateMetadata {
102 access(self) let _display: MetadataViews.Display
103 access(self) let _metadata: {String: String}
104
105 pub fun hash(): [UInt8] {
106 return []
107 }
108
109 pub fun display(): MetadataViews.Display {
110 return self._display
111 }
112
113 pub fun repr(): {String: String} {
114 return self.metadata()
115 }
116
117 pub fun metadata(): {String: String} {
118 return self._metadata
119 }
120
121 init(display: MetadataViews.Display, metadata: {String: String}) {
122 self._display = display
123 self._metadata = metadata
124 }
125 }
126
127 // NFT
128 //
129 // "Standard" NFTs that implement MetadataViews and point
130 // to a Template struct that give information about the NFTs metadata
131 pub resource NFT: NonFungibleToken.INFT, MetadataViews.Resolver {
132
133 // id is unique among all DriverzNFT NFTs on Flow, ordered sequentially from 0
134 pub let id: UInt64
135
136 // setID and templateID help us locate the specific template in the
137 // specific set which stores this NFTs metadata
138 pub let setID: UInt64
139 pub let templateID: UInt64
140
141 // The creator of the NFT
142 pub let creator: Address
143
144 // Fetch the metadata Template represented by this NFT
145 pub fun template(): {NFTTemplate} {
146 return DriverzNFT.getTemplate(setID: self.setID, templateID: self.templateID)
147 }
148
149 // Proxy for MetadataViews.Resolver.getViews implemented by Template
150 pub fun getViews(): [Type] {
151 return [
152 Type<MetadataViews.NFTView>(),
153 Type<MetadataViews.Display>(),
154 Type<MetadataViews.Royalties>(),
155 Type<MetadataViews.ExternalURL>(),
156 Type<MetadataViews.NFTCollectionDisplay>(),
157 Type<MetadataViews.NFTCollectionData>()
158 ]
159 }
160
161 pub fun resolveView(_ view: Type): AnyStruct? {
162 switch view {
163 case Type<MetadataViews.NFTView>():
164 let viewResolver = &self as &{MetadataViews.Resolver}
165 return MetadataViews.NFTView(
166 id : self.id,
167 uuid: self.uuid,
168 display: MetadataViews.getDisplay(viewResolver),
169 externalURL : MetadataViews.getExternalURL(viewResolver),
170 collectionData : MetadataViews.getNFTCollectionData(viewResolver),
171 collectionDisplay : MetadataViews.getNFTCollectionDisplay(viewResolver),
172 royalties : MetadataViews.getRoyalties(viewResolver),
173 traits : MetadataViews.getTraits(viewResolver)
174 )
175 case Type<MetadataViews.Display>():
176 let template = self.template()
177 if template.revealed() {
178 return template.metadata!.display()
179 }
180 return template.defaultDisplay
181 case Type<MetadataViews.Royalties>():
182 let royalties: [MetadataViews.Royalty] = []
183 let royaltyReceiverCap =
184 getAccount(DriverzNFT.royaltyAddress()).getCapability<&{FungibleToken.Receiver}>(/public/dapperUtilityCoinReceiver)
185 if royaltyReceiverCap.check() {
186 royalties.append(
187 MetadataViews.Royalty(
188 receiver: royaltyReceiverCap,
189 cut: 0.05,
190 description: "Creator royalty fee."
191 )
192 )
193 }
194 return MetadataViews.Royalties(royalties)
195 case Type<MetadataViews.ExternalURL>():
196 return DriverzNFT.externalURL()
197 case Type<MetadataViews.NFTCollectionData>():
198 return MetadataViews.NFTCollectionData(
199 storagePath: DriverzNFT.CollectionStoragePath,
200 publicPath: DriverzNFT.CollectionPublicPath,
201 providerPath: DriverzNFT.CollectionPrivatePath,
202 publicCollection: Type<@DriverzNFT.Collection>(),
203 publicLinkedType: Type<&DriverzNFT.Collection{NonFungibleToken.CollectionPublic, NonFungibleToken.Receiver, MetadataViews.ResolverCollection}>(),
204 providerLinkedType: Type<&DriverzNFT.Collection{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection}>(),
205 createEmptyCollectionFunction: fun(): @NonFungibleToken.Collection{
206 return <- DriverzNFT.createEmptyCollection()
207 }
208 )
209 case Type<MetadataViews.NFTCollectionDisplay>():
210 return MetadataViews.NFTCollectionDisplay(
211 name: "Driverz",
212 description: "An exclusive collection of energetic Driverz ready to vroom vroom on FLOW.",
213 externalURL: DriverzNFT.externalURL(),
214 squareImage:
215 MetadataViews.Media(
216 file: MetadataViews.IPFSFile(cid: DriverzNFT.squareImageCID(), path: nil),
217 mediaType: "image/svg+xml"
218 ),
219 bannerImage:
220 MetadataViews.Media(
221 file: MetadataViews.IPFSFile(cid: DriverzNFT.bannerImageCID(), path: nil),
222 mediaType: "image/svg+xml"
223 ),
224 socials: {
225 "twitter": MetadataViews.ExternalURL("https://twitter.com/DriverzWorld/"),
226 "discord": MetadataViews.ExternalURL("https://discord.gg/driverz"),
227 "instagram": MetadataViews.ExternalURL("https://www.instagram.com/driverzworld/")
228 }
229 )
230 }
231 return nil
232 }
233
234 // NFT needs to be told which Template it follows
235 init(setID: UInt64, templateID: UInt64, creator: Address) {
236 self.id = DriverzNFT.totalSupply
237 DriverzNFT.totalSupply = DriverzNFT.totalSupply + 1
238 self.setID = setID
239 self.templateID = templateID
240 self.creator = creator
241 let defaultDisplay = self.template().defaultDisplay
242 emit NFTMinted(
243 nftID: self.id,
244 setID: self.setID,
245 templateID: self.templateID,
246 displayName: defaultDisplay.name,
247 displayDescription: defaultDisplay.description,
248 displayURI: defaultDisplay.thumbnail.uri(),
249 creator: self.creator
250 )
251 }
252
253 // Emit NFTDestroyed when destroyed
254 destroy() {
255 emit NFTDestroyed(
256 id: self.id,
257 )
258 }
259 }
260
261 // Collection
262 //
263 // Collections provide a way for collectors to store DriverzNFT NFTs in their
264 // Flow account.
265
266 // Exposing this interface allows external parties to inspect a Flow
267 // account's DriverzNFT Collection and deposit NFTs
268 pub resource interface CollectionPublic {
269 pub fun deposit(token: @NonFungibleToken.NFT)
270 pub fun getIDs(): [UInt64]
271 pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT
272 pub fun borrowDriverzNFT(id: UInt64): &NFT
273 }
274
275 pub resource Collection: CollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection {
276
277 // NFTs are indexed by its globally assigned id
278 pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT}
279
280 // Deposit a DriverzNFT into the collection. Safe to assume id's are unique.
281 pub fun deposit(token: @NonFungibleToken.NFT) {
282 // Required to ensure this is a DriverzNFT
283 let token <- token as! @DriverzNFT.NFT
284 let id: UInt64 = token.id
285 let oldToken <- self.ownedNFTs[id] <- token
286 emit Deposit(id: id, to: self.owner?.address)
287 destroy oldToken
288 }
289
290 // Withdraw an NFT from the collection.
291 // Panics if NFT does not exist in the collection
292 pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT {
293 pre {
294 self.ownedNFTs.containsKey(withdrawID)
295 : "NFT does not exist in collection."
296 }
297 let token <- self.ownedNFTs.remove(key: withdrawID)!
298 emit Withdraw(id: token.id, from: self.owner?.address)
299 return <-token
300 }
301
302 // Return all the IDs from the collection.
303 pub fun getIDs(): [UInt64] {
304 return self.ownedNFTs.keys
305 }
306
307 // Borrow a reference to the specified NFT
308 // Panics if NFT does not exist in the collection
309 pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT {
310 pre {
311 self.ownedNFTs.containsKey(id)
312 : "NFT does not exist in collection."
313 }
314 return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)!
315 }
316
317 // Borrow a reference to the specified NFT as a DriverzNFT.
318 // Panics if NFT does not exist in the collection
319 pub fun borrowDriverzNFT(id: UInt64): &NFT {
320 pre {
321 self.ownedNFTs.containsKey(id)
322 : "NFT does not exist in collection."
323 }
324 let ref = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
325 return ref as! &NFT
326 }
327
328 // Return the MetadataViews.Resolver of the specified NFT
329 // Panics if NFT does not exist in the collection
330 pub fun borrowViewResolver(id: UInt64): &{MetadataViews.Resolver} {
331 pre {
332 self.ownedNFTs.containsKey(id)
333 : "NFT does not exist in collection."
334 }
335 let nft = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
336 let typedNFT = nft as! &NFT
337 return typedNFT
338 }
339
340 init() {
341 self.ownedNFTs <- {}
342 }
343
344 // If the collection is destroyed, destroy the NFTs it holds, as well
345 destroy() {
346 destroy self.ownedNFTs
347 }
348 }
349
350 // Anyone can make and store collections
351 pub fun createEmptyCollection(): @Collection {
352 return <-create Collection()
353 }
354
355 pub resource Set {
356
357 // Globally assigned id based on number of created Sets.
358 pub let id: UInt64
359
360 pub var isLocked: Bool
361
362 // Metadata for the Set
363 pub var metadata: SetMetadata
364
365 // Templates configured to be minted from this Set
366 access(contract) var templates: [Template]
367
368 // Number of NFTs that have minted from this Set
369 pub var minted: UInt64
370
371 // Add a new Template to the Set, only if the Set is Open
372 pub fun addTemplate(template: Template) {
373 pre {
374 !self.isLocked : "Set is locked. It cannot be modified"
375 }
376 let templateID = self.templates.length
377 self.templates.append(template)
378
379 let display = template.defaultDisplay
380 emit TemplateAdded(setID: self.id, templateID: UInt64(templateID), displayName: display.name, displayDescription: display.description, displayURI: display.thumbnail.uri())
381 }
382
383 // Lock the Set if it is Open. This signals that this Set
384 // will mint NFTs based only on the Templates configured in this Set.
385 pub fun lock() {
386 pre {
387 !self.isLocked : "Only an Open set can be locked."
388 self.templates.length > 0
389 : "Set must be configured with at least one Template."
390 }
391 self.isLocked = true
392 emit SetLocked(setID: self.id, numTemplates: UInt64(self.templates.length))
393 }
394
395 // Mint numToMint NFTs with the supplied creator attribute. The NFT will
396 // be minted into the provided receiver
397 pub fun mint(
398 templateID: UInt64,
399 creator: Address
400 ): @NFT {
401 pre {
402 templateID < UInt64(self.templates.length)
403 : "templateID does not exist in Set."
404 self.templates[templateID].mintID == nil
405 : "Template has already been marked as minted."
406 }
407 let nft <-create NFT(
408 setID: self.id,
409 templateID: templateID,
410 creator: creator
411 )
412 self.templates[templateID].markMinted(nftID: nft.id)
413 self.minted = self.minted + 1
414 return <- nft
415 }
416
417 // Reveal a specified Template in a Set.
418 pub fun revealTemplate(
419 templateID: UInt64,
420 metadata: {TemplateMetadata},
421 salt: [UInt8]
422 ) {
423 pre {
424 templateID < UInt64(self.templates.length)
425 : "templateId does not exist in Set."
426 self.templates[templateID].mintID != nil
427 : "Template has not been marked as minted."
428 }
429 let template = &self.templates[templateID] as &Template
430 template.reveal(metadata: metadata, salt: salt)
431
432 let display = metadata.display()
433 emit NFTRevealed(
434 nftID: template.mintID!,
435 setID: self.id,
436 templateID: templateID,
437 displayName: display.name,
438 displayDescription: display.description,
439 displayURI: display.thumbnail.uri(),
440 metadata: metadata.repr(),
441 )
442 }
443
444 init(id: UInt64, metadata: SetMetadata) {
445 self.id = id
446 self.metadata = metadata
447
448 self.isLocked = false
449 self.templates = []
450
451 self.minted = 0
452 emit SetCreated(setID: id, metadata: metadata)
453 }
454 }
455
456 // Create and store a new Set. Return the id of the new Set.
457 access(contract) fun createSet(metadata: SetMetadata): UInt64 {
458 let setID = DriverzNFT.totalSets
459
460 let newSet <- create Set(
461 id: setID,
462 metadata: metadata
463 )
464 DriverzNFT.sets[setID] <-! newSet
465 DriverzNFT.totalSets = DriverzNFT.totalSets + 1
466 return setID
467 }
468
469 // Number of sets created by contract
470 pub fun setsCount(): UInt64 {
471 return DriverzNFT.totalSets
472 }
473
474 // Metadata for the Set
475 pub struct SetMetadata {
476 pub var name: String
477 pub var description: String
478 pub var externalID: String
479
480 init(name: String, description: String, externalID: String) {
481 self.name = name
482 self.description = description
483 self.externalID = externalID
484 }
485 }
486
487 // A summary report of a Set
488 pub struct SetReport {
489 pub let id: UInt64
490 pub let isLocked: Bool
491 pub let metadata: SetMetadata
492 pub let numTemplates: Int
493 pub let numMinted: UInt64
494 init(
495 id: UInt64,
496 isLocked: Bool,
497 metadata: SetMetadata,
498 numTemplates: Int,
499 numMinted: UInt64
500 ) {
501 self.id = id
502 self.isLocked = isLocked
503 self.metadata = metadata
504 self.numTemplates = numTemplates
505 self.numMinted = numMinted
506 }
507 }
508
509 // Generate a SetReport for informational purposes (to be used with scripts)
510 pub fun generateSetReport(setID: UInt64): SetReport {
511 let setRef = (&self.sets[setID] as &Set?)!
512 return SetReport(
513 id: setID,
514 isLocked: setRef.isLocked,
515 metadata: setRef.metadata,
516 numTemplates: setRef.templates.length,
517 numMinted: setRef.minted
518 )
519 }
520
521 // Template
522 //
523 // Templates are mechanisms for handling NFT metadata. These should ideally
524 // have a one to one mapping with NFTs, with the assumption that NFTs are
525 // designed to be unique. Template allows the creator to commit to an NFTs
526 // metadata without having to reveal the metadata itself. The constructor
527 // accepts a byte array checksum. After construction, anyone with access
528 // to this struct will be able to reveal the metadata, which must be any
529 // struct which implements TemplateMetadata and MetadataViews.Resolver such that
530 // SHA3_256(salt || metadata.hash()) == checksum.
531 //
532 // Templates can be seen as metadata managers for NFTs. As such, Templates
533 // also implement the MetadataResolver interface to conform with standards.
534
535 // Safe Template interface for anyone inspecting NFTs
536 pub struct interface NFTTemplate {
537 pub let defaultDisplay: MetadataViews.Display
538 pub var metadata: {TemplateMetadata}?
539 pub var mintID: UInt64?
540 pub fun checksum(): [UInt8]
541 pub fun salt(): [UInt8]?
542 pub fun revealed(): Bool
543 }
544
545 pub struct Template: NFTTemplate {
546
547 // checksum as described above
548 access(self) let _checksum: [UInt8]
549
550 // Default Display in case the Template has not yet been revealed
551 pub let defaultDisplay: MetadataViews.Display
552
553 // salt and metadata are optional so they can be revealed later, such that
554 // SHA3_256(salt || metadata.hash()) == checksum
555 access(self) var _salt: [UInt8]?
556 pub var metadata: {TemplateMetadata}?
557
558 // Convenience attribute to mark whether or not Template has minted NFT
559 pub var mintID: UInt64?
560
561 // Helper function to check if a proposed metadata and salt reveal would
562 // produce the configured checksum in a Template
563 pub fun validate(metadata: {TemplateMetadata}, salt: [UInt8]): Bool {
564 let hash = String.encodeHex(
565 HashAlgorithm.SHA3_256.hash(
566 salt.concat(metadata.hash())
567 )
568 )
569 let checksum = String.encodeHex(self.checksum())
570 return hash == checksum
571 }
572
573 // Reveal template metadata and salt. validate() is called as a precondition
574 // so collector can be assured metadata was not changed
575 pub fun reveal(metadata: AnyStruct{TemplateMetadata}, salt: [UInt8]) {
576 pre {
577 self.mintID != nil
578 : "Template has not yet been minted."
579 !self.revealed()
580 : "NFT Template has already been revealed"
581 self.validate(metadata: metadata, salt: salt)
582 : "salt || metadata.hash() does not hash to checksum"
583 }
584 self.metadata = metadata
585 self._salt = salt
586 }
587
588 pub fun checksum(): [UInt8] {
589 return self._checksum
590 }
591
592 pub fun salt(): [UInt8]? {
593 return self._salt
594 }
595
596 // Check to see if metadata has been revealed
597 pub fun revealed(): Bool {
598 return self.metadata != nil
599 }
600
601 // Mark the NFT as minted
602 pub fun markMinted(nftID: UInt64) {
603 self.mintID = nftID
604 }
605
606 init(checksum: [UInt8], defaultDisplay: MetadataViews.Display) {
607 self._checksum = checksum
608 self.defaultDisplay = defaultDisplay
609
610 self._salt = nil
611 self.metadata = nil
612 self.mintID = nil
613 }
614 }
615
616 // Public helper function to be able to inspect any Template
617 pub fun getTemplate(setID: UInt64, templateID: UInt64): {NFTTemplate} {
618 let setRef = (&self.sets[setID] as &Set?)!
619 return setRef.templates[templateID]
620 }
621
622 pub resource SetMinter {
623 pub let setID: UInt64
624
625 init(setID: UInt64) {
626 self.setID = setID
627 }
628
629 pub fun mint(templateID: UInt64, creator: Address): @NFT {
630 let set = (&DriverzNFT.sets[self.setID] as &Set?)!
631 return <- set.mint(templateID: templateID, creator: creator)
632 }
633 }
634
635 // Admin
636 //
637 // The Admin is meant to be a singleton superuser of the contract. The Admin
638 // is responsible for creating Sets and SetManagers for managing the sets.
639 pub resource Admin {
640
641 // Create a set with the provided SetMetadata.
642 pub fun createSet(metadata: SetMetadata): UInt64 {
643 return DriverzNFT.createSet(metadata: metadata)
644 }
645
646 pub fun borrowSet(setID: UInt64): &Set {
647 return (&DriverzNFT.sets[setID] as &Set?)!
648 }
649
650 pub fun createSetMinter(setID: UInt64): @SetMinter {
651 return <- create SetMinter(setID: setID)
652 }
653 }
654
655 // Contract constructor
656 init() {
657
658 // Collection Paths
659 self.CollectionStoragePath = /storage/DriverzNFTCollection
660 self.CollectionPublicPath = /public/DriverzNFTCollection
661 self.CollectionPrivatePath = /private/DriverzNFTCollection
662
663 // Admin Storage Path. Save the singleton Admin resource to contract
664 // storage.
665 self.AdminStoragePath = /storage/DriverzNFTAdmin
666 self.account.save<@Admin>(<- create Admin(), to: self.AdminStoragePath)
667
668 // Initializations
669 self.totalSupply = 0
670 self.totalSets = 0
671 self.sets <- {}
672
673 emit ContractInitialized()
674 }
675}
676