Smart Contract
PackNFT
A.87ca73a41bb50ad5.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: [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}