Smart Contract
FindPack
A.097bafa4e0b48eef.FindPack
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