Using Pinata: Part 3

This is part three of a three-part series about creating NFTs like NBA Top Shot using Flow and IPFS.

Part one:

How to Create NFTs Like NBA Top Shot With Flow and IPFS
Using Pinata

Part two:

How to Display Your NFT Collection Like NBA Top Shot With Flow and IPFS
Using Pinata: Part 2

In this final part of the series, we’re going to finish things up by enabling the transfer of NFTs. As we’ve come to expect, Flow has some great documentation around this concept, but we are going to extend those docs and make them fit the paradigm of IPFS-hosted content. Let’s dive in!

Setting Up

Hopefully, you’ve been following along with the prior two tutorials. If so, you have all the starter code necessary to continue, and we’re going to simply add to that code. If you have not yet started the other tutorials, you’re going to be lost So definitely go back and work your way through those tutorials.

Now, go ahead and open your project. That’s all the setup we need. Congratulations!

Creating Contracts

A marketplace requires a few more things beyond what we have already built. Let’s list those things here:

  • Payment mechanism (i.e. fungible tokens)
  • Token transfer capabilities
  • Token supply settings

Because the Flow Emulator is an in-memory representation of the Flow blockchain, you’ll want to make sure you execute the previous tutorials before this step and make sure to keep the emulator running. Assuming you’ve done that, let’s create a fungible token contract that can be used for payments when buying NFTs.

To be clear, creating a purchasing mechanism for these fungible tokens we’re about to create is outside the scope of this tutorial. We’re simply going to mint and transfer tokens to accounts that will be purchasing NFTs.

Inside your pinata-party directory that you created in part one of this series, go into the cadence/contracts folder and create a new file called PinnieToken.cdc. This is going to be our fungible token contract. We’ll start by defining the empty contract like this:pub contract PinnieToken {}

Each piece of code within the main contract code will be its own Github gist, and I’ll provide the full contract code at the end. The first piece we’re going to add to this contract is the token pub variables associated with our token and a Provider resource.

Add the above code right inside the empty contract we created initially. The totalSupply and the tokenName variables are self-explanatory. We’ll set these later when we initialize our token contract.

The resource interface called Provider that we created takes a little more explanation. This resource simply defines a function that will be public, but interestingly, it will still only be callable by the account owner from whom the withdraw is being executed against. That is to say that I could not execute a withdraw request against your account.

Next, we’re going to define two more public resource interfaces:

These go directly below the Provider resource interface. The Receiver interface includes a function that will be executable by anyone. This ensures that deposits into an account can be executed as long as the recipient has initialized a vault that can handle these tokens we’re creating through this contract. You’ll see the reference to a Vault coming up soon.

The Balance resource will simply return a balance of our new token for any given account.

Let’s now create the Vault resource we mentioned above. Add the following below the Balance resource:

The Vault resource is the main attraction here. I say this because without it, nothing can happen. If a reference to the Vault resource is not stored in an account’s storage, that account cannot receive these particular tokens. Which means that account can’t send these tokens. Which also means that account can’t buy NFTs. And darn it, we want to buy some NFTs.

Now, let’s take a look at what the Vault resource implements. You can see that our Vault resource inherits the Provider, Receiver, and Balance resource interfaces, and then it defines two functions: withdraw and deposit. If you remember, the Provider interface gave access to the withdraw function, so we’re simply defining that function here. And the Receiver interface gave access to the deposit function, which we are defining here.

You will also note that we have a balance variable that is initialized with the Vault resource. This balance represents a given account’s balance.

Now, let’s figure out how we can make sure an account gets access to the Vault interface. Remember, without it, nothing can happen with this token we want to create. Below the Vault interface, add the following function:pub fun createEmptyVault(): @Vault {
    return <-create Vault(balance: 0.0)
}

This function, as the name implies, creates an empty Vault resource for an account. The balance, of course, is 0.

I think it’s about time to set up our minting capability. Add this below the createEmptyVault function:

The VaultMinter resource is public, but by default it is only available to the contract account owner. It is possible to make this resource available to others, but we’re not going to be focusing on that within this tutorial.

The VaultMinter resource has just one function: mintTokens. That function requires an amount to mint and a recipient. As long as the recipient has the Vault resource stored, the newly minted tokens can be deposited into that account. When tokens are minted, the totalSupply variable has to be updated, so we add the amount minted to the previous supply to get our new supply.

Ok, we’ve made it this far. There’s one thing left to do with our contract. We need to initialize it. Add this after the VaultMinter resource.

When we initialize our contract, we need to set a total supply. This could be any number you choose. For the sake of our example, we’re initializing this with a supply of 30. We are setting our tokenName to “Pinnie” because this is all about Pinata Parties after all. We are also creating a vault variable that creates a Vault resource with the initial supply and stores it in the contract creator’s account.

That’s it. That’s the contract. Here’s the full code.

Deploying and Minting Tokens

We need to update the flow.json file in our project so that we can deploy this new contract. One thing that you may have discovered in the previous tutorials is the structure of the flow.json file needs to be slightly different when you are deploying contracts compared to when you are executing transactions. Make sure your flow.json references the new contract and has the emulator-account key reference like this:

In another terminal window from within your pinata-party project directory, run flow project deploy. You’ll get the account for the deployed contract (which is the same account that the NFT contract was deployed under). Keep that somewhere because we will be making use of it soon.

Now, let’s test out our minting function. We’re going to create a transaction that allows us to mint Pinnie tokens, but first, we need to again update our flow.json. (There may be a better way to do this, but it’s what I’ve found to work when doing things on the emulator). Under the emulator-account change your json back to look something like this:"emulator-account": {
    "address": "f8d6e0586b0a20c7",
    "privateKey": "e5ca2b0946358223f0555206144fe4d74e65cbd58b0933c5232ce195b9058cdd",
    "sigAlgorithm": "ECDSA_P256",
    "hashAlgorithm": "SHA3_256",
    "chain": "flow-emulator"
},

The key field once again becomes the privateKey field, and then we add in our sigAlogrithm, hashAlgorithm, and chain properties. For whatever reason, this format works for sending transactions whereas the other format works for deploying contracts.

Ok, we need to do one more thing to allow our account we used to deploy the contract to mint some Pinnies. We need to create a link. This is an easy transaction that just gives access to the minting functionality. So, inside your transactions folder, add a file called LinkPinnie.cdc. In that file, add:

This transaction imports our Pinnie contract. It then creates a transaction that links the Receiver resource to the count that will ultimately be doing the minting. We would do this same thing for other accounts that need a reference to that resource.

With the transaction created, let’s go ahead and run it. In your terminal at the root of the project, run:flow transactions send --code transactions/LinkPinnie.cdc

Now, we’re ready to move on. Let’s mint some Pinnies! To do this, we need to write a transaction. This transaction is pretty straightforward so I’m going to drop the full code snippet below:

This code should be added to a file called MintPinnie.cdc within your transactions folder. This transaction is importing our PinnieToken contract at the top, then it is creating references to two of the resources we defined in that contract. We defined a VaultMinter resource and a Receiver resource, among others. Those two resources are being used here. The VaultMinter is, as you’d expect, being used to mint the token. The Receiver resource is being used to handle the depositing of the new tokens into an account.

This is just a test to make sure we can mint tokens and deposit them into our own account. Very soon, we will create a new account, mint tokens, and deposit them into another account.

Run the transaction from the command line like this:flow transactions send --code /transactions/MintPinnie.cdc --signer emulator-account

Remember, we deployed the contract with the emulator-account, so unless we provide a link and allow some other account to mint, the emulator-account is the one that has to do the minting.

Let’s now create a script to check our Pinnie balance to make sure this all worked. Inside the scripts folder of your project, create a file called CheckPinnieBalance.cdc and add the following:

Again, we are importing the contract, we are hard-coding the account we want to check on (the emulator-account), and we are borrowing a reference to the Balance resource for the Pinnie Token. We return the balance at the end of the script so that it is printed on the command line.

When we created the contract, remember that we set an initial supply of 30 tokens. So when we ran that MintPinnie transaction, we should have minted and deposited an additional 30 tokens into the emulator-account. This means, if all went well, this balance script should show 60 tokens.

We can run the script with this command:flow scripts execute --code scripts/CheckPinnieBalance.cdc

And the result should look something like this:{"type":"UFix64","value":"60.00000000"}

Fantastic! We can mint tokens. Let’s make sure we can mint some and deposit them into a fresh account for someone else (really, it’s still just you, but we can pretend).

To create a new account, you’ll need to first generate a new keypair. To do this, run the following command:flow keys generate

This will generate a private key and a public key. We need the public key to generate a new account, and we will soon use the private key to update our flow.json. So, let’s create that new account now. Run this command:flow accounts create --key YourNewPublicKey

This will create a transaction, and the result of that transaction will include the new account address. You should have received a transaction ID as a result of creating the new account. Copy that transaction ID, and run the following command:flow transactions status YourTransactionId

This command should result in something like this:Status: SEALEDEvents:Event 0: flow.AccountCreated: 0x5af6470379d5e29d7ca6825b5899def6681e66f2fe61cb49113d56406e815efaFields:address (Address): 01cf0e2f2f715450Event 1: flow.AccountKeyAdded: 0x4e9368146889999ab86aafc919a31bb9f64279176f2db247b9061a3826c5e232Fields:address (Address): 01cf0e2f2f715450publicKey (Unknown): f847b840c294432d731bfa29ae85d15442ddb546635efc4a40dced431eed6f35547d3897ba6d116d6d5be43b5614adeef8f468730ef61db5657fc8c4e5e03068e823823e8

The address listed is the new account address. Let’s use that to update our flow.json file.

Inside that file, under your accounts, object, let’s create a new reference to this account. Remember that private key from earlier? We’re going to need that now. Set your accounts object to look something like this:"accounts": {
 "emulator-account": {
   "address": "f8d6e0586b0a20c7",
   "keys": "e5ca2b0946358223f0555206144fe4d74e65cbd58b0933c5232ce195b9058cdd"
 },
 "second-account": {
   "address": "01cf0e2f2f715450",
   "keys": "9bde7092cc0695c67f896e4375bffa0b5bf0a63ce562195a36f864ba7c3b09e3"
 }
},

We now have a second account we can use to send Pinnie tokens to. Let’s see how that looks.

Sending Fungible Tokens

Our main account (the one that created the Pinnie Token) currently has 60 tokens. Let’s see if we can send some of those tokens to our second account.

If you remember from earlier, each account needs to have an empty vault in order to accept Pinnie Tokens, and it needs to have a link to the resources on the Pinnie Token contract. Let’s start by creating an empty vault. We need a new transaction for this. So, create a file in your transactions folder called CreateEmptyPinnieVault.cdc . Inside that file, add the following:

In this transaction, we are importing the Pinnie Token contract, we are calling the public function createEmptyVault, and we are using the Receiver resource on the contract to link it up with the new account.

Notice in the post section that we have a check in place. Make sure you replace NEW_ACCOUNT_ADDRESS with the account address you just created previously and prefix it with a 0x.

Let’s run the transaction now. In the root of your project, run:flow transactions send --code transactions/CreateEmptyPinnieVault.cdc --signer second-account

Notice, we defined the signer as second-account. This is making sure that the transaction is executed by and for the correct account and not our original emulator-account. Once this is done, we can now link to the Pinnie Token resource. Run the following command:flow transactions send --code transactions/LinkPinnie.cdc --signer second-account

All of this has been setup so that we can transfer tokens from the emulator-account to the second-account. To do this, we need—you guessed it—another transaction. Let’s write that now.

In your transactions folder, create a file called TransferPinnieToken.cdc. Inside that file, add the following:

We are importing the Pinnie Token contract, like always. We are then creating a temporary reference to the Pinnie Token vault. We do this because when dealing with fungible tokens, everything takes place inside a vault. So we will need to withdraw tokens from the emulator-account's vault, place them into the temporary vault, then send that temporary vault to the recipient (second-account).

On line 10, you see the amount we are withdrawing and sending to the second-account is 10 tokens. That seems fair. Our friend, second-account isn’t too greedy.

Be sure to replace the value of NEW_ACCOUNT_ADDRESS with the second-account address. Prefix it with a 0x. With that done, let’s execute the transaction. Run:flow transactions send --code transactions/TransferPinnieTokens.cdc --signer emulator-account

The signer needs to be the emulator account since the emulator account is the only one with tokens right now. After you execute the above transaction, we will now have two accounts with tokens. Let’s prove this.

Open up your CheckPinnieBalance script and replace the account address on line 3 with the address for second-account. Again, make sure you prefix the address with a 0x. Save that and then run the script like this:flow scripts execute --code scripts/CheckPinnieBalance.cdc

You should see the following result:{
 "type": "UFix64",
 "value": "10.00000000"
}

And with that, you have now minted a fungible token that can be used as a currency, and you have transferred some of those tokens to another user. Now, all that’s left is allowing that second account to purchase our NFT from a marketplace.

Creating a Marketplace

We are going to simply update our React code from the second tutorial in this series to act as our marketplace. We will need to make it so that the NFT is displayed alongside a price in Pinnie Tokens. We will need a button that allows a user to purchase the NFT.

Before we can work on the frontend code, we need to create one more contract. For us to have a marketplace, we need a contract that can handle the marketplace creation and management. Let’s take care of that now.

In your cadence/contracts folder, create a new file called MarketplaceContract.cdc. The contract is larger than some of our others, so I’ll break it into a few code snippets and then reference the full contract when we’ve finished building it.

Start by adding the following to your file:

We are importing both our NFT contract and our fungible token contract as they will both work in conjunction with this marketplace contract. Inside the contract definition, we define four events: ForSale (indicates the NFT is for sale), PriceChanged (indicates a change in price for the NFT), TokenPurchased (indicates an NFT was purchased), and SaleWithdrawn (indicates an NFT was removed from the marketplace).

Below those event emitters, we have a resource interface called SalePublic. This is an interface we are going to make publicly available to anyone, not just the contract owner. Inside this interface, we are exposing three functions that we will write soon.

Next, below the SalePublic interface, we’re going to add a SaleCollection resource. This is the main focus of the contract, so unfortunately, I couldn’t easily break it into smaller pieces. This code snippet is longer than I’d like, but we’ll walk through it:

Within this resource, we are first defining a few variables. We define a mapping of tokens for sale called forSale, we define a mapping of prices for each token for sale in the prices variable, and then we define a protected variable that can only be accessed by the contract owner called ownerVault.

As usual, when we define variables on a resource, we need to initialize them. So we do that in our init function and simply initialize with empty values and the owner’s vault resource.

Next up is the meat of this resource. We are defining the functions that control all of our marketplace’s actions. Those functions are:

  • withdraw
  • listForSale
  • changePrice
  • purchase
  • idPrice
  • getIDs
  • destroy

If you remember, we previously had exposed only three of those functions publicly. That means, withdraw, listForSale, changePrice, and destroy are only available to the owner of the NFT being listed, which makes sense. We don’t want anyone being able to change an NFT’s price, for example.

The final part of our Marketplace contract is createSaleCollection function. This allows a collection to be added as a resource to an account. It looks like this and comes after the SaleCollection resource:pub fun createSaleCollection(ownerVault: Capability<&AnyResource{PinnieToken.Receiver}>): @SaleCollection {
 return <- create SaleCollection(vault: ownerVault)
}

Here’s the full contract for you to reference.

With that contract in place, let’s deploy it with our emulator account. From the root of your project, run:flow project deploy

This will deploy the Marketplace contract and will allow us to use it from within our frontend application. So, let’s get to updating the frontend app.

Frontend

As I mentioned before, we’re going to use the foundation we laid in the last post to build our marketplace. So in your project, you should already have a frontend directory. Change into that directory and let’s take a look at the App.js file.

Right now, we have authentication and the ability to fetch a single NFT and display its metadata. We want to replicate this but fetch all of the tokens stored within a Marketplace contract. We also want to enable purchasing capabilities. And if you own the token, you should be able to set the token for sale and change the price of the token.

We will want to change our TokenData.js file to support all this, so open that up. Replace everything in that file with this:

We’re hard-coding some values here, so in a real app be sure to think about how you would dynamically get info like account addresses. You’ll also notice that in the checkMarketplace function we’ve wrapped everything in a try/catch. This is because the fcl.send function will throw when there are no NFTs listed for sale.

If you start your frontend app by changing into the frontend directory and running npm start, you should see in the console “NO NFTs FOR SALE”.

Let’s fix that!

For the sake of brevity, we are going to list our minted NFT for sale via the Flow CLI tool. However, you could extend this tutorial to do so from the UI. In your root pinata-party project, and inside the transactions folder, create a file called ListTokenForSale.cdc. Inside that file, add the following:

In this transaction, we are importing all three of the contracts we created. We need the the PinnieToken receiver capability since we are accepting payment in PinnieTokens. We also need to get access to the createSaleCollection function on the MarketplaceContract. Then, we need to reference the NFT we want to put up for sale. We withdraw that NFT, we list it for sale for 10.0 PinnieTokens, and we save it into the NFTSale storage path.

If you run the following command, you should successfully be able to list the NFT your minted earlier.flow transactions execute --code transactions/ListTokenForSale.cdc

Now, go back to your React App page and refresh. In the console, you should see something like this:

This is an array of tokenIDs for the account address specified that are listed for sale. That means, we know what IDs to look up and fetch metadata for. In our simple example, there is only one token listed and it is the one and only token we created, so its tokenID is 1.

Before we add code to our React App, add the following import to the top of the TokenData.js file:

import * as t from "@onflow/types"

This allows us to pass in arguments to the scripts we send using fcl.

Ok, now we can take our array of tokenIDs and make use of some of the code we previously had to fetch token metadata. Inside the TokenData.js file and within the checkMarketplace function, add this after the decoded variable:

If you look in the console, you should now see an array of metadata associated specifically with the tokens we have for sale. The last thing we need to do before we can render anything is find out the prices for our listed tokens.

Right below the decodedMetadata variable and before the marketplaceMetadata.push(decodedMetadata) function, add the following:

We are getting the price for each NFT that has been listed, and when we receive the price back, we add it to the token metadata before pushing that metadata into the marketplaceMetadata array.

Now in your console, you should see something like this:

This is great! We can now render our listed tokens and show the price. Let’s do that now.

Under the console.log statement that shows the marketplaceMetadata array, add the following:setTokensToSell(marketplaceMetadata)

You will need to also add the following right below the start of the TokenData main function declaration:const TokenData = () => {  const [tokensToSell, setTokensToSell] = useState([])
}

With these things in place, you can render your marketplace. In the return statement, add the following:

If you’re using my styling, this is what I added to my App.css file:.listing {
 max-width: 30%;
 padding: 50px;
 margin: 2.5%;
 box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
}

Your app should now look something like this:

The last thing we need to do is connect that Buy Now button and allow someone who is not the NFT owner to purchase the NFT.

Buying the NFT

Normally, you would need to have wallet discovery and transaction handling happen through a remote discovery node endpoint. We actually set this up in our React app in part two of this series. However, we are working with a local Flow emulator. Because of that, we need to run a local dev wallet, and then we need to update our React app’s environment variables.

Let’s get this set up. First, clone the local dev wallet. From the root of your pinata-party project run:git clone git@github.com:onflow/fcl-dev-wallet.git

When that’s done, change into the folder:cd fcl-dev-wallet

Now, we need to copy the sample env file and create our local env file that the dev wallet will use:cp .env.example .env.local

Install dependencies:npm install

Ok, when that is done, open up the .env.local file. You’ll see it references an account and a private key. Earlier, we created a new account that would be buying the NFT from the marketplace. Change the account in the .env.local file to match that new account you created. Change the private key as well and public keys as well. For the FLOW_ACCOUNT_KEY_ID environment variable, change this to 1. The emulator-account is key 0.

Now, you can run npm run dev to start the wallet server.

Back in the frontend directory of the project, find the .env file, and let’s update REACT_APP_WALLET_DISCOVERY to point to http://localhost:3000/fcl/authz. After doing so, you’ll need to restart the React app.

The next step is to wire the frontend Buy Now button to actually send a transaction to purchase the token. Open up the TokenData.js file, and let’s create a buyToken function like this:

Now, we just need to add an onClick handler for our Buy Now button. This is as simple as updating the button to look like this:<button onClick={() => buyToken(1)} className="btn-primary">Buy Now</button>

We are hard-coding the tokenID here, but you could easily fetch that from our early scripts we executed.

Now, when you go to your React app and click the Buy Now, button, you should see a screen like this:

The fcl-dev-wallet, as it says in the header is in an alpha state, so the truth is, the execution of the transaction may or may not end up working. But getting this far illustrates that your app does work and the fcl library does work.

Conclusion

This has been an especially long post in a series of long posts, but I hope they help illustrate how to combine the power of IPFS and Flow to create NFTs that are backed by verifiable identifiers.

If you have trouble with this tutorial or any of the others, I highly recommend experimenting with the Flow Playground. It’s really phenomenal. You may also want to bypass the emulator testing and after working in the Playground start testing on Testnet.

No matter what you do, I hope you’ve come away armed with more knowledge of how we can propel the NFT space forward. If you’d like to access the full source code from all these tutorials, the repository is open source and available here.