The NFTStorefrontV2
contract makes it simple for Sellers to list NFTs in dApp specific marketplaces. DApp developers leverage the APIs provided by the contract to manage listings being offered for sale and to transact NFT trades.
Listings made through a specific dApp can be simultaneously listed on third-party marketplaces beyond that dApp. Well-known third-party marketplaces listen for compatible NFT listing events enabling the automation of listings into their marketplace UIs.
Marketplaces facilitate a NFT trade through direct interaction with seller storefront resources. Flow's account based model ensures that NFTs listed for sale remain in the Seller account until traded, regardless of how many listings are posted across any number of marketplaces, for the same NFT.
Contract basics
NFTStorefrontV2
is a general purpose sales support contract for NFTs. Each account that wants to list NFTs for sale creates a Storefront
resource to store in their account and lists individual sales within that Storefront as Listing
s. There is usually one Storefront
per account stored at /storage/NFTStorefrontV2
and the contract supports all tokens using the NonFungibleToken
standard.
Each listing defines a price, optional 0-n sale cuts to be deducted, with each saleCut
amount sent to the linked address. Listings can specify an optional list of marketplace receiver capabilities used to pay commission to that marketplace at time of sale. Royalties are paid as a saleCut
for NFTs supporting the Royalty Metadata View standard. SaleCut
generalizes support for alternative models of revenue sharing at time of sale.
The same NFT can be referenced in one or more listings across multiple marketplaces and the contract provides APIs to manage listings across those.
Interested parties can globally track Listing
events on-chain and filter by NFT type, ID and other characteristics to determine which are of interest, simplifying the process of publishing a listed NFT for sale within your dApp marketplace UI.
The NFTStorefrontV2
offers a standardized process and the APIs for creating and managing the listings for a seller's NFTs.
Users are required to create the Storefront
resource once only in their account after which the same resource can be re-used, see example.
Listed below are some different ways which you might list your NFTs for sale.
Sellers can create a basic listing using the sell_item transaction providing the marketplacesAddress
with an empty array. The seller can optionally configure commission to the facilitator of sale. All listings made using the NFTStorefrontV2
standard are broadcast on-chain through the ListingAvailable
event.
Sellers typically create a listing by specifying one or more marketplacesAddress
and the corresponding commissionReceivers
required for them. It is assumed that the seller has first confirmed the correct address values for specific marketplaces and their expected commissions, which differs between vendors. On receiving ListingAvailable
events, marketplaces select listings matching their address and minimum expected commission. This enables multiple marketplaces to each publish the same NFT for sale in their UI with the full confidence that they will earn their required commission from facilitating the sale.
Example - Bob wants to list on marketplace 0xA, 0xB & 0xC and is willing to offer 10% commission on the sale price of the listing to interested marketplaces. In this diagram we see that all the marketplaces accept his listing given the commission amount!
An alternate approach is to create separate listing for each marketplace using the sell_item_with_marketplace_cut transaction. This is targeted towards marketplaces which select listings purely based on saleCut
amounts.
The NFTStorefrontV2
contract has no default support for multiple token types in an individual listing. The simplest way to solve this is to create multiple listings for the same NFT, one for each different token.
Example - Alice wants to sell a kitty and is open to receiving FLOW and FUSD
Sellers can create a basic Listing
using the sell_item transaction which requires certain details including the receiving token type Capability. This capability will transact the specified tokens when the NFT is sold. More detailed specifics are available here.
To accept a different token type for the same NFT sellers must specify an alternate Receiver token type, eg: salePaymentVaultType
, in another listing. The only difference between the two listings is that salePaymentVaultType
specifies different token types while the NFT being sold remains the same for both. Another more advanced option for handling multiple token types is using the FungibleTokenSwitchboard
standard.
-
Ghost listings - Ghost listings are listings which don’t have an underlying NFT in the seller’s account. However, the listing is still available for buyers to attempt to purchase and which fails.
Ghost listings occur for two reasons:
- When a seller's NFT is sold in one marketplace but listings for that NFT in other marketplaces are not removed.
- When the seller transfers out the listed NFT from the account that made the listings.
If ghost listings are not removed, they will eventually result in a prospective purchaser’s transaction to fail which is annoying in isolated cases. However, ghost listings negatively impact everyone's user experience when they are widespread. To address this and ensure that listings are always accurate the
cleanupPurchasedListings
function has been provided.The recommended standard practice is for marketplaces to execute the
cleanupPurchasedListing
function after the sale has completed within the same transaction. This requires minimal gas, ensures the best experience for all participants in the marketplace ecosystem and also significantly minimizes the likelihood of transaction failure.Ghost listings which are not cleaned up may be specifically problematic for sellers in the unique case when a previously sold or gifted NFT returns to the seller’s account some time later. In this case, previously ghost listings for which purchase attempts would have failed, once again become enabled to facilitate purchases. Since some time may have passed since the listing was created, ghost listings remaining against NFTs returned to an account may implicitly make the listing available for purchase below market rates.
To mitigate this, the storefront contract provides global access to all seller's inventory of ghost listings using the
read_all_unique_ghost_listings
script. Sellers who have active listings for an NFT are strongly advised to purge ghost listings using thecleanup_ghost_listing
transaction when the listed NFT is transferred to another account, not sold through a marketplace. -
Expired listings
NFTStorefrontV2
introduces a safety measure to flag an NFT listing as expired after a certain period. This can be set during listing creation to prevent the purchase through the listing after expiry has been reached. Once expiry has been reached the listing can no longer facilitate the purchase of the NFT.We recommend that using the
cleanupExpiredListings
function to manage expired listings.Note: We recommend that marketplaces and dApps filter out expired listings as they cannot be purchased.
Purchasing NFTs through the NFTStorefrontV2
is simple. The buyer has to provide the payment vault and the commissionRecipient
, if applicable, during the purchase. The purchase
API offered by the Listing
facilitates the trade with the buyer in the seller's Storefront
.
During the listing purchase all saleCuts
are paid automatically. This also includes distributing royalties for that NFT, if applicable. If the vault provided by the buyer lacks sufficient funds then the transaction will fail.
-
Auto cleanup the
NFTStorefrontV2
standard automates the cleanup of duplicate listings at time of sale. However, if an NFT has a large number of duplicate listings, it may slow the purchase and, in the worst case, may trigger an out-of-gas error.Note: We recommend maintaining <= 50(TBD) duplicate listings of any given NFT.
-
Unsupported receiver capability A common pitfall during the purchase of an NFT is if
saleCut
receivers don’t have a supported receiver capability because that entitled sale cut would transfer to first valid sale cut receiver. To mitigate this we recommend using the generic receiver from theFungibleTokenSwitchboard
contract, adding capabilities to support whichever token types the beneficiary wishes to receive.
The NFTStorefrontV2
contract optionally supports paying royalties to the minter account for secondary resales of a NFT. When seller NFTs support the Royalty Metadata View, NFTStorefrontV2
stores the royalty amount as a saleCut
based on the specified royalty percentage of the sale price, calculated at the time of listing. The saleCut
amount is only paid to the minter at the time of sale.
// Check whether the NFT implements the MetadataResolver or not.
if nft.getViews().contains(Type<MetadataViews.Royalties>()) {
// Resolve the royalty view
let royaltiesRef = nft.resolveView(Type<MetadataViews.Royalties>())?? panic("Unable to retrieve the royalties")
// Fetch the royalties.
let royalties = (royaltiesRef as! MetadataViews.Royalties).getRoyalties()
// Append the royalties as the salecut
for royalty in royalties {
self.saleCuts.append(NFTStorefrontV2.SaleCut(receiver: royalty.receiver, amount: royalty.cut * effectiveSaleItemPrice))
totalRoyaltyCut = totalRoyaltyCut + royalty.cut * effectiveSaleItemPrice
}
}
Complete transaction available here.
saleCut
only supports a single token receiver type and therefore beneficiaries of a saleCut
can only receive the token type used for the purchase. To support different token types for saleCuts we recommend using the FungibleTokenSwitchboard contract.
Note: We recommend that marketplaces honor creator royalties across the Flow ecosystem
NFTStorefrontV2
enables optional commissions on trades for marketplaces which require it as a condition to list a NFT for sale. Commission & commission receivers are set by the seller during initial listing creation. At time of purchase the commission amount is paid once only to the commission receiver matching the marketplace receiver address which facilitated the sale. For NFT listings in marketplaces which don't require commission, commission receivers can be set as nil
. The default behavior when commissionRecipient
s are set to nil
with a commission amount >0 results in a discount for the buyer who is paid the commission.
resource interface ListingPublic {
access(all) fun borrowNFT(): &{NonFungibleToken.NFT}?
access(all) fun purchase(
payment: @{FungibleToken.Vault},
commissionRecipient: Capability<&{FungibleToken.Receiver}>?,
): @{NonFungibleToken.NFT}
access(all) view fun getDetails(): ListingDetails
access(all) fun getAllowedCommissionReceivers(): [Capability<&{FungibleToken.Receiver}>]?
access(all) fun hasListingBecomeGhosted(): Bool
}
An interface providing a useful public interface to a Listing.
fun borrowNFT()
fun borrowNFT(): &{NonFungibleToken.NFT}?
This will assert in the same way as the NFT standard borrowNFT() if the NFT is absent, for example if it has been sold via another listing.
fun purchase()
fun purchase(payment FungibleToken.Vault, commissionRecipient Capability<&{FungibleToken.Receiver}>?): @{NonFungibleToken.NFT}
Facilitates the purchase of the listing by providing the payment vault and the commission recipient capability if there is a non-zero commission for the given listing. Respective saleCuts are transferred to beneficiaries and funtion return underlying or listed NFT.
fun getDetails()
fun getDetails(): ListingDetails
Fetches the details of the listings
fun getAllowedCommissionReceivers()
fun getAllowedCommissionReceivers(): [Capability<&{FungibleToken.Receiver}>]?
Fetches the allowed marketplaces capabilities or commission receivers for the underlying listing.
If it returns nil
then commission paid to the receiver by default.
fun hasListingBecomeGhosted()
fun hasListingBecomeGhosted(): Bool
Tells whether a listed NFT that was put up for sale is still available in the provided listing.
If it returns true
then it means the listing is "ghosted" because there is no available nft to fulfill the listing.
resource Storefront {
access(Creatable) fun createListing(
nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Collection}>,
nftType: Type,
nftID: UInt64,
salePaymentVaultType: Type,
saleCuts: [SaleCut],
marketplacesCapability: [Capability<&{FungibleToken.Receiver}>]?,
customID: String?,
commissionAmount: UFix64,
expiry: UInt64
): UInt64
access(Removable) fun removeListing(listingResourceID: UInt64)
access(all) view fun getListingIDs(): [UInt64]
access(all) fun getDuplicateListingIDs(nftType: Type, nftID: UInt64, listingID: UInt64): [UInt64]
access(all) fun cleanupExpiredListings(fromIndex: UInt64, toIndex: UInt64)
access(all) view fun borrowListing(listingResourceID: UInt64): &{ListingPublic}?
}
A resource that allows it's owner to manage a list of Listings, and anyone to interact with them in order to query their details and purchase the NFTs that they represent.
Implemented Interfaces:
StorefrontManager
StorefrontPublic
fun init()
fun createListing()
fun createListing(nftProviderCapability Capability<&{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>, nftType Type, nftID UInt64, salePaymentVaultType Type, saleCuts [SaleCut], marketplacesCapability [Capability<&{FungibleToken.Receiver}>]?, customID String?, commissionAmount UFix64, expiry UInt64): UInt64
insert Create and publish a Listing for a NFT.
fun removeListing()
fun removeListing(listingResourceID UInt64)
removeListing Remove a Listing that has not yet been purchased from the collection and destroy it.
fun getListingIDs()
fun getListingIDs(): [UInt64]
getListingIDs Returns an array of the Listing resource IDs that are in the collection
fun getDuplicateListingIDs()
fun getDuplicateListingIDs(nftType Type, nftID UInt64, listingID UInt64): [UInt64]
getDuplicateListingIDs
Returns an array of listing IDs that are duplicates of the given nftType
and nftID
.
fun cleanupExpiredListings()
fun cleanupExpiredListings(fromIndex UInt64, toIndex UInt64)
cleanupExpiredListings Cleanup the expired listing by iterating over the provided range of indexes.
fun borrowListing()
fun borrowListing(listingResourceID UInt64): &Listing{ListingPublic}?
borrowListing Returns a read-only view of the listing for the given listingID if it is contained by this collection.
resource interface StorefrontPublic {
access(all) view fun getListingIDs(): [UInt64]
access(all) fun getDuplicateListingIDs(nftType: Type, nftID: UInt64, listingID: UInt64): [UInt64]
access(all) view fun borrowListing(listingResourceID: UInt64): &{ListingPublic}?
access(all) fun cleanupExpiredListings(fromIndex: UInt64, toIndex: UInt64)
access(contract) fun cleanup(listingResourceID: UInt64)
access(all) fun getExistingListingIDs(nftType: Type, nftID: UInt64): [UInt64]
access(all) fun cleanupPurchasedListings(listingResourceID: UInt64)
access(all) fun cleanupGhostListings(listingResourceID: UInt64)
}
StorefrontPublic An interface to allow listing and borrowing Listings, and purchasing items via Listings in a Storefront.
fun getListingIDs()
fun getListingIDs(): [UInt64]
getListingIDs Returns an array of the Listing resource IDs that are in the collection
fun getDuplicateListingIDs()
fun getDuplicateListingIDs(nftType Type, nftID UInt64, listingID UInt64): [UInt64]
getDuplicateListingIDs Returns an array of listing IDs that are duplicates of the given nftType and nftID.
fun borrowListing()
fun borrowListing(listingResourceID UInt64): &Listing{ListingPublic}?
borrowListing Returns a read-only view of the listing for the given listingID if it is contained by this collection.
fun cleanupExpiredListings()
fun cleanupExpiredListings(fromIndex UInt64, toIndex UInt64)
cleanupExpiredListings Cleanup the expired listing by iterating over the provided range of indexes.
fun cleanupPurchasedListings()
fun cleanupPurchasedListings(listingResourceID: UInt64)
cleanupPurchasedListings Allows anyone to remove already purchased listings.
fun getExistingListingIDs()
fun getExistingListingIDs(nftType Type, nftID UInt64): [UInt64]
getExistingListingIDs
Returns an array of listing IDs of the given nftType
and nftID
.
fun cleanupGhostListings()
pub fun cleanupGhostListings(listingResourceID: UInt64)
cleanupGhostListings Allow callers to clean up ghost listings for this seller. Listings which remain orphaned on marketplaces because the stored provider capability cannot acquire the NFT any more.
event StorefrontInitialized
event StorefrontInitialized(storefrontResourceID: UInt64)
A Storefront resource has been created. Consumers can now expect events from this Storefront. Note that we do not specify an address: we cannot and should not. Created resources do not have an owner address, and may be moved
after creation in ways we cannot check. ListingAvailable
events can be used to determine the address
of the owner of the Storefront at the time of the listing but only at that exact moment in that specific transaction. If the seller moves the Storefront while the listing is valid it will not be possible to transact trades for the assocaited listings.
event ResourceDestroyed
event ResourceDestroyed(storefrontResourceID: UInt64 = self.uuid)
A Storefront has been destroyed. Event consumers can now stop processing events from this Storefront. Note - we do not specify an address.
event ListingAvailable
event ListingAvailable(storefrontAddress: Address, listingResourceID: UInt64, nftType: Type, nftUUID: UInt64, nftID: UInt64, salePaymentVaultType: Type, salePrice: UFix64, customID: String?, commissionAmount: UFix64, commissionReceivers: [Address]?, expiry: UInt64)
Above event gets emitted when a listing has been created and added to a Storefront resource. The Address values here are valid when the event is emitted, but the state of the accounts they refer to may change outside of the
NFTStorefrontV2
workflow, so be careful to check when using them.
event ListingCompleted
event ListingCompleted(listingResourceID: UInt64, storefrontResourceID: UInt64, purchased: Bool, nftType: Type, nftUUID: UInt64, nftID: UInt64, salePaymentVaultType: Type, salePrice: UFix64, customID: String?, commissionAmount: UFix64, commissionReceiver: Address?, expiry: UInt64)
The listing has been resolved. It has either been purchased, removed or destroyed.
event UnpaidReceiver
event UnpaidReceiver(receiver: Address, entitledSaleCut: UFix64)
A entitled receiver has not been paid during the sale of the NFT.
Holistic process flow diagram of NFTStorefrontV2 -
SaleCut - A struct consists a recipient and amount of token, eg: cut that must be sent to recipient when a NFT get sold.