Introduction
On this information, we’re going to create an version the place folks will have the ability to enroll their wallets to an allowlist and people wallets can later mint the NFT!
Sounds cool, proper!? Let’s get began.
Setup
I’m going to make use of the Next typescript starter template of thirdweb for this information. If you have already got a Subsequent.js app you’ll be able to merely observe these steps to get began:
- Set up
@thirdweb-dev/react
and@thirdweb-dev/sdk
- Add metamask authentication to the location. You possibly can observe this information
- You all are set to go now!
If you’re additionally going to make use of the Next TypeScript template then observe these steps
- Click on on the use template button within the repo
- Clone the repo
- Set up the dependencies:
npm set up # npm
yarn set up # yarn
By default the community in _app.tsx
is Mainnet, we have to change it to Mumbai
import kind { AppProps } from 'subsequent/app';
import { ChainId, ThirdwebProvider } from '@thirdweb-dev/react';
// That is the chainId your dApp will work on.
const activeChainId = ChainId.Mumbai;
operate MyApp({ Element, pageProps }: AppProps) {
return (
<ThirdwebProvider desiredChainId={activeChainId}>
<Element {...pageProps} />
</ThirdwebProvider>
);
}
export default MyApp;
Creating an version and NFT
We additionally must create an version and an NFT within the version to have the ability to mint them to the customers. So, go to the thirdweb dashboard and create an version. I’m doing it with an Version contract for this demo however it may be additionally achieved with certainly one of one NFTs (ERC-721).
Fill out the small print and deploy the contract!
Now, let’s create and mint a brand new NFT within it. Fill out the small print and ensure to set the preliminary provide to 0.
Making the web site
Creating an nft card
To showcase our nft, we’re going to create a easy card and we’ll get the information from the version contract itself! So, inside index.tsx
add the useEdition hook like this:
const version = useEdition("EDITION_CONTRACT_ADDRESS");
We have to import it from the thirdweb sdk:
import { useEdition } from "@thirdweb-dev/react";
Now, we’re going to create a brand new state to carry the nft knowledge:
const [nftData, setNftData] = useState<EditionMetadata["metadata"] | null>(
null
);
We additionally must import EditionMetadata and useState:
import { EditionMetadata } from "@thirdweb-dev/sdk";
import { useState } from "react";
Lastly, create a useEffect to get the information from the contract and replace the variable:
useEffect(() => {
version?.get("0").then((nft) => {
setNftData(nft.metadata);
});
}, [edition]);
If you happen to console log the nft metadata you’re going to get one thing like this:
If the person is logged in we’ll present them this nft as a substitute of there tackle, so substitute the fragment (<>)
with this:
<div>
{nftData?.picture && (
<Picture
src={nftData?.picture}
alt={nftData?.title}
width="280"
top="210"
objectFit="include"
/>
)}
<p>
<span>Title:</span> {nftData?.title}
</p>
<p>
<span> Description:</span> {nftData?.description}
</p>
<button>Mint</button>
</div>
You’ll now get an error as a result of we’re utilizing the subsequent <Picture />
element however have not whitelisted the area, so go to subsequent.config.js
and add the next:
/** @kind {import('subsequent').NextConfig} */
const nextConfig = {
reactStrictMode: true,
photographs: {
domains: ["gateway.ipfscdn.io"],
},
};
module.exports = nextConfig;
Since we’re altering the subsequent.js config we have to restart the server. When you restart the server you will note one thing like this
Creating the mint api
We’re going to use the signature minting on our backend to make sure that the tackle is current within the airtable, however first let’s get the signature minting working.
Create a brand new folder api
within the pages folder and generate-mint-sig.ts
inside it.
We’ll now construct a fundamental api that can output “gm wagmi”
import kind { NextApiRequest, NextApiResponse } from "subsequent";
const generateMintSignature = async (
req: NextApiRequest,
res: NextApiResponse
) => {
res.ship("gm wagmi");
};
export default generateMintSignature;
This creates a fundamental api for us, in case you now go to the api/generate-mint-sig
endpoint you’re going to get a response of Gm wagmi
.
Let’s now initialize the thirdweb sdk!
const sdk = new ThirdwebSDK(
new ethers.Pockets(
course of.env.PRIVATE_KEY as string,
ethers.getDefaultProvider(course of.env.ALCHEMY_API_URL)
)
);
as you’ll be able to see we’re utilizing 2 surroundings variables to initialize the sdk. The primary is the PRIVATE_KEY
which is the personal key of the pockets. The second is the ALCHEMY_API_URL
which is the url of the Alchemy api. Create a brand new file .env.native
and add the 2 variables.
PRIVATE_KEY=<private_key>
ALCHEMY_API_URL=<alchemy_api_url>
Let’s examine methods to get these two variables.
Getting the pockets personal key
In your metamask pockets, click on on the three dots, then click on account particulars. You will note an choice to export personal key there. Export your personal key and paste it into the PRIVATE_KEY
variable.
It will give full entry to your pockets to ensure to maintain it secret!
Getting the alchemy api key
Go to Alchemy and join an account. You must create a brand new app, so click on on create app. Fill out the small print, and hit submit. I’m utilizing Mumbai community for this demo however it is advisable use the community that you’re utilizing!
Lastly, paste this within the .env.native
file too. Since we’ve got modified the env variables we have to restart the server. So, lower the terminal and run yarn dev
once more.
We additionally must import ethers
and ThirdwebSDK
:
import { ThirdwebSDK } from "@thirdweb-dev/sdk";
import { ethers } from "ethers";
Producing the siganture mint
Within the api we’re going to use the sdk to get entry to the version contract and generate a mint signature:
const version = sdk.getEdition("EDITION_CONTRACT_ADDRESS");
attempt {
const signedPayload = await version.signature.generate({
tokenId: 0,
amount: "1",
to: tackle,
});
res.standing(200).json({
signedPayload: signedPayload,
});
} catch (err) {
res.standing(500).json({
error: err,
});
}
We’ll get the tackle from the frontend so, add this:
const { tackle } = JSON.parse(req.physique);
Let’s now mint the api on the frontend by calling this api:
const mintWithSignature = async () => {
const signedPayloadReq = await fetch(`/api/generate-mint-sig`, {
technique: "POST",
physique: JSON.stringify({ tackle }),
});
const signedPayload = await signedPayloadReq.json();
attempt {
const nft = await version?.signature.mint(signedPayload.signedPayload);
return nft;
} catch (err) {
console.error(err);
return null;
}
};
This operate will request the api that we simply constructed with the tackle. It should give us a signed payload that we will use to mint the nft through our version contract. So, let’s connect it to the button:
<button onClick={() => mintWithSignature()}>Mint</button>
Now our mint works! However wait, everybody can mint now so let’s create an airtable database to retailer the addresses that may mint.
Creating an allowlist with airtable
Go to Airtable and create a brand new base.
After you create a brand new base, give a reputation to your base and add two columns: Deal with
and Minted
like this:
Getting airtable api keys and id
We now must get some api key’s and id’s to work together with the bottom. So, go to your Airtable account and generate an api key
Retailer this api key someplace secure as we’re going to want it.
Now to get the bottom id go to the Airtable API and click on on the bottom that you simply simply created. While you open the web page, on the high itself you’ll see “Your base id is app……”.
Inside .env.native
add three new variables:
AIRTABLE_API_KEY=
AIRTABLE_BASE_ID=
AIRTABLE_TABLE_NAME=
Making a utility operate for accessing the desk
To maintain our code clear we’re going to create a file the place we initialize the airtable with the api key, title, and id. So, create a brand new folder utils
and Airtable.ts
inside it. Now, add within the following in Airtable.ts
:
import Airtable from "airtable";
// Authenticate
Airtable.configure({
apiKey: course of.env.AIRTABLE_API_KEY,
});
// Initialize a base
const base = Airtable.base(course of.env.AIRTABLE_BASE_ID!);
// Reference a desk
const desk = base(course of.env.AIRTABLE_TABLE_NAME!);
export { desk };
As you’ll be able to see we’re going to want to put in a brand new package deal referred to as airtable
:
npm i airtable # npm
yarn add airtable # yarn
Now, we have to replace the api to first verify if the pockets is current within the desk. So, in api/generate-mint-sig.ts
add the next:
const document = await desk
.choose({
fields: ["Addresses", "minted"],
filterByFormula: `NOT({Addresses} != '${tackle}')`,
})
.all();
if (document.size === 0) {
res.standing(404).json({
error: "Person is not in allowlist",
});
}
So, this queries the airtable to get the data of the tackle current within the addresses column and we’re checking if the size document is 0. Whether it is 0, then we’re not permitting the person to mint and ship an error. We may also wrap the half the place we ship the response in an else block. The api ought to now look just like this:
import kind { NextApiRequest, NextApiResponse } from "subsequent";
import { ThirdwebSDK } from "@thirdweb-dev/sdk";
import { ethers } from "ethers";
import { desk } from "../../utils/Airtable";
const generateMintSignature = async (
req: NextApiRequest,
res: NextApiResponse
) => {
const { tackle } = JSON.parse(req.physique);
const document = await desk
.choose({
fields: ["Addresses", "minted"],
filterByFormula: `NOT({Addresses} != '${tackle}')`,
})
.all();
if (document.size === 0) {
res.standing(404).json({
error: "Person is not in allowlist",
});
} else {
const sdk = new ThirdwebSDK(
new ethers.Pockets(
course of.env.PRIVATE_KEY as string,
ethers.getDefaultProvider(course of.env.ALCHEMY_API_URL)
)
);
const version = sdk.getEdition(
"0x62C84CC051544c43d05a5Ff0E8Da596fBdB15032"
);
attempt {
const signedPayload = await version.signature.generate({
tokenId: 0,
amount: "1",
to: tackle,
});
res.standing(200).json({
signedPayload: signedPayload,
});
} catch (err) {
res.standing(500).json({
error: err,
});
}
}
};
export default generateMintSignature;
Within the mintWithSignature operate we’ll now add a easy alert to point out the error if the person shouldn’t be within the allowlist.
const mintWithSignature = async () => {
const signedPayloadReq = await fetch(`/api/generate-mint-sig`, {
technique: "POST",
physique: JSON.stringify({ tackle }),
});
const signedPayload = await signedPayloadReq.json();
if (signedPayload.error) {
alert(signedPayload.error);
return;
}
attempt {
const nft = await version?.signature.mint(signedPayload.signedPayload);
return nft;
} catch (err) {
console.error(err);
return null;
}
};
Now in case you attempt minting you must now see an error that you’re not within the allowlist. However in case you add the tackle you might be utilizing to mint to the tackle column, it might assist you to mint!
Setting the minted to true when the tackle mints
We’ll create one other api to not expose the api keys, so within the api
folder create a brand new file set-minted.ts
and add the next:
import kind { NextApiRequest, NextApiResponse } from "subsequent";
import { desk } from "../../utils/Airtable";
const generateMintSignature = async (
req: NextApiRequest,
res: NextApiResponse
) => {
const { tackle } = JSON.parse(req.physique);
const document = await desk
.choose({
fields: ["Addresses", "minted"],
filterByFormula: `NOT({Addresses} != '${tackle}')`,
})
.all();
attempt {
document[0].updateFields({
minted: "true",
});
res.standing(200).json({
success: true,
});
} catch (err) {
res.standing(500).json({
error: err,
});
}
};
export default generateMintSignature;
Principally what this code block is doing is getting the document of the tackle after which updating the minted column to true. And for fundamental error dealing with if there’s an error we’ll return the error. Now, within the mintSignature operate I’ll add the fetch request to the set-minted
api if the nft was minted profitable like this:
if (nft) {
await fetch(`/api/set-minted`, {
technique: "POST",
physique: JSON.stringify({ tackle }),
});
}
If you happen to attempt minting once more and verify the airtable it might now set the minted column to true! If you happen to get an error just like the column title not discovered then guarantee that the names are identical.
Including customers to allowlist
I’m going so as to add a easy button for including customers to allowlist. You possibly can even create an early entry kind or one thing comparable. So, create a button in index.tsx
:
<button>Add pockets to allowlist</button>
Now, let’s create an api so as to add the pockets to the allowlist. So, create a brand new file add-to-allowlist.ts
and add the next:
import kind { NextApiRequest, NextApiResponse } from "subsequent";
import { desk } from "../../utils/Airtable";
const addToAllowlist = async (req: NextApiRequest, res: NextApiResponse) => {
const { tackle } = JSON.parse(req.physique);
const document = await desk
.choose({
fields: ["Addresses", "minted"],
filterByFormula: `NOT({Addresses} != '${tackle}')`,
})
.all();
if (document.size > 0) {
res.standing(400).json({
success: false,
error: "Person is already in allowlist",
});
}
if (document.size === 0) {
attempt {
await desk.create([
{
fields: {
Addresses: address,
},
},
]);
res.standing(200).json({
success: true,
message: "Person added to allowlist",
});
} catch (err) {
res.standing(500).json({
success: false,
error: err,
});
}
}
};
export default addToAllowlist;
Right here, we’re first checking if the person already exists and if it exists we’ll return an error saying that the person is already within the allowlist. If the person would not exist we’ll create the person within the allowlist. Now, let’s head again to the frontend code and create a operate to name this api:
const addWallet = async () => {
const payload = await fetch(`/api/add-to-allowlist`, {
technique: "POST",
physique: JSON.stringify({ tackle }),
});
const payloadJson = await payload.json();
console.log(payloadJson);
if (payloadJson.success) {
alert(payloadJson.message);
} else {
alert(payloadJson.error);
}
};
Lastly, add an onClick occasion to the button:
<button onClick={addWallet}>Add pockets to allowlist</button>
If you happen to attempt switching the related pockets or take away the pockets from the database you will note that the person’s pockets tackle is being added to the bottom!
Minor enhancements (Optionally available)
Let’s simply add some minor enhancements to our app!
Styling
At the moment the location appears fairly boring, so let’s add some easy styling. Create a brand new file globals.css
within the types
folder and add the next to reset the types:
html,
physique {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
colour: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}
Import it within the _app.tsx file:
import "../types/globals.css";
Now, create a brand new file Dwelling.module.css
within the types
folder and add these easy stylings:
.container {
show: flex;
flex-direction: column;
align-items: middle;
justify-content: middle;
top: 100vh;
background-color: #c0ffee;
}
.container > button,
.btn {
background: #1ce;
colour: #fff;
font-size: 1rem;
padding: 0.5rem 1rem;
border: none;
border-radius: 0.5rem;
cursor: pointer;
}
.btn {
margin-top: 10px;
}
.NFT {
show: flex;
flex-direction: column;
justify-content: middle;
}
.nftDesc {
margin: 10px 0px;
font-size: 0.8rem;
colour: #000;
}
.nftDesc > span {
font-weight: daring;
}
Now let’s implement these stylings in index.tsx
by including the classNames:
<div className={types.container}>
{tackle ? (
<div className={types.NFT}>
{nftData?.picture && (
<Picture
src={nftData?.picture}
alt={nftData?.title}
width="280"
top="210"
objectFit="include"
/>
)}
<p className={types.nftDesc}>
<span>Title:</span> {nftData?.title}
</p>
<p className={types.nftDesc}>
<span> Description:</span> {nftData?.description}
</p>
<button className={types.btn} onClick={addWallet}>
Add pockets to allowlist
</button>
<button className={types.btn} onClick={() => mintWithSignature()}>
Mint
</button>
</div>
) : (
<button onClick={connectWithMetamask}>Join with Metamask</button>
)}
</div>
Our web site now appears a lot better!
Loading
At the moment in case you click on the mint button or add to allowlist button nothing occurs for some time, so let’s add a loading textual content to inform the person that one thing is going on. So, create 2 states within the Dwelling.tsx
file:
const [addWalletLoading, setAddWalletLoading] = useState(false);
const [mintLoading, setMintLoading] = useState(false);
Add this ternary operator for altering the textual content of the buttons and disable the button whether it is loading:
<button
className={types.btn}
disabled={addWalletLoading}
onClick={addWallet}
>
{addWalletLoading ? "loading..." : "Add pockets to allowlist"}
</button>
<button
className={types.btn}
disabled={mintLoading}
onClick={() => mintWithSignature()}
>
{mintLoading ? "loading..." : "Mint"}
</button>
Nothing occurs but as a result of we aren’t altering the states so, within the capabilities we have to change the loading states:
const mintWithSignature = async () => {
setMintLoading(true);
const signedPayloadReq = await fetch(`/api/generate-mint-sig`, {
technique: "POST",
physique: JSON.stringify({ tackle }),
});
const signedPayload = await signedPayloadReq.json();
if (signedPayload.error) {
alert(signedPayload.error);
return;
}
attempt {
const nft = await version?.signature.mint(signedPayload.signedPayload);
if (nft) {
await fetch(`/api/set-minted`, {
technique: "POST",
physique: JSON.stringify({ tackle }),
});
}
return nft;
} catch (err) {
console.error(err);
return null;
} lastly {
setMintLoading(false);
}
};
const addWallet = async () => {
setAddWalletLoading(true);
const payload = await fetch(`/api/add-to-allowlist`, {
technique: "POST",
physique: JSON.stringify({ tackle }),
});
const payloadJson = await payload.json();
setAddWalletLoading(false);
if (payloadJson.success) {
alert(payloadJson.message);
} else {
alert(payloadJson.error);
}
};
If you happen to now attempt to click on any of the buttons you’ll see the loading textual content for a second.
Error dealing with
At the moment, the person might be on the mistaken community and get bizarre errors, so we’ll disable the button if the person shouldn’t be on the proper community.
Get entry to the community through the use of the useNetwork
hook:
const community = useNetwork();
Will probably be imported from the thirdweb sdk:
import {
useAddress,
useEdition,
useMetamask,
useNetwork,
} from "@thirdweb-dev/react";
Now, within the mint button add the checks-
<button
className={types.btn}
disabled= community[0]?.knowledge?.chain?.id !== ChainId.Mumbai
onClick={() => mintWithSignature()}
>
{community[0]?.knowledge?.chain?.id === ChainId.Mumbai
? mintLoading
? "loading..."
: "Mint"
: "Swap to Mumbai"}
</button>
I constructed this Dapp on the Mumbai community so I’m checking for the Mumbai community however it is advisable do that for the community you constructed upon.
Conclusion
This was rather a lot, now give your self a pat on the again and share your wonderful apps with us! If you wish to take a look on the code, take a look at the GitHub Repository.