Hi everyone, I’m Jay. I work as a developer on blockchain as well as an entrepreneur in the NFT sector. NFTs are growing rapidly in adoption which I’ve found to be really exciting.
We can see huge growth in trading volumes on NFT marketplaces like Rarible, OpenSea and Foundation, but these platforms are all built on ETH. I’ve recently been looking at another blockchain — Flow, a new blockchain built by Dapper Labs. The Flow blockchain is general purpose, but happens to be an especially great fit for making NFTs. Flow is engineered to scale — enabling developers to build scalable NFTs, like the massively popular NBA Top Shot which enables the buying and selling of video moments of NBA highlights. NBA Top Shot demonstrates Flow’s unique ability to support large-scale transactions, providing a better experience for developers and collectors. Given the flexibility and performance of Flow, there has been a strong growth in the community — with more and more applications being developed on Flow each day.
Because Flow is still early in its development, you can find more cutting-edge discussions around best practices for creating NFTs, such as the storage of metadata on-chain. In this issue, you can also see that many people would recommend using IPFS to store NFT asset files, and in this blogpost I’ll share with you how to create an NFT on a Flow powered by IPFS and Filecoin. We will create an NFT on the Flow, use IPFS to create an immutable reference to the asset files, and use Filecoin to ensure the data is preserved.
For most developers who want to focus on their development tasks, using IPFS and Filecoin directly can be a bit of a hassle. Fortunately, thanks to the extensive IPFS ecosystem there are tools that make this easier. Today I will use nft.storage as an example to store files on IPFS and Filecoin in a few lines of code.
Preparation
We will store the source file on IPFS and Filecoin using nft.storage. You can sign up for a free account here.
We also need to have NodeJS installed and a text editor that can help with syntax highlighting of Flow smart contract code (which is written in a language called Cadence). Visual Studio Code has an extension that supports Cadence.
Let’s create a directory to house our project.
mkdir nft-storage-and-flow
Change into that directory.
cd nft-storage-and-flow
After that I will create three folders. contracts
, scripts
and transactions
, I will create the main contract files in the contracts
folder, some scripts for queries in the scripts
folder, and the code for sending transactions in the transactions
folder.Of course, this is just my preference, the official staff also gave their advice.
All the code I stored in this repository
Our folder structure should look like the following
├── contracts
│ └── example_nft.cdc
├── scripts
│ └── get_account_info.cdc
└── transactions
└── mint_nft.cdc
3 directories, 3 files
Contract
Flow has a great tutorial on creating NFT contracts,but there is no enforceable standard for how to store metadata and the associated assets. If you want to store 4k video or AR/VR assets, you may need an alternative solution. If you’re familiar with NFTs on the Ethereum blockchain, you may know that many of the assets those tokens back are stored on traditional data stores and cloud hosting providers using centralized URLs. This works as someone makes sure that the content is available (and doesn’t swap out the content) — but eventually someone will stop paying for storage and that content may disappear.
I think this is only feasible in the short term, but longer term we need something better. We need a way of immutably referencing the assets on-chain regardless of where the content is stored, and we need to know that content won’t accidentally get deleted or become unavailable. This is where IPFS and Filecoin can help! When you add a file to IPFS you create a unique identifier for the content — which can be used to retrieve the content out of the IPFS network. This means the content could be stored anywhere (the cloud, a thumbdrive, a local machine) — so long as the correct bytes are there you can add that content back to the network and others can successfully retrieve it! Filecoin solves the permanence question — ensuring a set of storage providers are storing the data properly (and submitting cryptographic proofs that the data is still intact). Together, we’re able to create immutable references to our data, without worrying the content might go away!
Open up example_nft.cdc
.and let's get to work.
pub contract ExampleNFT: NonFungibleToken {
//you can extend these fields if you need
pub struct Metadata {
pub let name: String
pub let ipfsLink: String
init(name: String,ipfsLink: String){
self.name=name
//Stored in the ipfs
self.ipfsLink=ipfsLink
}
}
pub resource NFT: NonFungibleToken.INFT, Public {
pub let id: UInt64
pub let metadata: Metadata
init(initID: UInt64,metadata: Metadata) {
self.id = initID
self.metadata = metadata
}
}
}
The first step is to define our contract. We started by defining the Metadata
struct, which has the name
and ipfsLink
fields, but of course you can expand these fields, and it is very necessary to expand them if you use in your products.
Next,I created a resource.Resource is an item that is stored in a user account and accessible through access control measures. In this case, the NFT resource is ultimately owned because of the thing used to represent the NFT. The NFT must be uniquely identifiable. The id
property allows us to identify our tokens.
Next, we need to create a resource interface, which we will use to define which functions are available to others.
pub resource interface ExampleNFTCollectionPublic {
pub fun deposit(token: @NonFungibleToken.NFT)
pub fun getIDs(): [UInt64]
pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT
pub fun borrowArt(id: UInt64): &ExampleNFT.NFT? {
post {
(result == nil) || (result?.id == id):
"Cannot borrow ExampleNFT reference: The ID of the returned reference is incorrect"
}
}
}
Put it right below the NFT resource code. This ExampleNFTCollectionPublic
resource interface is saying that the person we define as having access to the resource will be able to call the following methods.
- deposit
- getIDs
- borrowNFT
- borrowArt
We have all the available features of the NFT collection resource. Please note that not all of these features are available for global use. If you recall, we have previously defined functions in our ExampleNFTCollectionPublic
resource interface that anyone can access.
pub resource Collection: ExampleNFTCollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken. CollectionPublic {
pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT}
pub fun withdraw(withdrawID: UInt64): @NonFungibleToken.NFT {
let token <- self.ownedNFTs.remove(key: withdrawID) ? panic("Missing NFT")
emit Withdraw(id: token.id, from: self.owner?.address)
return <-token
}
pub fun deposit(token: @NonFungibleToken.NFT) {
let token <- token as! @ExampleNFT.NFT
let id: UInt64 = token.id
let oldToken <- self.ownedNFTs[id] <- token
emit Deposit(id: id, to: self.owner?.address)
destroy oldToken
}
pub fun getIDs(): [UInt64] {
return self.ownedNFTs.keys
}
pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT {
return &self.ownedNFTs[id] as &NonFungibleToken.NFT
}
pub fun borrowArt(id: UInt64): &ExampleNFT.NFT? {
if self.ownedNFTs[id] ! = nil {
let ref = &self.ownedNFTs[id] as auth &NonFungibleToken.NFT
return ref as! &ExampleNFT.NFT
} else {
return nil
}
}
destroy() {
destroy self.ownedNFTs
}
init () {
self.ownedNFTs <- {}
}
}
Then, directly below the Collection resource, add the following.
pub fun createEmptyCollection(): @NonFungibleToken.Collection {
return <- create Collection()
}
pub resource NFTMinter {
pub fun mintNFT(
recipient: &{NonFungibleToken.CollectionPublic},
name: String,
ipfsLink: String) {
emit Minted(id: ExampleNFT.totalSupply, name: name, ipfsLink: ipfsLink)
recipient.deposit(token: <-create ExampleNFT.NFT(
initID: ExampleNFT.totalSupply,
metadata: Metadata(
name: name,
ipfsLink:ipfsLink,
)))
ExampleNFT.totalSupply = ExampleNFT.totalSupply + (1 as UInt64)
}
}
First, we have a function that creates an empty NFT set when it is called. This is how the user interacting with our contract for the first time creates a storage location that maps to the resource defined by Collection us.
After that, we create a resource. This is important because without it, we can’t mint tokens. This NFTMinter resource includes idCount whose incremental to make sure we never have duplicate totalSupply
for our NFT. it also has the functionality to actually create our NFT.
Initialization procedure:
init() {
self.CollectionStoragePath = /storage/ExampleNFTCollection
self.CollectionPublicPath = /public/ExampleNFTCollection
self.MinterStoragePath = /storage/ExampleNFTMinter
self.totalSupply = 0
let minter <- create NFTMinter()
self.account.save(<-minter, to: self.MinterStoragePath)
let collection <- ExampleNFT.createEmptyCollection()
self.account.save(<-collection, to: ExampleNFT.CollectionStoragePath)
self.account.link<&ExampleNFT.Collection{NonFungibleToken.CollectionPublic, ExampleNFT.ExampleNFTCollectionPublic}>(ExampleNFT.CollectionPublicPath, target: ExampleNFT.CollectionStoragePath)
emit ContractInitialized()
}
This initialization function is called only when the contract is deployed.
Creates an empty collection for the deployer of the collection so that the owner of the contract can cast and own the NFT from that contract.
The Collection references the interface we created at the beginning of ExampleNFTCollectionPublic
to publish the resource in a public location.
Use nft.storage
If we want to make it easier to use IPFS and Filecoin, I’d like to recommend trying nft.storage, it’s really convenient.
We can upload files directly through their http endpoints, docs here, and if you are a js developer, here is also the js sdk.Btw, you can use nft.storage as a remote pinning service in IPFS.
Before we start, we need to apply for an api key and note it down, the application is very simple, after registering and logging in, go to here to apply for it.
I will store nft.storage loge! Like this.
curl --location --request POST 'https://api.nft.storage/upload'
--header 'Authorization: Bearer YOUR_API_KEY'
--header 'Content-Type: image/png'
--data-binary '/jay/Downloads/nftstorage.png'
If it works, you might see
{
"ok": true,
"value": {
"cid": "bafkreibmgqtwvytkqmppk53dcl6ca6nav2z4hbxzcppyaufihpkc54rxyq",
"created": "2021-07-26T03:20:39.304Z",
"type": "application/car",
"scope": "session",
"files": [],
"size": 84768,
"pin": {
"cid": "bafkreibmgqtwvytkqmppk53dcl6ca6nav2z4hbxzcppyaufihpkc54rxyq",
"created": "2021-07-26T03:20:39.304Z",
"size": 84768,
"status": "pinned"
},
"deals": []
}
}
You’ve successfully stored images to IPFS and Filecoin, it’s really simple, isn’t it?
Mint NFT
It’s time to combine Flow, IPFS and Filecoin, we have the contract ready, let’s mint it!
We can test it on the Flow Playground(If you are a beginner in flow, you might be better off using playground).
First we need to deploy the official NFT contract ourselves, because our contract will have some references to it. The code is here. You just need to copy them to playground and deploy them. We can deploy to address 0x01, after successful deployment, if all goes well, you should see this log in the log window at the bottom of the screen.
11:10:49
Deployment
Deployed Contract To: 0x01
Then we need to deploy our contract, we can deploy it to address 0x02, make a distinction, after successful deployment, you should see this.
11:17:05
Deployment
Deployed Contract To: 0x02
Then we need to write a creation script, which will be
import NonFungibleToken from 0x01
import ExampleNFT from 0x02
transaction(recipient: Address,name: String,ipfsLink: String) {
let minter: &ExampleNFT.NFTMinter
prepare(signer: AuthAccount) {
self.minter = signer.borrow<&ExampleNFT.NFTMinter>(from: ExampleNFT.MinterStoragePath)
?? panic("Could not borrow a reference to the NFT minter")
}
execute {
let recipient = getAccount(recipient)
let receiver = recipient
.getCapability(ExampleNFT.CollectionPublicPath)!
.borrow<&{NonFungibleToken.CollectionPublic}>()
?? panic("Could not get receiver reference to the NFT Collection")
self.minter.mintNFT(recipient: receiver, name: name,ipfsLink:ipfsLink)
}
}
Then copy it into the page, fill in the required parameters, like this
Then click the send
button, congratulations, you have successfully created an nft on Flow with IPFS metadata.
Checking Account Status
We may also need a script that queries what NFT’s are available at a certain address, in that file, add the following.
import NonFungibleToken from 0x01
import ExampleNFT from 0x02
pub fun main(address:Address) : [ExampleNFT.NftData] {
let account = getAccount(address)
let nft = ExampleNFT.getNft(address: address)
return nft
}
This script can be thought of in a similar way to using read-only methods on an ethereum smart contract. They are free and only require data to be returned from the contract.
Now, we can call the function. In this case, we can see the nft under the specified address and see the metadata corresponding to it.
Look up the 0x02
address, you should be able to see it.
Congratulations! You have successfully created an NFT, this NFT contains the metadata stored on IPFS and Filecoin, and you can use scripts to query NFT status and data on chain!
See you in the next article 👋