Smart Contract

NFTStorefrontV2

A.3cdbb3d569211ff3.NFTStorefrontV2

Valid From

139,981,189

Deployed

1w ago
Feb 15, 2026, 04:03:58 PM UTC

Dependents

66 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3import MetadataViews from 0x1d7e57aa55817448
4import LostAndFound from 0x473d6a2c37eab5be
5
6import DapperUtilityCoin from 0xead892083b3e2c6c
7
8import Permitted from 0x3cdbb3d569211ff3
9import FlowtyUtils from 0x3cdbb3d569211ff3
10import RoyaltiesOverride from 0x3cdbb3d569211ff3
11import FlowtyListingCallback from 0x3cdbb3d569211ff3
12import DNAHandler from 0x3cdbb3d569211ff3
13import Burner from 0xf233dcee88fe0abe
14
15/// NFTStorefrontV2
16///
17/// A general purpose sale support contract for NFTs that implement the Flow NonFungibleToken standard.
18///
19/// Each account that wants to list NFTs for sale installs a Storefront,
20/// and lists individual sales within that Storefront as Listings.
21/// There is one Storefront per account, it handles sales of all NFT types
22/// for that account.
23///
24/// Each Listing can have one or more "cuts" of the sale price that
25/// goes to one or more addresses. Cuts can be used to pay listing fees
26/// or other considerations.
27/// Each Listing can include a commission amount that is paid to whoever facilitates
28/// the purchase. The seller can also choose to provide an optional list of marketplace
29/// receiver capabilities. In this case, the commission amount must be transferred to
30/// one of the capabilities in the list.
31///
32/// Each NFT may be listed in one or more Listings, the validity of each
33/// Listing can easily be checked.
34///
35/// Purchasers can watch for Listing events and check the NFT type and
36/// ID to see if they wish to buy the listed item.
37/// Marketplaces and other aggregators can watch for Listing events
38/// and list items of interest.
39///
40access(all) contract NFTStorefrontV2 {
41		// needed to make new listings
42		access(all) entitlement List
43		// needed to remove existing valid listings (anyone can remove invalid listings)
44		access(all) entitlement Cancel
45		// needed to fill listings
46		access(all) entitlement Acceptor
47
48		// administrative actions, not used by the Storefront resource
49		access(all) entitlement Administrator
50
51		access(all) event StorefrontInitialized(storefrontResourceID: UInt64)
52
53		access(all) event StorefrontDestroyed(storefrontResourceID: UInt64)
54
55		/// ListingAvailable
56		/// A listing has been created and added to a Storefront resource.
57		/// The Address values here are valid when the event is emitted, but
58		/// the state of the accounts they refer to may change outside of the
59		/// NFTStorefrontV2 workflow, so be careful to check when using them.
60		///
61		access(all) event ListingAvailable(
62			storefrontAddress: Address,
63			listingResourceID: UInt64,
64			nftType: String,
65			nftUUID: UInt64,
66			nftID: UInt64,
67			salePaymentVaultType: String,
68			salePrice: UFix64,
69			customID: String?,
70			commissionAmount: UFix64,
71			commissionReceivers: [Address]?,
72			expiry: UInt64,
73			buyer: Address?,
74			providerAddress: Address
75		)
76
77		/// ListingCompleted
78		/// The listing has been resolved. It has either been purchased, removed or destroyed.
79		///
80		access(all) event ListingCompleted(
81			listingResourceID: UInt64,
82			storefrontResourceID: UInt64,
83			storefrontAddress: Address?,
84			purchased: Bool,
85			nftType: String,
86			nftUUID: UInt64,
87			nftID: UInt64,
88			salePaymentVaultType: String,
89			salePrice: UFix64,
90			customID: String?,
91			commissionAmount: UFix64,
92			commissionReceiver: Address?,
93			expiry: UInt64,
94			buyer: Address?
95		)
96
97
98		/// left here for legacy reasons, we do not use it.
99		access(all) event UnpaidReceiver()
100
101		/// MissingReceiver
102		access(all) event MissingReceiver(receiver: Address, amount: UFix64)
103
104		/// StorefrontStoragePath
105		/// The location in storage that a Storefront resource should be located.
106		access(all) let StorefrontStoragePath: StoragePath
107
108		/// StorefrontPublicPath
109		/// The public location for a Storefront link.
110		access(all) let StorefrontPublicPath: PublicPath
111
112		access(all) let AdminStoragePath: StoragePath
113
114		access(contract) let CommissionRecipients: {Type: Address}
115
116		/// SaleCut
117		/// A struct representing a recipient that must be sent a certain amount
118		/// of the payment when a token is sold.
119		///
120		access(all) struct SaleCut {
121			/// The receiver for the payment.
122			/// Note that we do not store an address to find the Vault that this represents,
123			/// as the link or resource that we fetch in this way may be manipulated,
124			/// so to find the address that a cut goes to you must get this struct and then
125			/// call receiver.borrow()!.owner.address on it.
126			/// This can be done efficiently in a script.
127			access(all) let receiver: Capability<&{FungibleToken.Receiver}>
128
129			/// The amount of the payment FungibleToken that will be paid to the receiver.
130			access(all) let amount: UFix64
131
132			/// initializer
133			///
134			init(receiver: Capability<&{FungibleToken.Receiver}>, amount: UFix64) {
135					self.receiver = receiver
136					self.amount = amount
137			}
138		}
139
140
141		/// ListingDetails
142		/// A struct containing a Listing's data.
143		///
144		access(all) struct ListingDetails {
145			access(all) var storefrontID: UInt64
146			/// Whether this listing has been purchased or not.
147			access(all) var purchased: Bool
148			/// The Type of the NonFungibleToken.NFT that is being listed.
149			access(all) let nftType: Type
150			/// The Resource ID of the NFT which can only be set in the contract
151			access(all) let nftUUID: UInt64
152			/// The unique identifier of the NFT that will get sell.
153			access(all) let nftID: UInt64
154			/// The Type of the FungibleToken that payments must be made in.
155			access(all) let salePaymentVaultType: Type
156			/// The amount that must be paid in the specified FungibleToken.
157			access(all) let salePrice: UFix64
158			/// This specifies the division of payment between recipients.
159			access(all) let saleCuts: [SaleCut]
160			/// Allow different dapp teams to provide custom strings as the distinguisher string
161			/// that would help them to filter events related to their customID.
162			access(all) var customID: String?
163			/// Commission available to be claimed by whoever facilitates the sale.
164			access(all) let commissionAmount: UFix64
165			/// Expiry of listing
166			access(all) let expiry: UInt64
167			/// Optional specified purchasing address for private listings
168			access(all) let buyer: Address?
169
170			/// Irreversibly set this listing as purchased.
171			///
172			access(contract) fun setToPurchased() {
173				self.purchased = true
174			}
175
176			/// Initializer
177			///
178			init (
179				nftType: Type,
180				nftUUID: UInt64,
181				nftID: UInt64,
182				salePaymentVaultType: Type,
183				saleCuts: [SaleCut],
184				storefrontID: UInt64,
185				customID: String?,
186				commissionAmount: UFix64,
187				expiry: UInt64,
188				buyer: Address?
189			) {
190				pre {
191					// Validate the expiry
192					expiry > UInt64(getCurrentBlock().timestamp) : "Expiry should be in the future"
193					// Validate the length of the sale cut
194					saleCuts.length > 0: "Listing must have at least one payment cut recipient"
195				}
196				self.storefrontID = storefrontID
197				self.purchased = false
198				self.nftType = nftType
199				self.nftUUID = nftUUID
200				self.nftID = nftID
201				self.salePaymentVaultType = salePaymentVaultType
202				self.customID = customID
203				self.commissionAmount = commissionAmount
204				self.expiry = expiry
205				self.saleCuts = saleCuts
206				self.buyer = buyer
207
208				// Calculate the total price from the cuts
209				var salePrice = commissionAmount
210				// Perform initial check on capabilities, and calculate sale price from cut amounts.
211
212				for cut in self.saleCuts {
213					// Add the cut amount to the total price
214					salePrice = salePrice + cut.amount
215				}
216				assert(salePrice > 0.0, message: "Listing must have non-zero price")
217
218				// Store the calculated sale price
219				self.salePrice = salePrice
220			}
221		}
222
223		access(all) resource interface ListingPublic {
224			access(all) view fun borrowNFT(): &{NonFungibleToken.NFT}?
225
226			/// purchase
227			/// Purchase the listing, buying the token.
228			/// This pays the beneficiaries and returns the token to the buyer.
229			///
230			access(all) fun purchase(
231				payment: @{FungibleToken.Vault},
232				commissionRecipient: Capability<&{FungibleToken.Receiver}>?,
233				privateListingAcceptor: auth(Acceptor) &{PrivateListingAcceptor}
234			): @{NonFungibleToken.NFT} {
235				pre {
236					privateListingAcceptor.getType() == Type<@Storefront>(): "incorrect privateListingAcceptor type"
237				}
238			}
239
240			/// getDetails
241			/// Fetches the details of the listing.
242			access(all) view fun getDetails(): ListingDetails
243
244			/// getAllowedCommissionReceivers
245			/// Fetches the allowed marketplaces capabilities or commission receivers.
246			/// If it returns `nil` then commission is up to grab by anyone.
247			access(all) view fun getAllowedCommissionReceivers(): [Capability<&{FungibleToken.Receiver}>]?
248
249			access(all) fun isValid(): Bool
250		}
251
252		access(all) resource Listing: ListingPublic, FlowtyListingCallback.Listing, Burner.Burnable {
253			access(all) event ResourceDestroyed(
254				listingResourceID: UInt64 = self.uuid,
255				purchased: Bool = self.details.purchased
256			)
257
258			access(self) let details: ListingDetails
259
260			access(contract) let nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>
261
262			access(contract) let marketplacesCapability: [Capability<&{FungibleToken.Receiver}>]?
263
264			access(all) view fun borrowNFT(): &{NonFungibleToken.NFT}? {
265				if !self.nftProviderCapability.check() {
266					return nil
267				}
268
269				let ref = self.nftProviderCapability.borrow()!.borrowNFT(self.details.nftID)
270				if ref == nil {
271					return nil
272				}
273
274				return ref!.isInstance(self.details.nftType) && ref!.id == self.details.nftID ? ref! : nil
275			}
276
277			access(all) view fun getDetails(): ListingDetails {
278				return self.details
279			}
280
281			/// getAllowedCommissionReceivers
282			/// Fetches the allowed marketplaces capabilities or commission receivers.
283			/// If it returns `nil` then commission is up to grab by anyone.
284			access(all) view fun getAllowedCommissionReceivers(): [Capability<&{FungibleToken.Receiver}>]? {
285				return self.marketplacesCapability
286			}
287
288			/// purchase
289			/// Purchase the listing, buying the token.
290			/// This pays the beneficiaries and commission to the facilitator and returns extra token to the buyer.
291			/// This also cleans up duplicate listings for the item being purchased.
292			access(all) fun purchase(
293				payment: @{FungibleToken.Vault},
294				commissionRecipient: Capability<&{FungibleToken.Receiver}>?,
295				privateListingAcceptor: auth(Acceptor) &{PrivateListingAcceptor}
296			): @{NonFungibleToken.NFT} {
297				pre {
298					self.details.purchased == false: "listing has already been purchased"
299					payment.isInstance(self.details.salePaymentVaultType): "payment vault is not requested fungible token"
300					payment.balance == self.details.salePrice: "payment vault does not contain requested price"
301					self.details.expiry > UInt64(getCurrentBlock().timestamp): "Listing is expired"
302					self.owner != nil : "Resource doesn't have the assigned owner"
303					self.details.buyer == nil || self.details.buyer! == privateListingAcceptor.owner!.address: "incorrect buyer for private listing"
304					commissionRecipient == nil || commissionRecipient!.address == NFTStorefrontV2.account.address: "invalid commission recipient"
305				}
306
307				post {
308					Permitted.isPermitted(result): "type of nft is not permitted"
309				}
310
311				let tokenInfo = FlowtyUtils.getTokenInfo(self.details.salePaymentVaultType) ?? panic("unsupported payment token")
312
313				// Make sure the listing cannot be purchased again.
314				self.details.setToPurchased()
315
316				if self.details.commissionAmount > 0.0 {
317					// If commission recipient is nil, Throw panic.
318					let commissionReceiver = commissionRecipient ?? panic("Commission recipient can't be nil")
319					if self.marketplacesCapability != nil {
320						var isCommissionRecipientHasValidType = false
321						var isCommissionRecipientAuthorised = commissionReceiver.address == NFTStorefrontV2.account.address
322						for cap in self.marketplacesCapability! {
323							// Check 1: Should have the same type
324							if cap.getType() == commissionReceiver.getType() {
325								isCommissionRecipientHasValidType = true
326								// Check 2: Should have the valid market address that holds approved capability.
327								if cap.address == commissionReceiver.address && cap.check() {
328										isCommissionRecipientAuthorised = true
329										break
330								}
331							}
332						}
333						assert(isCommissionRecipientHasValidType, message: "Given recipient does not has valid type")
334						assert(isCommissionRecipientAuthorised,	 message: "Given recipient has not authorised to receive the commission")
335					}
336					let commissionPayment <- payment.withdraw(amount: self.details.commissionAmount)
337					let recipient = commissionReceiver.borrow() ?? panic("Unable to borrow the recipent capability")
338					recipient.deposit(from: <- commissionPayment)
339				}
340				// Fetch the token to return to the purchaser.
341				let provider = self.nftProviderCapability.borrow()
342					?? panic("nft provider capability is invalid")
343				let nft <- provider.withdraw(withdrawID: self.details.nftID)
344				// Neither receivers nor providers are trustworthy, they must implement the correct
345				// interface but beyond complying with its pre/post conditions they are not gauranteed
346				// to implement the functionality behind the interface in any given way.
347				// Therefore we cannot trust the Collection resource behind the interface,
348				// and we must check the NFT resource it gives us to make sure that it is the correct one.
349				assert(nft.isInstance(self.details.nftType), message: "withdrawn NFT is not of specified type")
350				assert(nft.id == self.details.nftID, message: "withdrawn NFT does not have specified ID")
351
352				// Fetch the duplicate listing for the given NFT
353				// Access the StoreFrontManager resource reference to remove the duplicate listings if purchase would happen successfully.
354				let storeFrontPublicRef = self.owner!.capabilities.get<&{NFTStorefrontV2.StorefrontPublic}>(NFTStorefrontV2.StorefrontPublicPath)!
355					.borrow() ?? panic("Unable to borrow the storeFrontManager resource")
356				let duplicateListings = storeFrontPublicRef.getDuplicateListingIDs(nftType: self.details.nftType, nftID: self.details.nftID, listingID: self.uuid)
357
358				// Let's force removal of the listing in this storefront for the NFT that is being purchased.
359				for listingID in duplicateListings {
360					storeFrontPublicRef.cleanup(listingResourceID: listingID)
361				}
362
363				let depositor = NFTStorefrontV2.account.storage.borrow<auth(LostAndFound.Deposit) &LostAndFound.Depositor>(from: LostAndFound.DepositorStoragePath)!
364				let isDapperToken = self.details.salePaymentVaultType == Type<@DapperUtilityCoin.Vault>() // || self.details.salePaymentVaultType == Type<@FlowUtilityToken.Vault>()
365				for cut in self.details.saleCuts {
366					if isDapperToken && !cut.receiver.check() {
367						emit MissingReceiver(receiver: cut.receiver.address, amount: cut.amount)
368						continue
369					}
370					let paymentCut <- payment.withdraw(amount: cut.amount)
371					FlowtyUtils.trySendFungibleTokenVault(vault: <-paymentCut, receiver: cut.receiver, depositor: depositor)
372				}
373
374				if payment.balance > 0.0 {
375					// send whatever is left to the seller who is the last receiver
376					FlowtyUtils.trySendFungibleTokenVault(vault: <-payment, receiver: self.details.saleCuts[self.details.saleCuts.length-1].receiver, depositor: depositor)
377				} else {
378					destroy payment
379				}
380
381				// If the listing is purchased, we regard it as completed here.
382				// Otherwise we regard it as completed in the destructor.
383				emit ListingCompleted(
384					listingResourceID: self.uuid,
385					storefrontResourceID: self.details.storefrontID,
386					storefrontAddress: self.owner?.address,
387					purchased: self.details.purchased,
388					nftType: self.details.nftType.identifier,
389					nftUUID: self.details.nftUUID,
390					nftID: self.details.nftID,
391					salePaymentVaultType: self.details.salePaymentVaultType.identifier,
392					salePrice: self.details.salePrice,
393					customID: self.details.customID,
394					commissionAmount: self.details.commissionAmount,
395					commissionReceiver: commissionRecipient?.address,
396					expiry: self.details.expiry,
397					buyer: privateListingAcceptor.owner?.address
398				)
399
400				if let callback = NFTStorefrontV2.borrowCallbackContainer() {
401					callback.handle(stage: FlowtyListingCallback.Stage.Filled, listing: &self as &{FlowtyListingCallback.Listing}, nft: &nft as &{NonFungibleToken.NFT} )
402				}
403
404				return <-nft
405			}
406
407			access(all) fun isValid(): Bool {
408				if UInt64(getCurrentBlock().timestamp) > self.details.expiry {
409					return false
410				}
411
412				if !self.nftProviderCapability.check() {
413					return false
414				}
415
416				let collection = self.nftProviderCapability.borrow()!
417
418				let nft = collection.borrowNFT(self.details.nftID)
419				if nft == nil {
420					return false
421				}
422
423				if nft!.getType() != self.details.nftType || nft!.uuid != self.details.nftUUID || nft!.id != self.details.nftID {
424					return false
425				}
426
427				if let callback = NFTStorefrontV2.borrowCallbackContainer() {
428					let res = callback.validateListing(listing: &self as &{FlowtyListingCallback.Listing}, nft: nft!)
429					if !res {
430						return false
431					}
432				}
433
434				return true
435			}
436
437			access(contract) fun burnCallback() {
438				let listingDetails = self.details
439				if !listingDetails.purchased {
440					if let callback = NFTStorefrontV2.borrowCallbackContainer() {
441						callback.handle(stage: FlowtyListingCallback.Stage.Destroyed, listing: &self as &{FlowtyListingCallback.Listing}, nft: nil)
442					}
443
444					emit ListingCompleted(
445						listingResourceID: self.uuid,
446						storefrontResourceID: listingDetails.storefrontID,
447						storefrontAddress: self.owner?.address,
448						purchased: listingDetails.purchased,
449						nftType: listingDetails.nftType.identifier,
450						nftUUID: listingDetails.nftUUID,
451						nftID: listingDetails.nftID,
452						salePaymentVaultType: listingDetails.salePaymentVaultType.identifier,
453						salePrice: listingDetails.salePrice,
454						customID: listingDetails.customID,
455						commissionAmount: listingDetails.commissionAmount,
456						commissionReceiver: nil,
457						expiry: listingDetails.expiry,
458						buyer: nil
459					)
460				}
461			}
462
463			/// initializer
464			///
465			init (
466				nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>,
467				nftType: Type,
468				nftUUID: UInt64,
469				nftID: UInt64,
470				salePaymentVaultType: Type,
471				saleCuts: [SaleCut],
472				marketplacesCapability: [Capability<&{FungibleToken.Receiver}>]?,
473				storefrontID: UInt64,
474				customID: String?,
475				commissionAmount: UFix64,
476				expiry: UInt64,
477				buyer: Address?
478			) {
479				// Store the sale information
480				self.details = ListingDetails(
481					nftType: nftType,
482					nftUUID: nftUUID,
483					nftID: nftID,
484					salePaymentVaultType: salePaymentVaultType,
485					saleCuts: saleCuts,
486					storefrontID: storefrontID,
487					customID: customID,
488					commissionAmount: commissionAmount,
489					expiry: expiry,
490					buyer: buyer
491				)
492
493				// Store the NFT provider
494				self.nftProviderCapability = nftProviderCapability
495				self.marketplacesCapability = marketplacesCapability
496
497				// Check that the provider contains the NFT.
498				// We will check it again when the token is sold.
499				// We cannot move this into a function because initializers cannot call member functions.
500				let provider = self.nftProviderCapability.borrow()
501				assert(provider != nil, message: "cannot borrow nftProviderCapability")
502
503				// This will precondition assert if the token is not available.
504				let nft = provider!.borrowNFT(self.details.nftID)
505
506				assert(nft != nil, message: "nft is nil")
507				assert(nft!.isInstance(self.details.nftType), message: "token is not of specified type")
508				assert(nft!.id == self.details.nftID, message: "token does not have specified ID")
509			}
510		}
511
512		/// StorefrontManager
513		/// An interface for adding and removing Listings within a Storefront,
514		/// intended for use by the Storefront's owner
515		///
516		access(all) resource interface StorefrontManager {
517			/// createListing
518			/// Allows the Storefront owner to create and insert Listings.
519			///
520			access(List) fun createListing(
521				nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>,
522				paymentReceiver: Capability<&{FungibleToken.Receiver}>,
523				nftType: Type,
524				nftID: UInt64,
525				salePaymentVaultType: Type,
526				price: UFix64,
527				customID: String?,
528				expiry: UInt64,
529				buyer: Address?
530			): UInt64
531
532			/// removeListing
533			/// Allows the Storefront owner to remove any sale listing, acepted or not.
534			///
535			access(Cancel) fun removeListing(listingResourceID: UInt64)
536		}
537
538		/// StorefrontPublic
539		/// An interface to allow listing and borrowing Listings, and purchasing items via Listings
540		/// in a Storefront.
541		///
542		access(all) resource interface StorefrontPublic {
543			access(all) view fun getListingIDs(): [UInt64]
544			access(all) fun getDuplicateListingIDs(nftType: Type, nftID: UInt64, listingID: UInt64): [UInt64]
545			access(all) view fun borrowListing(listingResourceID: UInt64): &{ListingPublic}? {
546				post {
547					result == nil || result!.getType() == Type<@Listing>()
548				}
549			}
550			access(all) fun cleanupExpiredListings(fromIndex: UInt64, toIndex: UInt64)
551			access(contract) fun cleanup(listingResourceID: UInt64)
552			access(all) view fun getExistingListingIDs(nftType: Type, nftID: UInt64): [UInt64]
553			access(all) fun cleanupPurchasedListings(listingResourceID: UInt64)
554			access(all) fun cleanupInvalidListing(listingResourceID: UInt64)
555			access(contract) fun adminRemoveListing(listingResourceID: UInt64)
556		}
557
558		/// PrivateListingAcceptor
559		/// Interface for accepting a private listing
560		///
561		/// Importantly, we will need to ensure that our Storefront is checking
562		/// the entire type (&Storefront{PrivateListingAcceptor}) otherwise malicious actors might
563		/// be able to impersonate a buyer.
564		access(all) resource interface PrivateListingAcceptor {
565			// Simple function just to ensure that we don't have an empty interface.
566			// we'll use this method when purchasing a private listing to verify that a reference
567			// is owned by the right address.
568			access(all) view fun getOwner(): Address?
569		}
570
571		/// Storefront
572		/// A resource that allows its owner to manage a list of Listings, and anyone to interact with them
573		/// in order to query their details and purchase the NFTs that they represent.
574		///
575		access(all) resource Storefront : StorefrontManager, StorefrontPublic, PrivateListingAcceptor, Burner.Burnable {
576			access(all) event ResourceDestroyed(flowtyStorefrontID: UInt64 = self.uuid)
577
578			/// The dictionary of Listing uuids to Listing resources.
579			access(contract) var listings: @{UInt64: Listing}
580			/// Dictionary to keep track of listing ids for same NFTs listing.
581			/// nftType.identifier -> nftID -> [listing resource ID]
582			access(contract) var listedNFTs: {String: {UInt64 : [UInt64]}}
583
584			access(all) view fun getOwner(): Address? {
585				return self.owner!.address
586			}
587
588			/// insert
589			/// Create and publish a Listing for an NFT.
590			///
591			access(List) fun createListing(
592				nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>,
593				paymentReceiver: Capability<&{FungibleToken.Receiver}>,
594				nftType: Type,
595				nftID: UInt64,
596				salePaymentVaultType: Type,
597				price: UFix64,
598				customID: String?,
599				expiry: UInt64,
600				buyer: Address?
601				): UInt64 {
602				pre {
603					paymentReceiver.check(): "payment receiver is invalid"
604				}
605
606				// Dapper has temporarily waived their dapper balance fee so this minumum is not needed for now.
607				// if salePaymentVaultType == Type<@DapperUtilityCoin.Vault>() {
608				// 	assert(price >= 0.75, message: "price must be at least 0.75")
609				// }
610				assert(price <= 10_000_000.0, message: "price must be less than 10 million")
611
612				let commission = NFTStorefrontV2.getFee(p: price, t: salePaymentVaultType)
613
614				let marketplacesCapability = [NFTStorefrontV2.getCommissionReceiver(t: salePaymentVaultType)]
615				// let's ensure that the seller does indeed hold the NFT being listed
616				let collectionRef = nftProviderCapability.borrow()
617						?? panic("Could not borrow reference to collection")
618				let nftRef = collectionRef.borrowNFT(nftID)
619				assert(nftRef != nil, message: "nft cannot be nil")
620				assert(Permitted.isPermitted(nftRef!), message: "type of nft is not permitted")
621
622				let cuts = NFTStorefrontV2.getPaymentCuts(r: paymentReceiver, n: nftRef!, p: price, tokenType: salePaymentVaultType)
623
624				// Instead of letting an arbitrary value be set for the UUID of a given NFT, the contract
625				// should fetch it itelf
626				let uuid = nftRef!.uuid
627				let listing <- create Listing(
628					nftProviderCapability: nftProviderCapability,
629					nftType: nftType,
630					nftUUID: uuid,
631					nftID: nftID,
632					salePaymentVaultType: salePaymentVaultType,
633					saleCuts: cuts,
634					marketplacesCapability: marketplacesCapability,
635					storefrontID: self.uuid,
636					customID: customID,
637					commissionAmount: commission,
638					expiry: expiry,
639					buyer: buyer
640				)
641
642				if let callback = NFTStorefrontV2.borrowCallbackContainer() {
643					callback.handle(stage: FlowtyListingCallback.Stage.Created, listing: &listing as &{FlowtyListingCallback.Listing}, nft: nftRef)
644				}
645
646				let listingResourceID = listing.uuid
647				let listingPrice = listing.getDetails().salePrice
648				// Add the new listing to the dictionary.
649				let oldListing <- self.listings[listingResourceID] <- listing
650				destroy oldListing
651
652				// Add the `listingResourceID` in the tracked listings.
653				self.addDuplicateListing(nftIdentifier: nftType.identifier, nftID: nftID, listingResourceID: listingResourceID)
654
655				// Scraping addresses from the capabilities to emit in the event.
656				var allowedCommissionReceivers : [Address] = []
657				for c in marketplacesCapability {
658					allowedCommissionReceivers.append(c.address)
659				}
660
661				emit ListingAvailable(
662					storefrontAddress: self.owner?.address!,
663					listingResourceID: listingResourceID,
664					nftType: nftType.identifier,
665					nftUUID: uuid,
666					nftID: nftID,
667					salePaymentVaultType: salePaymentVaultType.identifier,
668					salePrice: listingPrice,
669					customID: customID,
670					commissionAmount: commission,
671					commissionReceivers: allowedCommissionReceivers,
672					expiry: expiry,
673					buyer: buyer,
674					providerAddress: nftProviderCapability.address
675				)
676
677				return listingResourceID
678			}
679
680			/// addDuplicateListing
681			/// Helper function that allows to add duplicate listing of given nft in a map.
682			///
683			access(contract) fun addDuplicateListing(nftIdentifier: String, nftID: UInt64, listingResourceID: UInt64) {
684				if !self.listedNFTs.containsKey(nftIdentifier) {
685					self.listedNFTs.insert(key: nftIdentifier, {nftID: [listingResourceID]})
686				} else {
687					if !self.listedNFTs[nftIdentifier]!.containsKey(nftID) {
688							self.listedNFTs[nftIdentifier]!.insert(key: nftID, [listingResourceID])
689					} else {
690							self.listedNFTs[nftIdentifier]![nftID]!.append(listingResourceID)
691					}
692				}
693			}
694
695			/// removeDuplicateListing
696			/// Helper function that allows to remove duplicate listing of given nft from a map.
697			///
698			access(contract) fun removeDuplicateListing(nftIdentifier: String, nftID: UInt64, listingResourceID: UInt64) {
699				// Remove the listing from the listedNFTs dictionary.
700				let listingIndex = self.listedNFTs[nftIdentifier]![nftID]!.firstIndex(of: listingResourceID) ?? panic("Should contain the index")
701				self.listedNFTs[nftIdentifier]![nftID]!.remove(at: listingIndex)
702			}
703
704			access(Cancel) fun removeListing(listingResourceID: UInt64) {
705				let listing <- self.listings.remove(key: listingResourceID)
706					?? panic("missing Listing")
707				let listingDetails = listing.getDetails()
708
709				self.removeDuplicateListing(nftIdentifier: listingDetails.nftType.identifier, nftID: listingDetails.nftID, listingResourceID: listingResourceID)
710				// This will emit a ListingCompleted event.
711
712				Burner.burn(<- listing)
713			}
714
715			access(all) view fun getListingIDs(): [UInt64] {
716				return self.listings.keys
717			}
718
719			access(all) view fun getExistingListingIDs(nftType: Type, nftID: UInt64): [UInt64] {
720				if self.listedNFTs[nftType.identifier] == nil || self.listedNFTs[nftType.identifier]![nftID] == nil {
721					return []
722				}
723				var listingIDs = self.listedNFTs[nftType.identifier]![nftID]!
724				return listingIDs
725			}
726
727			access(all) fun cleanupPurchasedListings(listingResourceID: UInt64) {
728				pre {
729					self.listings[listingResourceID] != nil: "could not find listing with given id"
730					self.borrowListing(listingResourceID: listingResourceID)!.getDetails().purchased == true: "listing not purchased yet"
731				}
732				let listing <- self.listings.remove(key: listingResourceID)!
733				let listingDetails = listing.getDetails()
734				self.removeDuplicateListing(nftIdentifier: listingDetails.nftType.identifier, nftID: listingDetails.nftID, listingResourceID: listingResourceID)
735
736				Burner.burn(<-listing)
737			}
738
739			access(all) fun getDuplicateListingIDs(nftType: Type, nftID: UInt64, listingID: UInt64): [UInt64] {
740				var listingIDs = self.getExistingListingIDs(nftType: nftType, nftID: nftID)
741
742				// Verify that given listing Id also a part of the `listingIds`
743				let doesListingExist = listingIDs.contains(listingID)
744				// Find out the index of the existing listing.
745				if doesListingExist {
746					var index: Int = 0
747					for id in listingIDs {
748						if id == listingID {
749								break
750						}
751						index = index + 1
752					}
753					listingIDs.remove(at:index)
754					return listingIDs
755				}
756				return []
757			}
758
759			access(all) fun cleanupExpiredListings(fromIndex: UInt64, toIndex: UInt64) {
760				pre {
761					fromIndex <= toIndex : "Incorrect start index"
762					Int(toIndex - fromIndex) < self.getListingIDs().length : "Provided range is out of bound"
763				}
764				var index = fromIndex
765				let listingsIDs = self.getListingIDs()
766				while index <= toIndex {
767					// There is a possibility that some index may not have the listing.
768					// becuase of that instead of failing the transaction, Execution moved to next index or listing.
769
770					if let listing = self.borrowListing(listingResourceID: listingsIDs[index]) {
771							if listing.getDetails().expiry <= UInt64(getCurrentBlock().timestamp) {
772									self.cleanup(listingResourceID: listingsIDs[index])
773							}
774					}
775					index = index + 1
776				}
777			}
778
779			access(contract) fun adminRemoveListing(listingResourceID: UInt64) {
780				pre {
781					self.listings[listingResourceID] != nil: "could not find listing with given id"
782				}
783				let listing <- self.listings.remove(key: listingResourceID)
784					?? panic("missing Listing")
785				let listingDetails = listing.getDetails()
786				self.removeDuplicateListing(nftIdentifier: listingDetails.nftType.identifier, nftID: listingDetails.nftID, listingResourceID: listingResourceID)
787				// This will emit a ListingCompleted event.
788				Burner.burn(<-listing)
789			}
790
791			/// borrowSaleItem
792			/// Returns a read-only view of the SaleItem for the given listingID if it is contained by this collection.
793			///
794			access(all) view fun borrowListing(listingResourceID: UInt64): &{ListingPublic}? {
795				return &self.listings[listingResourceID]
796			}
797
798			/// cleanup
799			/// Remove an listing, When given listing is duplicate or expired
800			/// Only contract is allowed to execute it.
801			///
802			access(contract) fun cleanup(listingResourceID: UInt64) {
803				pre {
804					self.listings[listingResourceID] != nil: "could not find listing with given id"
805				}
806				let listing <- self.listings.remove(key: listingResourceID)!
807				let listingDetails = listing.getDetails()
808				self.removeDuplicateListing(nftIdentifier: listingDetails.nftType.identifier, nftID: listingDetails.nftID, listingResourceID: listingResourceID)
809
810				Burner.burn(<-listing)
811			}
812
813			/*
814			Removes a listing that is not valid anymore. This could be because the listed nft is no longer in
815			an account's storage, or it could be because the listing has expired or is otherwise completed.
816			*/
817			access(all) fun cleanupInvalidListing(listingResourceID: UInt64) {
818				pre {
819					self.listings[listingResourceID] != nil: "could not find listing with given id"
820				}
821				let listing <- self.listings.remove(key: listingResourceID)!
822				assert(!listing.isValid(), message: "listing is valid and cannot be removed")
823
824				let listingDetails = listing.getDetails()
825				self.removeDuplicateListing(nftIdentifier: listingDetails.nftType.identifier, nftID: listingDetails.nftID, listingResourceID: listingResourceID)
826				Burner.burn(<-listing)
827			}
828
829			access(contract) fun burnCallback() {
830				let ids = self.listings.keys
831				for id in ids {
832					let listing <- self.listings.remove(key: id)!
833					Burner.burn(<- listing)
834				}
835
836				emit StorefrontDestroyed(storefrontResourceID: self.uuid)
837			}
838
839			init () {
840				self.listings <- {}
841				self.listedNFTs = {}
842
843				// Let event consumers know that this storefront exists
844				emit StorefrontInitialized(storefrontResourceID: self.uuid)
845			}
846		}
847
848		access(all) resource Admin {
849			access(Administrator) fun removeListing(addr: Address, listingResourceID: UInt64) {
850				let s = getAccount(addr).capabilities.get<&{StorefrontPublic}>(NFTStorefrontV2.StorefrontPublicPath)!.borrow() ?? panic("storefront not found")
851				s.adminRemoveListing(listingResourceID: listingResourceID)
852			}
853		}
854
855		access(all) fun getCommissionReceiver(t: Type): Capability<&{FungibleToken.Receiver}> {
856			let tokenInfo = FlowtyUtils.getTokenInfo(t) ?? panic("invalid token type")
857			return self.account.capabilities.get<&{FungibleToken.Receiver}>(tokenInfo.receiverPath)!
858		}
859
860		access(all) fun createStorefront(): @Storefront {
861			return <-create Storefront()
862		}
863
864		access(all) fun getFee(p: UFix64, t: Type): UFix64 {
865			var fee = p * 0.02 // flowty has a fee of 2%
866			var dwFee = 0.0
867			// Dapper has temporarily waived their Dapper Balance fee
868			// if t == Type<@DapperUtilityCoin.Vault>() {
869			// 	dwFee = p * 0.01 // Dapper Wallet charges 1% to use DUC
870			// 	dwFee = dwFee > 0.44 ? dwFee : 0.44 // but the minimum it charges is 0.44 DUC
871			// }
872			return fee + dwFee // flowty fee of 2% (dapper fee temporarily removed)
873		}
874
875		access(all) fun getPaymentCuts(r: Capability<&{FungibleToken.Receiver}>, n: &{NonFungibleToken.NFT}, p: UFix64, tokenType: Type): [SaleCut] {
876			let t = n.getType()
877			let ti = FlowtyUtils.getTokenInfo(tokenType) ?? panic("unsupported token type")
878
879			let fee = NFTStorefrontV2.getFee(p: p, t: tokenType)
880			var royalties: MetadataViews.Royalties? = nil
881
882			// collection royalties may be overridden for various reasons such as misconfiguration.
883			//
884			// if they are not in the override, pull them and then we will calculate our cuts.
885			if !RoyaltiesOverride.get(t) {
886				royalties = n.resolveView(Type<MetadataViews.Royalties>()) as! MetadataViews.Royalties?
887			}
888
889			let cuts: [SaleCut] = []
890			var remainder = p - fee
891			if royalties != nil {
892				for c in royalties!.getRoyalties() {
893					// make sure that receivers are pointing where we expect them to
894					let rec = getAccount(c.receiver.address).capabilities.get<&{FungibleToken.Receiver}>(ti.receiverPath)
895					if rec == nil {
896						continue
897					}
898					cuts.append(SaleCut(receiver: rec!, amount: p * c.cut))
899					remainder = remainder - c.cut * p
900				}
901			}
902
903			cuts.append(SaleCut(receiver: r, amount: remainder))
904
905			return cuts
906		}
907
908		access(all) view fun getAddress(): Address {
909			return self.account.address
910		}
911
912		access(contract) fun borrowCallbackContainer(): auth(FlowtyListingCallback.Handle) &FlowtyListingCallback.Container? {
913			return self.account.storage.borrow<auth(FlowtyListingCallback.Handle) &FlowtyListingCallback.Container>(from: FlowtyListingCallback.ContainerStoragePath)
914		}
915
916		init () {
917			let pathIdentifier = "NFTStorefrontV2".concat(self.account.address.toString())
918			let adminIdentifier = "NFTStorefrontV2Admin".concat(self.account.address.toString())
919
920			self.StorefrontStoragePath = StoragePath(identifier: pathIdentifier)!
921			self.StorefrontPublicPath = PublicPath(identifier: pathIdentifier)!
922			self.AdminStoragePath = StoragePath(identifier: adminIdentifier)!
923
924			self.CommissionRecipients = {}
925
926			NFTStorefrontV2.account.storage.save(<- create Admin(), to: self.AdminStoragePath)
927
928			if self.account.storage.borrow<&AnyResource>(from: FlowtyListingCallback.ContainerStoragePath) == nil {
929				let dnaHandler <- DNAHandler.createHandler()
930				let listingHandler <- FlowtyListingCallback.createContainer(defaultHandler: <-dnaHandler)
931				self.account.storage.save(<-listingHandler, to: FlowtyListingCallback.ContainerStoragePath)
932			}
933		}
934}
935