If you’ve followed along with the Flow series so far, you already know that the Flow Blockchain excels in handling digital assets, such as NFTs. It was built from the ground up as a better alternative to Ethereum’s network congestion and high fee issues.
In addition, the Cadence smart contract language is a first-of-its-kind resource-oriented programming language that makes creating and managing digital assets easy and efficient. Although Solidity is excellent at facilitating Web3 through smart contracts, there are drawbacks. Cadence improves upon Solidity’s flaws by providing the ability to upgrade smart contracts and features that reduce the risk of human error, among other improvements.
And finally, the list of tools and libraries available to developers looking to get started is extensive. So let’s put it all together and build something on Flow.
This article is a tutorial about creating a full-fledged NFT-minting dapp for the Flow Blockchain.
For the rest of this article, we will walk through the process of creating an NFT minting dapp on the Flow blockchain.
We will start with setting up and deploying a Cadence smart contract. Then, we will build a front end to connect to our smart contract and mint an NFT into the user’s account.
The functionality we build will allow users to connect their Flow account, create an account if they don’t already have one, then select from one of three images to mint into an NFT. Then, the dapp will display the NFTs from our collection that are in the user’s account. It will be an excellent project to highlight how easy and efficient creating NFTs are on Flow and how effective the Flow Client Library (FCL) is for interacting with the blockchain.
To follow along with this tutorial, you’ll need the following things:
With all of these installed, let’s get started!
Before we start building, we’ll need to set up an account on the Flow blockchain so we can deploy our smart contract. Run the following command to generate a new public and private key pair:
flow keys generate
Be sure to write down the values your console outputs, as we’ll need them in the following steps.
Next, we’ll head over to Flow Faucet to create a new address based on our keys and fund our account with some test tokens. Complete the following steps to create your account:
With a successful account generation, we get a dialogue with our new Flow address containing 1,000 FLOW tokens.
Copy the address for use in the next step.
Before we build the project frontend, let’s create the smart contract we will interact with later.
In the command terminal, navigate to the folder you would like to work from and type the following command to initiate a project:
flow init
This command creates a flow.json
file inside the folder, where we’ll place all the information we need to deploy our smart contract.
Open the flow.json
file in your code editor and we’ll set up a testnet account. Inside the accounts
section, we’ll add a new entry called testnet-account
, which contains our new address and the private key generated in the flow keys generate
command earlier.
{
"emulators": {
"default": {
"port": 3569,
"serviceAccount": "emulator-account"
}
},
"contracts": {},
"networks": {
"emulator": "127.0.0.1:3569",
"mainnet": "access.mainnet.nodes.onflow.org:9000",
"testnet": "access.devnet.nodes.onflow.org:9000"
},
"accounts": {
"emulator-account": {
"address": "f8d6e0586b0a20c7",
"key": "2becfbede2fb89796ab68df3ec2a23c3627235ec250a3e5da41df850a8dd4349"
},
"testnet-account": {
"address": "0x8e0dac5df6e8489e",
"key": "c91f4716a51a66683ccb090ca3eb3e213b90e9f9ae2b1edd12defffe06c57edc"
}
},
"deployments": {}
}
Next, we’ll create a new file to write our smart contract.
When writing out the code, you may notice some differences in how Cadence handles NFT creation compared to Solidity. For example, NFTs in Cadence are created as a resource and minted directly into the account of the user. In contrast, Solidity NFTs are essentially just an ID number referenced in a mapping to a specific address on the digital ledger.
So with that in mind, in the same folder as the flow.json file, create a new file called FlowTutorialMint.cdc and type the following code:
/*
*
* This is an example implementation of a Flow Non-Fungible Token.
* This contract does not implement any sophisticated classification
* system for its NFTs. It defines a simple NFT with minimal metadata.
*
*/
import NonFungibleToken from 0x631e88ae7f1d7c20
import MetadataViews from 0x631e88ae7f1d7c20
pub contract FlowTutorialMint: NonFungibleToken {
pub var totalSupply: UInt64
pub event ContractInitialized()
pub event Withdraw(id: UInt64, from: Address?)
pub event Deposit(id: UInt64, to: Address?)
pub let CollectionStoragePath: StoragePath
pub let CollectionPublicPath: PublicPath
pub let MinterStoragePath: StoragePath
pub struct FlowTutorialMintData{
pub let id: UInt64
pub let type: String
pub let url: String
init(_id: UInt64, _type: String, _url: String){
self.id = _id
self.type = _type
self.url = _url
}
}
pub resource NFT: NonFungibleToken.INFT, MetadataViews.Resolver {
pub let id: UInt64
pub let type: String
pub let url: String
init(
id: UInt64,
type: String,
url: String,
) {
self.id = id
self.type = type
self.url = url
}
pub fun getViews(): [Type] {
return [ Type<FlowTutorialMintData>() ]
}
pub fun resolveView(_ view: Type): AnyStruct? {
switch view {
case Type<FlowTutorialMintData>():
return FlowTutorialMintData(
_id: self.id,
_type: self.type,
_url: self.url
)
}
return nil
}
}
pub resource interface FlowTutorialMintCollectionPublic {
pub fun deposit(token: @NonFungibleToken.NFT)
pub fun getIDs(): [UInt64]
pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT
pub fun borrowFlowTutorialMint(id: UInt64): &FlowTutorialMint.NFT? {
post {
(result == nil) || (result?.id == id):
"Cannot borrow FlowTutorialMint reference: the ID of the returned reference is incorrect"
}
}
}
pub resource Collection: FlowTutorialMintCollectionPublic, NonFungibleToken.Provider, NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection {
// dictionary of NFT conforming tokens
// NFT is a resource type with an `UInt64` ID field
pub var ownedNFTs: @{UInt64: NonFungibleToken.NFT}
init () {
self.ownedNFTs <- {}
}
// withdraw removes an NFT from the collection and moves it to the caller
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
}
// deposit takes an NFT and adds it to the collections dictionary
// and adds the ID to the id array
pub fun deposit(token: @NonFungibleToken.NFT) {
let token <- token as! @FlowTutorialMint.NFT
let id: UInt64 = token.id
// add the new token to the dictionary which removes the old one
let oldToken <- self.ownedNFTs[id] <- token
emit Deposit(id: id, to: self.owner?.address)
destroy oldToken
}
// getIDs returns an array of the IDs that are in the collection
pub fun getIDs(): [UInt64] {
return self.ownedNFTs.keys
}
// borrowNFT gets a reference to an NFT in the collection
// so that the caller can read its metadata and call its methods
pub fun borrowNFT(id: UInt64): &NonFungibleToken.NFT {
return (&self.ownedNFTs[id] as &NonFungibleToken.NFT?)!
}
pub fun borrowFlowTutorialMint(id: UInt64): &FlowTutorialMint.NFT? {
if self.ownedNFTs[id] != nil {
// Create an authorized reference to allow downcasting
let ref = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
return ref as! &FlowTutorialMint.NFT
}
return nil
}
pub fun borrowViewResolver(id: UInt64): &AnyResource{MetadataViews.Resolver} {
let nft = (&self.ownedNFTs[id] as auth &NonFungibleToken.NFT?)!
let flowTutorialMintNFT = nft as! &FlowTutorialMint.NFT
return flowTutorialMintNFT as &AnyResource{MetadataViews.Resolver}
}
destroy() {
destroy self.ownedNFTs
}
}
// public function that anyone can call to create a new empty collection
pub fun createEmptyCollection(): @NonFungibleToken.Collection {
return <- create Collection()
}
pub fun mintNFT(
recipient: &{NonFungibleToken.CollectionPublic},
type: String,
url: String,
) {
// create a new NFT
var newNFT <- create NFT(
id: FlowTutorialMint.totalSupply,
type: type,
url: url
)
// deposit it in the recipient's account using their reference
recipient.deposit(token: <-newNFT)
FlowTutorialMint.totalSupply = FlowTutorialMint.totalSupply + UInt64(1)
}
init() {
// Initialize the total supply
self.totalSupply = 0
// Set the named paths
self.CollectionStoragePath = /storage/flowTutorialMintCollection
self.CollectionPublicPath = /public/flowTutorialMintCollection
self.MinterStoragePath = /storage/flowTutorialMintMinter
// Create a Collection resource and save it to storage
let collection <- create Collection()
self.account.save(<-collection, to: self.CollectionStoragePath)
// create a public capability for the collection
self.account.link<&FlowTutorialMint.Collection{NonFungibleToken.CollectionPublic, FlowTutorialMint.FlowTutorialMintCollectionPublic, MetadataViews.ResolverCollection}>(
self.CollectionPublicPath,
target: self.CollectionStoragePath
)
emit ContractInitialized()
}
}
Important things to note in the smart contract above:
NonFungibleToken
and MetadataViews
contracts to create our NFTs using Flow standardspub resource NFT
functionmintNFT
function mints an NFT into the account that calls the function
Now we need to go back into our flow.json
file to add a few things:
contracts
section, add the contract and its path.deployments
section add the network (testnet
), the account that we will use to perform the deployment (testnet-account)
, and the contract name (FlowTutorialMint
).
{
"emulators": {
"default": {
"port": 3569,
"serviceAccount": "emulator-account"
}
},
"contracts": {
"FlowTutorialMint": "./FlowTutorialMint.cdc"
},
"networks": {
"emulator": "127.0.0.1:3569",
"mainnet": "access.mainnet.nodes.onflow.org:9000",
"testnet": "access.devnet.nodes.onflow.org:9000"
},
"accounts": {
"emulator-account": {
"address": "f8d6e0586b0a20c7",
"key": "2becfbede2fb89796ab68df3ec2a23c3627235ec250a3e5da41df850a8dd4349"
},
"testnet-account": {
"address": "0x8e0dac5df6e8489e",
"key": "c91f4716a51a66683ccb090ca3eb3e213b90e9f9ae2b1edd12defffe06c57edc"
}
},
"deployments": {
"testnet": {
"testnet-account": [
"FlowTutorialMint"
]
}
}
}
The final step in setting up the smart contract is to deploy it to the testnet. To do that, type the following command in the project folder in your terminal:
flow project deploy -n=testnet
We should receive an output stating the contract was deployed successfully:
It’s important to note here that Cadence smart contracts exist in the storage of the account that deploys them, whereas with Solidity, the smart contract exists at its own address on the blockchain.
Although there are limits to the account’s storage capacity, these are relative to the amount of FLOW tokens reserved in the account. You can learn more about account storage in the Flow Developer Portal.
Awesome! Now let’s build a simple frontend to interact with our contract.
For the frontend of this project, we will be using React. First, navigate to a new folder and run the following command to create a React project:
npx create-react-app flow-tutorial
Next, navigate into the flow-tutorial folder and install the Flow Client Library (FCL):
npm i -S @onflow/fcl
The FCL will allow us to communicate with the Flow blockchain, call transactions, and integrate all other FCL-compatible wallets without needing to add custom integrations. Once that finishes, we will install a few additional dependencies:
npm i elliptic sha3 styled-components
After installing all our dependencies, we are ready to start working on the dapp frontend.
Before we begin structuring and styling things, let’s create an FCL configuration file where we’ll define important settings, such as whether we will interact with testnet or mainnet.
In the src
directory, create a new folder named flow
. Within this new folder, create a file called config.js.
In this config.js
file, we will import the FCL, call the fcl.config
function and create some settings for our dapp, such as:
Open the config.js
file and fill it with the following code:
const fcl = require("@onflow/fcl");
fcl.config({
"app.detail.title": "Flow Mint Page Tutorial", // this adds a custom name to our wallet
"accessNode.api": "https://rest-testnet.onflow.org", // this is for the local emulator
"discovery.wallet": "https://fcl-discovery.onflow.org/testnet/authn", // this is for the local dev wallet
})
There are additional settings we can configure for our dapp, but for now, this is all we will need.
With the configuration out of the way, let’s move on to building!
First, navigate to the App.js
file in the src
folder and replace the code with this:
import './App.css';
function App() {
return (
<div className="App">
<h1>Mint Your Dog!</h1>
</div>
);
}
export default App;
This will give us the initial structure of our dapp, from which we will expand upon.
Next, we’ll style this structure. Open the index.css
file and replace the code with the following:
@import url('https://fonts.googleapis.com/css2?family=Michroma&family=Montserrat:wght@200;300;600;700&display=swap');
body {
margin: 0;
font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
If you run npm start, you will see a blank page with the title Mint Your Dog!
Next, let’s create some components!
Inside the src
directory, create a new folder called components
, where we will build all of our custom React components.
The first component we will create is the Navbar, which will show the Login button if the user isn’t connected, or the Logout button next to the user’s address and the number of FLOW tokens the account has if they are connected.
Create a file called Navbar.jsx
and fill it with the following code:
import * as fcl from "@onflow/fcl";
import styled from "styled-components";
import { useState, useEffect } from "react";
import "../flow/config";
const Wrapper = styled.nav`
width: -webkit-fill-available;
background-color: #8dfe89;
position: fixed;
top: 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 50px;
button {
background-color: white;
padding: 5px 40px;
max-width: 200px;
border: none;
border-radius: 20px;
font-size: 18px;
height: 50px;
&:hover {
color: white;
background-color: black;
cursor: pointer;
}
}
div {
display: flex;
gap: 15px;
}
box {
display: flex;
flex-direction: column;
gap: 10px;
}
`;
function Navbar() {
const [user, setUser] = useState({ loggedIn: false, addr: undefined });
const [flow, setFlow] = useState(0);
useEffect(() => {
fcl.currentUser.subscribe(setUser);
if (user.addr !== "") getFlow(user.addr);
}, [user.addr]);
const logOut = async () => {
await fcl.unauthenticate();
setUser({ addr: undefined, loggedIn: false });
};
const logIn = async () => {
await fcl.authenticate();
};
async function getFlow(address) {
try {
const res = await fcl.query({
cadence: `
import FlowToken from 0x7e60df042a9c0868
import FungibleToken from 0x9a0766d93b6608b7
pub fun main(address: Address): UFix64{
let balanceVault = getAccount(address).getCapability(/public/flowTokenBalance).borrow<&FlowToken.Vault{FungibleToken.Balance}>()!
return balanceVault.balance
}`,
args: (arg, t) => [arg(address, t.Address)],
});
setFlow(res);
} catch (error) {
console.log("err:", error);
}
}
return (
<Wrapper>
<h1>Flow Tutorial Mint</h1>
{user.loggedIn ? (
<div>
<button onClick={() => logOut()}>Logout</button>
<box>
<span>Address - {user.addr}</span>
<span>Flow Balance - {flow}</span>
</box>
</div>
) : (
<button onClick={() => logIn()}>Login</button>
)}
</Wrapper>
);
}
export default Navbar;
Let’s walk through the code to see what’s going on here.
authenticate
, unauthenticate
, and determine the currentUser
.Wrapper
variable.user
and flow
).html
for the Navbar wrapped in our styling.
With a complete Navbar
component, we can now import it into the App.js
file:
import './App.css';
import Navbar from './components/Navbar.jsx';
function App() {
return (
<div className="App">
<Navbar />
<h1>Mint your Dog!</h1>
</div>
);
}
export default App;
Now, if we run the project with npm start
, we see our Navbar
gives us the functionality we defined in our code. Awesome!
Next, let’s build our NFT minting component!
Inside the components
folder, create a new file called MintComponent.jsx
, then copy the following code:
import styled from "styled-components";
import * as fcl from "@onflow/fcl";
const Wrapper = styled.div`
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
justify-content: center;
margin-top: 80px;
padding: 100px;
main{
display: flex;
}
div{
width: 300px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 5px;
}
button{
width: 100px;
padding: 10px;
border: none;
background-color: #8dfe89;
border-radius: 20px;
font-weight: 500;
&:hover {
color: white;
background-color: black;
cursor: pointer;
}
}
img{
width: 200px;
}
`;
function MintComponent() {
async function mintNFT(type, url) {
try {
const res = await fcl.mutate({
cadence: `
import FlowTutorialMint from 0x8e0dac5df6e8489e
import NonFungibleToken from 0x631e88ae7f1d7c20
import MetadataViews from 0x631e88ae7f1d7c20
transaction(type: String, url: String){
let recipientCollection: &FlowTutorialMint.Collection{NonFungibleToken.CollectionPublic}
prepare(signer: AuthAccount){
if signer.borrow<&FlowTutorialMint.Collection>(from: FlowTutorialMint.CollectionStoragePath) == nil {
signer.save(<- FlowTutorialMint.createEmptyCollection(), to: FlowTutorialMint.CollectionStoragePath)
signer.link<&FlowTutorialMint.Collection{NonFungibleToken.CollectionPublic, MetadataViews.ResolverCollection}>(FlowTutorialMint.CollectionPublicPath, target: FlowTutorialMint.CollectionStoragePath)
}
self.recipientCollection = signer.getCapability(FlowTutorialMint.CollectionPublicPath)
.borrow<&FlowTutorialMint.Collection{NonFungibleToken.CollectionPublic}>()!
}
execute{
FlowTutorialMint.mintNFT(recipient: self.recipientCollection, type: type, url: url)
}
}
`,
args: (arg, t) => [arg(type, t.String), arg(url, t.String)],
limit: 9999,
});
fcl.tx(res).subscribe((res) => {
if (res.status === 4 && res.errorMessage === "") {
window.alert("NFT Minted!")
window.location.reload(false);
}
});
console.log("txid", res);
} catch (error) {
console.log("err", error);
}
}
return (
<Wrapper>
<h1>Mint your Dog!</h1>
<main>
<div>
<img src="https://images.unsplash.com/photo-1517849845537-4d257902454a" alt="Mad Dog"/>
<h3>Mad Dog</h3>
<button onClick={() => mintNFT("Mad Dog", "https://images.unsplash.com/photo-1517849845537-4d257902454a")}>Mint</button>
</div>
<div>
<img src="https://images.unsplash.com/photo-1517423568366-8b83523034fd" alt="Swag Dog"/>
<h3>Swag Dog</h3>
<button onClick={() => mintNFT("Swag Dog", "https://images.unsplash.com/photo-1517423568366-8b83523034fd")}>Mint</button>
</div>
<div>
<img src="https://images.unsplash.com/photo-1517519014922-8fc06b814a0e" alt="French Dog"/>
<h3>French Dog</h3>
<button onClick={() => mintNFT("French Dog", "https://images.unsplash.com/photo-1517519014922-8fc06b814a0e")}>Mint</button>
</div>
</main>
</Wrapper>
)
}
export default MintComponent;
Again, let’s walk through the code to ensure we understand what’s going on.
styled-components
to add some styling.mintNFT
function uses the fcl.mutate
function to perform the actual mint by:
fcl.mutate
function, we are importing the smart contract we deployed with the line: import FlowTutorialMint from 0x8e0dac5df6e8489e
NonFngibleToken
and MetadataViews
standards.type
and url
of the image.prepare
and execute
prepare
– we ask for the user’s signature to access their account and perform private functions. In this case, creating a new FlowTutorial
Mint collection if they don’t already have one. We also initialize a public Capability
restricted to NonFungibleToken.CollectionPublic
. For more context on Capabilities, check out this link.execute
– call the mintNFT
function inside of our contract on the testnet.html
portion of the code, we display three images from which the user can mint an NFT.
With our MintComponent
complete, we can import it into the App.js
file:
import './App.css';
import Navbar from './components/Navbar.jsx';
import MintComponent from './components/MintComponent.jsx';
function App() {
return (
<div className="App">
<Navbar />
<h1>Mint your Dog!</h1>
<MintComponent />
</div>
);
}
export default App;
Now the user can log into the dapp and mint an NFT to their account!
The final piece of the puzzle is to create a component that will fetch the user’s NFTs and display them.
In the components
folder, create a new file called ShowNfts.jsx
, and we will use the following code:
import * as fcl from "@onflow/fcl";
import { useState, useEffect } from "react";
import styled from "styled-components";
const Wrapper = styled.div`
background-color: #e5e5e5;
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
justify-content: center;
padding: 50px;
button {
width: 100px;
padding: 10px;
border: none;
background-color: #8dfe89;
border-radius: 10px;
font-weight: 700;
&:hover {
color: white;
background-color: black;
cursor: pointer;
}
}
section {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 30px;
padding: 10%;
}
.nftDiv{
padding: 10px;
background-color: #141414;
border-radius: 20px;
color: white;
box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.25);
img{
width: 140px;
border-radius: 10px;
}
p{
font-size: 14px;
}
}
`;
export default function ShowNfts() {
const [nfts, setNfts] = useState([]);
const [user, setUser] = useState({ loggedIn: false, addr: undefined });
useEffect(() => {
fcl.currentUser.subscribe(setUser);
getNFTs(user.addr)
}, [user.addr]);
async function getNFTs(addr) {
try {
const result = await fcl.query({
cadence: `
import FlowTutorialMint from 0x8e0dac5df6e8489e
import MetadataViews from 0x631e88ae7f1d7c20
pub fun main(address: Address): [FlowTutorialMint.FlowTutorialMintData] {
let collection = getAccount(address).getCapability(FlowTutorialMint.CollectionPublicPath)
.borrow<&{MetadataViews.ResolverCollection}>()
?? panic("Could not borrow a reference to the nft collection")
let ids = collection.getIDs()
let answer: [FlowTutorialMint.FlowTutorialMintData] = []
for id in ids {
let nft = collection.borrowViewResolver(id: id)
let view = nft.resolveView(Type<FlowTutorialMint.FlowTutorialMintData>())!
let display = view as! FlowTutorialMint.FlowTutorialMintData
answer.append(display)
}
return answer
}
`,
args: (arg, t) => [arg(addr, t.Address)],
});
setNfts(result);
} catch (error) {
console.log("err", error);
}
}
return (
<Wrapper>
<h1>My NFTs</h1>
<main>
<button onClick={() => getNFTs(user.addr)}>Get NFTs</button>
<section>
{nfts.map((nft, index) => {
return (
<div key={index} className="nftDiv">
<img src={nft.url} alt="nft" />
<p>Type: {nft.type}</p>
<p>Id: {nft.id}</p>
</div>
);
})}
</section>
</main>
</Wrapper>
);
}
Essentially what we are doing in this code is querying the Flow Blockchain using the FCL, and gathering the NFTs in the connected account that are from our FlowTutorialMint collection.
We just need to add this component to our App.js
, and we are good to go!
import './App.css';
import Navbar from './components/Navbar.jsx';
import MintComponent from './components/MintComponent.jsx';
import ShowNfts from './components/ShowNfts';
function App() {
return (
<div className="App">
<Navbar />
<h1>Mint your Dog!</h1>
<MintComponent />
<ShowNfts />
</div>
);
}
export default App;
That’s everything! Now let’s test our dapp and make sure we can mint some NFTs.
So first, let’s start the app with npm start
and then open our browser to http://localhost:3000/.
If everything goes well, your screen should look like this:
The beautiful thing about using the FCL in our Login sequence is that it gives our users easy access to making an account right on the spot using just an email address. Let's walk through the process to make sure it works properly. By clicking the Login button, a dialogue will pop up, giving us two options to login with. We will choose Blocto.
Blocto will prompt us to enter an email address and, upon doing so, gives us the ability to Register a new account. Then, once we input the code emailed to our address, Blocto sets us up with a shiny, new Flow address!
From here, we can choose which dog image we want to mint as an NFT. I chose the Swag Dog because it reminds me a bit of myself!
Pressing the Mint button will pop up another dialogue telling us about the transaction we are about to perform. We can see that Blocto is graciously covering the minting fees, and if we want to look at the script we are calling, we can do so.
Several seconds after hitting Approve, we should receive a message that our mint was successful, and our newly minted Swag Dog will display under the My NFTs section of our dapp.
Here’s a link to our dapp in action:
https://s1.gifyu.com/images/flow_tutorial-min.gif
The entire source code for this project can be found in this repository.
As you can see, building an NFT minting dapp on the Flow Blockchain is straightforward once you understand how it all works together. Additionally, the Flow Client Library is a powerful tool at our disposal that gives us access to extensive built-in functionality and helps give our dapp a better user experience.
In contrast to Ethereum, Flow handles NFT creation and management much more efficiently and securely. This is achieved by deploying smart contracts and minting the NFTs directly into the user’s account, rather than creating a reference to addresses or mappings stored on the digital ledger.
For more information about building on Flow, check out the Flow Developer Portal.
Have a really great day!