Smart Contract

PackNFT

A.87ca73a41bb50ad5.PackNFT

Deployed

14h ago
Feb 28, 2026, 02:28:25 AM UTC

Dependents

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