Marketplace
In this tutorial, we're going to create a simplified marketplace that uses both the fungible and non-fungible token (NFT) contracts that we built in previous tutorials.
This tutorial uses the simplified fungible and non-fungible tokens you built in this series. It is not suitable for production. If you're ready to build or work with a production-quality marketplace, check out the NFT storefront repo. This contract is already deployed to testnet and mainnet and can be used by anyone for any generic NFT sale!
Marketplaces are a popular application of blockchain technology and smart contracts. People with digital collectibles such as NFTs need to be able to buy and sell them — either with the network token or their fungible tokens.
More than just a convenience, marketplaces demonstrate one of the most compelling arguments for developing digital property on blockchains. In web 2, each developer needed to build their own bespoke systems for buying, selling, trading, and storing digital property. Onchain, if you build digital property that adheres to the appropriate standards, your digital collectibles, items, etc., will automatically appear on several marketplace apps built by experts in marketplaces who have made them the focus of their attention and business.
Objectives
After completing this tutorial, you'll be able to:
- Construct an NFT marketplace that allows users to buy and sell NFTs in exchange for $FLOW or your fungible token.
- Utilize interfaces, resources, and capabilities to write composable code that takes advantage of resources built by others and allows others to build on your products.
- Construct and emit events to share contract actions and states with other apps and services
Prerequisites
To complete this tutorial, you must have completed the Marketplace Setup Tutorial. If you need to, you can start from the Setup Reference Solution, but you'll need to follow the Marketplace Setup Tutorial to deploy the contracts and call the setup transactions.
Building with composability
Now that there are contracts deployed for both fungible and non-fungible tokens, we can build a marketplace that uses both. We've picked the words there are in the prior sentence on purpose. It doesn't matter that you created these contracts. If they were deployed onchain, instead of in the ephemeral simulation in the playground, anyone could complete this tutorial to build a marketplace that works with your NFTs and tokens.
It's one of the most powerful and useful properties of building onchain and it's called composability — the ability for developers to leverage shared resources, such as code, digital property, and user bases, and use them as building blocks for a new application.
This isn't an entirely new concept — we're used to reusing code, open source projects, etc. But the degree and scale are much higher. For example, if you're building an onchain version of a web forum, you don't need to do anything to allow your users to have a profile picture beyond allowing them to select which PFP they own from the list of PFP collections you choose to incorporate into your app.
You're happy because you get a solution that works for your users for minimal effort, and the PFP collection creator is happy because their work becomes more valuable and desirable the more places it can be used an seen. Everybody wins!
Flow is designed to enable composability through interfaces, resources and capabilities:
- Interfaces allow projects to support any generic type as long as it supports a standard set of functionality specified by an interface.
- Resources can be passed around and owned by accounts, contracts or even other resources, unlocking different use cases depending on where the resource is stored.
- Capabilities allow exposing user-controlled sets of functionality and permissions through special objects that enforce strict security with Cadence's type system.
The combination of these features allow developers to do more with less, re-using known safe code and design patterns to create new, powerful, and unique interactions!
Building a marketplace
To create a marketplace, we need to integrate the functionality of both fungible and non-fungible tokens into a single contract that gives users control over their money and assets. To accomplish this, we'll create a composable smart contract.
Marketplace design
A traditional way to implement a marketplace is to have a central smart contract that users deposit their NFTs and their price into, and anyone can come by and buy the token for that price.
This approach is reasonable, but it centralizes the process and takes away options from the owners. A better option that's possible with Cadence is to allow users to maintain ownership of the NFTs that they are trying to sell while they are trying to sell them. Instead of taking a centralized approach, each user can list a sale from within their own account.
They'll do this by using a marketplace contract you'll build to store an instance of a @SaleCollection
resource in their account storage.
Then, the seller, independently or through an app, can either provide a link to their sale to an application that can list it centrally on a website, or even to a central sale aggregator smart contract if they want the entire transaction to stay onchain.
Validating setup
If you haven't just completed the Marketplace Setup tutorial, run the Validate Setup
script to double-check that your contracts and accounts are in the correct state to begin building the marketplace.
Remember, we only need to do this again to ensure that the ephemeral state of the playground is set up correctly. Otherwise, you'd already have contracts and users with accounts that are configured ready to go.
The following output appears if your accounts are set up correctly:
_10s.8250c68d2bb3c5398d7f9eac7114a4ac1b7df1d0984d92058b9373f696a1d6a9.OwnerInfo(acct8Balance: 40.00000000, acct9Balance: 40.00000000, acct8IDs: [1], acct9IDs: [])
Setting up an NFT marketplace
Add a new contract called BasicMarketplace
. It needs to import both of the existing contracts:
_10import ExampleToken from 0x06_10import IntermediateNFT from 0x07_10_10access(all) contract BasicMarketplace {_10 // TODO_10}
Remember, you don't need to own a contract to be able to import it or use any of its public functionality!
Adding appropriate events
As in Solidity, Cadence smart contracts can emit developer-defined events during execution, which can be used to log data that can be observed offchain. This can be used by frontends, and other apps or platforms, including block explorers and data aggregators, which can monitor the state of the contract and related NFTs.
Events in Cadence are declared in a similar fashion as functions, but they start with an access control declaration. The event
keyword follows, then the name and parameters in parentheses. You can use most of the same types as functions, but you cannot use resources. Resources are moved when used as an argument, and using them and events don't have a method to put them somewhere else or destroy them.
_10access(all) event ForSale(id: UInt64, price: UFix64, owner: Address?)_10access(all) event PriceChanged(id: UInt64, newPrice: UFix64, owner: Address?)_10access(all) event NFTPurchased(id: UInt64, price: UFix64, seller: Address?, buyer: Address?)_10access(all) event SaleCanceled(id: UInt64, seller: Address?)
We can anticipate that we'll want to emit events when users take standard actions with the contract, such as when NFTs are listed, purchased, the price is changed, or the sale is cancelled.
We're marking the addresses as optional, because there's some circumstances where an NFT might not have an owner, so those addresses would be nil
.
Creating a resource to put items up for sale
Next, we need to configure a resource that users can use to put their NFTs up for sale, and other users can use to then purchase those NFTs for fungible tokens. In it, you'll need to add:
- A capability to access the owner's collection.
- A place to store the prices of NFTs for sale.
- A capability to deposit tokens into the sellers vault when an NFT is purchased.
You'll also need functions to:
- Allow the owner to list an NFT for sale.
- Allow the owner to cancel a sale.
- Allow the owner to change the price.
- Allow a third party to buy the NFT, and deposit the purchase price in the seller's vault.
Definition and initialization
To define and initialize:
-
Create the resource definition:
_10access(all) resource SaleCollection {_10// TODO_10}ReminderIn this case,
access(all)
is giving public scope to the definition of the resource type, not any given instance of the resource or anything in one of those instances. It's good to make these public so that others can build contracts and apps that interact with yours. -
In it, add a variable to store a capability for the owner's collection with the ability to withdraw from the collection:
_10access(self) let ownerCollection: Capability<auth(ExampleNFT.Withdraw) &ExampleNFT.Collection>ReminderYou'll get errors until after you write the
init
function and assign values to these properties. -
Add a dictionary to relate NFT ids to the sale price for that NFT:
_10access(self) let prices: {UInt64: UFix64}reminderaccess(self)
limits access to the resource itself, from within the resource. -
Add a variable to store a capability for a sellers fungible token vault's receiver:
_10access(account) let ownerVault: Capability<&{ExampleToken.Receiver}>
Resource-owned capabilities
You first learned about basic function and use of capabilities in the capabilities tutorial. They're links to private objects in account storage that specify and expose a subset of the resource they are linked to.
With the marketplace contract, we are utilizing a new feature of capabilities — they can be stored anywhere! Lots of functionality is contained within resources, and developers will sometimes want to be able to access some of the functionality of resources from within different resources or contracts.
We stored two different capabilities in the marketplace sale collection:
_10access(self) var ownerCollection: Capability<auth(ExampleNFT.Withdraw) &ExampleNFT.Collection>_10access(account) let ownerVault: Capability<&{ExampleToken.Receiver}>
If an object like a contract or resource owns a capability, they can borrow a reference to that capability at any time to access that functionality without having to get it from the owner's account every time.
This is especially important if the owner wants to expose some functionality that is only intended for one person, meaning that the link for the capability is not stored in a public path.
We do that in this example, because the sale collection stores a capability that can access the withdraw
functionality of the IntermediateNFT.Collection
with the IntermediateNFT.Withdraw
entitlement. It needs this because it withdraws the specified NFT in the purchase()
method to send to the buyer.
It is important to remember that control of a capability does not equal ownership of the underlying resource. You can use the capability to access that resource's functionality, but you can't use it to fake ownership. You need the actual resource (identified by the prefixed @
symbol) to prove ownership.
Additionally, these capabilities can be stored anywhere, but if a user decides that they no longer want the capability to be used, they can revoke it by getting the controller for the capability from their account with the getControllers
method and delete the capability with delete
.
Here is an example that deletes all of the controllers for a specified storage path:
_10let controllers = self.account.capabilities.storage.getControllers(forPath: storagePath)_10for controller in controllers {_10 controller.delete()_10}
After this, any capabilities that use that storage path are rendered invalid.
Initializing the Resource
Initialize the resource with arguments for the capabilities needed from the account calling the create transaction.
In init
, we can take advantage of preconditions to make sure that the user has the appropriate capabilities needed to support this functionality by using .check()
for the relevant capabilities.
You could use the pattern we've used before with errors, but since these won't be useful outside of init
, we can also just include them inside it:
_28access(all) resource SaleCollection {_28 access(self) let ownerCollection: Capability<auth(IntermediateNFT.Withdraw) &IntermediateNFT.Collection>_28 access(self) let prices: {UInt64: UFix64}_28 access(account) let ownerVault: Capability<&{ExampleToken.Receiver}>_28_28 init (ownerCollection: Capability<auth(IntermediateNFT.Withdraw) &IntermediateNFT.Collection>,_28 ownerVault: Capability<&{ExampleToken.Receiver}>) {_28_28 pre {_28 // Check that the owner's collection capability is correct_28 ownerCollection.check():_28 "ExampleMarketplace.SaleCollection.init: "_28 .concat("Owner's NFT Collection Capability is invalid! ")_28 .concat("Make sure the owner has set up an `IntermediateNFT.Collection` ")_28 .concat("in their account and provided a valid capability")_28_28 // Check that the fungible token vault capability is correct_28 ownerVault.check():_28 "ExampleMarketplace.SaleCollection.init: "_28 .concat("Owner's Receiver Capability is invalid! ")_28 .concat("Make sure the owner has set up an `ExampleToken.Vault` ")_28 .concat("in their account and provided a valid capability")_28 }_28 self.ownerCollection = ownerCollection_28 self.ownerVault = ownerVault_28 self.prices = {}_28 }_28}
Owner functions
Next, we can add the functions that allow the owner to manage their sales. For this, you'll need to first create an entitlement to lock the functionality away so that only the owner can use it. Remember, entitlements are declared at the contract level:
_10// Existing events_10access(all) event ForSale(id: UInt64, price: UFix64, owner: Address?)_10access(all) event PriceChanged(id: UInt64, newPrice: UFix64, owner: Address?)_10access(all) event NFTPurchased(id: UInt64, price: UFix64, seller: Address?, buyer: Address?)_10access(all) event SaleCanceled(id: UInt64, seller: Address?)_10_10// New entitlement_10access(all) entitlement Owner
Strictly speaking, we're not actually going to use this entitlement. We're using it to "lock" the functionality, but we're not giving the entitlement to any other accounts. The owner doesn't need to use this "key" to unlock the functions limited with it — they automatically have access.
-
Add a function that the owner of the resource can use to list one of their tokens for sale, and
emit
anevent
that they've done so. -
Use a
pre
condition to return an error if they don't own the token they're trying to list. As before, this is probably the only place where this error will be useful, so it can be placed directly in the function:_14access(Owner) fun listForSale(tokenID: UInt64, price: UFix64) {_14pre {_14self.ownerCollection.borrow()!.idExists(id: tokenID):_14"ExampleMarketplace.SaleCollection.listForSale: "_14.concat("Cannot list token ID ").concat(tokenID.toString())_14.concat(" . This NFT ID is not owned by the seller.")_14.concat("Make sure an ID exists in the sellers NFT Collection")_14.concat(" before trying to list it for sale")_14}_14// store the price in the price array_14self.prices[tokenID] = price_14_14emit ForSale(id: tokenID, price: price, owner: self.owner?.address)_14} -
Add a function to allow changing the price. It should also
emit
the appropriateevent
:_10access(Owner) fun changePrice(tokenID: UInt64, newPrice: UFix64) {_10self.prices[tokenID] = newPrice_10_10emit PriceChanged(id: tokenID, newPrice: newPrice, owner: self.owner?.address)_10} -
Add a function that allows the owner to cancel their sale. You don't need to do anything with the token itself, as it hasn't left the owners account:
_10access(Owner) fun cancelSale(tokenID: UInt64) {_10// remove the price_10self.prices.remove(key: tokenID)_10self.prices[tokenID] = nil_10_10// Nothing needs to be done with the actual token because it is already in the owner's collection_10}
Solidity devs, take note here! In Cadence, you can build an NFT marketplace without needing to transfer NFTs to a third party or needing to give a third party permission to take the NFT.
Purchasing an NFT
Now, you need to add a function that anyone can call and use to purchase the NFT. It needs to accept arguments for:
- The token to be purchased.
- The recipient's collection that is going to receive the NFT.
- A vault containing the purchase price.
_10access(all) fun purchase(_10 tokenID: UInt64,_10 recipient: Capability<&IntermediateNFT.Collection>, buyTokens: @ExampleToken.Vault_10 ) {_10 // TODO_10}
You are not providing the purchaser's vault here — that's an anti-pattern. Instead, create a temporary vault and use that to transfer the tokens.
You'll also want to use pre
conditions to check and provide errors as appropriate for:
- The NFT with the provided ID is for sale.
- The buyer has included the correct amount of tokens in the provided vault.
- The buyer has the collection capability needed to receive the NFT.
_23pre {_23 self.prices[tokenID] != nil:_23 "ExampleMarketplace.SaleCollection.purchase: "_23 .concat("Cannot purchase NFT with ID ")_23 .concat(tokenID.toString())_23 .concat(" There is not an NFT with this ID available for sale! ")_23 .concat("Make sure the ID to purchase is correct.")_23 buyTokens.balance >= (self.prices[tokenID] ?? 0.0):_23 "ExampleMarketplace.SaleCollection.purchase: "_23 .concat(" Cannot purchase NFT with ID ")_23 .concat(tokenID.toString())_23 .concat(" The amount provided to purchase (")_23 .concat(buyTokens.balance.toString())_23 .concat(") is less than the price of the NFT (")_23 .concat(self.prices[tokenID]!.toString())_23 .concat("). Make sure the ID to purchase is correct and ")_23 .concat("the correct amount of tokens have been used to purchase.")_23 recipient.borrow != nil:_23 "ExampleMarketplace.SaleCollection.purchase: "_23 .concat(" Cannot purchase NFT with ID ")_23 .concat(tokenID.toString())_23 .concat(". The buyer's NFT Collection Capability is invalid.")_23}
Assuming these checks all pass, your function then needs to:
- Get a reference of the price of the token then clear it.
- Get a reference to the owner's vault and deposit the tokens from the transaction vault.
- Get a reference to the NFT receiver for the buyer.
- Deposit the NFT into the buyer's collection.
- Emit the appropriate event.
_19// get the value out of the optional_19let price = self.prices[tokenID]!_19_19self.prices[tokenID] = nil_19_19let vaultRef = self.ownerVault.borrow()_19 ?? panic("Could not borrow reference to owner token vault")_19_19// deposit the purchasing tokens into the owners vault_19vaultRef.deposit(from: <-buyTokens)_19_19// borrow a reference to the object that the receiver capability links to_19// We can force-cast the result here because it has already been checked in the pre-conditions_19let receiverReference = recipient.borrow()!_19_19// deposit the NFT into the buyers collection_19receiverReference.deposit(token: <-self.ownerCollection.borrow()!.withdraw(withdrawID: tokenID))_19_19emit NFTPurchased(id: tokenID, price: price, seller: self.owner?.address, buyer: receiverReference.owner?.address)
The full function should be similar to:
_42// purchase lets a user send tokens to purchase an NFT that is for sale_42access(all) fun purchase(tokenID: UInt64,_42 recipient: Capability<&IntermediateNFT.Collection>, buyTokens: @ExampleToken.Vault) {_42 pre {_42 self.prices[tokenID] != nil:_42 "ExampleMarketplace.SaleCollection.purchase: "_42 .concat("Cannot purchase NFT with ID ")_42 .concat(tokenID.toString())_42 .concat(" There is not an NFT with this ID available for sale! ")_42 .concat("Make sure the ID to purchase is correct.")_42 buyTokens.balance >= (self.prices[tokenID] ?? 0.0):_42 "ExampleMarketplace.SaleCollection.purchase: "_42 .concat(" Cannot purchase NFT with ID ")_42 .concat(tokenID.toString())_42 .concat(" The amount provided to purchase (")_42 .concat(buyTokens.balance.toString())_42 .concat(") is less than the price of the NFT (")_42 .concat(self.prices[tokenID]!.toString())_42 .concat("). Make sure the ID to purchase is correct and ")_42 .concat("the correct amount of tokens have been used to purchase.")_42 recipient.borrow != nil:_42 "ExampleMarketplace.SaleCollection.purchase: "_42 .concat(" Cannot purchase NFT with ID ")_42 .concat(tokenID.toString())_42 .concat(". The buyer's NFT Collection Capability is invalid.")_42 }_42_42 let price = self.prices[tokenID]!_42 self.prices[tokenID] = nil_42_42 let vaultRef = self.ownerVault.borrow()_42 ?? panic("Could not borrow reference to owner token vault")_42 vaultRef.deposit(from: <-buyTokens)_42_42 // borrow a reference to the object that the receiver capability links to_42 // We can force-cast the result here because it has already been checked in the pre-conditions_42 let receiverReference = recipient.borrow()!_42_42 receiverReference.deposit(token: <-self.ownerCollection.borrow()!.withdraw(withdrawID: tokenID))_42_42 emit NFTPurchased(id: tokenID, price: price, seller: self.owner?.address, buyer: receiverReference.owner?.address)_42}
Views
Finally, add a couple of views so that others can read the prices for NFTs and which ones are for sale:
_10access(all) view fun idPrice(tokenID: UInt64): UFix64? {_10 return self.prices[tokenID]_10}_10_10access(all) view fun getIDs(): [UInt64] {_10 return self.prices.keys_10}
Creating a SaleCollection
Last, but not least, you need to add a contract-level function that allows users to create their own SaleCollection
resource. It needs to accept the same arguments as the init
for the resource, pass them into the create
call, and return the newly-created resource:
Make sure you don't accidentally put this function inside the SaleCollection
resource!
_10access(all) fun createSaleCollection(_10 ownerCollection: Capability<auth(IntermediateNFT.Withdraw) &IntermediateNFT.Collection>,_10 ownerVault: Capability<&{ExampleToken.Receiver}>_10): @SaleCollection_10{_10 return <- create SaleCollection(ownerCollection: ownerCollection, ownerVault: ownerVault)_10}
Marketplace contract summary
That's it! You've completed the contract needed to allow anyone who owns the NFTs and fungible tokens you've created to sell one, accepting payment in the other! This marketplace contract has resources that function similarly to the NFT Collection
you built in Non-Fungible Tokens, with a few differences and additions.
This marketplace contract has methods to add and remove NFTs, but instead of storing the NFT resource object in the sale collection, the user provides a capability to their main collection that allows the listed NFT to be withdrawn and transferred when it is purchased. When a user wants to put their NFT up for sale, they do so by providing the ID and the price to the listForSale()
function.
Then, another user can call the purchase()
function, sending an ExampleToken.Vault
that contains the currency they are using to make the purchase. The buyer also includes a capability to their NFT ExampleNFT.Collection
so that the purchased token can be immediately deposited into their collection when the purchase is made.
The owner of the sale saves a capability to their Fungible Token Receiver
within the sale. This allows the sale resource to be able to immediately deposit the currency that was used to buy the NFT into the owners Vault
when a purchase is made.
Finally, a marketplace contract includes appropriate event
s that are emitted when important actions happen. External apps can monitor these events to know the state of the smart contract.
Deployment
Deploy the marketplace contract with account 0x0a
.
Using the marketplace
Now that you've set up your user accounts, and deployed the contracts for the NFT, fungible token, and marketplace, it's time to write a few transaction
s to tie everything together.
One of the most useful features of Cadence is that transactions are code written in Cadence. You can use this to add functionality after deploying your contracts — you're not limited to only the functions you thought of when you wrote the contract.
Building a transaction to create a sale
Now it's time to write a transaction
to create
a SaleCollection
and list account 0x08
's token for sale.
Depending on your app design, you might want to break these steps up into separate transactions to set up the the SaleCollection
and add an NFT to it.
-
Import the three contracts and add a
prepare
statement with auth toSaveValue
,StorageCapabilities
, andPublishCapability
:_10import ExampleToken from 0x06_10import IntermediateNFT from 0x07_10import BasicMarketplace from 0x0a_10_10transaction {_10prepare(acct: auth(SaveValue, StorageCapabilities, PublishCapability) &Account) {_10// TODO_10}_10} -
Complete the following in
prepare
:- Borrow a reference to the user's vault.
- Create an entitled capability to the user's NFT collection.
- Use these to to create a
SaleCollection
and store it in a constant.
_10let receiver = acct.capabilities.get<&{ExampleToken.Receiver}>(ExampleToken.VaultPublicPath)_10let collectionCapability = acct.capabilities.storage.issue_10<auth(IntermediateNFT.Withdraw) &IntermediateNFT.Collection>_10(IntermediateNFT.CollectionStoragePath)_10let sale <- BasicMarketplace.createSaleCollection(ownerCollection: collectionCapability, ownerVault: receiver) -
Use your
sale
instance of the collection to create a sale. Afterwards,move (<-)
it into account storage:_10sale.listForSale(tokenID: 1, price: 10.0)_10acct.storage.save(<-sale, to: /storage/NFTSale)tipYou might be tempted to change the order here to handle creating the
SaleCollection
and storing it first, then using it to create a sale.This won't work because resources can only be moved — they can't be copied. Once you
move (<-)
sale
to storage,sale
is no longer usable. -
Create and publish a public capability so that others can use the public functions of this resource to find and purchase NFTs:
_10let publicCap = acct.capabilities.storage.issue<&BasicMarketplace.SaleCollection>(/storage/NFTSale)_10acct.capabilities.publish(publicCap, at: /public/NFTSale) -
Call the transaction with account
0x08
.
Checking for NFTs to purchase
Let's create a script to ensure that the sale was created correctly:
-
Add a new one called
GetSaleIDsAndPrices
. -
Import the contracts and stub out a script that accepts an
Address
as an argument and returns aUInt64
array:_10import ExampleToken from 0x06_10import IntermediateNFT from 0x07_10import BasicMarketplace from 0x0a_10_10access(all)_10fun main(address: Address): [UInt64] {_10// TODO_10} -
In the script:
- Use the
address
to get a public account object for that address. - Attempt to borrow a reference to the public capability for the
SaleCollection
in that account:- Panic and return an error if it's not found.
- Call
getIDs
if it is, and return the list of NFTs for sale.
_14import ExampleToken from 0x06_14import IntermediateNFT from 0x07_14import BasicMarketplace from 0x0a_14_14access(all)_14fun main(address: Address): [UInt64] {_14_14let account = getAccount(address)_14_14let saleRef = account.capabilities.borrow<&BasicMarketplace.SaleCollection>(/public/NFTSale)_14?? panic("Could not borrow a reference to the SaleCollection capability for the address provided")_14_14return saleRef.getIDs()_14} - Use the
-
Run the script. You should be part of the way there:
_10[1]The script returns an array containing the one NFT for sale, but what about the prices? We added a function to return the price of a given NFT, but not a list or array.
We could update the contract since we own it (another power of Cadence), but even if we didn't, we could always add functionality via a script.
-
Update your script to create a
struct
to return the data in, then fetch the list of IDs, loop through them to get the prices, and return an array with the prices:_33import ExampleToken from 0x06_33import IntermediateNFT from 0x07_33import BasicMarketplace from 0x0a_33_33access(all) struct Pair {_33access(all) let id: UInt64_33access(all) let value: UFix64_33_33init(id: UInt64, value: UFix64) {_33self.id = id_33self.value = value_33}_33}_33_33access(all)_33fun main(address: Address): [Pair] {_33_33let account = getAccount(address)_33_33let saleRef = account.capabilities.borrow<&BasicMarketplace.SaleCollection>(/public/NFTSale)_33?? panic("Could not borrow a reference to the SaleCollection capability for the address provided")_33_33let ids = saleRef.getIDs()_33_33let pricePairs: [Pair] = []_33_33for id in ids {_33let pair = Pair(id: id, value: saleRef.idPrice(tokenID: id) ?? 0.0)_33pricePairs.append(pair)_33}_33_33return pricePairs_33}
Purchasing an NFT
Finally, you can add a transaction that a buyer can use to purchase the seller's NFT with their fungible tokens.
-
Create a
transaction
calledPurchaseNFT
, import the contract, and stub it out:_17import ExampleToken from 0x06_17import IntermediateNFT from 0x07_17import BasicMarketplace from 0x0a_17_17transaction(sellerAddress: Address, tokenID: UInt64, price: UFix64) {_17_17let collectionCapability: Capability<&IntermediateNFT.Collection>_17let temporaryVault: @ExampleToken.Vault_17_17prepare(acct: auth(BorrowValue) &Account) {_17// TODO_17}_17_17execute {_17// TODO_17}_17} -
Complete the following in
prepare
:get
thecollectionCapability
for the caller's NFT collection.borrow
an authorized reference to the buyers token vault.- Withdraw the purchase price from the buyers vault and
move (<-)
it into the temporary vault.
_10self.collectionCapability = acct.capabilities.get<&IntermediateNFT.Collection>(IntermediateNFT.CollectionPublicPath)_10_10let vaultRef = acct_10.storage.borrow<auth(ExampleToken.Withdraw) &ExampleToken.Vault>(from: /storage/CadenceFungibleTokenTutorialVault)_10?? panic("Could not borrow a reference to "_10.concat("ExampleToken.Vault")_10.concat(". Make sure the user has set up an account ")_10.concat("with an ExampleToken Vault and valid capability."))_10_10self.temporaryVault <- vaultRef.withdraw(amount: price) -
Complete the following in
execute
:- Get a reference to the public account for the
sellerAddress
. borrow
a reference to the seller'sSaleCollection
.- Call
purchase
with thetokenID
, buyers collection capability, and the temporary vault.
_10let seller = getAccount(sellerAddress)_10_10let saleRef = seller.capabilities.get<&BasicMarketplace.SaleCollection>(/public/NFTSale)_10.borrow()_10?? panic("Could not borrow a reference to "_10.concat("the seller's ExampleMarketplace.SaleCollection")_10.concat(". Make sure the seller has set up an account ")_10.concat("with an ExampleMarketplace SaleCollection and valid capability."))_10_10saleRef.purchase(tokenID: tokenID, recipient: self.collectionCapability, buyTokens: <-self.temporaryVault) - Get a reference to the public account for the
-
Call the transaction with account
0x09
to purchase the token with id1
from0x08
for10.0
tokens.
Verifying the NFT was purchased correctly
You've already written the scripts you need to check for NFT ownership and token balances. Copy them over from your earlier projects, or use the ones below:
_15import ExampleToken from 0x06_15_15access(all)_15fun main(address: Address): String {_15 let account = getAccount(address)_15_15 let accountReceiverRef = account.capabilities.get<&{ExampleToken.Balance}>(ExampleToken.VaultPublicPath)_15 .borrow()_15 ?? panic(ExampleToken.vaultNotConfiguredError(address: address))_15_15 return("Balance for "_15 .concat(address.toString())_15 .concat(": ").concat(accountReceiverRef.balance.toString())_15 )_15}
_19import IntermediateNFT from 0x07_19_19access(all) fun main(address: Address): [UInt64] {_19 let nftOwner = getAccount(address)_19_19 let capability = nftOwner.capabilities.get<&IntermediateNFT.Collection>(IntermediateNFT.CollectionPublicPath)_19_19 let receiverRef = nftOwner.capabilities_19 .borrow<&IntermediateNFT.Collection>(IntermediateNFT.CollectionPublicPath)_19 ?? panic(IntermediateNFT.collectionNotConfiguredError(address: address))_19_19_19 log("Account "_19 .concat(address.toString())_19 .concat(" NFTs")_19 )_19_19 return receiverRef.getIDs()_19}
Creating a marketplace for any generic NFT
The previous examples show how a simple marketplace can be created for a specific class of NFTs. However, users will want to have a marketplace where they can buy and sell any NFT they want, regardless of its type.
To learn more about a completely decentralized example of a generic marketplace, check out the NFT storefront repo. This contract is already deployed to testnet and mainnet and can be used by anyone for any generic NFT sale!
Accepting payment in $FLOW
What about accepting payment in the network token, $FLOW? We can't quite update this simplified marketplace to accept it, but it's actually quite simple to do so because the network token follows the Flow Fungible Token standard.
In other words, if you configure your marketplace to accept any token that follows the full standard, it will also be able to use the Flow token!
Conclusion
In this tutorial, you constructed a simplified NFT marketplace on Flow using the composability of Cadence resources, interfaces, and capabilities. You learned how to:
- Build a marketplace contract that allows users to list, buy, and sell NFTs in exchange for fungible tokens.
- Leverage capabilities and entitlements to securely manage access and transfers.
- Emit and observe events to track marketplace activity.
- Write and execute transactions and scripts to interact with the marketplace and verify asset ownership and balances.
By completing this tutorial, you are now able to:
- Construct composable smart contracts that integrate multiple token standards.
- Implement secure and flexible resource management using Cadence's type system.
- Develop and test end-to-end flows for NFT sales and purchases on Flow.
If you're ready to take your skills further, explore the NFT storefront repo for a production-ready, generic NFT marketplace, or try extending your marketplace to support additional features and token types!
Reference solution
You are not saving time by skipping the reference implementation. You'll learn much faster by doing the tutorials as presented!
Reference solutions are functional, but may not be optimal.