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