Smart Contract
SyllablesNFTStorefrontV2
A.440776a8205e9f80.SyllablesNFTStorefrontV2
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3
4/// SyllablesNFTStorefrontV2
5///
6/// A general purpose sale support contract for NFTs that implement the Flow NonFungibleToken standard.
7///
8/// Each account that wants to list NFTs for sale installs a Storefront,
9/// and lists individual sales within that Storefront as Listings.
10/// There is one Storefront per account, it handles sales of all NFT types
11/// for that account.
12///
13/// Each Listing can have one or more "cuts" of the sale price that
14/// goes to one or more addresses. Cuts can be used to pay listing fees
15/// or other considerations.
16/// Each Listing can include a commission amount that is paid to whoever facilitates
17/// the purchase. The seller can also choose to provide an optional list of marketplace
18/// receiver capabilities. In this case, the commission amount must be transferred to
19/// one of the capabilities in the list.
20///
21/// Each NFT may be listed in one or more Listings, the validity of each
22/// Listing can easily be checked.
23///
24/// Purchasers can watch for Listing events and check the NFT type and
25/// ID to see if they wish to buy the listed item.
26/// Marketplaces and other aggregators can watch for Listing events
27/// and list items of interest.
28///
29access(all) contract SyllablesNFTStorefrontV2 {
30
31 access(all) entitlement CreateListing
32 access(all) entitlement RemoveListing
33
34 /// StorefrontInitialized
35 /// A Storefront resource has been created.
36 /// Event consumers can now expect events from this Storefront.
37 /// Note that we do not specify an address: we cannot and should not.
38 /// Created resources do not have an owner address, and may be moved
39 /// after creation in ways we cannot check.
40 /// ListingAvailable events can be used to determine the address
41 /// of the owner of the Storefront (...its location) at the time of
42 /// the listing but only at that precise moment in that precise transaction.
43 /// If the seller moves the Storefront while the listing is valid,
44 /// that is on them.
45 ///
46 access(all) event StorefrontInitialized(storefrontResourceID: UInt64)
47
48 /// ListingAvailable
49 /// A listing has been created and added to a Storefront resource.
50 /// The Address values here are valid when the event is emitted, but
51 /// the state of the accounts they refer to may change outside of the
52 /// SyllablesNFTStorefrontV2 workflow, so be careful to check when using them.
53 ///
54 access(all) event ListingAvailable(
55 storefrontAddress: Address,
56 listingResourceID: UInt64,
57 nftType: Type,
58 nftUUID: UInt64,
59 nftID: UInt64,
60 salePaymentVaultType: Type,
61 salePrice: UFix64,
62 customID: String?,
63 commissionAmount: UFix64,
64 commissionReceivers: [Address]?,
65 expiry: UInt64
66 )
67
68 /// ListingCompleted
69 /// The listing has been resolved. It has either been purchased, removed or destroyed.
70 ///
71 access(all) event ListingCompleted(
72 listingResourceID: UInt64,
73 storefrontResourceID: UInt64,
74 purchased: Bool,
75 nftType: Type,
76 nftUUID: UInt64,
77 nftID: UInt64,
78 salePaymentVaultType: Type,
79 salePrice: UFix64,
80 customID: String?,
81 commissionAmount: UFix64,
82 commissionReceiver: Address?,
83 expiry: UInt64
84 )
85
86 /// UnpaidReceiver
87 /// A entitled receiver has not been paid during the sale of the NFT.
88 ///
89 access(all) event UnpaidReceiver(receiver: Address, entitledSaleCut: UFix64)
90
91 /// StorefrontStoragePath
92 /// The location in storage that a Storefront resource should be located.
93 access(all) let StorefrontStoragePath: StoragePath
94
95 /// StorefrontPublicPath
96 /// The public location for a Storefront link.
97 access(all) let StorefrontPublicPath: PublicPath
98
99
100 /// SaleCut
101 /// A struct representing a recipient that must be sent a certain amount
102 /// of the payment when a token is sold.
103 ///
104 access(all) struct SaleCut {
105 /// The receiver for the payment.
106 /// Note that we do not store an address to find the Vault that this represents,
107 /// as the link or resource that we fetch in this way may be manipulated,
108 /// so to find the address that a cut goes to you must get this struct and then
109 /// call receiver.borrow()!.owner.address on it.
110 /// This can be done efficiently in a script.
111 access(all) let receiver: Capability<&{FungibleToken.Receiver}>
112
113 /// The amount of the payment FungibleToken that will be paid to the receiver.
114 access(all) let amount: UFix64
115
116 /// initializer
117 ///
118 init(receiver: Capability<&{FungibleToken.Receiver}>, amount: UFix64) {
119 self.receiver = receiver
120 self.amount = amount
121 }
122 }
123
124
125 /// ListingDetails
126 /// A struct containing a Listing's data.
127 ///
128 access(all) struct ListingDetails {
129 /// The Storefront that the Listing is stored in.
130 /// Note that this resource cannot be moved to a different Storefront,
131 /// so this is OK. If we ever make it so that it *can* be moved,
132 /// this should be revisited.
133 access(all) var storefrontID: UInt64
134 /// Whether this listing has been purchased or not.
135 access(all) var purchased: Bool
136 /// The Type of the NonFungibleToken.NFT that is being listed.
137 access(all) let nftType: Type
138 /// The Resource ID of the NFT which can only be set in the contract
139 access(all) let nftUUID: UInt64
140 /// The unique identifier of the NFT that will get sell.
141 access(all) let nftID: UInt64
142 /// The Type of the FungibleToken that payments must be made in.
143 access(all) let salePaymentVaultType: Type
144 /// The amount that must be paid in the specified FungibleToken.
145 access(all) let salePrice: UFix64
146 /// This specifies the division of payment between recipients.
147 access(all) let saleCuts: [SaleCut]
148 /// Allow different dapp teams to provide custom strings as the distinguished string
149 /// that would help them to filter events related to their customID.
150 access(all) var customID: String?
151 /// Commission available to be claimed by whoever facilitates the sale.
152 access(all) let commissionAmount: UFix64
153 /// Expiry of listing
154 access(all) let expiry: UInt64
155
156 /// Irreversibly set this listing as purchased.
157 ///
158 access(contract) fun setToPurchased() {
159 self.purchased = true
160 }
161
162 access(contract) fun setCustomID(customID: String?){
163 self.customID = customID
164 }
165
166 /// Initializer
167 ///
168 init (
169 nftType: Type,
170 nftUUID: UInt64,
171 nftID: UInt64,
172 salePaymentVaultType: Type,
173 saleCuts: [SaleCut],
174 storefrontID: UInt64,
175 customID: String?,
176 commissionAmount: UFix64,
177 expiry: UInt64
178 ) {
179
180 pre {
181 // Validate the expiry
182 expiry > UInt64(getCurrentBlock().timestamp): "Expiry should be in the future"
183 // Validate the length of the sale cut
184 saleCuts.length > 0: "Listing must have at least one payment cut recipient"
185 }
186
187 self.storefrontID = storefrontID
188 self.purchased = false
189 self.nftType = nftType
190 self.nftUUID = nftUUID
191 self.nftID = nftID
192 self.salePaymentVaultType = salePaymentVaultType
193 self.customID = customID
194 self.commissionAmount = commissionAmount
195 self.expiry = expiry
196 self.saleCuts = saleCuts
197
198 // Calculate the total price from the cuts
199 var salePrice = commissionAmount
200 // Perform initial check on capabilities, and calculate sale price from cut amounts.
201 for cut in self.saleCuts {
202 // Make sure we can borrow the receiver.
203 // We will check this again when the token is sold.
204 cut.receiver.borrow()
205 ?? panic("Cannot borrow receiver")
206 // Add the cut amount to the total price
207 salePrice = salePrice + cut.amount
208 }
209 assert(salePrice > 0.0, message: "Listing must have non-zero price")
210
211 // Store the calculated sale price
212 self.salePrice = salePrice
213 }
214 }
215
216
217 /// ListingPublic
218 /// An interface providing a useful public interface to a Listing.
219 ///
220 access(all) resource interface ListingPublic {
221 /// borrowNFT
222 /// This will assert in the same way as the NFT standard borrowNFT()
223 /// if the NFT is absent, for example if it has been sold via another listing.
224 ///
225 access(all) fun borrowNFT(): &{NonFungibleToken.NFT}?
226
227 /// purchase
228 /// Purchase the listing, buying the token.
229 /// This pays the beneficiaries and returns the token to the buyer.
230 ///
231 access(all) fun purchase(
232 payment: @{FungibleToken.Vault},
233 commissionRecipient: Capability<&{FungibleToken.Receiver}>?,
234 ): @{NonFungibleToken.NFT}
235
236 /// getDetails
237 /// Fetches the details of the listing.
238 access(all) view fun getDetails(): ListingDetails
239
240 /// getAllowedCommissionReceivers
241 /// Fetches the allowed marketplaces capabilities or commission receivers.
242 /// If it returns `nil` then commission is up to grab by anyone.
243 access(all) view fun getAllowedCommissionReceivers(): [Capability<&{FungibleToken.Receiver}>]?
244
245 /// hasListingBecomeGhosted
246 /// Tells whether listed NFT is present in provided capability.
247 /// If it returns `false` then it means listing becomes ghost or sold out.
248 access(all) view fun hasListingBecomeGhosted(): Bool
249
250 }
251
252
253 /// Listing
254 /// A resource that allows an NFT to be sold for an amount of a given FungibleToken,
255 /// and for the proceeds of that sale to be split between several recipients.
256 ///
257 access(all) resource Listing: ListingPublic {
258 /// The simple (non-Capability, non-complex) details of the sale
259 access(self) let details: ListingDetails
260
261 /// A capability allowing this resource to withdraw the NFT with the given ID from its collection.
262 /// This capability allows the resource to withdraw *any* NFT, so you should be careful when giving
263 /// such a capability to a resource and always check its code to make sure it will use it in the
264 /// way that it claims.
265 access(contract) let nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>
266
267 /// An optional list of marketplaces capabilities that are approved
268 /// to receive the marketplace commission.
269 access(contract) let marketplacesCapability: [Capability<&{FungibleToken.Receiver}>]?
270
271 /// borrowNFT
272 /// Return the reference of the NFT that is listed for sale.
273 /// if the NFT is absent, for example if it has been sold via another listing.
274 /// it will return nil.
275 ///
276 access(all) fun borrowNFT(): &{NonFungibleToken.NFT}? {
277 let ref = self.nftProviderCapability.borrow()!.borrowNFT(self.details.nftID)
278 if ref.isInstance(self.details.nftType) && ref?.id == self.details.nftID {
279 return ref as &{NonFungibleToken.NFT}?
280 }
281 return nil
282 }
283
284 /// getDetails
285 /// Get the details of listing.
286 ///
287 access(all) view fun getDetails(): ListingDetails {
288 return self.details
289 }
290
291 /// getAllowedCommissionReceivers
292 /// Fetches the allowed marketplaces capabilities or commission receivers.
293 /// If it returns `nil` then commission is up to grab by anyone.
294 access(all) view fun getAllowedCommissionReceivers(): [Capability<&{FungibleToken.Receiver}>]? {
295 return self.marketplacesCapability
296 }
297
298 /// hasListingBecomeGhosted
299 /// Tells whether listed NFT is present in provided capability.
300 /// If it returns `false` then it means listing becomes ghost or sold out.
301 access(all) view fun hasListingBecomeGhosted(): Bool {
302 if let providerRef = self.nftProviderCapability.borrow() {
303 return providerRef.borrowNFT(self.details.nftID) != nil
304 }
305 return false
306 }
307
308 /// purchase
309 /// Purchase the listing, buying the token.
310 /// This pays the beneficiaries and commission to the facilitator and returns extra token to the buyer.
311 /// This also cleans up duplicate listings for the item being purchased.
312 access(all) fun purchase(
313 payment: @{FungibleToken.Vault},
314 commissionRecipient: Capability<&{FungibleToken.Receiver}>?,
315 ): @{NonFungibleToken.NFT} {
316
317 pre {
318 self.details.purchased == false: "listing has already been purchased"
319 payment.isInstance(self.details.salePaymentVaultType): "payment vault is not requested fungible token"
320 payment.balance == self.details.salePrice: "payment vault does not contain requested price"
321 self.details.expiry > UInt64(getCurrentBlock().timestamp): "Listing is expired"
322 self.owner != nil : "Resource doesn't have the assigned owner"
323 }
324
325 // Make sure the listing cannot be purchased again.
326 self.details.setToPurchased()
327
328 if self.details.commissionAmount > 0.0 {
329 // If commission recipient is nil, Throw panic.
330 let commissionReceiver = commissionRecipient ?? panic("Commission recipient can't be nil")
331 if self.marketplacesCapability != nil {
332 var isCommissionRecipientHasValidType = false
333 var isCommissionRecipientAuthorised = false
334 for cap in self.marketplacesCapability! {
335 // Check 1: Should have the same type
336 if cap.getType() == commissionReceiver.getType() {
337 isCommissionRecipientHasValidType = true
338 // Check 2: Should have the valid market address that holds approved capability.
339 if cap.address == commissionReceiver.address && cap.check() {
340 isCommissionRecipientAuthorised = true
341 break
342 }
343 }
344 }
345 assert(isCommissionRecipientHasValidType, message: "Given recipient does not has valid type")
346 assert(isCommissionRecipientAuthorised, message: "Given recipient is not authorised to receive the commission")
347 }
348 let commissionPayment <- payment.withdraw(amount: self.details.commissionAmount)
349 let recipient = commissionReceiver.borrow() ?? panic("Unable to borrow the recipient capability")
350 recipient.deposit(from: <- commissionPayment)
351 }
352 // Fetch the token to return to the purchaser.
353 let nft <-self.nftProviderCapability.borrow()!.withdraw(withdrawID: self.details.nftID)
354 // Neither receivers nor providers are trustworthy, they must implement the correct
355 // interface but beyond complying with its pre/post conditions they are not guaranteed
356 // to implement the functionality behind the interface in any given way.
357 // Therefore we cannot trust the Collection resource behind the interface,
358 // and we must check the NFT resource it gives us to make sure that it is the correct one.
359 assert(nft.getType() == self.details.nftType, message: "withdrawn NFT is not of specified type")
360 assert(nft.id == self.details.nftID, message: "withdrawn NFT does not have specified ID")
361
362 // Fetch the duplicate listing for the given NFT
363 // Access the StoreFrontManager resource reference to remove the duplicate listings if purchase would happen successfully.
364 let storeFrontPublicRef = getAccount(self.owner!.address).capabilities.borrow<&{SyllablesNFTStorefrontV2.StorefrontPublic}>(
365 SyllablesNFTStorefrontV2.StorefrontPublicPath
366 ) ?? panic("Unable to borrow the storeFrontManager resource")
367 let duplicateListings = storeFrontPublicRef.getDuplicateListingIDs(
368 nftType: self.details.nftType,
369 nftID: self.details.nftID,
370 listingID: self.uuid
371 )
372
373 // Let's force removal of the listing in this storefront for the NFT that is being purchased.
374 for listingID in duplicateListings {
375 storeFrontPublicRef.cleanup(listingResourceID: listingID)
376 }
377
378 // Rather than aborting the transaction if any receiver is absent when we try to pay it,
379 // we send the cut to the first valid receiver.
380 // The first receiver should therefore either be the seller, or an agreed recipient for
381 // any unpaid cuts.
382 var residualReceiver: &{FungibleToken.Receiver}? = nil
383 // Pay the commission
384 // Pay each beneficiary their amount of the payment.
385
386 for cut in self.details.saleCuts {
387 if let receiver = cut.receiver.borrow() {
388 let paymentCut <- payment.withdraw(amount: cut.amount)
389 receiver.deposit(from: <-paymentCut)
390 if (residualReceiver == nil) {
391 residualReceiver = receiver
392 }
393 } else {
394 emit UnpaidReceiver(receiver: cut.receiver.address, entitledSaleCut: cut.amount)
395 }
396 }
397
398 assert(residualReceiver != nil, message: "No valid payment receivers")
399
400 // At this point, if all receivers were active and available, then the payment Vault will have
401 // zero tokens left, and this will functionally be a no-op that consumes the empty vault
402 residualReceiver!.deposit(from: <-payment)
403
404 // If the listing is purchased, we regard it as completed here.
405 // Otherwise we regard it as completed in the destructor.
406
407 var commissionReceiver: Address? = nil
408 if (self.details.commissionAmount != 0.0) {
409 commissionReceiver = commissionRecipient!.address
410 }
411
412 emit ListingCompleted(
413 listingResourceID: self.uuid,
414 storefrontResourceID: self.details.storefrontID,
415 purchased: self.details.purchased,
416 nftType: self.details.nftType,
417 nftUUID: self.details.nftUUID,
418 nftID: self.details.nftID,
419 salePaymentVaultType: self.details.salePaymentVaultType,
420 salePrice: self.details.salePrice,
421 customID: self.details.customID,
422 commissionAmount: self.details.commissionAmount,
423 commissionReceiver: commissionReceiver,
424 expiry: self.details.expiry
425 )
426
427 return <-nft
428 }
429
430 // destructor event
431 //
432 access(all) event ResourceDestroyed(
433 listingResourceID: UInt64 = self.uuid,
434 storefrontResourceID: UInt64 = self.details.storefrontID,
435 purchased: Bool = self.details.purchased,
436 nftType: String = self.details.nftType.identifier,
437 nftUUID: UInt64 = self.details.nftUUID,
438 nftID: UInt64 = self.details.nftID,
439 salePaymentVaultType: String = self.details.salePaymentVaultType.identifier,
440 salePrice: UFix64 = self.details.salePrice,
441 customID: String? = self.details.customID,
442 commissionAmount: UFix64 = self.details.commissionAmount,
443 commissionReceiver: Address? = nil,
444 expiry: UInt64 = self.details.expiry
445 )
446
447 /// initializer
448 ///
449 init (
450 nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection}>,
451 nftType: Type,
452 nftUUID: UInt64,
453 nftID: UInt64,
454 salePaymentVaultType: Type,
455 saleCuts: [SaleCut],
456 marketplacesCapability: [Capability<&{FungibleToken.Receiver}>]?,
457 storefrontID: UInt64,
458 customID: String?,
459 commissionAmount: UFix64,
460 expiry: UInt64
461 ) {
462 // Store the sale information
463 self.details = ListingDetails(
464 nftType: nftType,
465 nftUUID: nftUUID,
466 nftID: nftID,
467 salePaymentVaultType: salePaymentVaultType,
468 saleCuts: saleCuts,
469 storefrontID: storefrontID,
470 customID: customID,
471 commissionAmount: commissionAmount,
472 expiry: expiry
473 )
474
475 // Store the NFT provider
476 self.nftProviderCapability = nftProviderCapability
477 self.marketplacesCapability = marketplacesCapability
478
479 // Check that the provider contains the NFT.
480 // We will check it again when the token is sold.
481 // We cannot move this into a function because initializers cannot call member functions.
482 let provider = self.nftProviderCapability.borrow()
483 assert(provider != nil, message: "cannot borrow nftProviderCapability")
484
485 // This will precondition assert if the token is not available.
486 let nft = provider!.borrowNFT(self.details.nftID)
487 assert(nft!.getType() == self.details.nftType, message: "token is not of specified type")
488 assert(nft?.id == self.details.nftID, message: "token does not have specified ID")
489 }
490 }
491
492 /// StorefrontManager
493 /// An interface for adding and removing Listings within a Storefront,
494 /// intended for use by the Storefront's owner
495 ///
496 access(all) resource interface StorefrontManager {
497 /// createListing
498 /// Allows the Storefront owner to create and insert Listings.
499 ///
500 access(CreateListing) fun createListing(
501 nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection}>,
502 nftType: Type,
503 nftID: UInt64,
504 salePaymentVaultType: Type,
505 saleCuts: [SaleCut],
506 marketplacesCapability: [Capability<&{FungibleToken.Receiver}>]?,
507 customID: String?,
508 commissionAmount: UFix64,
509 expiry: UInt64
510 ): UInt64
511
512 /// removeListing
513 /// Allows the Storefront owner to remove any sale listing, accepted or not.
514 ///
515 access(RemoveListing) fun removeListing(listingResourceID: UInt64)
516 }
517
518 /// StorefrontPublic
519 /// An interface to allow listing and borrowing Listings, and purchasing items via Listings
520 /// in a Storefront.
521 ///
522 access(all) resource interface StorefrontPublic {
523 access(all) view fun getListingIDs(): [UInt64]
524 access(all) fun getDuplicateListingIDs(nftType: Type, nftID: UInt64, listingID: UInt64): [UInt64]
525 access(all) view fun borrowListing(listingResourceID: UInt64): &{ListingPublic}?
526 access(all) fun cleanupExpiredListings(fromIndex: UInt64, toIndex: UInt64)
527 access(contract) fun cleanup(listingResourceID: UInt64)
528 access(all) fun getExistingListingIDs(nftType: Type, nftID: UInt64): [UInt64]
529 access(all) fun cleanupPurchasedListings(listingResourceID: UInt64)
530 access(all) fun cleanupGhostListings(listingResourceID: UInt64)
531 }
532
533 /// Storefront
534 /// A resource that allows its owner to manage a list of Listings, and anyone to interact with them
535 /// in order to query their details and purchase the NFTs that they represent.
536 ///
537 access(all) resource Storefront : StorefrontManager, StorefrontPublic {
538 // Resource destroyed event
539 access(all) event ResourceDestroyed(
540 storefrontResourceID: UInt64 = self.uuid
541 )
542
543 /// The dictionary of Listing uuids to Listing resources.
544 access(contract) var listings: @{UInt64: Listing}
545 /// Dictionary to keep track of listing ids for same NFTs listing.
546 /// nftType.identifier -> nftID -> [listing resource ID]
547 access(contract) var listedNFTs: {String: {UInt64 : [UInt64]}}
548
549 /// insert
550 /// Create and publish a Listing for an NFT.
551 ///
552 access(CreateListing) fun createListing(
553 nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection}>,
554 nftType: Type,
555 nftID: UInt64,
556 salePaymentVaultType: Type,
557 saleCuts: [SaleCut],
558 marketplacesCapability: [Capability<&{FungibleToken.Receiver}>]?,
559 customID: String?,
560 commissionAmount: UFix64,
561 expiry: UInt64
562 ): UInt64 {
563
564 // let's ensure that the seller does indeed hold the NFT being listed
565 let collectionRef = nftProviderCapability.borrow()
566 ?? panic("Could not borrow reference to collection")
567 let nftRef = collectionRef.borrowNFT(nftID)
568 ?? panic("Could not borrow a reference to the desired NFT ID")
569
570 // Instead of letting an arbitrary value be set for the UUID of a given NFT, the contract
571 // should fetch it itself
572 let uuid = nftRef.uuid
573 let listing <- create Listing(
574 nftProviderCapability: nftProviderCapability,
575 nftType: nftType,
576 nftUUID: uuid,
577 nftID: nftID,
578 salePaymentVaultType: salePaymentVaultType,
579 saleCuts: saleCuts,
580 marketplacesCapability: marketplacesCapability,
581 storefrontID: self.uuid,
582 customID: customID,
583 commissionAmount: commissionAmount,
584 expiry: expiry
585 )
586
587 let listingResourceID = listing.uuid
588 let listingPrice = listing.getDetails().salePrice
589 // Add the new listing to the dictionary.
590 let oldListing <- self.listings[listingResourceID] <- listing
591 // Note that oldListing will always be nil, but we have to handle it.
592
593 destroy oldListing
594
595 // Add the `listingResourceID` in the tracked listings.
596 self.addDuplicateListing(nftIdentifier: nftType.identifier, nftID: nftID, listingResourceID: listingResourceID)
597
598 // Scraping addresses from the capabilities to emit in the event.
599 var allowedCommissionReceivers : [Address]? = nil
600 if let allowedReceivers = marketplacesCapability {
601 // Small hack here to make `allowedCommissionReceivers` variable compatible to
602 // array properties.
603 allowedCommissionReceivers = []
604 for receiver in allowedReceivers {
605 allowedCommissionReceivers!.append(receiver.address)
606 }
607 }
608
609 emit ListingAvailable(
610 storefrontAddress: self.owner?.address!,
611 listingResourceID: listingResourceID,
612 nftType: nftType,
613 nftUUID: uuid,
614 nftID: nftID,
615 salePaymentVaultType: salePaymentVaultType,
616 salePrice: listingPrice,
617 customID: customID,
618 commissionAmount: commissionAmount,
619 commissionReceivers: allowedCommissionReceivers,
620 expiry: expiry
621 )
622
623 return listingResourceID
624 }
625
626 /// addDuplicateListing
627 /// Helper function that allows to add duplicate listing of given nft in a map.
628 ///
629 access(contract) fun addDuplicateListing(nftIdentifier: String, nftID: UInt64, listingResourceID: UInt64) {
630 if !self.listedNFTs.containsKey(nftIdentifier) {
631 self.listedNFTs.insert(key: nftIdentifier, {nftID: [listingResourceID]})
632 } else {
633 if !self.listedNFTs[nftIdentifier]!.containsKey(nftID) {
634 self.listedNFTs[nftIdentifier]!.insert(key: nftID, [listingResourceID])
635 } else {
636 self.listedNFTs[nftIdentifier]![nftID]!.append(listingResourceID)
637 }
638 }
639 }
640
641 /// removeDuplicateListing
642 /// Helper function that allows to remove duplicate listing of given nft from a map.
643 ///
644 access(contract) fun removeDuplicateListing(nftIdentifier: String, nftID: UInt64, listingResourceID: UInt64) {
645 // Remove the listing from the listedNFTs dictionary.
646 let listingIndex = self.listedNFTs[nftIdentifier]![nftID]!.firstIndex(of: listingResourceID) ?? panic("Should contain the index")
647 self.listedNFTs[nftIdentifier]![nftID]!.remove(at: listingIndex)
648 }
649
650 /// removeListing
651 /// Remove a Listing that has not yet been purchased from the collection and destroy it.
652 /// It can only be executed by the StorefrontManager resource owner.
653 ///
654 access(RemoveListing) fun removeListing(listingResourceID: UInt64) {
655 let listing <- self.listings.remove(key: listingResourceID)
656 ?? panic("missing Listing")
657 let listingDetails = listing.getDetails()
658 self.removeDuplicateListing(nftIdentifier: listingDetails.nftType.identifier, nftID: listingDetails.nftID, listingResourceID: listingResourceID)
659 // This will emit a ListingCompleted event.
660 destroy listing
661 }
662
663 /// getListingIDs
664 /// Returns an array of the Listing resource IDs that are in the collection
665 ///
666 access(all) view fun getListingIDs(): [UInt64] {
667 return self.listings.keys
668 }
669
670 /// getExistingListingIDs
671 /// Returns an array of listing IDs of the given `nftType` and `nftID`.
672 ///
673 access(all) fun getExistingListingIDs(nftType: Type, nftID: UInt64): [UInt64] {
674 if self.listedNFTs[nftType.identifier] == nil || self.listedNFTs[nftType.identifier]![nftID] == nil {
675 return []
676 }
677 var listingIDs = self.listedNFTs[nftType.identifier]![nftID]!
678 return listingIDs
679 }
680
681 /// cleanupPurchasedListings
682 /// Allows anyone to remove already purchased listings.
683 ///
684 access(all) fun cleanupPurchasedListings(listingResourceID: UInt64) {
685 pre {
686 self.listings[listingResourceID] != nil: "could not find listing with given id"
687 self.borrowListing(listingResourceID: listingResourceID)!.getDetails().purchased == true: "listing not purchased yet"
688 }
689 let listing <- self.listings.remove(key: listingResourceID)!
690 let listingDetails = listing.getDetails()
691 self.removeDuplicateListing(nftIdentifier: listingDetails.nftType.identifier, nftID: listingDetails.nftID, listingResourceID: listingResourceID)
692
693 destroy listing
694 }
695
696 /// getDuplicateListingIDs
697 /// Returns an array of listing IDs that are duplicates of the given `nftType` and `nftID`.
698 ///
699 access(all) fun getDuplicateListingIDs(nftType: Type, nftID: UInt64, listingID: UInt64): [UInt64] {
700 var listingIDs = self.getExistingListingIDs(nftType: nftType, nftID: nftID)
701
702 // Verify that given listing Id also a part of the `listingIds`
703 let doesListingExist = listingIDs.contains(listingID)
704 // Find out the index of the existing listing.
705 if doesListingExist {
706 var index: Int = 0
707 for id in listingIDs {
708 if id == listingID {
709 break
710 }
711 index = index + 1
712 }
713 listingIDs.remove(at:index)
714 return listingIDs
715 }
716 return []
717 }
718
719 /// cleanupExpiredListings
720 /// Cleanup the expired listing by iterating over the provided range of indexes.
721 ///
722 access(all) fun cleanupExpiredListings(fromIndex: UInt64, toIndex: UInt64) {
723 pre {
724 fromIndex <= toIndex : "Incorrect start index"
725 Int(toIndex - fromIndex) < self.getListingIDs().length : "Provided range is out of bound"
726 }
727 var index = fromIndex
728 let listingsIDs = self.getListingIDs()
729 while index <= toIndex {
730 // There is a possibility that some index may not have the listing.
731 // because of that instead of failing the transaction, Execution moved to next index or listing.
732
733 if let listing = self.borrowListing(listingResourceID: listingsIDs[index]) {
734 if listing.getDetails().expiry <= UInt64(getCurrentBlock().timestamp) {
735 self.cleanup(listingResourceID: listingsIDs[index])
736 }
737 }
738 index = index + UInt64(1)
739 }
740 }
741
742 /// borrowSaleItem
743 /// Returns a read-only view of the SaleItem for the given listingID if it is contained by this collection.
744 ///
745 access(all) view fun borrowListing(listingResourceID: UInt64): &{ListingPublic}? {
746 return &self.listings[listingResourceID]
747 }
748
749 /// cleanup
750 /// Remove an listing, When given listing is duplicate or expired
751 /// Only contract is allowed to execute it.
752 ///
753 access(contract) fun cleanup(listingResourceID: UInt64) {
754 pre {
755 self.listings[listingResourceID] != nil: "Could not find listing with given id"
756 }
757 let listing <- self.listings.remove(key: listingResourceID)!
758 let listingDetails = listing.getDetails()
759 self.removeDuplicateListing(nftIdentifier: listingDetails.nftType.identifier, nftID: listingDetails.nftID, listingResourceID: listingResourceID)
760
761 destroy listing
762 }
763
764 /// cleanupGhostListings
765 /// Allow anyone to cleanup ghost listings
766 /// Listings will become ghost listings if stored provider capability doesn't hold
767 /// the NFT anymore.
768 ///
769 /// @param listingResourceID ID of the listing resource which would get removed if it become ghost listing.
770 access(all) fun cleanupGhostListings(listingResourceID: UInt64) {
771 pre {
772 self.listings[listingResourceID] != nil: "Could not find listing with given id"
773 }
774 let listingRef = self.borrowListing(listingResourceID: listingResourceID)!
775 let details = listingRef.getDetails()
776 assert(!details.purchased, message: "Given listing is already purchased")
777 assert(!listingRef.hasListingBecomeGhosted(), message: "Listing is not ghost listing")
778 let listing <- self.listings.remove(key: listingResourceID)!
779 let duplicateListings = self.getDuplicateListingIDs(nftType: details.nftType, nftID: details.nftID, listingID: listingResourceID)
780
781 // Let's force removal of the listing in this storefront for the NFT that is being ghosted.
782 for listingID in duplicateListings {
783 self.cleanup(listingResourceID: listingID)
784 }
785 destroy listing
786 }
787
788 /// constructor
789 ///
790 init () {
791 self.listings <- {}
792 self.listedNFTs = {}
793
794 // Let event consumers know that this storefront exists
795 emit StorefrontInitialized(storefrontResourceID: self.uuid)
796 }
797 }
798
799 /// createStorefront
800 /// Make creating a Storefront publicly accessible.
801 ///
802 access(all) fun createStorefront(): @Storefront {
803 return <-create Storefront()
804 }
805
806 init () {
807 self.StorefrontStoragePath = /storage/SyllablesNFTStorefrontV2
808 self.StorefrontPublicPath = /public/SyllablesNFTStorefrontV2
809 }
810}
811