Smart Contract

PackNFT

A.e4cf4bdc1751c65d.PackNFT

Deployed

2w ago
Feb 11, 2026, 05:25:46 PM UTC

Dependents

2235 imports
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}