NFT Marketplace Part 1
Introduction
Welcome to the first part of our four-part series on building an NFT Marketplace. This tutorial aims to equip you with the knowledge and tools to create a robust NFT platform.
In the first part, you learn:
- The concepts of FA, IPFS, and smart contracts.
- How to build an NFT Marketplace from the ligo/fa library.
This training course is provided by Marigold. You can find the 4 parts on github (solution + materials to build the UI)
Key Concepts
What is FA?
Business objects managed by a blockchain are called assets. On Tezos you find the term Financial Asset or FA with different versions 1, 2, or 2.1.
What is IPFS?
The InterPlanetary File System is a protocol and peer-to-peer network for storing and sharing data in a distributed file system. IPFS uses content-addressing to uniquely identify each file in a global namespace connecting all computing devices. This tutorial is using Pinata (free developer plan) to store the metadata for NFTs. An alternative would be to install a local IPFS node or an API gateway backend with a usage quota.
Smart Contracts Overview
There are two contracts for the marketplace.
1. The token contract
On Tezos, FA2 is the standard for Non-Fungible Token contracts. The template provided by Ligo will be used to build out the Token Contract. The template contains the basic entrypoints for building a Fungible or Non-fungible token including:
- Transfer
- Balance_of
- Update_operators
2. Marketplace unique contract
Next, you need to import the token contract into the marketplace unique contract. The latter is bringing missing features as:
- Mint
- Buy
- Sell
Wine marketplace
After grasping the key concepts, we'll proceed to build a Wine marketplace extending the @ligo/fa
package from the Ligo repository. The goal is to showcase how to extend an existing smart contract and build a frontend on top of it.
The Wine marketplace is adding these features on top of a generic NFT contract :
- mint new wine bottles
- update wine bottle metadata details
- buy wine bottles
- sell wine bottles
You can play with the final demo.
Token template | # of token_type | # of item per token_type |
---|---|---|
NFT | 0..n | 1 |
single asset | 0..1 | 1..n |
multi asset | 0..n | 1..n |
Because of web3, buy or sell features are a real payment system using on-chain XTZ tokens as money. This differs from traditional web2 applications where you have to integrate a payment system and so, pay extra fees
Prerequisites
Before building an NFT marketplace, ensure you have the following tools on hand.
Required
- npm: front-end is a TypeScript React client app
- taqueria >= v0.40.0 : Tezos app project tooling
- Docker: needed for
taqueria
- jq: extract
taqueria
JSON data
Recommended
VS Code
: as code editoryarn
: to build and run the front-end (see this article for more details about differences betweennpm
andyarn
)- ligo VS Code extension: for smart contract highlighting, completion, etc.
- Temple wallet: an easy to use Tezos wallet in your browser (or any other one with ghostnet support)
Optional
- taqueria VS Code extension: visualize your project and execute tasks
Smart Contract Modification
Use Taqueria to shape the project structure, then create the NFT marketplace smart contract thanks to the ligo/fa
library.
You require to copy some code from this git repository later, so you can clone it with:
git clone https://github.com/marigold-dev/training-nft-1.git
Step 1: Taq'ify your project
First, set up our smart contract structure.
taq init training
cd training
taq install @taqueria/plugin-ligo
Your project is ready!
Step 2: FA2 contract
Next, you need to build the FA2 contract which relies on the Ligo FA library. To understand in detail how assets work on Tezos, please read the notes below.
-
Additional contract metadata can be added to ease displaying token pictures, etc., this is described in the TZIP-21 standard
Install the ligo/fa
library locally:
echo '{ "name": "app", "dependencies": { "@ligo/fa": "^1.0.8" } }' >> ligo.json
TAQ_LIGO_IMAGE=ligolang/ligo:1.0.0 taq ligo --command "install @ligo/fa"
Step 3: NFT marketplace contract
Then, create the NFT marketplace contract with taqueria
taq create contract nft.jsligo
Remove the default code and paste this code instead
#import "@ligo/fa/lib/fa2/nft/nft.impl.jsligo" "FA2Impl"
/* ERROR MAP FOR UI DISPLAY or TESTS
const errorMap : map<string,string> = Map.literal(list([
["0", "Enter a positive and not null amount"],
["1", "Operation not allowed, you need to be administrator"],
["2", "You cannot sell more than your current balance"],
["3", "Cannot find the offer you entered for buying"],
["4", "You entered a quantity to buy than is more than the offer quantity"],
["5", "Not enough funds, you need to pay at least quantity * offer price to get the tokens"],
["6", "Cannot find the contract relative to implicit address"],
]));
*/
export type storage = {
administrators: set<address>,
ledger: FA2Impl.NFT.ledger,
metadata: FA2Impl.TZIP16.metadata,
token_metadata: FA2Impl.TZIP12.tokenMetadata,
operators: FA2Impl.NFT.operators
};
type ret = [list<operation>, storage];
Explanations:
- the first line
#import "@ligo/fa/lib/fa2/nft/nft.impl.jsligo" "FA2Impl"
imports the Ligo FA library implementation that your code is extending. Then, add new entrypoints to the base code. storage
definition is an extension of the imported library storage. You need to point to the original types keeping the same namingFA2Impl.NFT.ledger
: keep/trace ownership of tokensFA2Impl.TZIP16.metadata
: tzip-16 complianceFA2Impl.TZIP12.tokenMetadata
: tzip-12 complianceFA2Impl.NFT.operators
: permissions part of FA2 standard
storage
has more fields to support a set ofadministrators
The contract compiles, now let's write transfer,balance_of,update_operators
entrypoints. You do a passthrough call to the underlying library.
@entry
const transfer = (p: FA2Impl.TZIP12.transfer, s: storage): ret => {
const ret2: [list<operation>, FA2Impl.NFT.storage] =
FA2Impl.NFT.transfer(
p,
{
ledger: s.ledger,
metadata: s.metadata,
token_metadata: s.token_metadata,
operators: s.operators,
}
);
return [
ret2[0],
{
...s,
ledger: ret2[1].ledger,
metadata: ret2[1].metadata,
token_metadata: ret2[1].token_metadata,
operators: ret2[1].operators,
}
]
};
@entry
const balance_of = (p: FA2Impl.TZIP12.balance_of, s: storage): ret => {
const ret2: [list<operation>, FA2Impl.NFT.storage] =
FA2Impl.NFT.balance_of(
p,
{
ledger: s.ledger,
metadata: s.metadata,
token_metadata: s.token_metadata,
operators: s.operators,
}
);
return [
ret2[0],
{
...s,
ledger: ret2[1].ledger,
metadata: ret2[1].metadata,
token_metadata: ret2[1].token_metadata,
operators: ret2[1].operators,
}
]
};
@entry
const update_operators = (p: FA2Impl.TZIP12.update_operators, s: storage): ret => {
const ret2: [list<operation>, FA2Impl.NFT.storage] =
FA2Impl.NFT.update_operators(
p,
{
ledger: s.ledger,
metadata: s.metadata,
token_metadata: s.token_metadata,
operators: s.operators,
}
);
return [
ret2[0],
{
...s,
ledger: ret2[1].ledger,
metadata: ret2[1].metadata,
token_metadata: ret2[1].token_metadata,
operators: ret2[1].operators,
}
]
};
Explanation:
- every
FA2Impl.NFT.xxx()
called function is taking the storage type of the NFT library, so you need to send a partial object from our storage definition to match the type definition - the return type contains also the storage type of the library, so you need to reconstruct the storage by copying the modified fields
The LIGO team is working on merging type definitions, so you then can do type union or merge 2 objects like in Typescript
Let's add the Mint
function now. Add the new function
@entry
const mint = (
[token_id, name, description, symbol, ipfsUrl]: [
nat,
bytes,
bytes,
bytes,
bytes
],
s: storage
): ret => {
if (! Set.mem(Tezos.get_sender(), s.administrators)) return failwith("1");
const token_info: map<string, bytes> =
Map.literal(
list(
[
["name", name],
["description", description],
["interfaces", (bytes `["TZIP-12"]`)],
["artifactUri", ipfsUrl],
["displayUri", ipfsUrl],
["thumbnailUri", ipfsUrl],
["symbol", symbol],
["decimals", (bytes `0`)]
]
)
) as map<string, bytes>;
return [
list([]) as list<operation>,
{
...s,
ledger: Big_map.add(token_id, Tezos.get_sender(), s.ledger) as
FA2Impl.NFT.ledger,
token_metadata: Big_map.add(
token_id,
{ token_id: token_id, token_info: token_info },
s.token_metadata
),
operators: Big_map.empty as FA2Impl.NFT.operators,
}
]
};
Explanation:
mint
function allows you to create a unique NFT. You have to declare the name, description, symbol, and ipfsUrl for the picture to display- to simplify, the code here does not manage the increment of the token_id here it is done by the front end later. You should manage this counter on-chain to avoid overriding an existing NFT. There is no rule to allocate a specific number to the token_id but people increment it from 0. Also, there is no rule if you have a burn function to reallocate the token_id to a removed index and just continue the sequence from the greatest index.
- most of the fields are optional except
decimals
that is set to0
. A unique NFT does not have decimals, it is a unit - by default, the
quantity
for an NFT is1
, that is why every bottle is unique and there is no need to set a total supply on each NFT. - if you want to know the
size of the NFT collection
, you need an indexer on the frontend side. It is not possible to have this information on the contract (because big_map has not a .keys() function returning the keys) unless you add and additional element on the storage to cache it
Smart contract implementation for this first training is finished, let's prepare the deployment to ghostnet.
Compile the file to create a default taqueria initial storage and parameter file
TAQ_LIGO_IMAGE=ligolang/ligo:1.0.0 taq compile nft.jsligo
Edit the new storage file nft.storageList.jsligo
as it. (⚠️ you can change the administrator
address to your own address or keep alice address tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb
)
#import "nft.jsligo" "Contract"
const default_storage : Contract.storage = {
administrators: Set.literal(
list(["tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb" as address])
) as set<address>,
ledger: Big_map.empty as Contract.FA2Impl.NFT.ledger,
metadata: Big_map.literal(
list(
[
["", bytes `tezos-storage:data`],
[
"data",
bytes
`{
"name":"FA2 NFT Marketplace",
"description":"Example of FA2 implementation",
"version":"0.0.1",
"license":{"name":"MIT"},
"authors":["Marigold<contact@marigold.dev>"],
"homepage":"https://marigold.dev",
"source":{
"tools":["Ligo"],
"location":"https://github.com/ligolang/contract-catalogue/tree/main/lib/fa2"},
"interfaces":["TZIP-012"],
"errors": [],
"views": []
}`
]
]
)
) as Contract.FA2Impl.TZIP16.metadata,
token_metadata: Big_map.empty as Contract.FA2Impl.TZIP12.tokenMetadata,
operators: Big_map.empty as Contract.FA2Impl.NFT.operators,
};
Compile and deploy to ghostnet
TAQ_LIGO_IMAGE=ligolang/ligo:1.0.0 taq compile nft.jsligo
taq install @taqueria/plugin-taquito
taq deploy nft.tz -e "testing"
If this is the first time you're using taqueria, you may want to run through this training.
For advanced users, just go to
.taq/config.local.testing.json
and change the default account by alice one's (publicKey,publicKeyHash,privateKey) and then redeploy:{
"networkName": "ghostnet",
"accounts": {
"taqOperatorAccount": {
"publicKey": "edpkvGfYw3LyB1UcCahKQk4rF2tvbMUk8GFiTuMjL75uGXrpvKXhjn",
"publicKeyHash": "tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb",
"privateKey": "edsk3QoqBuvdamxouPhin7swCvkQNgq4jP5KZPbwWNnwdZpSpJiEbq"
}
}
}
Deploy again
taq deploy nft.tz -e "testing"
┌──────────┬──────────────────────────────────────┬───────┬──────────────────┬────────────────────────────────┐
│ Contract │ Address │ Alias │ Balance In Mutez │ Destination │
├──────────┼───────────────────────────────────── ─┼───────┼──────────────────┼────────────────────────────────┤
│ nft.tz │ KT18sgGX5nu4BzwV2JtpQy4KCqc8cZU5MwnN │ nft │ 0 │ https://ghostnet.ecadinfra.com │
└──────────┴──────────────────────────────────────┴───────┴──────────────────┴────────────────────────────────┘
Backend is finished!
NFT Marketplace frontend
This section guides you step-by-step in setting up an intuitive frontend.
Step 1: Get the react boilerplate
To save time, a boilerplate ready for the UI is ready for you.
Copy this code into your folder (⚠️ assuming you have cloned this repo and your current path is $REPO/training
)
cp -r ../reactboilerplateapp/ ./app
Note : if you want to understand how it has been made from scratch look at this training
It is easier on frontend side to use typed objects. Taqueria provides a plugin to generate Typescript classes from your Michelson code.
Install the plugin, then generate a representation of your smart contract objects that writes these files to your frontend app source code.
Finally, run the server
taq install @taqueria/plugin-contract-types
taq generate types ./app/src
cd app
yarn install
yarn dev
Note : On a Mac 🍏, sometimes
sed
commands do not work exactly the same as Unix commands. Look at the start script on package.json for Mac below :"dev": "if test -f .env; then sed -i '' \"s/\\(VITE_CONTRACT_ADDRESS *= *\\).*/\\1$(jq -r 'last(.tasks[]).output[0].address' ../.taq/testing-state.json)/\" .env ; else jq -r '\"VITE_CONTRACT_ADDRESS=\" + last(.tasks[]).output[0].address' ../.taq/testing-state.json > .env ; fi && vite",
The website is ready! You have:
- last deployed contract address always is refreshed from taqueria configuration at each start
- login/logout
- the general layout / navigation
If you try to connect you are redirected to /
path that is also the wine catalog.
There are no bottle collections yet, so you have to create the mint page.
Step 2: Mint Page
Edit default mint Page on ./src/MintPage.tsx
Add a form to create the NFT
In MintPage.tsx
, replace the HTML template starting with <Paper>
with this one :
<Paper>
{storage ? (
<Button
disabled={storage.administrators.indexOf(userAddress! as address) < 0}
sx={{
p: 1,
position: "absolute",
right: "0",
display: formOpen ? "none" : "block",
zIndex: 1,
}}
onClick={toggleDrawer(!formOpen)}
>
{" Mint Form " +
(storage!.administrators.indexOf(userAddress! as address) < 0
? " (You are not admin)"
: "")}
<OpenWithIcon />
</Button>
) : (
""
)}
<SwipeableDrawer
onClose={toggleDrawer(false)}
onOpen={toggleDrawer(true)}
anchor="right"
open={formOpen}
variant="temporary"
>
<Toolbar
sx={
isTablet
? { marginTop: "0", marginRight: "0" }
: { marginTop: "35px", marginRight: "125px" }
}
/>
<Box
sx={{
width: isTablet ? "40vw" : "60vw",
borderColor: "text.secondary",
borderStyle: "solid",
borderWidth: "1px",
height: "calc(100vh - 64px)",
}}
>
<Button
sx={{
position: "absolute",
right: "0",
display: !formOpen ? "none" : "block",
}}
onClick={toggleDrawer(!formOpen)}
>
<Close />
</Button>
<form onSubmit={formik.handleSubmit}>
<Stack spacing={2} margin={2} alignContent={"center"}>
<Typography variant="h5">Mint a new collection</Typography>
<TextField
id="standard-basic"
name="token_id"
label="token_id"
value={formik.values.token_id}
disabled
variant="filled"
/>
<TextField
id="standard-basic"
name="name"
label="name"
required
value={formik.values.name}
onChange={formik.handleChange}
error={formik.touched.name && Boolean(formik.errors.name)}
helperText={formik.touched.name && formik.errors.name}
variant="filled"
/>
<TextField
id="standard-basic"
name="symbol"
label="symbol"
required
value={formik.values.symbol}
onChange={formik.handleChange}
error={formik.touched.symbol && Boolean(formik.errors.symbol)}
helperText={formik.touched.symbol && formik.errors.symbol}
variant="filled"
/>
<TextField
id="standard-basic"
name="description"
label="description"
required
multiline
minRows={2}
value={formik.values.description}
onChange={formik.handleChange}
error={
formik.touched.description &&
Boolean(formik.errors.description)
}
helperText={
formik.touched.description && formik.errors.description
}
variant="filled"
/>
{pictureUrl ? (
<img height={100} width={100} src={pictureUrl} />
) : (
""
)}
<Button variant="contained" component="label" color="primary">
<AddCircleOutlined />
Upload an image
<input
type="file"
hidden
name="data"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const data = e.target.files ? e.target.files[0] : null;
if (data) {
setFile(data);
}
e.preventDefault();
}}
/>
</Button>
<Button variant="contained" type="submit">
Mint
</Button>
</Stack>
</form>
</Box>
</SwipeableDrawer>
<Typography variant="h5">Mint your wine collection</Typography>
{nftContratTokenMetadataMap.size != 0 ? (
"//TODO"
) : (
<Typography sx={{ py: "2em" }} variant="h4">
Sorry, there is not NFT yet, you need to mint bottles first
</Typography>
)}
</Paper>
Inside your MintPage
Component function, all all following elements :
- A
formik
form :
const validationSchema = yup.object({
name: yup.string().required("Name is required"),
description: yup.string().required("Description is required"),
symbol: yup.string().required("Symbol is required"),
});
const formik = useFormik({
initialValues: {
name: "",
description: "",
token_id: 0,
symbol: "WINE",
} as TZIP21TokenMetadata,
validationSchema: validationSchema,
onSubmit: (values) => {
mint(values);
},
});
Now, add pictureUrl
and setFile
declaration to display the token image after pinning it to IPFS, and to get the upload file on the form:
const [pictureUrl, setPictureUrl] = useState<string>("");
const [file, setFile] = useState<File | null>(null);
Add drawer variables to manage the side popup of the form:
//open mint drawer if admin
const [formOpen, setFormOpen] = useState<boolean>(false);
useEffect(() => {
if (storage && storage.administrators.indexOf(userAddress! as address) < 0)
setFormOpen(false);
else setFormOpen(true);
}, [userAddress]);
const toggleDrawer =
(open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => {
if (
event.type === "keydown" &&
((event as React.KeyboardEvent).key === "Tab" ||
(event as React.KeyboardEvent).key === "Shift")
) {
return;
}
setFormOpen(open);
};
Finally, fix the missing imports at the beginning of the file :
import { AddCircleOutlined, Close } from "@mui/icons-material";
import OpenWithIcon from "@mui/icons-material/OpenWith";
import {
Box,
Button,
Stack,
SwipeableDrawer,
TextField,
Toolbar,
useMediaQuery,
} from "@mui/material";
import Paper from "@mui/material/Paper";
import Typography from "@mui/material/Typography";
import { useFormik } from "formik";
import React, { useEffect, useState } from "react";
import * as yup from "yup";
import { TZIP21TokenMetadata, UserContext, UserContextType } from "./App";
import { address } from "./type-aliases";
Add mint missing function
First, add the mint
function and related imports :
import { useSnackbar } from "notistack";
import { BigNumber } from "bignumber.js";
import { address, bytes, nat } from "./type-aliases";
import { char2Bytes } from "@taquito/utils";
import { TransactionInvalidBeaconError } from "./TransactionInvalidBeaconError";
Add the mint
function inside your MintPage
Component function
const { enqueueSnackbar } = useSnackbar();
const mint = async (newTokenDefinition: TZIP21TokenMetadata) => {
try {
//IPFS
if (file) {
const formData = new FormData();
formData.append("file", file);
const requestHeaders: HeadersInit = new Headers();
requestHeaders.set(
"pinata_api_key",
`${import.meta.env.VITE_PINATA_API_KEY}`
);
requestHeaders.set(
"pinata_secret_api_key",
`${import.meta.env.VITE_PINATA_API_SECRET}`
);
const resFile = await fetch(
"https://api.pinata.cloud/pinning/pinFileToIPFS",
{
method: "post",
body: formData,
headers: requestHeaders,
}
);
const responseJson = await resFile.json();
console.log("responseJson", responseJson);
const thumbnailUri = `ipfs://${responseJson.IpfsHash}`;
setPictureUrl(
`https://gateway.pinata.cloud/ipfs/${responseJson.IpfsHash}`
);
const op = await nftContrat!.methods
.mint(
new BigNumber(newTokenDefinition.token_id) as nat,
char2Bytes(newTokenDefinition.name!) as bytes,
char2Bytes(newTokenDefinition.description!) as bytes,
char2Bytes(newTokenDefinition.symbol!) as bytes,
char2Bytes(thumbnailUri) as bytes
)
.send();
//close directly the form
setFormOpen(false);
enqueueSnackbar(
"Wine collection is minting ... it will be ready on next block, wait for the confirmation message before minting another collection",
{ variant: "info" }
);
await op.confirmation(2);
enqueueSnackbar("Wine collection minted", { variant: "success" });
refreshUserContextOnPageReload(); //force all app to refresh the context
}
} catch (error) {
console.table(`Error: ${JSON.stringify(error, null, 2)}`);
let tibe: TransactionInvalidBeaconError = new TransactionInvalidBeaconError(
error
);
enqueueSnackbar(tibe.data_message, {
variant: "error",
autoHideDuration: 10000,
});
}
};
Note : organize/fix duplicated import declarations if necessary
Explanations:
- on Mint button click, upload a file and then call the pinata API to push the file to IPFS. It returns the hash
- hash is used in two different ways
- https pinata gateway link (or any other ipfs http viewer)
- ipfs link for the backend thumbnail url
- TZIP standard requires storing data in
bytes
. As there is no Michelson function to convert string to bytes (using Micheline data PACK is not working, as it alters the final bytes), do the conversion usingchar2Bytes
on the frontend side
Note : Finally, if you remember on the backend, token_id increment management was done in the ui, so you can write this code. It is not a good security practice as it supposes that the counter is managed on frontend side, but it is ok for demo purpose.
Add this code inside your MintPage
Component function , every time you have a new token minted, you increment the counter for the next one
useEffect(() => {
(async () => {
if (nftContratTokenMetadataMap && nftContratTokenMetadataMap.size > 0) {
formik.setFieldValue("token_id", nftContratTokenMetadataMap.size);
}
})();
}, [nftContratTokenMetadataMap?.size]);
Display all minted bottles
Replace the "//TODO"
keyword with this template
<Box sx={{ width: "70vw" }}>
<SwipeableViews
axis="x"
index={activeStep}
onChangeIndex={handleStepChange}
enableMouseEvents
>
{Array.from(nftContratTokenMetadataMap!.entries()).map(
([token_id, token]) => (
<Card
sx={{
display: "block",
maxWidth: "80vw",
overflow: "hidden",
}}
key={token_id.toString()}
>
<CardHeader
titleTypographyProps={
isTablet ? { fontSize: "1.5em" } : { fontSize: "1em" }
}
title={token.name}
/>
<CardMedia
sx={
isTablet
? {
width: "auto",
marginLeft: "33%",
maxHeight: "50vh",
}
: { width: "100%", maxHeight: "40vh" }
}
component="img"
image={token.thumbnailUri?.replace(
"ipfs://",
"https://gateway.pinata.cloud/ipfs/"
)}
/>
<CardContent>
<Box>
<Typography>{"ID : " + token_id}</Typography>
<Typography>{"Symbol : " + token.symbol}</Typography>
<Typography>
{"Description : " + token.description}
</Typography>
</Box>
</CardContent>
</Card>
)
)}
</SwipeableViews>
<MobileStepper
variant="text"
steps={Array.from(nftContratTokenMetadataMap!.entries()).length}
position="static"
activeStep={activeStep}
nextButton={
<Button
size="small"
onClick={handleNext}
disabled={
activeStep ===
Array.from(nftContratTokenMetadataMap!.entries()).length - 1
}
>
Next
<KeyboardArrowRight />
</Button>
}
backButton={
<Button
size="small"
onClick={handleBack}
disabled={activeStep === 0}
>
<KeyboardArrowLeft />
Back
</Button>
}
/>
</Box>
Finally, your imports at beginning of the file should be like this :
import SwipeableViews from "react-swipeable-views";
import OpenWithIcon from "@mui/icons-material/OpenWith";
import {
Box,
Button,
CardHeader,
CardMedia,
MobileStepper,
Stack,
SwipeableDrawer,
TextField,
Toolbar,
useMediaQuery,
} from "@mui/material";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import {
AddCircleOutlined,
Close,
KeyboardArrowLeft,
KeyboardArrowRight,
} from "@mui/icons-material";
import Paper from "@mui/material/Paper";
import Typography from "@mui/material/Typography";
import { useFormik } from "formik";
import React, { useEffect, useState } from "react";
import * as yup from "yup";
import { TZIP21TokenMetadata, UserContext, UserContextType } from "./App";
import { useSnackbar } from "notistack";
import { BigNumber } from "bignumber.js";
import { address, bytes, nat } from "./type-aliases";
import { char2Bytes } from "@taquito/utils";
import { TransactionInvalidBeaconError } from "./TransactionInvalidBeaconError";
and some variables inside your MintPage
Component function
const [activeStep, setActiveStep] = React.useState(0);
const handleNext = () => {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
};
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};
const handleStepChange = (step: number) => {
setActiveStep(step);
};
Let's play
- Connect with your wallet and choose alice account (or the administrator you set on the smart contract earlier). You are redirected to the Administration /mint page as there is no NFT minted yet.
- Create your first wine bottle, for example:
name
: Saint Emilion - Franc la Rosesymbol
: SEMILdescription
: Grand cru 2007
- Click on Upload an image and select a bottle picture on your computer
- Click on the Mint button
Your picture is be pushed to IPFS and displayed.
Then, Temple Wallet (or whatever other wallet you choose) asks you to sign the operation. Confirm it, and less than 1 minute after the confirmation notification, the page is automatically refreshed to display your wine collection with your first NFT!
Now you can see all NFTs
Summary
You are able to create an NFT collection marketplace from the ligo/fa
library.
On next training, you will add the buy and sell functions to your smart contract and update the frontend to allow these actions.
To continue, let's go to Part 2.