Building a Flow NFT Pet Store : Part 1

  1. NFT and blockchain basic, understanding Flow and Cadence, and interacting with the smart contract using the Flow command line tool.
  2. Building a front-end React app and using the FCL library to interact with the smart contract.

Who this is for

Set up

What you will learn

  • Smart contracts with Cadence Language
  • User wallet authentication
  • Minting tokens and storing metadata on Filecoin/IPFS via NFT.storage
  • Transferring tokens

Understanding ownership and resource

  • Resource at play. In this case a currency.
  • Accounts to own the resource, or the access to it.
  • Contract or a ruleset to govern the economy.

Resource

Accounts

Contract

Quick tour of Cadence

Contract

Resources

Interfaces

pub contract PetShop {

// A map recording owners of NFTs
pub var owners: {UInt64 : Address}

// A Transferrable interface declaring some methods or "capabilities"
pub resource interface Transferrable {
pub fun owner(): Address
pub fun transferTo(recipient: Address)
}

// NFT resource implements Transferrable
pub resource NFT: Transferrable {

// Unique id for each NFT.
pub let id: UInt64

// Constructor method
init(initId: UInt64) {
self.id = initId
}

pub fun owner(): Address {
return owners[self.id]!
}

pub fun transferTo(recipient: Address) {
// Code to transfer this NFT resource to the recipient's address.
}
}
}

Script

// Takes a target token ID as a parameter and returns an 
// address of the current owner.
pub fun main(id: UInt64) : Address {
return PetStore.owner[id]!
}

Building pet store

npx create-react-app petstore; cd petstore
flow init

Project structure

{
// ...
"contracts": {
"PetStore": "./src/flow/contracts/PetStore.cdc"
},
"deployments": {
"emulator": {
"emulator-account": ["PetStore"]
}
},
// ...
}
mkdir -p src/flow/{contract,transaction,script}
.
|— flow
| |— contract
| | |
| | `— PetStore.cdc
| |— script
| | |
| | `— GetTokenIds.cdc
| `— transaction
| |
| `— MintToken.cdc
|
...

PetStore contract

pub contract PetStore {    // 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
}
}
}

NFTReceiver

pub contract PetStore {    // ...    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})
}
}

NFTCollection

pub contract PetStore {    // ... The @NFT code ...    // ... The @NFTReceiver code ...    pub resource NFTCollection: NFTReceiver {        // Keeps track of NFTs this collection.
access(account) var ownedNFTs: @{UInt64: NFT}
// Constructor
init() {
self.ownedNFTs <- {}
}
// Destructor
destroy() {
destroy self.ownedNFTs
}
// Withdraws and return 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()
}
}
for key in metadata.keys {
self.ownedNFTs[id]?.metadata?.insert(key: key, metadata[key]!)
}

NFTMinter

pub contract PetStore {    // ... 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.
PetStore.owners[self.idCount] = PetStore.account.address
// Increment the ID
self.idCount = self.idCount + 1 as UInt64
return <-token
}
}
}
pub contract PetStore {    // ... @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, accesible
// only by the contract owner's account.
self.account.save(<-create NFTMinter(), to: /storage/NFTMinter)
}
}

/storage

/private

/public

flow emulator> INFO[0000] ⚙️   Using service account 0xf8d6e0586b0a20c7  serviceAddress=f8d6e0586b0a20c7 serviceHashAlgo=SHA3_256 servicePrivKey=bd7a891abd496c9cf933214d2eab26b2a41d614d81fc62763d2c3f65d33326b0 servicePubKey=5f5f1442afcf0c817a3b4e1ecd10c73d151aae6b6af74c0e03385fb840079c2655f4a9e200894fd40d51a27c2507a8f05695f3fba240319a8a2add1c598b5635 serviceSigAlgo=ECDSA_P256
> INFO[0000] 📜 Flow contracts FlowFees=0xe5a8b7f23e8b548f FlowServiceAccount=0xf8d6e0586b0a20c7 FlowStorageFees=0xf8d6e0586b0a20c7 FlowToken=0x0ae53cb6e3f42a79 FungibleToken=0xee82856bf20e2aa6
> INFO[0000] 🌱 Starting gRPC server on port 3569 port=3569
> INFO[0000] 🌱 Starting HTTP server on port 8080 port=8080
flow project deploy> Deploying 1 contracts for accounts: emulator-account
>
> PetStore -> 0xf8d6e0586b0a20c7 (11e3afe90dc3a819ec9736a0a36d29d07a2f7bca856ae307dcccf4b455788710)
>
>
> ✨ All contracts deployed successfully

MintToken transaction

// MintToken.cdc// Import the `PetStore` contract instance from the master account address.
// This is a fixed address for used with the emulator only.
import PetStore from 0xf8d6e0586b0a20c7
transaction(metadata: {String: String}) { // Declare an "unauthorized" reference to `NFTReceiver` interface.
let receiverRef: &{PetStore.NFTReceiver}
// Declare an "authorized" reference to the `NFTMinter` interface.
let minterRef: &PetStore.NFTMinter
// `prepare` block always take one or more `AuthAccount` parameter(s) to indicate
// who are signing the transaction.
// It takes the account info of the user trying to execute the transaction and
// validate. In this case, the contract owner's account.
// 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(account: AuthAccount) {
// Note that we have to call `getCapability(_ domain: Domain)` on the account
// object before we can `borrow()`.
self.receiverRef = account.getCapability<&{PetStore.NFTReceiver}>(/public/NFTReceiver)
.borrow()
?? panic("Could not borrow receiver reference")
// With an authorized reference, we can just `borrow()` it.
// Note that `NFTMinter` is borrowed from `/storage` domain namespace, which
// means it is only accessible to this account.
self.minterRef = account.borrow<&PetStore.NFTMinter>(from: /storage/NFTMinter)
?? panic("Could not borrow minter reference")
}
// `execute` block executes after the `prepare` block is signed and validated.
execute {
// Mint the token by calling `mint(metadata: {String: String})` on `@NFTMinter` resource, which returns an `@NFT` resource, and move it to a variable `newToken`.
let newToken <- self.minterRef.mint(metadata)
// Call `deposit(token: @NFT)` on the `@NFTReceiver` resource to deposit the token.
// Note that this is where the metadata can be changed before transferring.
self.receiverRef.deposit(token: <-newToken)
}
}
flow transactions send src/flow/transaction/MintToken.cdc '{"name": "Max", "breed": "Bulldog"}'> Transaction ID: b10a6f2a1f1d88f99e562e72b2eb4fa3ae690df591d5a9111318b07b8a72e060
>
> Status ✅ SEALED
> ID b10a6f2a1f1d88f99e562e72b2eb4fa3ae690df591d5a9111318b07b8a72e060
> Payer f8d6e0586b0a20c7
> Authorizers [f8d6e0586b0a20c7]
> ...

TransferToken transaction

flow keys generate> 🔴️ Store private key safely and don't share with anyone!
> Private Key f410328ecea1757efd2e30b6bc692277a51537f30d8555106a3186b3686a2de6
> Public Key be393a6e522ae951ed924a88a70ae4cfa4fd59a7411168ebb8330ae47cf02aec489a7e90f6c694c4adf4c95d192fa00143ea8639ea795e306a27e7398cd57bd9
{
"private": "f410328ecea1757efd2e30b6bc692277a51537f30d8555106a3186b3686a2de6",
"public": "be393a6e522ae951ed924a88a70ae4cfa4fd59a7411168ebb8330ae47cf02aec489a7e90f6c694c4adf4c95d192fa00143ea8639ea795e306a27e7398cd57bd9"
}
flow accounts create —key <PUBLIC_KEY> —signer emulator-account> Transaction ID: b19f64d3d6e05fdea5dd2ac75832d16dc61008eeacb9d290f153a7a28187d016
>
> Address 0xf3fcd2c1a78f5eee
> Balance 0.00100000
> Keys 1
>
> ...
{
// ...
"accounts": {
"emulator-account": {
"address": "f8d6e0586b0a20c7",
"key": "bd7a891abd496c9cf933214d2eab26b2a41d614d81fc62763d2c3f65d33326b0"
},
"test-account": {
"address": "0xf3fcd2c1a78f5eee",
"key": <PRIVATE_KEY>
}
}
// ...
}
// InitCollection.cdcimport PetStore from 0xf8d6e0586b0a20c7// This transaction will be signed by any user account who wants to receive tokens.
transaction {
prepare(acct: AuthAccount) {
// Create a new empty collection for this account
let collection <- PetStore.NFTCollection.new()
// store the empty collection in this account storage.
acct.save<@PetStore.NFTCollection>(<-collection, to: /storage/NFTCollection)
// Link a public capability for the collection.
// This is so that the sending account can deposit the token to this account's
// collection by calling its `deposit(token: @NFT)` method.
acct.link<&{PetStore.NFTReceiver}>(/public/NFTReceiver, target: /storage/NFTCollection)
}
}
flow transactions send src/flow/transaction/InitCollection.cdc —signer test-account
// TransferToken.cdcimport PetStore from 0xf8d6e0586b0a20c7// This transaction transfers a token from one user's
// collection to another user's collection.
transaction(tokenId: UInt64, recipientAddr: Address) {
// The field holds the NFT as it is being transferred to the other account.
let token: @PetStore.NFT
prepare(account: AuthAccount) { // Create a reference to a borrowed `NFTCollection` capability.
// Note that because `NFTCollection` is publicly defined in the contract, any account can access it.
let collectionRef = account.borrow<&PetStore.NFTCollection>(from: /storage/NFTCollection)
?? panic("Could not borrow a reference to the owner's collection")
// Call the withdraw function on the sender's Collection to move the NFT out of the collection
self.token <- collectionRef.withdraw(id: tokenId)
}
execute {
// Get the recipient's public account object
let recipient = getAccount(recipientAddr)
// This is familiar since we have done this before in the last `MintToken` transaction block.
let receiverRef = recipient.getCapability<&{PetStore.NFTReceiver}>(/public/NFTReceiver)
.borrow()
?? panic("Could not borrow receiver reference")
// Deposit the NFT in the receivers collection
receiverRef.deposit(token: <-self.token)
// Save the new owner into the `owners` dictionary for look-ups.
PetStore.owners[tokenId] = recipientAddr
}
}
flow transactions send src/flow/transaction/TransferToken.cdc 1 0xf3fcd2c1a78f5eee> Transaction ID: 4750f983f6b39d87a1e78c84723b312c1010216ba18e233270a5dbf1e0fdd4e6
>
> Status ✅ SEALED
> ID 4750f983f6b39d87a1e78c84723b312c1010216ba18e233270a5dbf1e0fdd4e6
> Payer f8d6e0586b0a20c7
> Authorizers [f8d6e0586b0a20c7]
>
> ...

GetTokenOwner script

// GetTokenOwner.cdcimport PetStore 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 = PetStore.owners[id]!
return ownerAddress
}
flow scripts execute src/flow/script/GetTokenOwner.cdc <TOKEN_ID>

GetTokenMetadata script

// GetTokenMetadata.cdcimport PetStore 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 = PetStore.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<&{PetStore.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)
}
flow scripts execute src/flow/script/GetTokenMetadata.cdc <TOKEN_ID>

GetAllTokenIds (Bonus)

// GetAllTokenIds.cdcimport PetStore from 0xPetStorepub fun main() : [UInt64] {
// We basically just return all the UInt64 keys of `owners`
// dictionary as an array to get all IDs of all tokens in existence.
return PetStore.owners.keys
}

Wrapping up

Check out Pan’s original tutorial.

In a hurry to get to part 2? Find the original version on NFT School.

--

--

--

The Filecoin Foundation (FF) is an independent organization dedicated to developing Filecoin and related technologies. The Foundation does this by coordinating

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Filecoin Foundation

Filecoin Foundation

The Filecoin Foundation (FF) is an independent organization dedicated to developing Filecoin and related technologies. The Foundation does this by coordinating

More from Medium

Why .ETH names?

Metaverse — Another Revolution?

NFT Trading Platform, ‘HancomArtpia’, Has Launched

What is Metaverse, called the platform of the future?