Smart Contract
PackNFT
A.e4cf4bdc1751c65d.PackNFT
1import Crypto
2import NonFungibleToken from 0x1d7e57aa55817448
3import FungibleToken from 0xf233dcee88fe0abe
4import IPackNFT from 0x18ddf0823a55a0ee
5import MetadataViews from 0x1d7e57aa55817448
6import ViewResolver from 0x1d7e57aa55817448
7
8/// Contract that defines Pack NFTs.
9///
10access(all) contract PackNFT: NonFungibleToken, IPackNFT {
11
12 access(all) var totalSupply: UInt64
13 access(all) let version: String
14 access(all) let CollectionStoragePath: StoragePath
15 access(all) let CollectionPublicPath: PublicPath
16 access(all) let CollectionIPackNFTPublicPath: PublicPath
17 access(all) let OperatorStoragePath: StoragePath
18
19 /// Dictionary that stores Pack resources in the contract state (i.e., Pack NFT representations to keep track of states).
20 ///
21 access(contract) let packs: @{UInt64: Pack}
22
23 access(all) event RevealRequest(id: UInt64, openRequest: Bool)
24 access(all) event OpenRequest(id: UInt64)
25 access(all) event Revealed(id: UInt64, salt: String, nfts: String)
26 access(all) event Opened(id: UInt64)
27 access(all) event Mint(id: UInt64, commitHash: String, distId: UInt64)
28 access(all) event ContractInitialized()
29
30 // TODO: Consider removing 'Withdraw' and 'Deposit' events now that similar 'Withdrawn' and 'Deposited' events are emitted in NonFungibleToken contract interface
31 access(all) event Withdraw(id: UInt64, from: Address?)
32 access(all) event Deposit(id: UInt64, to: Address?)
33
34 /// Enum that defines the status of a Pack resource.
35 ///
36 access(all) enum Status: UInt8 {
37 access(all) case Sealed
38 access(all) case Revealed
39 access(all) case Opened
40 }
41
42 /// Resource that defines a Pack NFT Operator, responsible for:
43 /// - Minting Pack NFTs and the corresponding Pack resources that keep track of states,
44 /// - Revealing sealed Pack resources, and
45 /// - opening revealed Pack resources.
46 ///
47 access(all) resource PackNFTOperator: IPackNFT.IOperator {
48
49 /// Mint a new Pack NFT resource and corresponding Pack resource; store the Pack resource in the contract's packs dictionary
50 /// and return the Pack NFT resource to the caller.
51 ///
52 access(IPackNFT.Operate) fun mint(distId: UInt64, commitHash: String, issuer: Address): @{IPackNFT.NFT} {
53 let nft <- create NFT(commitHash: commitHash, issuer: issuer)
54 PackNFT.totalSupply = PackNFT.totalSupply + 1
55 let p <- create Pack(commitHash: commitHash, issuer: issuer)
56 PackNFT.packs[nft.id] <-! p
57 emit Mint(id: nft.id, commitHash: commitHash, distId: distId)
58 return <- nft
59 }
60
61 /// Reveal a Sealed Pack resource.
62 ///
63 access(IPackNFT.Operate) fun reveal(id: UInt64, nfts: [{IPackNFT.Collectible}], salt: String) {
64 let p <- PackNFT.packs.remove(key: id) ?? panic("no such pack")
65 p.reveal(id: id, nfts: nfts, salt: salt)
66 PackNFT.packs[id] <-! p
67 }
68
69 /// Open a Revealed Pack NFT resource.
70 ///
71 access(IPackNFT.Operate) fun open(id: UInt64, nfts: [{IPackNFT.Collectible}]) {
72 let p <- PackNFT.packs.remove(key: id) ?? panic("no such pack")
73 p.open(id: id, nfts: nfts)
74 PackNFT.packs[id] <-! p
75 }
76
77 /// PackNFTOperator resource initializer.
78 ///
79 view init() {}
80 }
81
82 /// Resource that defines a Pack NFT.
83 ///
84 access(all) resource Pack {
85 access(all) let commitHash: String
86 access(all) let issuer: Address
87 access(all) var status: Status
88 access(all) var salt: String?
89
90 access(all) view fun verify(nftString: String): Bool {
91 assert(self.status != Status.Sealed, message: "Pack not revealed yet")
92 var hashString = self.salt!
93 hashString = hashString.concat(",").concat(nftString)
94 let hash = HashAlgorithm.SHA2_256.hash(hashString.utf8)
95 assert(self.commitHash == String.encodeHex(hash), message: "CommitHash was not verified")
96 return true
97 }
98
99 access(self) fun _verify(nfts: [{IPackNFT.Collectible}], salt: String, commitHash: String): String {
100 var hashString = salt
101 var nftString = nfts[0].hashString()
102 var i = 1
103 while i < nfts.length {
104 let s = nfts[i].hashString()
105 nftString = nftString.concat(",").concat(s)
106 i = i + 1
107 }
108 hashString = hashString.concat(",").concat(nftString)
109 let hash = HashAlgorithm.SHA2_256.hash(hashString.utf8)
110 assert(self.commitHash == String.encodeHex(hash), message: "CommitHash was not verified")
111 return nftString
112 }
113
114 access(contract) fun reveal(id: UInt64, nfts: [{IPackNFT.Collectible}], salt: String) {
115 assert(self.status == Status.Sealed, message: "Pack status is not Sealed")
116 let v = self._verify(nfts: nfts, salt: salt, commitHash: self.commitHash)
117 self.salt = salt
118 self.status = Status.Revealed
119 emit Revealed(id: id, salt: salt, nfts: v)
120 }
121
122 access(contract) fun open(id: UInt64, nfts: [{IPackNFT.Collectible}]) {
123 assert(self.status == Status.Revealed, message: "Pack status is not Revealed")
124 self._verify(nfts: nfts, salt: self.salt!, commitHash: self.commitHash)
125 self.status = Status.Opened
126 emit Opened(id: id)
127 }
128
129 /// Pack resource initializer.
130 ///
131 view init(commitHash: String, issuer: Address) {
132 // Set the hash and issuer from the arguments.
133 self.commitHash = commitHash
134 self.issuer = issuer
135
136 // Initial status is Sealed.
137 self.status = Status.Sealed
138
139 // Salt is nil until reveal.
140 self.salt = nil
141 }
142 }
143
144 /// Resource that defines a Pack NFT.
145 ///
146 access(all) resource NFT: NonFungibleToken.NFT, ViewResolver.Resolver, IPackNFT.NFT, IPackNFT.IPackNFTToken, IPackNFT.IPackNFTOwnerOperator {
147 /// This NFT's unique ID.
148 ///
149 access(all) let id: UInt64
150
151 /// This NFT's commit hash, used to verify the IDs of the NFTs in the Pack.
152 ///
153 access(all) let commitHash: String
154
155 /// This NFT's issuer.
156 ///
157 access(all) let issuer: Address
158
159 /// Event emitted when a NFT is destroyed (replaces Burned event before Cadence 1.0 update)
160 ///
161 access(all) event ResourceDestroyed(id: UInt64 = self.id)
162
163 /// Executed by calling the Burner contract's burn method (i.e., conforms to the Burnable interface)
164 ///
165 access(contract) fun burnCallback() {
166 PackNFT.totalSupply = PackNFT.totalSupply - 1
167 destroy <- PackNFT.packs.remove(key: self.id) ?? panic("no such pack")
168 }
169
170 /// NFT resource initializer.
171 ///
172 view init(commitHash: String, issuer: Address) {
173 self.id = self.uuid
174 self.commitHash = commitHash
175 self.issuer = issuer
176 }
177
178 /// Create an empty Collection for Pinnacle NFTs and return it to the caller
179 ///
180 access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
181 return <- PackNFT.createEmptyCollection(nftType: Type<@NFT>())
182 }
183
184 /// Return the metadata view types available for this NFT.
185 ///
186 access(all) view fun getViews(): [Type] {
187 return [
188 Type<MetadataViews.Display>(),
189 Type<MetadataViews.ExternalURL>(),
190 Type<MetadataViews.Medias>(),
191 Type<MetadataViews.NFTCollectionData>(),
192 Type<MetadataViews.NFTCollectionDisplay>(),
193 Type<MetadataViews.Royalties>(),
194 Type<MetadataViews.Serial>()
195 ]
196 }
197
198 /// Resolve this NFT's metadata views.
199 ///
200 access(all) view fun resolveView(_ view: Type): AnyStruct? {
201 switch view {
202 case Type<MetadataViews.Display>():
203 return MetadataViews.Display(
204 name: "NFL All Day Pack",
205 description: "Reveals official NFL All Day Moments when opened",
206 thumbnail: MetadataViews.HTTPFile(url: self.getImage(imageType: "image", format: "jpeg", width: 256))
207 )
208 case Type<MetadataViews.ExternalURL>():
209 return MetadataViews.ExternalURL("https://nflallday.com/packnfts/".concat(self.id.toString())) // might have to make a URL that redirects to packs page based on packNFT id -> distribution id
210 case Type<MetadataViews.Medias>():
211 return MetadataViews.Medias(
212 [
213 MetadataViews.Media(
214 file: MetadataViews.HTTPFile(url: self.getImage(imageType: "image", format: "jpeg", width: 512)),
215 mediaType: "image/jpeg"
216 )
217 ]
218 )
219 case Type<MetadataViews.NFTCollectionData>():
220 return MetadataViews.NFTCollectionData(
221 storagePath: PackNFT.CollectionStoragePath,
222 publicPath: PackNFT.CollectionPublicPath,
223 publicCollection: Type<&Collection>(),
224 publicLinkedType: Type<&Collection>(),
225 createEmptyCollectionFunction: (fun (): @{NonFungibleToken.Collection} {
226 return <-PackNFT.createEmptyCollection(nftType: Type<@NFT>())
227 })
228 )
229 case Type<MetadataViews.NFTCollectionDisplay>():
230 let bannerImage = MetadataViews.Media(
231 file: MetadataViews.HTTPFile(
232 url: "https://assets.nflallday.com/flow/catalogue/NFLAD_BANNER.png"
233 ),
234 mediaType: "image/png"
235 )
236 let squareImage = MetadataViews.Media(
237 file: MetadataViews.HTTPFile(
238 url: "https://assets.nflallday.com/flow/catalogue/NFLAD_SQUARE.png"
239 ),
240 mediaType: "image/png"
241 )
242 return MetadataViews.NFTCollectionDisplay(
243 name: "NFL All Day Packs",
244 description: "Officially Licensed Digital Collectibles Featuring the NFL’s Best Highlights. Buy, Sell and Collect Your Favorite NFL Moments",
245 externalURL: MetadataViews.ExternalURL("https://nflallday.com/"),
246 squareImage: squareImage,
247 bannerImage: bannerImage,
248 socials: {
249 "instagram": MetadataViews.ExternalURL("https://www.instagram.com/nflallday/"),
250 "twitter": MetadataViews.ExternalURL("https://twitter.com/NFLAllDay"),
251 "discord": MetadataViews.ExternalURL("https://discord.com/invite/5K6qyTzj2k")
252 }
253 )
254 case Type<MetadataViews.Royalties>():
255 let royaltyReceiver: Capability<&{FungibleToken.Receiver}> =
256 getAccount(0xe4cf4bdc1751c65d).capabilities.get<&{FungibleToken.Receiver}>(MetadataViews.getRoyaltyReceiverPublicPath())
257 return MetadataViews.Royalties(
258 [
259 MetadataViews.Royalty(
260 receiver: royaltyReceiver,
261 cut: 0.05,
262 description: "NFL All Day marketplace royalty"
263 )
264 ]
265 )
266 case Type<MetadataViews.Serial>():
267 return MetadataViews.Serial(self.id)
268 }
269 return nil
270 }
271
272 /// Return an asset path.
273 ///
274 access(all) view fun assetPath(): String {
275 return "https://media.nflallday.com/packnfts/".concat(self.id.toString()).concat("/media/")
276 }
277
278 /// Return an image path.
279 ///
280 access(all) view fun getImage(imageType: String, format: String, width: Int): String {
281 return self.assetPath().concat(imageType).concat("?format=").concat(format).concat("&width=").concat(width.toString())
282 }
283 }
284
285 /// Resource that defines a Collection of Pack NFTs.
286 ///
287 access(all) resource Collection: NonFungibleToken.Collection, ViewResolver.ResolverCollection, IPackNFT.IPackNFTCollectionPublic {
288 /// Dictionary of NFT conforming tokens.
289 /// NFT is a resource type with a UInt64 ID field.
290 ///
291 access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
292
293 /// Collection resource initializer,
294 ///
295 view init() {
296 self.ownedNFTs <- {}
297 }
298
299 /// Remove an NFT from the collection and moves it to the caller.
300 ///
301 access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
302 let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("missing NFT")
303
304 // Withdrawn event emitted from NonFungibleToken contract interface.
305 emit Withdraw(id: token.id, from: self.owner?.address) // TODO: Consider removing
306 return <- token
307 }
308
309 /// Deposit an NFT into this Collection.
310 ///
311 access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
312 let token <- token as! @NFT
313 let id: UInt64 = token.id
314 // Add the new token to the dictionary which removes the old one.
315 let oldToken <- self.ownedNFTs[id] <- token
316
317 // Deposited event emitted from NonFungibleToken contract interface.
318 emit Deposit(id: id, to: self.owner?.address) // TODO: Consider removing
319 destroy oldToken
320 }
321
322 /// Emit a RevealRequest event to signal a Sealed Pack NFT should be revealed.
323 ///
324 access(NonFungibleToken.Update) fun emitRevealRequestEvent(id: UInt64, openRequest: Bool) {
325 pre {
326 self.borrowNFT(id) != nil: "NFT with provided ID must exist in the collection"
327 PackNFT.borrowPackRepresentation(id: id)!.status.rawValue == Status.Sealed.rawValue: "Pack status must be Sealed for reveal request"
328 }
329 emit RevealRequest(id: id, openRequest: openRequest)
330 }
331
332 /// Emit an OpenRequest event to signal a Revealed Pack NFT should be opened.
333 ///
334 access(NonFungibleToken.Update) fun emitOpenRequestEvent(id: UInt64) {
335 pre {
336 self.borrowNFT(id) != nil: "NFT with provided ID must exist in the collection"
337 PackNFT.borrowPackRepresentation(id: id)!.status.rawValue == Status.Revealed.rawValue: "Pack status must be Revealed for open request"
338 }
339 emit OpenRequest(id: id)
340 }
341
342 /// Return an array of the IDs that are in the collection.
343 ///
344 access(all) view fun getIDs(): [UInt64] {
345 return self.ownedNFTs.keys
346 }
347
348 /// Return the amount of NFTs stored in the collection.
349 ///
350 access(all) view fun getLength(): Int {
351 return self.ownedNFTs.length
352 }
353
354 /// Return a list of NFT types that this receiver accepts.
355 ///
356 access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
357 let supportedTypes: {Type: Bool} = {}
358 supportedTypes[Type<@NFT>()] = true
359 return supportedTypes
360 }
361
362 /// Return whether or not the given type is accepted by the collection.
363 ///
364 access(all) view fun isSupportedNFTType(type: Type): Bool {
365 if type == Type<@NFT>() {
366 return true
367 }
368 return false
369 }
370
371 /// Return a reference to an NFT in the Collection.
372 ///
373 access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
374 return &self.ownedNFTs[id]
375 }
376
377 /// Return a reference to a ViewResolver for an NFT in the Collection.
378 ///
379 access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? {
380 if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? {
381 return nft as &{ViewResolver.Resolver}
382 }
383 return nil
384 }
385
386 /// Create an empty Collection of the same type and returns it to the caller.
387 ///
388 access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
389 return <-PackNFT.createEmptyCollection(nftType: Type<@NFT>())
390 }
391 }
392
393 access(all) fun publicReveal(id: UInt64, nfts: [{IPackNFT.Collectible}], salt: String) {
394 let p = PackNFT.borrowPackRepresentation(id: id) ?? panic ("No such pack")
395 p.reveal(id: id, nfts: nfts, salt: salt)
396 }
397
398 /// Return a reference to a Pack resource stored in the contract state.
399 ///
400 access(all) view fun borrowPackRepresentation(id: UInt64): &Pack? {
401 return (&self.packs[id] as &Pack?)!
402 }
403
404 /// Create an empty Collection for Pack NFTs and return it to the caller.
405 ///
406 access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {
407 if nftType != Type<@NFT>() {
408 panic("NFT type is not supported")
409 }
410 return <- create Collection()
411 }
412
413 /// Return the metadata views implemented by this contract.
414 ///
415 /// @return An array of Types defining the implemented views. This value will be used by
416 /// developers to know which parameter to pass to the resolveView() method.
417 ///
418 access(all) view fun getContractViews(resourceType: Type?): [Type] {
419 return [
420 Type<MetadataViews.NFTCollectionData>(),
421 Type<MetadataViews.NFTCollectionDisplay>()
422 ]
423 }
424
425 /// Resolve a metadata view for this contract.
426 ///
427 /// @param view: The Type of the desired view.
428 /// @return A structure representing the requested view.
429 ///
430 access(all) view fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
431 switch viewType {
432 case Type<MetadataViews.NFTCollectionData>():
433 let collectionData = MetadataViews.NFTCollectionData(
434 storagePath: PackNFT.CollectionStoragePath,
435 publicPath: PackNFT.CollectionPublicPath,
436 publicCollection: Type<&Collection>(),
437 publicLinkedType: Type<&Collection>(),
438 createEmptyCollectionFunction: (fun(): @{NonFungibleToken.Collection} {
439 return <-PackNFT.createEmptyCollection(nftType: Type<@NFT>())
440 })
441 )
442 return collectionData
443 case Type<MetadataViews.NFTCollectionDisplay>():
444 let bannerImage = MetadataViews.Media(
445 file: MetadataViews.HTTPFile(
446 url: "https://assets.nflallday.com/flow/catalogue/NFLAD_BANNER.png"
447 ),
448 mediaType: "image/png"
449 )
450 let squareImage = MetadataViews.Media(
451 file: MetadataViews.HTTPFile(
452 url: "https://assets.nflallday.com/flow/catalogue/NFLAD_SQUARE.png"
453 ),
454 mediaType: "image/png"
455 )
456 return MetadataViews.NFTCollectionDisplay(
457 name: "NFL All Day Packs",
458 description: "Officially Licensed Digital Collectibles Featuring the NFL’s Best Highlights. Buy, Sell and Collect Your Favorite NFL Moments",
459 externalURL: MetadataViews.ExternalURL("https://nflallday.com/"),
460 squareImage: squareImage,
461 bannerImage: bannerImage,
462 socials: {
463 "instagram": MetadataViews.ExternalURL("https://www.instagram.com/nflallday/"),
464 "twitter": MetadataViews.ExternalURL("https://twitter.com/NFLAllDay"),
465 "discord": MetadataViews.ExternalURL("https://discord.com/invite/5K6qyTzj2k")
466 }
467 )
468 }
469 return nil
470 }
471
472 /// PackNFT contract initializer.
473 ///
474 init(
475 CollectionStoragePath: StoragePath,
476 CollectionPublicPath: PublicPath,
477 CollectionIPackNFTPublicPath: PublicPath,
478 OperatorStoragePath: StoragePath,
479 version: String
480 ) {
481 self.totalSupply = 0
482 self.packs <- {}
483 self.CollectionStoragePath = CollectionStoragePath
484 self.CollectionPublicPath = CollectionPublicPath
485 self.CollectionIPackNFTPublicPath = CollectionIPackNFTPublicPath
486 self.OperatorStoragePath = OperatorStoragePath
487 self.version = version
488
489 // Create a collection to receive Pack NFTs and publish public receiver capabilities.
490 self.account.storage.save(<- create Collection(), to: self.CollectionStoragePath)
491 self.account.capabilities.publish(
492 self.account.capabilities.storage.issue<&{NonFungibleToken.CollectionPublic}>(self.CollectionStoragePath),
493 at: self.CollectionPublicPath
494 )
495 self.account.capabilities.publish(
496 self.account.capabilities.storage.issue<&{IPackNFT.IPackNFTCollectionPublic}>(self.CollectionStoragePath),
497 at: self.CollectionIPackNFTPublicPath
498 )
499
500 // Create a Pack NFT operator to share mint capability with proxy.
501 self.account.storage.save(<- create PackNFTOperator(), to: self.OperatorStoragePath)
502 self.account.capabilities.storage.issue<&{IPackNFT.IOperator}>(self.OperatorStoragePath)
503 }
504
505}