Smart Contract

FindPack

A.097bafa4e0b48eef.FindPack

Valid From

87,333,331

Deployed

3d ago
Feb 24, 2026, 11:31:48 PM UTC

Dependents

18 imports
1import NonFungibleToken from 0x1d7e57aa55817448
2import FungibleToken from 0xf233dcee88fe0abe
3import MetadataViews from 0x1d7e57aa55817448
4import Crypto
5import Clock from 0x097bafa4e0b48eef
6import Debug from 0x097bafa4e0b48eef
7import FindForge from 0x097bafa4e0b48eef
8import FindVerifier from 0x097bafa4e0b48eef
9import FINDNFTCatalog from 0x097bafa4e0b48eef
10import ViewResolver from 0x1d7e57aa55817448
11
12access(all) contract FindPack: NonFungibleToken{
13    // Events
14    access(all) event ContractInitialized()
15    access(all) event Withdraw(id: UInt64, from: Address?)
16    access(all) event Deposit(id: UInt64, to: Address?)
17    access(all) event Minted(id: UInt64, typeId:UInt64)
18
19    access(all) event Requeued(packId: UInt64, address:Address)
20
21    access(all) event Opened(packTypeName: String, packTypeId:UInt64, packId: UInt64, address:Address, packFields: {String : String}, packNFTTypes: [String])
22    access(all) event Fulfilled(packTypeName: String, packTypeId:UInt64, packId:UInt64, address:Address, packFields: {String : String}, packNFTTypes: [String])
23    access(all) event PackReveal(packTypeName: String, packTypeId:UInt64, packId:UInt64, address:Address, rewardId:UInt64, rewardType:String, rewardFields:{String:String}, packFields: {String : String}, packNFTTypes: [String])
24
25    access(all) event Purchased(packTypeName: String, packTypeId: UInt64, packId: UInt64, address: Address, amount:UFix64, packFields: {String : String}, packNFTTypes: [String])
26    access(all) event MetadataRegistered(packTypeName: String, packTypeId: UInt64)
27    access(all) event FulfilledError(packTypeName: String, packTypeId: UInt64, packId:UInt64, address:Address?, reason:String)
28
29    access(all) let PackMetadataStoragePath: StoragePath
30
31    access(all) let CollectionStoragePath: StoragePath
32    access(all) let CollectionPublicPath: PublicPath
33
34    access(all) let OpenedCollectionPublicPath: PublicPath
35    access(all) let OpenedCollectionStoragePath: StoragePath
36
37    access(all) let DLQCollectionPublicPath: PublicPath
38    access(all) let DLQCollectionStoragePath: StoragePath
39
40    access(all) var totalSupply: UInt64
41
42    // Mapping of packTypeName (which is the find name) : {typeId : Metadata}
43    access(contract) let packMetadata: {String : {UInt64: Metadata}}
44
45    // this is a struct specific for airdropping packs
46    access(all) struct AirdropInfo {
47        access(all) let packTypeName: String
48        access(all) let packTypeId: UInt64
49        access(all) let users: [String]
50
51        access(all) let message: String
52
53        init(packTypeName: String , packTypeId: UInt64 , users: [String],  message: String){
54            self.packTypeName = packTypeName
55            self.packTypeId = packTypeId
56            self.users = users
57            self.message = message
58        }
59    }
60
61    // this is a struct specific for registering pack metadata
62    access(all) struct PackRegisterInfo {
63        access(all) let forge: String
64        access(all) let name: String
65        access(all) let description: String
66        access(all) let typeId: UInt64
67        access(all) let externalURL: String
68        access(all) let squareImageHash: String
69        access(all) let bannerHash: String
70        access(all) let socials: {String : String}
71        access(all) let paymentAddress: Address
72        access(all) let paymentType: String
73        access(all) let openTime: UFix64
74        access(all) let packFields: {String : String}
75        access(all) let primaryRoyalty: [Royalty]
76        access(all) let secondaryRoyalty: [Royalty]
77        access(all) let requiresReservation: Bool
78        access(all) let nftTypes: [String]
79        access(all) let storageRequirement: UInt64
80        access(all) let saleInfo: [PackRegisterSaleInfo]
81        access(all) let extra: {String: AnyStruct}
82
83        init(
84            forge: String,
85            name: String,
86            description: String,
87            typeId: UInt64,
88            externalURL: String,
89            squareImageHash: String,
90            bannerHash: String,
91            socials: {String : String},
92            paymentAddress: Address,
93            paymentType: String,
94            openTime: UFix64,
95            packFields: {String : String},
96            primaryRoyalty: [Royalty],
97            secondaryRoyalty: [Royalty],
98            requiresReservation: Bool,
99            nftTypes: [String],
100            storageRequirement: UInt64,
101            saleInfo: [PackRegisterSaleInfo]
102        ) {
103            self.forge=forge
104            self.name=name
105            self.description=description
106            self.typeId=typeId
107            self.externalURL=externalURL
108            self.squareImageHash=squareImageHash
109            self.bannerHash=bannerHash
110            self.socials=socials
111            self.paymentAddress=paymentAddress
112            self.paymentType=paymentType
113            self.openTime=openTime
114            self.packFields=packFields
115            self.primaryRoyalty=primaryRoyalty
116            self.secondaryRoyalty=secondaryRoyalty
117            self.requiresReservation=requiresReservation
118            self.nftTypes=nftTypes
119            self.storageRequirement=storageRequirement
120            self.saleInfo=saleInfo
121            self.extra={}
122        }
123
124        access(all) fun generateSaleInfo() : [SaleInfo] {
125            let saleInfo : [SaleInfo] = []
126            for s in self.saleInfo {
127                saleInfo.append(s.generateSaleInfo())
128            }
129            return saleInfo
130        }
131    }
132
133    access(all) struct Royalty {
134        access(all) let recipient: Address
135        access(all) let cut: UFix64
136        access(all) let description: String
137        access(contract) let extra: {String: AnyStruct}
138
139        init(
140            recipient: Address,
141            cut: UFix64,
142            description: String
143        ) {
144            self.recipient=recipient
145            self.cut=cut
146            self.description=description
147            self.extra={}
148        }
149    }
150
151    access(all) struct PackRegisterSaleInfo {
152        access(all) let name : String
153        access(all) let startTime : UFix64
154        access(all) let price : UFix64
155        //TODO:potential breaking change?
156        access(all) let verifiers : [{FindVerifier.Verifier}]
157        access(all) let verifyAll : Bool
158        access(contract) let extra: {String: AnyStruct}
159
160        init(
161            name : String,
162            startTime : UFix64,
163            price : UFix64,
164            verifiers : [{FindVerifier.Verifier}],
165            verifyAll : Bool,
166            extra: {String: AnyStruct}
167        ) {
168            self.name=name
169            self.startTime=startTime
170            self.price=price
171            self.verifiers=verifiers
172            self.verifyAll=verifyAll
173            self.extra=extra
174        }
175
176        access(all) fun generateSaleInfo() : SaleInfo {
177            var endTime : UFix64? = nil
178            if let et = self.extra["endTime"] {
179                endTime = et as? UFix64
180            }
181
182            var purchaseLimit : UInt64? = nil
183            if let pl = self.extra["purchaseLimit"] {
184                purchaseLimit = pl as? UInt64
185            }
186
187            return SaleInfo(
188                name : self.name,
189                startTime : self.startTime ,
190                endTime : endTime,
191                price : self.price,
192                purchaseLimit : purchaseLimit,
193                verifiers: self.verifiers,
194                verifyAll : self.verifyAll
195            )
196        }
197    }
198
199    // Verifier container for packs
200    // Each struct is one sale type. If they
201    access(all) struct SaleInfo {
202        access(all) let name : String
203        access(all) let startTime : UFix64
204        access(all) let endTime : UFix64?
205        access(all) let price : UFix64
206        access(all) let purchaseLimit : UInt64?
207        access(all) let purchaseRecord : {Address : UInt64}
208        access(all) let verifiers : [{FindVerifier.Verifier}]
209        access(all) let verifyAll : Bool
210
211        init(name : String, startTime : UFix64 , endTime : UFix64? , price : UFix64, purchaseLimit : UInt64?, verifiers: [{FindVerifier.Verifier}], verifyAll : Bool ) {
212            self.name = name
213            self.startTime = startTime
214            self.endTime = endTime
215            self.price = price
216            self.purchaseLimit = purchaseLimit
217            self.purchaseRecord = {}
218            self.verifiers = verifiers
219            self.verifyAll = verifyAll
220        }
221
222        access(all) fun inTime(_ time: UFix64) : Bool {
223            let started = time >= self.startTime
224            if self.endTime == nil {
225                return started
226            }
227
228            return started && time <= self.endTime!
229        }
230
231        access(all) fun buy(_ addr: Address) {
232
233            // If verified false, then panic
234
235            if !self.verify(addr) {
236                panic("You are not qualified to buy this pack at the moment")
237            }
238
239            let purchased = (self.purchaseRecord[addr] ?? 0 ) + 1
240            if self.purchaseLimit != nil && self.purchaseLimit! < purchased {
241                panic("You are only allowed to purchase ".concat(self.purchaseLimit!.toString()))
242            }
243            self.purchaseRecord[addr] = purchased
244        }
245
246        access(all) fun checkBought(_ addr: Address) : UInt64 {
247            return self.purchaseRecord[addr] ?? 0
248        }
249
250        access(all) fun checkBuyable(addr: Address, time: UFix64) : Bool {
251            // If not in time, return false
252            if !self.inTime(time) {
253                return false
254            }
255
256            // If verified false, then false
257            if !self.verify(addr) {
258                return false
259            }
260
261            // If exceed purchase limit, return false
262            let purchased = (self.purchaseRecord[addr] ?? 0 ) + 1
263            if self.purchaseLimit != nil && self.purchaseLimit! < purchased {
264                return false
265            }
266            // else return true
267            return true
268        }
269
270        access(contract) fun verify(_ addr: Address) : Bool {
271            if self.verifiers.length == 0 {
272                return true
273            }
274
275            if self.verifyAll {
276                for verifier in self.verifiers {
277                    if !verifier.verify(self.generateParam(addr)) {
278                        return false
279                    }
280                }
281                return true
282            }
283            // If only has to verify one
284            for verifier in self.verifiers {
285                if verifier.verify(self.generateParam(addr)) {
286                    return true
287                }
288            }
289            return false
290        }
291
292        access(contract) fun generateParam(_ addr: Address) : {String : AnyStruct} {
293            return {
294                "address" : addr
295            }
296        }
297
298    }
299
300    // Input for minting packs from forge
301    access(all) struct MintPackData {
302        access(all) let packTypeName: String
303        access(all) let typeId: UInt64
304        access(all) let hash: String
305        access(all) let verifierRef: &FindForge.Verifier
306
307        init(packTypeName: String, typeId: UInt64, hash: String, verifierRef: &FindForge.Verifier) {
308            self.packTypeName = packTypeName
309            self.typeId = typeId
310            self.hash = hash
311            self.verifierRef = verifierRef
312        }
313    }
314
315    access(all) struct PackRevealData {
316        access(all) let data: {String:String}
317
318        init(_ data: {String:String}) {
319            self.data=data
320        }
321    }
322
323    access(all) struct Metadata {
324        access(all) let name: String
325        access(all) let description: String
326
327        access(all) let thumbnailHash: String?
328        access(all) let thumbnailUrl:String?
329
330        access(all) let wallet: Capability<&{FungibleToken.Receiver}>
331        access(all) let walletType: Type
332
333        access(all) let openTime: UFix64
334        access(all) let saleInfos: [SaleInfo]
335
336        access(all) let storageRequirement: UInt64
337        access(all) let collectionDisplay: MetadataViews.NFTCollectionDisplay
338
339        access(all) let packFields: {String : String}
340        access(all) let extraData : {String : AnyStruct}
341
342        access(all) let itemTypes: [Type]
343        access(contract) let providerCaps: {Type : Capability<auth (NonFungibleToken.Withdraw) &{NonFungibleToken.Provider, ViewResolver.ResolverCollection}>}
344
345        access(contract) let primarySaleRoyalties : MetadataViews.Royalties
346        access(contract) let royalties : MetadataViews.Royalties
347
348        access(all) let requiresReservation: Bool
349
350        init(name: String, description: String, thumbnailUrl: String?,thumbnailHash: String?, wallet: Capability<&{FungibleToken.Receiver}>, openTime:UFix64, walletType:Type, itemTypes: [Type],  providerCaps: {Type : Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider, ViewResolver.ResolverCollection}>} , requiresReservation:Bool, storageRequirement: UInt64, saleInfos: [SaleInfo], primarySaleRoyalties : MetadataViews.Royalties, royalties : MetadataViews.Royalties, collectionDisplay: MetadataViews.NFTCollectionDisplay, packFields: {String : String} , extraData : {String : AnyStruct}) {
351            self.name = name
352            self.description = description
353            self.thumbnailUrl = thumbnailUrl
354            self.thumbnailHash = thumbnailHash
355            self.wallet=wallet
356            self.walletType=walletType
357
358            self.openTime=openTime
359            self.itemTypes=itemTypes
360            self.providerCaps=providerCaps
361
362            self.primarySaleRoyalties=primarySaleRoyalties
363            self.royalties=royalties
364
365            self.storageRequirement= storageRequirement
366            self.collectionDisplay= collectionDisplay
367
368            self.requiresReservation=requiresReservation
369            self.packFields=packFields
370
371            self.saleInfos=saleInfos
372            self.extraData=extraData
373        }
374
375        access(all) fun getThumbnail() : {MetadataViews.File} {
376            if let hash = self.thumbnailHash {
377                return MetadataViews.IPFSFile(cid: hash, path: nil)
378            }
379            return MetadataViews.HTTPFile(url:self.thumbnailUrl!)
380        }
381
382        access(all) fun getItemTypesAsStringArray() : [String] {
383            let types : [String] = []
384            for t in self.itemTypes {
385                types.append(t.identifier)
386            }
387            return types
388        }
389
390        access(all) fun canBeOpened() : Bool {
391            return self.openTime <= Clock.time()
392        }
393
394        access(contract) fun borrowSaleInfo(_ i: Int) : &SaleInfo {
395            return &self.saleInfos[i] 
396        }
397    }
398
399    access(account) fun registerMetadata(packTypeName: String, typeId: UInt64, metadata: Metadata) {
400        emit MetadataRegistered(packTypeName: packTypeName, packTypeId: typeId)
401        let mappingMetadata = self.packMetadata[packTypeName] ?? {} //<- if this is empty then setup the storage slot for this pack type
402
403        // first time we create this type ID, if its not there then we create it.
404        if mappingMetadata[typeId] == nil {
405            let pathIdentifier = self.getPacksCollectionPath(packTypeName: packTypeName, packTypeId: typeId)
406            let storagePath = StoragePath(identifier: pathIdentifier) ?? panic("Cannot create path from identifier : ".concat(pathIdentifier))
407            let publicPath = PublicPath(identifier: pathIdentifier) ?? panic("Cannot create path from identifier : ".concat(pathIdentifier))
408            FindPack.account.storage.save<@{NonFungibleToken.Collection}>( <- FindPack.createEmptyCollection(nftType: Type<@FindPack.NFT>()), to: storagePath)
409            let cap = FindPack.account.capabilities.storage.issue<&FindPack.Collection>(storagePath)
410            FindPack.account.capabilities.publish(cap, at: publicPath)
411        }
412
413        mappingMetadata[typeId] = metadata
414        self.packMetadata[packTypeName] = mappingMetadata
415    }
416
417    access(all) fun getMetadataById(packTypeName: String, typeId: UInt64): Metadata? {
418
419        if self.packMetadata[packTypeName] != nil {
420            return self.packMetadata[packTypeName]![typeId]
421        }
422
423        return nil
424    }
425
426    access(all) fun getMetadataByName(packTypeName: String): {UInt64 : Metadata} {
427
428        if self.packMetadata[packTypeName] != nil {
429            return self.packMetadata[packTypeName]!
430        }
431
432        return {}
433    }
434
435    access(all) resource NFT: NonFungibleToken.NFT, ViewResolver.Resolver {
436        // The token's ID
437        access(all) let id: UInt64
438        access(all) let packTypeName: String
439
440        // The token's typeId
441        access(self) var typeId: UInt64
442
443        //this is added to the NFT when it is opened
444        access(self) var openedBy: {Type : Capability<&{NonFungibleToken.Receiver}>}
445
446        access(account) let hash: String
447
448        access(self) let royalties : [MetadataViews.Royalty]
449
450        // init
451        //
452        init(packTypeName: String, typeId: UInt64, hash:String, royalties: [MetadataViews.Royalty]) {
453            self.id = self.uuid
454            self.typeId = typeId
455            self.openedBy={}
456            self.hash=hash
457            self.royalties=royalties
458            self.packTypeName=packTypeName
459        }
460
461        access(all) view fun getID() : UInt64 {
462            return self.id
463        }
464
465        access(all) fun getOpenedBy() : {Type : Capability<&{NonFungibleToken.Receiver}>} {
466            if self.openedBy== nil {
467                panic("Pack is not opened")
468            }
469            return self.openedBy
470        }
471
472        access(all) fun getHash() : String{
473            return self.hash
474        }
475
476        access(contract) fun setTypeId(_ id: UInt64) {
477            self.typeId=id
478        }
479
480        access(contract) fun resetOpenedBy() : Address {
481            if self.openedBy.length == 0 {
482                panic("Pack is not opened")
483            }
484            let cap = self.openedBy
485
486            self.openedBy={}
487            return cap.values[0].address
488        }
489
490        access(contract) fun setOpenedBy(_ cap:{Type : Capability<&{NonFungibleToken.Receiver}>}) {
491            if self.openedBy.length != 0 {
492                panic("Pack has already been opened")
493            }
494            self.openedBy=cap
495        }
496
497        access(all) fun getTypeID() :UInt64 {
498            return self.typeId
499        }
500
501        access(all) fun getMetadata(): Metadata {
502            return FindPack.getMetadataById(packTypeName: self.packTypeName, typeId: self.typeId)!
503        }
504
505        access(all) view fun getViews(): [Type] {
506            return [
507            Type<MetadataViews.Display>(),
508            Type<Metadata>(),
509            Type<String>(),
510            Type<MetadataViews.ExternalURL>(),
511            Type<MetadataViews.Royalties>(),
512            Type<MetadataViews.NFTCollectionData>(),
513            Type<MetadataViews.NFTCollectionDisplay>()
514            ]
515        }
516
517        access(all) fun resolveView(_ view: Type): AnyStruct? {
518            let metadata = self.getMetadata()
519            switch view {
520            case Type<MetadataViews.Display>():
521                return MetadataViews.Display(
522                    name: metadata.name,
523                    description: metadata.description,
524                    thumbnail: metadata.getThumbnail()
525                )
526            case Type<String>():
527                return metadata.name
528
529            case Type<FindPack.Metadata>():
530                return metadata
531            case Type<MetadataViews.ExternalURL>():
532                if self.owner != nil {
533                    return MetadataViews.ExternalURL("https://find.xyz/".concat(self.owner!.address.toString()).concat("/main/FindPackCollection/").concat(self.id.toString()))
534                }
535                return MetadataViews.ExternalURL("https://find.xyz/")
536
537            case Type<MetadataViews.Royalties>():
538                return MetadataViews.Royalties(self.royalties)
539
540            case Type<MetadataViews.NFTCollectionData>():
541                return FindPack.resolveContractView(resourceType: nil, viewType: Type<MetadataViews.NFTCollectionData>())
542            case Type<MetadataViews.NFTCollectionDisplay>():
543
544                return self.getMetadata().collectionDisplay
545            }
546            return nil
547        }
548
549        access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
550            return <- create Collection() 
551        }
552    }
553
554    access(all) view fun getContractViews(resourceType: Type?): [Type] {
555        return [
556        Type<MetadataViews.NFTCollectionData>(),
557        Type<MetadataViews.NFTCollectionDisplay>()
558        ]
559    }
560
561
562    access(all) fun resolveContractView(resourceType: Type?, viewType: Type): AnyStruct? {
563        switch viewType {
564        case Type<MetadataViews.NFTCollectionDisplay>():
565            return MetadataViews.NFTCollectionDisplay(
566                name: "Find Pack",
567                description: "A generic find pack that can contain lots of stuff",
568                externalURL: MetadataViews.ExternalURL("http://find.xyz"),
569                squareImage: MetadataViews.Media(file: MetadataViews.HTTPFile(url: "https://pbs.twimg.com/profile_images/1467546091780550658/R1uc6dcq_400x400.jpg") , mediaType: "image"),
570                bannerImage: MetadataViews.Media(file: MetadataViews.HTTPFile(url: "https://pbs.twimg.com/profile_banners/1448245049666510848/1652452073/1500x500") , mediaType: "image"),
571                socials: { 
572                    "Twitter" : MetadataViews.ExternalURL("https://twitter.com/findonflow") , 
573                    "Discord" : MetadataViews.ExternalURL("https://discord.gg/95P274mayM") 
574                }
575            )
576
577
578        case Type<MetadataViews.NFTCollectionData>():
579            let collectionData = MetadataViews.NFTCollectionData(
580                storagePath: FindPack.CollectionStoragePath,
581                publicPath: FindPack.CollectionPublicPath,
582                publicCollection: Type<&FindPack.Collection>(),
583                publicLinkedType: Type<&FindPack.Collection>(),
584                createEmptyCollectionFunction: (fun(): @{NonFungibleToken.Collection} {
585                    return <-FindPack.createEmptyCollection(nftType: Type<@FindPack.NFT>())
586                })
587            )
588            return collectionData
589        }
590        return nil
591    }
592
593    access(all) resource interface CollectionPublic {
594        access(all) fun deposit(token: @{NonFungibleToken.NFT})
595        access(all) view fun getIDs(): [UInt64]
596        access(all) fun contains(_ id: UInt64): Bool
597        access(all) fun getPacksLeft() : Int   // returns the no of a type
598        access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}?
599        access(all) fun borrowFindPack(id: UInt64): &FindPack.NFT?
600        access(all) fun buyWithSignature(packId: UInt64, signature:String, vault: @{FungibleToken.Vault}, collectionCapability: Capability<&Collection>)
601        access(all) fun buy(packTypeName: String, typeId: UInt64, vault: @{FungibleToken.Vault}, collectionCapability: Capability<&Collection>)
602    }
603
604    access(all) entitlement Owner
605
606    // Collection
607    // A collection of FindPack NFTs owned by an account
608    //
609    access(all) resource Collection: NonFungibleToken.Collection, CollectionPublic, ViewResolver.ResolverCollection {
610        // dictionary of NFT conforming tokens
611        // NFT is a resource type with an `UInt64` ID field
612        //
613        access(all) var ownedNFTs: @{UInt64: {NonFungibleToken.NFT}}
614
615        //this has to be called on the DLQ collection
616        access(Owner) fun requeue(packId:UInt64) {
617            let token <- self.withdraw(withdrawID: packId) as! @NFT
618
619            let address=token.resetOpenedBy()
620            let cap=getAccount(address).capabilities.get<&Collection>(FindPack.CollectionPublicPath)
621            let receiver = cap.borrow()!
622            receiver.deposit(token: <- token)
623            emit Requeued(packId:packId, address: cap.address)
624        }
625
626        access(Owner) fun open(packId: UInt64, receiverCap: {Type : Capability<&{NonFungibleToken.Receiver}>}) {
627            for cap in receiverCap.values {
628                if !cap.check() {
629                    panic("Receiver cap is not valid")
630                }
631            }
632            let pack=self.borrowFindPack(id:packId) ?? panic ("This pack is not in your collection")
633
634            if !pack.getMetadata().canBeOpened() {
635                panic("You cannot open the pack yet")
636            }
637
638            let token <- self.withdraw(withdrawID: packId) as! @FindPack.NFT
639            token.setOpenedBy(receiverCap)
640
641            // establish the receiver for Redeeming FindPack
642            let receiver = FindPack.account.capabilities.get<&{NonFungibleToken.Receiver}>(FindPack.OpenedCollectionPublicPath).borrow()!
643
644            let typeId=token.getTypeID()
645            let packTypeName=token.packTypeName
646            let metadata= token.getMetadata()
647            // deposit for consumption
648            receiver.deposit(token: <- token)
649
650            let packFields = metadata.packFields
651            packFields["packImage"] = packFields["packImage"] ?? metadata.getThumbnail().uri()
652            let packNFTTypes = metadata.getItemTypesAsStringArray()
653            emit Opened(packTypeName: packTypeName, packTypeId:typeId, packId: packId, address:self.owner!.address, packFields:packFields, packNFTTypes:packNFTTypes)
654        }
655
656        access(all) fun buyWithSignature(packId: UInt64, signature:String, vault: @{FungibleToken.Vault}, collectionCapability: Capability<&Collection>) {
657            pre {
658                self.owner!.address == FindPack.account.address : "You can only buy pack directly from the FindPack account"
659            }
660
661            let nft <- self.withdraw(withdrawID: packId) as!  @NFT
662            let metadata= nft.getMetadata()
663
664            // get the correct sale struct based on time and lowest price
665            let timestamp=Clock.time()
666            var lowestPrice : UFix64? = nil
667            var saleInfo : SaleInfo? = nil
668            var saleInfoIndex : Int? = nil
669            for i, info in metadata.saleInfos {
670                // for later implement : if it requires all sale info checks
671                if info.checkBuyable(addr: collectionCapability.address, time:timestamp) {
672                    if lowestPrice == nil || lowestPrice! > info.price {
673                        lowestPrice = info.price
674                        saleInfo = info
675                        saleInfoIndex = i
676                    }
677                }
678            }
679
680            if saleInfo == nil || saleInfoIndex == nil || lowestPrice == nil {
681                panic("You cannot buy the pack yet")
682            }
683
684            if !metadata.requiresReservation {
685                panic("This pack type does not require reservation, use the open buy method")
686            }
687
688            if vault.getType() != metadata.walletType {
689                panic("The vault sent in is not of the desired type ".concat(metadata.walletType.identifier))
690            }
691
692            if saleInfo!.price != vault.balance {
693                panic("Vault does not contain required amount of FT ".concat(saleInfo!.price.toString()))
694            }
695            let keyList = Crypto.KeyList()
696            let accountKey = self.owner!.keys.get(keyIndex: 0)!.publicKey
697
698            // Adds the public key to the keyList
699            keyList.add(
700                PublicKey(
701                    publicKey: accountKey.publicKey,
702                    signatureAlgorithm: accountKey.signatureAlgorithm
703                ),
704                hashAlgorithm: HashAlgorithm.SHA3_256,
705                weight: 1.0
706            )
707
708            // Creates a Crypto.KeyListSignature from the signature provided in the parameters
709            let signatureSet: [Crypto.KeyListSignature] = []
710            signatureSet.append(
711                Crypto.KeyListSignature(
712                    keyIndex: 0,
713                    signature: signature.decodeHex()
714                )
715            )
716
717            // Verifies that the signature is valid and that it was generated from the
718            // owner of the collection
719            if(!keyList.verify(signatureSet: signatureSet, signedData: nft.hash.utf8, domainSeparationTag: "FLOW-V0.0-user")){
720                panic("Unable to validate the signature for the pack!")
721            }
722
723            let packTypeId=nft.getTypeID()
724            let packTypeName = nft.packTypeName
725
726            for royalty in metadata.primarySaleRoyalties.getRoyalties() {
727                if royalty.receiver.check(){
728                    royalty.receiver.borrow()!.deposit(from: <- vault.withdraw(amount: saleInfo!.price * royalty.cut))
729                } else {
730                    //to-do :  emit events here ?
731                }
732            }
733
734            let wallet = getAccount(FindPack.account.address).capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
735            if wallet.check() {
736                let r = MetadataViews.Royalty(receiver: wallet, cut: 0.15, description: ".find")
737                r.receiver.borrow()!.deposit(from: <- vault.withdraw(amount: saleInfo!.price * r.cut))
738            }
739
740            metadata.wallet.borrow()!.deposit(from: <- vault)
741            collectionCapability.borrow()!.deposit(token: <- nft)
742
743            let packFields = metadata.packFields
744            packFields["packImage"] = packFields["packImage"] ?? metadata.getThumbnail().uri()
745            let packNFTTypes = metadata.getItemTypesAsStringArray()
746            emit Purchased(packTypeName: packTypeName, packTypeId: packTypeId, packId: packId, address: collectionCapability.address, amount:saleInfo!.price, packFields:packFields, packNFTTypes:packNFTTypes)
747        }
748
749        access(all) fun buy(packTypeName: String, typeId: UInt64, vault: @{FungibleToken.Vault}, collectionCapability: Capability<&Collection>) {
750            pre {
751                self.owner!.address == FindPack.account.address : "You can only buy pack directly from the FindPack account"
752            }
753
754            let keys = self.ownedNFTs.keys
755            if  keys.length == 0 {
756                panic("No more packs of this type. PackName: ".concat(packTypeName).concat(" packId : ").concat(typeId.toString()))
757            }
758
759            let key=keys[0]
760            let nft <- self.withdraw(withdrawID: key) as!  @NFT
761            let metadata= nft.getMetadata()
762
763            if metadata.requiresReservation {
764                panic("Cannot buy a pack that requires reservation without a reservation signature and id")
765            }
766
767            let user=collectionCapability.address
768            let timestamp=Clock.time()
769
770            var lowestPrice : UFix64? = nil
771            var saleInfo : SaleInfo? = nil
772            var saleInfoIndex : Int? = nil
773            for i, info in metadata.saleInfos {
774                // for later implement : if it requires all sale info checks
775                if info.checkBuyable(addr: collectionCapability.address, time:timestamp) {
776                    if lowestPrice == nil || lowestPrice! > info.price {
777                        lowestPrice = info.price
778                        saleInfo = info
779                        saleInfoIndex = i
780                    }
781                }
782            }
783
784            if saleInfo == nil || saleInfoIndex == nil || lowestPrice == nil {
785                panic("You cannot buy the pack yet")
786            }
787
788            if vault.getType() != metadata.walletType {
789                panic("The vault sent in is not of the desired type ".concat(metadata.walletType.identifier))
790            }
791
792            if saleInfo!.price != vault.balance {
793                panic("Vault does not contain required amount of FT ".concat(saleInfo!.price.toString()))
794            }
795
796            var royaltiesPaid=false
797            for royalty in metadata.primarySaleRoyalties.getRoyalties() {
798                if royalty.receiver.check(){
799                    royalty.receiver.borrow()!.deposit(from: <- vault.withdraw(amount: saleInfo!.price * royalty.cut))
800                    royaltiesPaid=true
801                } else {
802                    //to-do :  emit events here ?
803                }
804            }
805
806            //TODO: REMOVE THIS
807            if !royaltiesPaid {
808                let wallet = getAccount(FindPack.account.address).capabilities.get<&{FungibleToken.Receiver}>(/public/flowTokenReceiver)
809                if wallet.check() {
810                    let r = MetadataViews.Royalty(receiver: wallet, cut: 0.10, description: ".find")
811                    r.receiver.borrow()!.deposit(from: <- vault.withdraw(amount: saleInfo!.price * r.cut))
812                }
813            }
814
815            // record buy
816            FindPack.borrowSaleInfo(packTypeName: packTypeName, packTypeId: typeId, index: saleInfoIndex!).buy(collectionCapability.address)
817
818            metadata.wallet.borrow()!.deposit(from: <- vault)
819            collectionCapability.borrow()!.deposit(token: <- nft)
820
821            let packFields = metadata.packFields
822            packFields["packImage"] = packFields["packImage"] ?? metadata.getThumbnail().uri()
823            let packNFTTypes = metadata.getItemTypesAsStringArray()
824            emit Purchased(packTypeName: packTypeName, packTypeId: typeId, packId: key, address: collectionCapability.address, amount:saleInfo!.price, packFields: packFields, packNFTTypes:packNFTTypes)
825        }
826
827        // withdraw
828        // Removes an NFT from the collection and moves it to the caller
829        //
830        access(NonFungibleToken.Withdraw) fun withdraw(withdrawID: UInt64): @{NonFungibleToken.NFT} {
831            let token <- self.ownedNFTs.remove(key: withdrawID) ?? panic("Could not withdraw nft")
832
833            let nft <- token as! @NFT
834
835            emit Withdraw(id: nft.id, from: self.owner?.address)
836
837            return <-nft
838        }
839
840        access(all) view fun getIDsWithTypes(): {Type: [UInt64]} {
841            return {}
842        }
843
844        access(all) view fun getSupportedNFTTypes(): {Type: Bool} {
845            return {}
846        }
847
848
849        access(all) view fun isSupportedNFTType(type: Type) : Bool {
850            return type == Type<@FindPack.NFT>()
851        }
852
853        access(all) fun createEmptyCollection(): @{NonFungibleToken.Collection} {
854            return <- create Collection() 
855        }
856
857        access(all) view fun getLength() : Int {
858            return self.ownedNFTs.length
859        }
860
861        // deposit
862        // Takes a NFT and adds it to the collections dictionary
863        // and adds the ID to the id array
864        //
865        access(all) fun deposit(token: @{NonFungibleToken.NFT}) {
866            let token <- token as! @FindPack.NFT
867
868            let id: UInt64 = token.id
869            let tokenTypeId = token.getTypeID()
870
871            // add the new token to the dictionary which removes the old one
872            let oldToken <- self.ownedNFTs[id] <- token
873
874            emit Deposit(id: id, to: self.owner?.address)
875
876            destroy oldToken
877        }
878
879        // getIDs
880        // Returns an array of the IDs that are in the collection
881        //
882        access(all) view fun getIDs(): [UInt64] {
883            return self.ownedNFTs.keys
884        }
885
886        access(all) fun contains(_ id: UInt64) : Bool {
887            return self.ownedNFTs.containsKey(id)
888        }
889
890        //return the number of packs left of a type
891        access(all) fun getPacksLeft() : Int {
892            return self.ownedNFTs.length
893        }
894
895        // borrowNFT
896        // Gets a reference to an NFT in the collection
897        // so that the caller can read its metadata and call its methods
898        access(all) view fun borrowNFT(_ id: UInt64): &{NonFungibleToken.NFT}? {
899            return &self.ownedNFTs[id]
900        }
901
902        // borrowFindPack
903        // Gets a reference to an NFT in the collection as a FindPack.NFT,
904        // exposing all of its fields.
905        // This is safe as there are no functions that can be called on the FindPack.
906        //
907        access(all) fun borrowFindPack(id: UInt64): &FindPack.NFT? {
908            if self.ownedNFTs[id] != nil {
909                let ref = (&self.ownedNFTs[id] as &{NonFungibleToken.NFT}?)!
910                return ref as! &FindPack.NFT
911            } else {
912                return nil
913            }
914        }
915
916
917        access(all) view fun borrowViewResolver(id: UInt64): &{ViewResolver.Resolver}? {
918            if let nft = &self.ownedNFTs[id] as &{NonFungibleToken.NFT}? {
919                return nft as &{ViewResolver.Resolver}
920            }
921            return nil
922        }
923
924
925        access(all) view fun getDefaultStoragePath() : StoragePath {
926            return FindPack.CollectionStoragePath
927        }
928
929        access(all) view fun getDefaultPublicPath() : PublicPath {
930            return FindPack.CollectionPublicPath
931        }
932
933        // initializer
934        //
935        init () {
936            self.ownedNFTs <- {}
937        }
938    }
939
940    access(account) fun mintNFT(packTypeName: String, typeId: UInt64, hash: String, royalties: [MetadataViews.Royalty]) : @{NonFungibleToken.NFT} {
941
942        let nft <- create FindPack.NFT(packTypeName: packTypeName, typeId: typeId, hash:hash, royalties:royalties)
943        emit Minted(id: nft.id, typeId:typeId)
944
945        // deposit it in the recipient's account using their reference
946        return <- nft
947    }
948
949    access(account) fun fulfill(packId: UInt64, types:[Type], rewardIds: [UInt64], salt:String) {
950
951        let openedPacksCollection = FindPack.account.storage.borrow<auth (NonFungibleToken.Withdraw) &FindPack.Collection>(from: FindPack.OpenedCollectionStoragePath)!
952        let pack <- openedPacksCollection.withdraw(withdrawID: packId) as! @FindPack.NFT
953        let packTypeName = pack.packTypeName
954        let packTypeId = pack.getTypeID()
955        let metadata = FindPack.getMetadataById(packTypeName:packTypeName, typeId:packTypeId)!
956        let packFields = metadata.packFields
957        let packNFTTypes = metadata.getItemTypesAsStringArray()
958
959        let firstType = types[0]
960        let receiver= pack.getOpenedBy()
961        let	receivingAddress = receiver[firstType]!.address
962        let hash= pack.getHash()
963        let rewards=pack.getMetadata().providerCaps
964
965        let receiverAccount=getAccount(receivingAddress)
966        var freeStorage=0 as UInt64
967        // prevent underflow
968        if receiverAccount.storage.capacity >= receiverAccount.storage.used {
969            freeStorage = receiverAccount.storage.capacity- receiverAccount.storage.used
970        }
971        Debug.log("Free capacity from account ".concat(freeStorage.toString()))
972        if pack.getMetadata().storageRequirement > freeStorage {
973            emit FulfilledError(packTypeName: packTypeName, packTypeId: packTypeId, packId:packId, address:receivingAddress, reason: "Not enough flow to hold the content of the pack. Please top up your account")
974            self.transferToDLQ(<- pack)
975            return
976        }
977
978        let receiverCheck :{Type: Bool} = {}
979        var hashString = salt
980        for i, type in types {
981
982            if receiverCheck[type] == nil {
983                if !receiver[type]!.check() {
984                    emit FulfilledError(packTypeName: packTypeName, packTypeId: packTypeId, packId:packId, address:receiver[type]!.address, reason: "The receiver registered in this pack is not valid")
985                    self.transferToDLQ(<- pack)
986                    return
987                }
988
989                if !rewards[type]!.check() {
990                    Debug.log("type that is not present in rewards cap ".concat(type.identifier))
991                    emit FulfilledError(packTypeName: packTypeName, packTypeId: packTypeId, packId:packId, address:receiver[type]!.address, reason: "Cannot borrow provider capability to withdraw nfts")
992                    self.transferToDLQ(<- pack)
993                    return
994                }
995                receiverCheck[type]=true
996            }
997
998            let id = rewardIds[i]
999            hashString= hashString.concat(",").concat(type.identifier).concat(";").concat(id.toString())
1000        }
1001
1002        let digest = HashAlgorithm.SHA3_384.hash(hashString.utf8)
1003        let digestAsString=String.encodeHex(digest)
1004        if digestAsString != hash {
1005            emit FulfilledError(packTypeName: packTypeName, packTypeId: packTypeId, packId:packId, address:receivingAddress, reason: "The content of the pack was not verified with the hash provided at mint")
1006            Debug.log("digestAsString : ".concat(hashString))
1007            Debug.log("hash : ".concat(hash))
1008            self.transferToDLQ(<- pack)
1009            return
1010        }
1011
1012        for i, type in types {
1013            let id = rewardIds[i]
1014            let target=receiver[type]!.borrow()!
1015            let source=rewards[type]!.borrow()!
1016
1017            let viewType= Type<PackRevealData>()
1018            let nft=source.borrowViewResolver(id: id)!
1019
1020            var fields : {String: String}= {}
1021            if nft.getViews().contains(viewType) {
1022                let view=nft.resolveView(viewType)! as! PackRevealData
1023                fields=view.data
1024            } else {
1025                if let display=MetadataViews.getDisplay(nft) {
1026                    fields["nftName"]=display.name
1027                    fields["nftImage"]=display.thumbnail.uri()
1028                }
1029            }
1030
1031            if let cd = MetadataViews.getNFTCollectionData(nft) {
1032                fields["path"]=cd.storagePath.toString()
1033            }
1034            let token <- source.withdraw(withdrawID: id)
1035
1036            emit PackReveal(
1037                packTypeName: packTypeName,
1038                packTypeId: packTypeId,
1039                packId:packId,
1040                address:receiver[type]!.address,
1041                rewardId: id,
1042                rewardType: token.getType().identifier,
1043                rewardFields: fields,
1044                packFields: packFields,
1045                packNFTTypes: packNFTTypes
1046            )
1047            target.deposit(token: <-token)
1048        }
1049        emit Fulfilled(packTypeName: packTypeName, packTypeId: packTypeId, packId:packId, address:receivingAddress, packFields:packFields, packNFTTypes:packNFTTypes)
1050
1051        destroy pack
1052    }
1053
1054    access(account) fun transferToDLQ(_ pack: @NFT) {
1055        let dlq = FindPack.account.storage.borrow<&FindPack.Collection>(from: FindPack.DLQCollectionStoragePath)!
1056        dlq.deposit(token: <- pack)
1057    }
1058
1059    access(account) fun getPacksCollectionPath(packTypeName: String, packTypeId: UInt64) : String {
1060        return "FindPack_".concat(packTypeName).concat("_").concat(packTypeId.toString())
1061    }
1062
1063    access(all) fun getPacksCollection(packTypeName: String, packTypeId: UInt64) : &FindPack.Collection {
1064
1065        let pathIdentifier = self.getPacksCollectionPath(packTypeName: packTypeName, packTypeId: packTypeId)
1066        let path = PublicPath(identifier: pathIdentifier) ?? panic("Cannot create path from identifier : ".concat(pathIdentifier))
1067        return FindPack.account.capabilities.get<&FindPack.Collection>(path).borrow() ?? panic("Could not borow FindPack collection for path : ".concat(pathIdentifier))
1068    }
1069
1070    // given a path, lookin to the NFT Collection and return a new empty collection
1071    access(all) fun createEmptyCollectionFromPackData(packData: FindPack.Metadata, type: Type) : @{NonFungibleToken.Collection} {
1072        let cap = packData.providerCaps[type] ?? panic("Type passed in does not exist in this pack ".concat(type.identifier))
1073        if !cap.check() {
1074            panic("Provider capability of pack is not valid Name and ID".concat(type.identifier))
1075        }
1076        let ref = cap.borrow()!
1077        let resolver = ref.borrowViewResolver(id: ref.getIDs()[0])!  // if the ID length is 0, there must be some problem
1078        let collectionData = MetadataViews.getNFTCollectionData(resolver) ?? panic("Collection Data for this NFT Type is missing. Type : ".concat(resolver.getType().identifier))
1079        return <- collectionData.createEmptyCollection()
1080    }
1081
1082    access(all) fun canBuy(packTypeName: String, packTypeId:UInt64, user:Address) : Bool {
1083
1084        let packs=FindPack.getPacksCollection(packTypeName: packTypeName, packTypeId:packTypeId)
1085
1086        let packsLeft= packs.getPacksLeft()
1087        if packsLeft == 0 {
1088            return false
1089        }
1090
1091        let packMetadata=FindPack.getMetadataById(packTypeName: packTypeName, typeId: packTypeId)
1092
1093        if packMetadata==nil {
1094            return false
1095        }
1096        let timestamp=Clock.time()
1097
1098        let metadata=packMetadata!
1099
1100        for info in metadata.saleInfos {
1101            if info.checkBuyable(addr: user, time:timestamp) {
1102                return true
1103            }
1104        }
1105
1106        return false
1107    }
1108
1109    access(all) fun getCurrentPrice(packTypeName: String, packTypeId:UInt64, user:Address) : UFix64? {
1110
1111        let packs=FindPack.getPacksCollection(packTypeName: packTypeName, packTypeId:packTypeId)
1112
1113        let packsLeft= packs.getPacksLeft()
1114        if packsLeft == 0 {
1115            return nil
1116        }
1117
1118        let packMetadata=FindPack.getMetadataById(packTypeName: packTypeName, typeId: packTypeId)
1119
1120        if packMetadata==nil {
1121            return nil
1122        }
1123        let timestamp=Clock.time()
1124
1125        let metadata=packMetadata!
1126
1127        var lowestPrice : UFix64? = nil
1128        for info in metadata.saleInfos {
1129            if info.checkBuyable(addr: user, time:timestamp) {
1130                if lowestPrice == nil || lowestPrice! > info.price {
1131                    lowestPrice = info.price
1132                }
1133            }
1134        }
1135
1136        return lowestPrice
1137    }
1138
1139    access(contract) fun borrowSaleInfo(packTypeName: String, packTypeId: UInt64, index: Int) : &FindPack.SaleInfo {
1140        let mappingRef = (&FindPack.packMetadata[packTypeName] as &{UInt64: FindPack.Metadata}?)!
1141        let ref = mappingRef[packTypeId]!
1142        return ref.borrowSaleInfo(index)
1143    }
1144
1145    access(all) fun getOwnerCollection() : Capability<&FindPack.Collection> {
1146        return FindPack.account.capabilities.get<&FindPack.Collection>(FindPack.CollectionPublicPath)
1147    }
1148
1149    access(all) resource Forge: FindForge.Forge {
1150        access(FindForge.ForgeOwner) fun mint(platform: FindForge.MinterPlatform, data: AnyStruct, verifier: &FindForge.Verifier) : @{NonFungibleToken.NFT} {
1151
1152            let royalties : [MetadataViews.Royalty] = []
1153            // there should be no find cut for the pack.
1154            if platform.minterCut != nil && platform.minterCut! != 0.0 {
1155                royalties.append(MetadataViews.Royalty(receiver:platform.getMinterFTReceiver(), cut: platform.minterCut!, description: "creator"))
1156            }
1157            let input = data as? MintPackData ?? panic("The data passed in is not in MintPackData Struct")
1158            return <- FindPack.mintNFT(packTypeName: platform.name, typeId: input.typeId , hash: input.hash, royalties: royalties)
1159        }
1160
1161        access(FindForge.ForgeOwner) fun addContractData(platform: FindForge.MinterPlatform, data: AnyStruct, verifier: &FindForge.Verifier) {
1162            let type = data.getType()
1163
1164            switch type {
1165            case Type<{UInt64 : Metadata}>() :
1166                let typedData = data as! {UInt64 : Metadata}
1167                for key in typedData.keys {
1168                    FindPack.registerMetadata(packTypeName: platform.name, typeId: key, metadata: typedData[key]!)
1169                }
1170                return
1171
1172                default :
1173                panic("Type : ".concat(data.getType().identifier).concat("is not supported in Find Pack"))
1174            }
1175        }
1176    }
1177
1178    access(account) fun createForge() : @{FindForge.Forge} {
1179        return <- create Forge()
1180    }
1181
1182    access(all) fun createEmptyCollection(nftType: Type): @{NonFungibleToken.Collection} {
1183
1184        return <- create Collection()
1185    }
1186    // initializer
1187    //
1188    init() {
1189        self.CollectionStoragePath = /storage/FindPackCollection
1190        self.CollectionPublicPath = /public/FindPackCollection
1191
1192        self.OpenedCollectionStoragePath = /storage/FindPackOpenedCollection
1193        self.OpenedCollectionPublicPath = /public/FindPackOpenedCollection
1194
1195        self.DLQCollectionStoragePath = /storage/FindPackDLQCollection
1196        self.DLQCollectionPublicPath = /public/FindPackDLQCollection
1197
1198        self.PackMetadataStoragePath= /storage/FindPackMetadata
1199
1200        //this will not be used, we use UUID as id
1201        self.totalSupply = 0
1202
1203        self.packMetadata={}
1204
1205        // this contract will hold a Collection that FindPack can be deposited to and Admins can Consume them to transfer nfts to the depositing account
1206        let openedCollection <- create Collection()
1207        self.account.storage.save(<- openedCollection, to: self.OpenedCollectionStoragePath)
1208        let cap = self.account.capabilities.storage.issue<&Collection>(self.OpenedCollectionStoragePath)
1209        self.account.capabilities.publish(cap, at: self.OpenedCollectionPublicPath)
1210
1211        //a DLQ storage slot so that the opener can put items that cannot be opened/transferred here.
1212        let dlqCollection <- create Collection()
1213        self.account.storage.save(<- dlqCollection, to: self.DLQCollectionStoragePath)
1214        let dlqCap = self.account.capabilities.storage.issue<&Collection>(self.DLQCollectionStoragePath)
1215        self.account.capabilities.publish(dlqCap, at: self.DLQCollectionPublicPath)
1216
1217        FindForge.addForgeType(<- create Forge())
1218
1219        //TODO: Add the Forge resource aswell
1220        FindForge.addPublicForgeType(forgeType: Type<@Forge>())
1221
1222        emit ContractInitialized()
1223    }
1224}
1225
1226
1227
1228