Smart Contract
NFTStorefrontV2
A.3cdbb3d569211ff3.NFTStorefrontV2
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