Marketplace
In this tutorial, we're going to create a marketplace that uses both the fungible and non-fungible token (NFTs) contracts that we have learned about in previous tutorials. This is only for educational purposes and is not meant to be used in production See a production-ready marketplace in the NFT storefront repo. This contract is already deployed to testnet and mainnet and can be used by anyone for any generic NFT sale!
Open the starter code for this tutorial in the Flow Playground: https://play.onflow.org/45ae690e-c527-409c-970e-57f03df92790
The tutorial will be asking you to take various actions to interact with this code.
The marketplace setup guide shows you how to get the playground set up to do this tutorial.
Instructions that require you to take action are always included in a callout box like this one. These highlighted actions are all that you need to do to get your code running, but reading the rest is necessary to understand the language's design.
Marketplaces are a popular application of blockchain technology and smart contracts. When there are NFTs in existence, users usually want to be able to buy and sell them with their fungible tokens.
Now that there is an example for both fungible and non-fungible tokens, we can build a marketplace that uses both. This is referred to as composability: the ability for developers to leverage shared resources, such as code or userbases, and use them as building blocks for new applications. Flow is designed to enable composability because of the way that interfaces, resources and capabilities are designed. 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-defined sets of functionality through special objects that enforce strict security with Cadence's type system. The combination of these allows developers to do more with less, re-using known safe code and design patterns to create new, powerful, and unique interactions!
At some point before or after this tutorial, you should definitely check out the formal documentation linked above about interfaces, resources, and capabilities. It will help complete your understanding of these complex, but powerful features.
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're going to take you through these steps to create a composable smart contract and get comfortable with the marketplace:
- Ensure that your fungible token and non-fungible token contracts are deployed and set up correctly.
- Deploy the marketplace type declarations to account
0x03
. - Create a marketplace object and store it in your account storage, putting an NFT up for sale and publishing a public capability for your sale.
- Use a different account to purchase the NFT from the sale.
- Run a script to verify that the NFT was purchased.
Before proceeding with this tutorial, you need to complete the Fungible Tokens and Non-Fungible Token tutorials to understand the building blocks of this smart contract.
Marketplace Design
One way to implement a marketplace is to have a central smart contract that users deposit their NFTs and their price into, and have anyone come by and be able to buy the token for that price. This approach is reasonable, but it centralizes the process and takes away options from the owners. We want users to be able to maintain ownership of the NFTs that they are trying to sell while they are trying to sell them.
Instead of taking this centralized approach, each user can list a sale from within their own account.
Then, users could either provide a link to their sale to an application that can list it centrally on a website, or to a central sale aggregator smart contract if they want the entire transaction to stay on-chain. This way, the owner of the token keeps custody of their token while it is on sale.
Before we start, we need to confirm the state of your accounts.
If you haven't already, please perform the steps in the marketplace setup guide
to ensure that the Fungible Token and Non-Fungible Token contracts are deployed to account 1 and 2 and own some tokens.
Your accounts should look like this:
You can run the 1. CheckSetupScript.cdc
script to ensure that your accounts are correctly set up:
// CheckSetupScript.cdc
import ExampleToken from 0x01
import ExampleNFT from 0x02
// This script checks that the accounts are set up correctly for the marketplace tutorial.
//
// Account 0x01: Vault Balance = 40, NFT.id = 1
// Account 0x02: Vault Balance = 20, No NFTs
pub fun main() {
// Get the accounts' public account objects
let acct1 = getAccount(0x01)
let acct2 = getAccount(0x02)
// Get references to the account's receivers
// by getting their public capability
// and borrowing a reference from the capability
let acct1ReceiverRef = acct1.getCapability(/public/CadenceFungibleTokenTutorialReceiver)
.borrow<&ExampleToken.Vault{ExampleToken.Balance}>()
?? panic("Could not borrow acct1 vault reference")
let acct2ReceiverRef = acct2.getCapability(/public/CadenceFungibleTokenTutorialReceiver)
.borrow<&ExampleToken.Vault{ExampleToken.Balance}>()
?? panic("Could not borrow acct2 vault reference")
// Log the Vault balance of both accounts and ensure they are
// the correct numbers.
// Account 0x01 should have 40.
// Account 0x02 should have 20.
log("Account 1 Balance")
log(acct1ReceiverRef.balance)
log("Account 2 Balance")
log(acct2ReceiverRef.balance)
// verify that the balances are correct
if acct1ReceiverRef.balance != 40.0 || acct2ReceiverRef.balance != 20.0 {
panic("Wrong balances!")
}
// Find the public Receiver capability for their Collections
let acct1Capability = acct1.getCapability(ExampleNFT.CollectionPublicPath)
let acct2Capability = acct2.getCapability(ExampleNFT.CollectionPublicPath)
// borrow references from the capabilities
let nft1Ref = acct1Capability.borrow<&{ExampleNFT.NFTReceiver}>()
?? panic("Could not borrow acct1 nft collection reference")
let nft2Ref = acct2Capability.borrow<&{ExampleNFT.NFTReceiver}>()
?? panic("Could not borrow acct2 nft collection reference")
// Print both collections as arrays of IDs
log("Account 1 NFTs")
log(nft1Ref.getIDs())
log("Account 2 NFTs")
log(nft2Ref.getIDs())
// verify that the collections are correct
if nft1Ref.getIDs()[0] != 1 || nft2Ref.getIDs().length != 0 {
panic("Wrong Collections!")
}
}
You should see something similar to this output if your accounts are set up correctly. They are in the same state that they would have been in if you followed the Fungible Tokens and Non-Fungible Tokens tutorials in succession:
"Account 1 Vault Balance"
40
"Account 2 Vault Balance"
20
"Account 1 NFTs"
[1]
"Account 2 NFTs"
[]
Now that your accounts are in the correct state, we can build a marketplace that enables the sale of NFT's between accounts.
Setting up an NFT Marketplace
Every user who wants to sell an NFT will store an instance of a SaleCollection
resource in their account storage.
Switch to account 0x03
.
Open ExampleMarketplace.cdc
With ExampleMarketplace.cdc
open, click the Deploy
button that appears at the top right of the editor.
ExampleMarketplace.cdc
should contain the following contract definition:
import ExampleToken from 0x01
import ExampleNFT from 0x02
// ExampleMarketplace.cdc
//
// The ExampleMarketplace contract is a very basic sample implementation of an NFT ExampleMarketplace on Flow.
//
// This contract allows users to put their NFTs up for sale. Other users
// can purchase these NFTs with fungible tokens.
//
// Learn more about marketplaces in this tutorial: https://docs.onflow.org/cadence/tutorial/06-marketplace-compose/
//
// This contract is a learning tool and is not meant to be used in production.
// See the NFTStorefront contract for a generic marketplace smart contract that
// is used by many different projects on the Flow blockchain:
//
// https://github.com/onflow/nft-storefront
pub contract ExampleMarketplace {
// Event that is emitted when a new NFT is put up for sale
pub event ForSale(id: UInt64, price: UFix64, owner: Address?)
// Event that is emitted when the price of an NFT changes
pub event PriceChanged(id: UInt64, newPrice: UFix64, owner: Address?)
// Event that is emitted when a token is purchased
pub event TokenPurchased(id: UInt64, price: UFix64, seller: Address?, buyer: Address?)
// Event that is emitted when a seller withdraws their NFT from the sale
pub event SaleCanceled(id: UInt64, seller: Address?)
// Interface that users will publish for their Sale collection
// that only exposes the methods that are supposed to be public
//
pub resource interface SalePublic {
pub fun purchase(tokenID: UInt64, recipient: Capability<&AnyResource{ExampleNFT.NFTReceiver}>, buyTokens: @ExampleToken.Vault)
pub fun idPrice(tokenID: UInt64): UFix64?
pub fun getIDs(): [UInt64]
}
// SaleCollection
//
// NFT Collection object that allows a user to put their NFT up for sale
// where others can send fungible tokens to purchase it
//
pub resource SaleCollection: SalePublic {
/// A capability for the owner's collection
access(self) var ownerCollection: Capability<&ExampleNFT.Collection>
// Dictionary of the prices for each NFT by ID
access(self) var prices: {UInt64: UFix64}
// The fungible token vault of the owner of this sale.
// When someone buys a token, this resource can deposit
// tokens into their account.
access(account) let ownerVault: Capability<&AnyResource{ExampleToken.Receiver}>
init (ownerCollection: Capability<&ExampleNFT.Collection>,
ownerVault: Capability<&AnyResource{ExampleToken.Receiver}>) {
pre {
// Check that the owner's collection capability is correct
ownerCollection.check():
"Owner's NFT Collection Capability is invalid!"
// Check that the fungible token vault capability is correct
ownerVault.check():
"Owner's Receiver Capability is invalid!"
}
self.ownerCollection = ownerCollection
self.ownerVault = ownerVault
self.prices = {}
}
// cancelSale gives the owner the opportunity to cancel a sale in the collection
pub fun cancelSale(tokenID: UInt64) {
// remove the price
self.prices.remove(key: tokenID)
self.prices[tokenID] = nil
// Nothing needs to be done with the actual token because it is already in the owner's collection
}
// listForSale lists an NFT for sale in this collection
pub fun listForSale(tokenID: UInt64, price: UFix64) {
pre {
self.ownerCollection.borrow()!.idExists(id: tokenID):
"NFT to be listed does not exist in the owner's collection"
}
// store the price in the price array
self.prices[tokenID] = price
emit ForSale(id: tokenID, price: price, owner: self.owner?.address)
}
// changePrice changes the price of a token that is currently for sale
pub fun changePrice(tokenID: UInt64, newPrice: UFix64) {
self.prices[tokenID] = newPrice
emit PriceChanged(id: tokenID, newPrice: newPrice, owner: self.owner?.address)
}
// purchase lets a user send tokens to purchase an NFT that is for sale
pub fun purchase(tokenID: UInt64, recipient: Capability<&AnyResource{ExampleNFT.NFTReceiver}>, buyTokens: @ExampleToken.Vault) {
pre {
self.prices[tokenID] != nil:
"No token matching this ID for sale!"
buyTokens.balance >= (self.prices[tokenID] ?? 0.0):
"Not enough tokens to by the NFT!"
recipient.borrow != nil:
"Invalid NFT receiver capability!"
}
// get the value out of the optional
let price = self.prices[tokenID]!
self.prices[tokenID] = nil
let vaultRef = self.ownerVault.borrow()
?? panic("Could not borrow reference to owner token vault")
// deposit the purchasing tokens into the owners vault
vaultRef.deposit(from: <-buyTokens)
// borrow a reference to the object that the receiver capability links to
// We can force-cast the result here because it has already been checked in the pre-conditions
let receiverReference = receiver.borrow()!
// deposit the NFT into the buyers collection
receiverReference.deposit(token: <-self.ownerCollection.borrow()!.withdraw(withdrawID: tokenID))
emit TokenPurchased(id: tokenID, price: price, owner: self.owner?.address, buyer: receiverReference.owner?.address)
}
// idPrice returns the price of a specific token in the sale
pub fun idPrice(tokenID: UInt64): UFix64? {
return self.prices[tokenID]
}
// getIDs returns an array of token IDs that are for sale
pub fun getIDs(): [UInt64] {
return self.prices.keys
}
}
// createCollection returns a new collection resource to the caller
pub fun createSaleCollection(ownerCollection: Capability<&ExampleNFT.Collection>,
ownerVault: Capability<&AnyResource{ExampleToken.Receiver}>): @SaleCollection {
return <- create SaleCollection(ownerCollection: ownerCollection, ownerVault: ownerVault)
}
}
This marketplace contract has resources that function similarly to the NFT Collection
that was explained 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 thepurchase
method, sending theirExampleToken.Vault
that contains the currency they are using to make the purchase. The buyer also includes a capability to their NFTExampleNFT.Collection
so that the purchased token can be immediately deposited into their collection when the purchase is made. - This marketplace contract stores a capability:
pub let ownerVault: Capability<&AnyResource{FungibleToken.Receiver}>
. The owner of the sale saves a capability to their Fungible TokenReceiver
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 ownersVault
when a purchase is made. - This marketplace contract includes events. Cadence supports defining events within contracts that can be emitted when important actions happen. External apps can monitor these events to know the state of the smart contract.
// Event that is emitted when a new NFT is put up for sale
pub event ForSale(id: UInt64, price: UFix64, owner: Address?)
// Event that is emitted when the price of an NFT changes
pub event PriceChanged(id: UInt64, newPrice: UFix64, owner: Address?)
// Event that is emitted when a token is purchased
pub event TokenPurchased(id: UInt64, price: UFix64, seller: Address?, buyer: Address?)
// Event that is emitted when a seller withdraws their NFT from the sale
pub event SaleCanceled(id: UInt64, seller: Address?)
This contract has a few new features and concepts that are important to cover:
Events
Events are special values that can be emitted during the execution of a program. They usually contain information to indicate that some important action has happened in a smart contract, such as an NFT transfer, a permission change, or many other different things. Off-chain applications can monitor events using a Flow SDK to know what is happening on-chain without having to query a smart contract directly.
Many applications want to maintain an off-chain record of what is happening on-chain so they can have faster performance when getting information about their users' accounts or generating analytics.
Events are declared by indicating the access level, event
,
and the name and parameters of the event, like a function declaration:
pub event ForSale(id: UInt64, price: UFix64, owner: Address?)
Events cannot modify state at all; they indicate when important actions happen in the smart contract.
Events are emitted with the emit
keyword followed by the invocation of the event as if it were a function call.
emit ForSale(id: tokenID, price: price, owner: self.owner?.address)
External applications can monitor the blockchain to take action when certain events are emitted.
Resource-Owned Capabilities
[We have covered capabilities in previous tutorials(/cadence/tutorial/03-fungible-tokens/#ensuring-security-in-public-capability-security)], but only the basics. Capabilities can be used for so much more!
As you hopefully understand, capabilites are links to private objects in account storage that specify subset of functionality to expose.
To create a capability, a user typically uses the AuthAccount.link
method to create a link to a resource in their private storage, specifying a type to link the capability as:
// Create a public Receiver + Balance capability to the Vault
// acct is an `AuthAccount`
// The object being linked to has to be an `ExampleToken.Vault`,
// and the link only exposes the fields in the `ExampleToken.Receiver` and `ExampleToken.Balance` interfaces.
acct.link<&ExampleToken.Vault{ExampleToken.Receiver, ExampleToken.Balance}>
(/public/CadenceFungibleTokenTutorialReceiver, target: /storage/CadenceFungibleTokenTutorialVault)
Then, users can get that capability if it was created in a public path, borrow it, and access the functionality that the owner specified.
// Get account 0x01's PublicAccount object
let publicAccount = getAccount(0x01)
// Retrieve public Vault Receiver reference for the account
let acct1Capability = acct.getCapability<&AnyResource{ExampleToken.Receiver}>(/public/CadenceFungibleTokenTutorialReceiver)
// borrow the reference
let acct1ReceiverRef = acct1Capability.borrow()
?? panic("Could not borrow a receiver reference to the vault")
// deposit tokens
acct1ReceiverRef.deposit(from: <-tokens)
With the marketplace contract, we are utilizing a new feature of capabilities. Capabilities 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 store two different capabilities in the marketplace sale collection:
/// A capability for the owner's collection
access(self) var ownerCollection: Capability<&ExampleNFT.Collection>
// The fungible token vault of the owner of this sale.
// When someone buys a token, this resource can deposit
// tokens into their account.
access(account) let ownerVault: Capability<&AnyResource{ExampleToken.Receiver}>
If a 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 all of the functionality
of the ExampleNFT.Collection
. 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 with the @
symbol specified 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 with the AuthAccount.unlink()
method so any capabilities that use that link are rendered invalid.
One last piece to consider about capabilities is the decision about when to use them instead of storing the resource directly.
This tutorial used to have the SaleCollection
directly store the NFTs that were for sale, like so:
pub resource SaleCollection: SalePublic {
/// Dictionary of NFT objects for sale
/// Maps ID to NFT resource object
/// Not recommended
access(self) var forSale: @{UInt64: ExampleNFT.NFT}
}
This is a logical way to do it, and illustrates another important concept in Cadence, that resources can own other resources! Check out the Kitty Hats tutorial for a little more exploration of this concept.
In this case however, it doesn't make sense. If a user decides to store their for-sale NFTs in a separate place from their main collection, then, those NFTs are not available to be shown to any app or smart contract that queries the main collection, so it is as if the owner doesn't actually own the NFT! In cases like this, we usually recommend using a capability to the main collection so that the main collection can remain unchanged and fully usable by other smart contracts and apps. This also means that if a for-sale NFT gets transferred by some means other than a purchase, than you need a way to get rid of the stale listing. That is out of the scope of this tutorial though.
Enough explaining! Lets execute some code!
Deploying and using the Marketplace
At this point, we should have an ExampleToken.Vault
and an Example.NFT.Collection
in both accounts' storage.
Account 0x01
should have an NFT in their collection.
You can create a SaleCollection
and list account 0x01
's token for sale by following these steps:
Open Transaction 4, CreateSale.cdc
Select account 0x01
as the only signer and click the Send
button to submit the transaction.
// CreateSale.cdc
import ExampleToken from 0x01
import ExampleNFT from 0x02
import ExampleMarketplace from 0x03
// This transaction creates a new Sale Collection object,
// lists an NFT for sale, puts it in account storage,
// and creates a public capability to the sale so that others can buy the token.
transaction {
prepare(acct: AuthAccount) {
// Borrow a reference to the stored Vault
let receiver = acct.getCapability<&{ExampleToken.Receiver}>(/public/CadenceFungibleTokenTutorialReceiver)
// borrow a reference to the nftTutorialCollection in storage
let collectionCapability = acct.link<&ExampleNFT.Collection>(/private/nftTutorialCollection, target: ExampleNFT.CollectionStoragePath)
?? panic("Unable to create private link to NFT Collection")
// Create a new Sale object,
// initializing it with the reference to the owner's vault
let sale <- ExampleMarketplace.createSaleCollection(ownerCollection: collectionCapability, ownerVault: receiver)
// List the token for sale by moving it into the sale object
sale.listForSale(tokenID: 1, price: 10.0)
// Store the sale object in the account storage
acct.save(<-sale, to: /storage/NFTSale)
// Create a public capability to the sale so that others can call its methods
acct.link<&ExampleMarketplace.SaleCollection{ExampleMarketplace.SalePublic}>(/public/NFTSale, target: /storage/NFTSale)
log("Sale Created for account 1. Selling NFT 1 for 10 tokens")
}
}
This transaction:
- Gets a capability for the owners
Vault
- Creates the
SaleCollection
, which stores theirVault
capability. - Lists the token with ID=1 for sale and sets its price as 10.0.
- Stores the sale in their account storage and publishes a capability that allows others to purchase any NFTs for sale.
Let's run a script to ensure that the sale was created correctly.
Open Script 2: GetSaleIDs.cdc
Click the Execute
button to print the ID and price of the NFT that account 0x01
has for sale.
// GetSaleIDs.cdc
import ExampleToken from 0x01
import ExampleNFT from 0x02
import ExampleMarketplace from 0x03
// This script prints the NFTs that account 0x01 has for sale.
pub fun main() {
// Get the public account object for account 0x01
let account1 = getAccount(0x01)
// Find the public Sale reference to their Collection
let acct1saleRef = account1.getCapability(/public/NFTSale)
.borrow<&AnyResource{ExampleMarketplace.SalePublic}>()
?? panic("Could not borrow acct2 nft sale reference")
// Los the NFTs that are for sale
log("Account 1 NFTs for sale")
log(acct1saleRef.getIDs())
log("Price")
log(acct1saleRef.idPrice(tokenID: 1))
}
This script should complete and print something like this:
"Account 1 NFTs for sale"
[1]
"Price"
10
Purchasing an NFT
The buyer can now purchase the seller's NFT by using the transaction in Transaction2.cdc
.
Open Transaction 2: PurchaseSale.cdc
file
Select account 0x02
as the only signer and click the Send
button.
// PurchaseSale.cdc
import ExampleToken from 0x01
import ExampleNFT from 0x02
import ExampleMarketplace from 0x03
// This transaction uses the signers Vault tokens to purchase an NFT
// from the Sale collection of account 0x01.
transaction {
// Capability to the buyer's NFT collection where they
// will store the bought NFT
let collectionCapability: Capability<&AnyResource{ExampleNFT.NFTReceiver}>
// Vault that will hold the tokens that will be used to
// but the NFT
let temporaryVault: @ExampleToken.Vault
prepare(acct: AuthAccount) {
// get the references to the buyer's fungible token Vault and NFT Collection Receiver
self.collectionCapability = acct.getCapability<&AnyResource{ExampleNFT.NFTReceiver}>(from: ExampleNFT.CollectionPublicPath)
let vaultRef = acct.borrow<&ExampleToken.Vault>(from: /storage/CadenceFungibleTokenTutorialVault)
?? panic("Could not borrow owner's vault reference")
// withdraw tokens from the buyers Vault
self.temporaryVault <- vaultRef.withdraw(amount: 10.0)
}
execute {
// get the read-only account storage of the seller
let seller = getAccount(0x01)
// get the reference to the seller's sale
let saleRef = seller.getCapability(/public/NFTSale)!
.borrow<&AnyResource{ExampleMarketplace.SalePublic}>()
?? panic("Could not borrow seller's sale reference")
// purchase the NFT the the seller is selling, giving them the capability
// to your NFT collection and giving them the tokens to buy it
saleRef.purchase(tokenID: 1, recipient: self.collectionCapability, buyTokens: <-self.temporaryVault)
log("Token 1 has been bought by account 2!")
}
}
This transaction:
- Gets the public account object for account
0x01
- Gets the capability to the buyer's NFT receiver
- Borrow's a reference to the buyer's stored
ExampleToken.Vault
- Withdraws the tokens that the buyer will use to purchase the NFT
- Gets the reference to the seller's public sale
- Calls the
purchase
function, passing in the tokens and theCollection
reference. Thenpurchase
deposits the bought NFT directly into the buyer's collection.
Verifying the NFT Was Purchased Correctly
You can run now run a script to verify that the NFT was purchased correctly because:
- account
0x01
has 50 tokens and does not have any NFTs for sale or in their collection and account - account
0x02
has 10 tokens and an NFT with id=1
To run a script that verifies the NFT was purchased correctly, follow these steps:
Click `Execute` button `VerifyAfterPurchase.cdc` should contain the following code:
// VerifyAfterPurchase.cdc
import ExampleToken from 0x01
import ExampleNFT from 0x02
import ExampleMarketplace from 0x03
// This script checks that the Vault balances and NFT collections are correct
// for both accounts.
//
// Account 1: Vault balance = 50, No NFTs
// Account 2: Vault balance = 10, NFT ID=1
pub fun main() {
// Get the accounts' public account objects
let acct1 = getAccount(0x01)
let acct2 = getAccount(0x02)
// Get references to the account's receivers
// by getting their public capability
// and borrowing a reference from the capability
let acct1ReceiverRef = acct1.getCapability(/public/CadenceFungibleTokenTutorialReceiver)
.borrow<&ExampleToken.Vault{ExampleToken.Balance}>()
?? panic("Could not borrow acct1 vault reference")
let acct2ReceiverRef = acct2.getCapability(/public/CadenceFungibleTokenTutorialReceiver)
.borrow<&ExampleToken.Vault{ExampleToken.Balance}>()
?? panic("Could not borrow acct2 vault reference")
// Log the Vault balance of both accounts and ensure they are
// the correct numbers.
// Account 0x01 should have 50.
// Account 0x02 should have 10.
log("Account 1 Balance")
log(acct1ReceiverRef.balance)
log("Account 2 Balance")
log(acct2ReceiverRef.balance)
// verify that the balances are correct
if acct1ReceiverRef.balance != 50.0 || acct2ReceiverRef.balance != 10.0 {
panic("Wrong balances!")
}
// Find the public Receiver capability for their Collections
let acct1Capability = acct1.getCapability(ExampleNFT.CollectionPublicPath)
let acct2Capability = acct2.getCapability(ExampleNFT.CollectionPublicPath)
// borrow references from the capabilities
let nft1Ref = acct1Capability.borrow<&{ExampleNFT.NFTReceiver}>()
?? panic("Could not borrow acct1 nft collection reference")
let nft2Ref = acct2Capability.borrow<&{ExampleNFT.NFTReceiver}>()
?? panic("Could not borrow acct2 nft collection reference")
// Print both collections as arrays of IDs
log("Account 1 NFTs")
log(nft1Ref.getIDs())
log("Account 2 NFTs")
log(nft2Ref.getIDs())
// verify that the collections are correct
if nft2Ref.getIDs()[0] != 1 || nft1Ref.getIDs().length != 0 {
panic("Wrong Collections!")
}
// Get the public sale reference for Account 0x01
let acct1SaleRef = acct1.getCapability(/public/NFTSale)
.borrow<&AnyResource{ExampleMarketplace.SalePublic}>()
?? panic("Could not borrow acct1 nft sale reference")
// Print the NFTs that account 0x01 has for sale
log("Account 1 NFTs for sale")
log(acct1SaleRef.getIDs())
if acct1SaleRef.getIDs().length != 0 { panic("Sale should be empty!") }
}
If you did everything correctly, the transaction should succeed and it should print something similar to this:
"Account 1 Vault Balance"
50
"Account 2 Vault Balance"
10
"Account 1 NFTs"
[]
"Account 2 NFTs"
[1]
"Account 1 NFTs for Sale"
[]
Congratulations, you have successfully implemented a simple marketplace in Cadence and used it to allow one account to buy an NFT from another!
Scaling the Marketplace
A user can hold a sale in their account with these resources and transactions. Support for a central marketplace where users can discover sales is relatively easy to implement and can build on what we already have. If we wanted to build a central marketplace on-chain, we could use a contract that looks something like this:
// Marketplace would be the central contract where people can post their sale
// references so that anyone can access them
pub contract Marketplace {
// Data structure to store active sales
pub var tokensForSale: {Address: Capability<&SaleCollection>)}
// listSaleCollection lists a users sale reference in the array
// and returns the index of the sale so that users can know
// how to remove it from the marketplace
pub fun listSaleCollection(collection: Capability<&SaleCollection>) {
let saleRef = collection.borrow()
?? panic("Invalid sale collection capability")
self.tokensForSale[saleRef.owner!.address] = collection
}
// removeSaleCollection removes a user's sale from the array
// of sale references
pub fun removeSaleCollection(owner: Address) {
self.tokensForSale[owner] = nil
}
}
This contract isn't meant to be a working or production-ready contract, but it could be extended to make a complete central marketplace by having:
- Sellers list a capability to their
SaleCollection
in this contract - Other functions that buyers could call to get info about all the different sales and to make purchases.
A central marketplace in an off-chain application is easier to implement because:
- The app could host the marketplace and a user would simply log in to the app and give the app its account address.
- The app could read the user's public storage and find their sale reference.
- With the sale reference, the app could get all the information they need about how to display the sales on their website.
- Any buyer could discover the sale in the app and login with their account, which gives the app access to their public references.
- When the buyer wants to buy a specific NFT, the app would automatically generate the proper transaction to purchase the NFT from the seller.
Creating a Marketplace for Any Generic NFT
The previous examples show how a simple marketplace could 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. There are a few good examples of generic marketplaces on Flow right now.
- The Flow team has created a completely decentralized example of a generic marketplace in the NFT storefront repo. This contract is already deployed to testnet and mainnet and can be used by anyone for any generic NFT sale!
- VIV3 is a company that has a generic NFT marketplace.
Composable Resources on Flow
Now that you have an understanding of how composable smart contracts and the marketplace work on Flow, you're ready to play with composable resources! Check out the Kitty Hats tutorial!