Flow: How to Create an NFT Marketplace with Filebase + Flow

Learn how to create an NFT marketplace using Filebase and Flow.

NFT marketplaces like OpenSea are skyrocketing in value right now due to the rise in popularity of NFTs and the revenue generated by being a marketplace for users to buy and sell NFTs. Most marketplaces take a percentage of the NFT’s sale price, similar to other ecommerce marketplaces such as Etsy or Ebay.

But what if you create your own NFT marketplace for your NFT collection? You can cut out the middleman and avoid paying any fees associated with listing your NFTs on a third party marketplace.

What You’ll Learn

In this guide, we’ll go over how to create your own NFT marketplace app using the Flow blockchain and Filebase for storage. You will learn:

  • How to create smart contracts with Cadence Language.

  • How to mint tokens and store the metadata in a Filebase bucket.

  • How to authenticate user wallets.

  • How to transfer tokens.

Once you've completed this guide, you'll have all the backend fundamentals of an NFT marketplace that you can use to develop a frontend user interface to create a full marketplace.

Prerequisites:

Terminology

Let’s go over a few terms before we get started. Flow uses the Cadence Language for creating smart contracts, tracking transactions, and running scripts.

A blockchain is a distributed, digital ledger that tracks and records transactions and the ownership of a resource.

A resource can be any object or item, such as currency or goods, both physical and digital. In this case, we will be using cryptocurrency. Resources are stored in user accounts and are accessible through access control properties that are defined in a smart contract.

An interface defines the behaviors or abilities of a resource.

An account owns resources or has access to the resources. Each account keeps a personal ledger of its spending or transferring of resources.

A contract is a ruleset used to govern actions. Actions that break a contract are punishable and have repercussions. In our guide, contracts are the initial program that gets deployed to a blockchain that initiates your application’s ability to access resources you create on the blockchain.

A script in this guide refers to a Cadence program used to read blockchain information and availability. A common script is a blockchain explorer that queries the current state of the blockchain.

Now that we have a basic understanding of these terms, let’s get started building out the NFT marketplace app.

1. First, we need an image to use as an NFT. We’ll start by uploading an image to Filebase for us to use.

To do this, navigate to console.filebase.com. If you don’t have an account already, sign up, then log in.

2. Select ‘Buckets’ from the left side bar menu, or navigate to console.filebase.com/buckets.

Select ‘Create Bucket’ in the top right corner to create a new bucket for your NFTs.

3. Enter a bucket name and choose the IPFS storage network to create the bucket.

Bucket names must be unique across all Filebase users, be between 3 and 63 characters long, and can contain only lowercase characters, numbers, and dashes.

4. Next, select the bucket from your list of buckets, then select ‘Upload’ in the top right corner to upload an image file.

5. Select an image to be uploaded. Once uploaded, it will be listed in the bucket.

6. Click on your uploaded object to display the metadata for the object.

Take note of the IPFS CID. We will reference this later.

7. Next, open your command prompt. We need to create a new React app with the following command:

npx create-react-app nftmarketplace; cd nftmarketplace

Then we need to initialize a Flow project with the command:

flow init

This will result in a new React project with the file flow.json stored inside. This file is important because it contains configuration information for our app such as where our contracts will be stored.

8. Add the following lines to the flow.json file to configure the storage location for our contracts and the deployment app information.

Do not remove the other lines that are in the file:

{
// Existing code
"contracts": {
		"nftmarketplace": "./src/flow/contract/nftmarketplace.cdc"
},
"deployments": {
		"emulator": {
				"emulator-account": ["nftmarketplace"]
	}
},
// Existing code
}

9. Next we need to create directories for our contracts, scripts, and transactions.

Create these directories with the following command:

mkdir -p src/flow/{contract,transaction,script}

Then in each directory, create a Cadence file with the following names:

  • contract/nftMarketplace.cdc

  • transaction/mintToken.cdc

  • script/getTokenIds.cdc

10. Open the contract/nftMarketplace.cdc file in your favorite code editor or IDE.

Copy and paste the following code into the file:

pub contract nftmarketplace {

// This dictionary stores token owners' addresses.
pub var owners: {UInt64: Address}
pub resource NFT {

// The Unique ID for each token, starting from 1.
pub let id: UInt64

// String -> String dictionary to hold
// token's metadata.
pub var metadata: {String: String}

// The NFT's constructor. All declared variables are
// required to be initialized here.
init(id: UInt64, metadata: {String: String}) {
		self.id = id
		self.metadata = metadata
		}
	}
}

With this code we have declared a dictionary for storing the variable owners. This variable will be used to keep track of all current token owners globally in our marketplace.

11. Now let’s add the NFTReceiver interface code to define the capabilities of an NFT receiver.

Add the following code to the nftMarketplace.cdc file:

pub contract nftmarketplace {

// ... The @NFT code …

pub resource interface NFTReceiver {

// Can withdraw a token by its ID and returns
// the token.
		pub fun withdraw(id: UInt64): @NFT

// Can deposit an NFT to this NFTReceiver.
		pub fun deposit(token: @NFT)

// Can fetch all NFT IDs belonging to this
// NFTReceiver.
		pub fun getTokenIds(): [UInt64]

// Can fetch the metadata of an NFT instance
// by its ID.
		pub fun getTokenMetadata(id: UInt64) : {String: String}

// Can update the metadata of an NFT.
		pub fun updateTokenMetadata(id: UInt64, metadata: {String: String})
	}
}

In this code, the withdraw(id: UInt64): @NFT method takes in the id of an NFT and in return withdraws a token with the type @NFT.

The deposit(token: @NFT) method deposits the token to the current NFTReceiver.

The getTokenMetadata(id: UInt64) : {String : String} method takes the token ID and its associated metadata and returns the ID and metadata as a dictionary.

The updateTokenMetadata(id: UInt64, metadata: {String: String}) updates the target’s NFT metadata.

12. Next we’ll create the NFTCollection resource where NFTs will be deposited to or withdrawn from.

Copy and paste the following code into your file:

pub contract nftmarketplace {

// ... The @NFT code ...
// ... The @NFTReceiver code ...

pub resource NFTCollection: NFTReceiver {

// Keeps track of NFTs in this collection.
access(account) var ownedNFTs: @{UInt64: NFT}

// Constructor
init() {
		self.ownedNFTs <- {}
}

// Destructor
destroy() {
		destroy self.ownedNFTs
}

// Withdraws and returns an NFT token.

pub fun withdraw(id: UInt64): @NFT {
		let token <- self.ownedNFTs.remove(key: id)
		return <- token!
}

// Deposits a token to this NFTCollection instance.
pub fun deposit(token: @NFT) {
		self.ownedNFTs[token.id] <-! token
}

// Returns an array of the IDs that are in this collection.
pub fun getTokenIds(): [UInt64] {
		return self.ownedNFTs.keys
}

// Returns the metadata of an NFT based on the ID.
pub fun getTokenMetadata(id: UInt64): {String : String} {
		let metadata = self.ownedNFTs[id]?.metadata
		return metadata!
}

// Updates the metadata of an NFT based on the ID.
pub fun updateTokenMetadata(id: UInt64, metadata: {String: String}) {
		for key in metadata.keys {
				self.ownedNFTs[id]?.metadata?.insert(key: key,  metadata[key]!)
		}
	}
}

// Public factory method to create a collection
// so it is callable from the contract scope.
pub fun createNFTCollection(): @NFTCollection {
		return <- create NFTCollection()
}
}

This code creates a dictionary called ownedNFTs that can be edited. This dictionary works by mapping the token ID to the NFT resource.

Then in the constructor method, the code creates an empty directory called ownedNFTs that will contain any deposited NFTs, and includes a destructor method that will withdraw any sold NFTs.

13. Now let’s add the NFTMinter resource. This resource will enable our app to mint the NFT tokens.

Copy and paste the following code into your file:

pub contract nftmarketplace {

// ... NFT code ...
// ... NFTReceiver code ...
// ... NFTCollection code ...

pub resource NFTMinter {

// Declare a global variable to count ID.
		pub var idCount: UInt64
		init() {

// Instantialize the ID counter.
		self.idCount = 1
}
pub fun mint(_ metadata: {String: String}): @NFT {
// Create a new @NFT resource with the current ID.
		let token <- create NFT(id: self.idCount, metadata: metadata)
// Save the current owner's address to the dictionary.
		nftmarketplace.owners[self.idCount] = nftmarketplace.account.address

// Increment the ID
		self.idCount = self.idCount + 1 as UInt64
		return <-token
		}
	}
}

14. Lastly, we need a constructor method to initialize our contract when it gets deployed.

Copy and paste the following code into your file:

pub contract nftmarketplace {

// ... @NFT code ...
// ... @NFTReceiver code ...
// ... @NFTCollection code ...

// This contract constructor is called once when the contract is deployed.
// It does the following:

// - Creating an empty Collection for the deployer of the collection so
//   the owner of the contract can mint and own NFTs from that contract.

// - The `Collection` resource is published in a public location with reference
//   to the `NFTReceiver` interface. This is how we tell the contract that the functions defined
//   on the `NFTReceiver` can be called by anyone.

// - The `NFTMinter` resource is saved in the account storage for the creator of
//   the contract. Only the creator can mint tokens.

init() {
// Set `owners` to an empty dictionary.
		self.owners = {}

// Create a new `@NFTCollection` instance and save it in `/storage/NFTCollection` domain,
// which is only accessible by the contract owner's account.
		self.account.save(<-create NFTCollection(), to: /storage/NFTCollection)

// "Link" only the `@NFTReceiver` interface from the `@NFTCollection` stored at `/storage/NFTCollection` domain to the `/public/NFTReceiver` domain, which is accessible to any user.
		self.account.link<&{NFTReceiver}>(/public/NFTReceiver, target: /storage/NFTCollection)

// Create a new `@NFTMinter` instance and save it in `/storage/NFTMinter` domain, accessible
// only by the contract owner's account.
		self.account.save(<-create NFTMinter(), to: /storage/NFTMinter)
	}
}

This last bit of code uses the three domain namespaces that are used by Cadence. These are:

  • /public: This namespace is accessible by any account that can interact with the contract.

  • /private: This namespace is only accessible by accounts that are given access to the contract.

  • /storage: This namespace is only accessible to the owner of the account.

All together, your contract should look like this:

pub contract nftmarketplace {

    pub var ownerMap: {UInt64 : Address}
    pub resource NFT {

        pub let id: UInt64

        init(initId: UInt64) {
            self.id = initId
        }
    }
    pub resource interface NFTReceiver {

        pub fun withdraw(withdrawId: UInt64): @NFT
        pub fun deposit(token: @NFT, metadata: {String : String})
        pub fun getIds(): [UInt64]
        pub fun idExists(id: UInt64): Bool
        pub fun getMetadata(id: UInt64) : {String : String}
    }

    pub resource Collection: NFTReceiver {
        pub var ownedNFTs: @{UInt64: NFT}
        pub var metadataObjs: {UInt64: { String : String }}

        init() {
            self.ownedNFTs <- {}
            self.metadataObjs = {}
        }

        destroy() {
            destroy self.ownedNFTs
        }

        pub fun withdraw(withdrawId: UInt64): @NFT {
            let token <- self.ownedNFTs.remove(key: withdrawId)
            return <- token!
        }

        pub fun deposit(token: @NFT, metadata: {String : String}) {
            self.metadataObjs[token.id] = metadata
            self.ownedNFTs[token.id] <-! token
        }

        pub fun idExists(id: UInt64): Bool {
            return self.ownedNFTs[id] != nil
        }

        // getIds returns an array of the IDs that are in the collection
        pub fun getIds(): [UInt64] {
            return self.ownedNFTs.keys
        }

        pub fun getMetadata(id: UInt64): {String : String} {
            // return self.ownedNFTs[id]?.metadata!
             // let token <- self.ownedNFTs[id]!
            // return token.metadata
            return self.metadataObjs[id]!
        }

        pub fun updateMetadata(id: UInt64, metadata: {String: String}) {
            self.metadataObjs[id] = metadata
            // let token <- self.ownedNFTs[id]!
            // token.metadata = metadata
            // self.ownedNFTs[id] <- token
            // self.ownedNFTs[id].metadata! = metadata
            // self.ownedNFTs[id]?.updateMetadata(metadata: metadata)
        }
    }

    pub fun createEmptyCollection(): @Collection {
        return <- create Collection()
    }

    // The minting resource.
    pub resource NFTMinter {
        pub var idCount: UInt64

        init() {
            self.idCount = 1
        }

        pub fun mint(): @NFT {
            var newNFT <- create NFT(initId: self.idCount)
            nftmarketplace.ownerMap[self.idCount] = nftmarketplace.account.address
            self.idCount = self.idCount + 1 as UInt64
            return <-newNFT
        }
    }

    init() {
        self.ownerMap = {}
        self.account.save(<-self.createEmptyCollection(), to: /storage/NFTCollection)
        self.account.link<&{NFTReceiver}>(/public/NFTReceiver, target: /storage/NFTCollection)
        self.account.save(<-create NFTMinter(), to: /storage/NFTMinter)
    }

}

15. Now let’s deploy the contract. First we’ll need to start the flow emulator in our command prompt:

flow emulator

This will return information, such as the FlowServiceAccount and the port of your local HTTP server. Take note of these values.

16. Open another command prompt window and make sure you are in your project directory.

Use the following command to deploy your contract:

flow project deploy

You should receive the following output confirming your contract was successfully deployed:

Deploying 1 contracts for accounts: emulator-account
nftmarketplace -> 0xf8d6e0586b0a20c7 (589ab0b48ca8dd962a48ba991621a3e3bbd53c293f20b750f650bcab4d2fb083)
All contracts deployed successfully

17. Now it’s time to create the transaction responsible for minting tokens.

This is one of the most important pieces of any NFT app. Without this, the NFTs won’t be created to be sold or traded.

Open the mintToken.cdc file that we created earlier. Copy and paste the following code into the file:

import nftmarketplace from 0xf8d6e0586b0a20c7

transaction(metadata: {String : String}) {
    let receiverRef: &{nftmarketplace.NFTReceiver}
    let minterRef: &nftmarketplace.NFTMinter

    // Take the account info of the user trying to execute the transaction and validate.
    // Here we try to "borrow" the capabilities available on `NFTMinter` and `NFTReceiver`
    // resources, and will fail if the user executing this transaction does not have access
    // to these resources.
    prepare(acct: AuthAccount) {
        self.receiverRef = acct.getCapability<&{nftmarketplace.NFTReceiver}>(/public/NFTReceiver)
            .borrow()
            ?? panic("Could not borrow receiver reference")
        self.minterRef = acct.borrow<&nftmarketplace.NFTMinter>(from: /storage/NFTMinter)
            ?? panic("Could not borrow minter reference")
    }

    execute {
        let metadata : {String : String} = {
            "name": "Filebase Robot",
            "type": "Robot",
            "uri": "https://ipfs.filebase.io/ipfs/[IPFS-CID]"
        }

        // Mint the token by calling `mint()` on `@NFTMinter` resource, which returns
        // an `@NFT` resource, and move it to a variable `newNFT`.
        let newNFT <- self.minterRef.mint()

        // Call `deposit(..)` on the `@NFTReceiver` resource to deposit the token.
        // Note that this is where the metadata can be changed before transferring.
        self.receiverRef.deposit(token: <-newNFT, metadata: metadata)
        log("NFT Minted and deposited to Account 2's Collection")
    }
}

There are a few things in this code you will need to replace:

  • In the first line, replace the address to import your contract from. This will be the Flow address that you recorded when you ran the Flow emulator.

  • In the execute section, there are some metadata fields. Edit these to reflect the desired metadata for your NFT. In our example, the NFT has a name and a type metadata field. Add or remove as many metadata fields as desired.

  • Replace the URI metadata CID with your Filebase object IPFS CID that you recorded at the beginning of this guide.

18. Now let’s send this transaction to the flow emulator that is running and mint a token.

This transaction requires metadata to mint the NFT, so we’ll pass the metadata to the code with the following syntax:

flow transactions send src/flow/transaction/mintToken.cdc {}

This command should return a message that says that the transaction has been ‘sealed’. This means that the transaction was successful.

Transaction ID: 5eb708246e30d2d8d626ca7516e995511ea84ae548e8a9bfb36cd1aee7c9c40e
Status           SEALED
ID              5eb708246e30d2d8d626ca7516e995511ea84ae548e8a9bfb36cd1aee7c9c40e
Payer           f8d6e0586b0a20c7
Authorizers     [f8d6e0586b0a20c7]
Proposal Key:
Address     f8d6e0586b0a20c7
Index       0
Sequence    1

You’ve just minted your first NFT! For our example, we created an NFT with the name ‘Apple’ and the type of ‘Fruit’ with the image we uploaded to Filebase.

19. Next, we need to create the TransferToken transaction. This transaction allows NFTs to be transferred between different users.

This is vital to a marketplace, since what good is a marketplace where goods can’t be exchanged?

Since so far we’ve only used our Flow emulator account, we’ll need to create another account to test this transaction with. Start by getting a key pair for a new account with the command:

flow keys generate

To keep these keys safe and referenceable, create a new file called .keys.json and paste the keys that were returned from the generate command.

20. Then let’s create an account using these keys. Run the following command, but replace PUBLIC_KEY with the public key you just generated.

flow accounts create –key PUBLIC_KEY –signer emulator-account

Take note of the address that is returned, then open the flow.json file and add an entry under ‘accounts’ for your test account. Replace PRIVATE_KEY with the test account’s private key.

"accounts": {
		"emulator-account": {
			"address": "f8d6e0586b0a20c7",
			"key": "cdd4101e032a8a6fd0666276c496d2d79cabc9d889635e9d74c96c7039bf63e9"
		},
		"test-account": {
			"address": "0x01cf0e2f2f715450",
			"key": "PRIVATE_KEY"
		}

Like the mintToken.cdc file, you will need to replace the address to import your contract from. This will be the Flow address that you recorded when you ran the Flow emulator.

21. Now let’s send this transaction with the following command:

flow transactions send src/flow/transaction/initCollection.cdc –signer test-account

Now that our test account has been initialized, let’s create our transfer transaction to transfer an NFT from our emulator account to our test account.

22. Create another Cadence file in the transactions directory named transferToken.cdc.

Copy and paste the following code into this file:

import nftmarketplace from 0xf8d6e0586b0a20c7

// This transaction is a template for a transaction that
// could be used by anyone to send tokens to another account
// that owns a Vault.
//
transaction {
    // Temporary Vault object that holds the balance that is being transferred.
    var tempVault: @nftmarketplace.Vault

    // Recipient's account address.
    var recipientAddr: Address

    prepare(
        acct: AuthAccount,
        amount: UFix64,
        recipientAddr: Address
    ) {
        // withdraw tokens from your vault by borrowing a reerence to it
        // and calling the withdraw function with that reference.
        let vaultRef = acct.borrow<&nftmarketplace.Vault>(from: /storage/NFTCollection)
            ?? panic("Could not borrow a reference to the owner's vault")

        self.tempVault <-vaultRef.withdraw(amount: amount)
        self.recipientAddr = recipientAddr
    }

    execute {
        // get the recipient's public account object
        let recipient = getAccount(self.recipientAddr)

        let recipientRef = recipient.getCapability(/public/NFTCollection)
                            .borrow<&nftmarketplace.Vault{nftmarketplace.NFTCollection}>()
                            ?? panic("Could not borrow a reference to the recipient")

        // Deposit your tokens to their Vault
        receiverRef.deposit(from: <-self.tempVault)

        log("Transfer succeeded!")
    }
}

This code is a sequel to the initial minting process code. This code moves the token stored in a source account’s NFTCollection by receiving the destination account’s token and calling the withdraw(id: UInt64) and deposit(token: @NFT).

23. Let’s run this command to test our code.

This command needs the TokenID of the NFTs to move and the destination address. TokenIDs are unsigned integers starting from 1:

flow transactions send src/flow/transaction/transferToken.cdc 1 0x01cf0e2f2f715450

You just withdrew and deposited your NFT into the test account!

24. Now that we can send transactions, we need scripts that can query the state of the blockchain.

To start, let’s query the NFT Token that we just transferred to our test account to confirm it was successfully received.

In your scripts directory, create the file named getTokenOwner.cdc with the following content:

import nftmarketplace from 0xf8d6e0586b0a20c7
// All scripts start with the `main` function,
// which can take an arbitrary number of argument and return
// any type of data.
//
// This function accepts a token ID and returns an Address.
pub fun main(id: UInt64): Address {
    // Access the address that owns the NFT with the provided ID.
    let ownerAddress = nftmarketplace.owners[id]!
    return ownerAddress
}

25. Another script to query the blockchain that we should have is the getTokenMetadata.cdc script.

This script will return the NFT’s metadata, which in this case should be Apple, Fruit, and the Filebase image URL.

Create a new file in the scripts directory called getTokenMetadata.cdc with the following content:

import nftmarketplace from 0xf8d6e0586b0a20c7

// All scripts start with the `main` function,
// which can take an arbitrary number of argument and return
// any type of data.
//
// This function accepts a token ID and returns a metadata dictionary.
pub fun main(id: UInt64) : {String: String} {
    // Access the address that owns the NFT with the provided ID.
    let ownerAddress = nftmarketplace.owners[id]!
    // We encounter the `getAccount(_ addr: Address)` function again.
    // Get the `AuthAccount` instance of the current owner.
    let ownerAcct = getAccount(ownerAddress)
    // Borrow the `NFTReceiver` capability of the owner.
    let receiverRef = ownerAcct.getCapability<&{nftmarketplace.NFTReceiver}>(/public/NFTReceiver)
        .borrow()
            ?? panic("Could not borrow receiver reference")
    // Happily delegate this query to the owning collection
    // to do the grunt work of getting its token's metadata.
    return receiverRef.getTokenMetadata(id: id)
}

26. Then execute the script with the following command. This command also requires the TokenID:

flow scripts execute src/flow/script/getTokenMetadata.cdc 1

Now we have all the back end fundamentals for creating a marketplace! From here, you can create a user interface for the front end of your marketplace using Reactjs that references these scripts, or you can continue to use the scripts by themselves to mint and transfer NFTs.

If you have any questions, please join our Discord server, or send us an email at hello@filebase.com

Last updated