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