Skip to main content

为您即将推出的 NFT 收藏构建白名单 dApp

您正在启动名为 Crypto Devs 的 NFT 集合。 你想让你的早期支持者访问你的收藏的白名单,所以在这里你正在为 Crypto Devs 创建一个白名单 dapp

要求

  • 白名单访问权限应免费提供给想要进入的前 10 名用户。
  • 应该有一个网站,人们可以进入白名单。

让我们开始建造 🚀

先决条件

  • 您可以用 JavaScript 编写代码(新生课程 - 编程简介)
  • 已设置 Metamask 钱包(Freshman Track - 设置加密钱包)
  • 您的计算机已安装 Node.js。 如果不是从这里下载

喜欢视频?

如果您想从视频中学习,我们的 YouTube 上有本教程的录音。 单击下面的屏幕截图观看视频,或继续阅读教程!

构建

智能合约

为了构建智能合约,我们将使用 Hardhat。 Hardhat 是一个以太坊开发环境和框架,专为 Solidity 中的全栈开发而设计。简单来说,您可以编写智能合约、部署它们、运行测试和调试代码。

首先,您需要创建一个 Whitelist-Daap 文件夹,Hardhat 项目和您的 Next.js 应用程序稍后将进入该文件夹。打开终端并执行这些命令

mkdir Whitelist-Dapp
cd Whitelist-Dapp

然后,在 Whitelist-Dapp 文件夹中,我们将建立一个 Hardhat 项目

mkdir hardhat-tutorial
cd hardhat-tutorial
npm init --yes
npm install --save-dev hardhat

在安装 Hardhat 的同一目录中运行:

npx hardhat

确保选择创建 Javascript 项目,然后按照终端中的步骤完成安全帽设置。

设置项目后,首先在合约目录中创建一个名为 Whitelist.sol 的新文件。

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;


contract Whitelist {

// Max number of whitelisted addresses allowed
uint8 public maxWhitelistedAddresses;

// Create a mapping of whitelistedAddresses
// if an address is whitelisted, we would set it to true, it is false by default for all other addresses.
mapping(address => bool) public whitelistedAddresses;

// numAddressesWhitelisted would be used to keep track of how many addresses have been whitelisted
// NOTE: Don't change this variable name, as it will be part of verification
uint8 public numAddressesWhitelisted;

// Setting the Max number of whitelisted addresses
// User will put the value at the time of deployment
constructor(uint8 _maxWhitelistedAddresses) {
maxWhitelistedAddresses = _maxWhitelistedAddresses;
}

/**
addAddressToWhitelist - This function adds the address of the sender to the
whitelist
*/
function addAddressToWhitelist() public {
// check if the user has already been whitelisted
require(!whitelistedAddresses[msg.sender], "Sender has already been whitelisted");
// check if the numAddressesWhitelisted < maxWhitelistedAddresses, if not then throw an error.
require(numAddressesWhitelisted < maxWhitelistedAddresses, "More addresses cant be added, limit reached");
// Add the address which called the function to the whitelistedAddress array
whitelistedAddresses[msg.sender] = true;
// Increase the number of whitelisted addresses
numAddressesWhitelisted += 1;
}
}

我们将合约部署到 goerli 网络。创建一个新文件,或替换脚本文件夹下名为 deploy.js 的默认文件。现在我们将编写一些代码来在 deploy.js 文件中部署合约。

const { ethers } = require("hardhat");

async function main() {
/*
A ContractFactory in ethers.js is an abstraction used to deploy new smart contracts,
so whitelistContract here is a factory for instances of our Whitelist contract.
*/
const whitelistContract = await ethers.getContractFactory("Whitelist");

// here we deploy the contract
const deployedWhitelistContract = await whitelistContract.deploy(10);
// 10 is the Maximum number of whitelisted addresses allowed

// Wait for it to finish deploying
await deployedWhitelistContract.deployed();

// print the address of the deployed contract
console.log("Whitelist Contract Address:", deployedWhitelistContract.address);
}

// Call the main function and catch if there is any error
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});

现在在 hardhat-tutorial 文件夹中创建一个 .env 文件并添加以下行。请按照以下说明进行操作。

转到 Quicknode 并注册一个帐户。如果您已经有一个帐户,请登录。Quicknode 是一个节点提供商,可让您连接到各种不同的区块链。我们将使用它通过 Hardhat 部署我们的合约。创建账户后,在 Quicknode 上创建一个 endpoint,选择 Ethereum,然后选择 Goerli 网络。单击右下角的继续,然后单击创建端点。复制 HTTP Provider 中提供给您的链接,并将其添加到 QUICKNODE_HTTP_URL 下面的 .env 文件中。

注意:如果您之前在新生跟踪期间在 Quicknode 上设置了 Goerli Endpoint,则可以使用与之前相同的 URL。无需删除它并设置一个新的。

要获取您的私钥,您需要从 Metamask 中导出它。打开 Metamask,点击三个点,点击 Account Details,然后点击 Export Private Key。确保您使用的是没有主网资金的测试帐户。在您的 .env 文件中为 PRIVATE_KEY 变量添加此私钥。

QUICKNODE_HTTP_URL="add-quicknode-http-provider-url-here"
PRIVATE_KEY="add-the-private-key-here"

现在我们将安装 dotenv 包,以便能够导入 env 文件并在我们的配置中使用它。打开指向 hardhat-tutorial 目录的终端并执行此命令

npm install dotenv

现在打开 hardhat.config.js 文件,我们将在此处添加 goerli 网络,以便我们可以将合约部署到 Goerli 网络。用下面给出的行替换 hardhat.config.js 文件中的所有行

require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config({ path: ".env" });

const QUICKNODE_HTTP_URL = process.env.QUICKNODE_HTTP_URL;
const PRIVATE_KEY = process.env.PRIVATE_KEY;

module.exports = {
solidity: "0.8.9",
networks: {
goerli: {
url: QUICKNODE_HTTP_URL,
accounts: [PRIVATE_KEY],
},
},
};

编译合约,打开指向 hardhat-tutorial 目录的终端并执行此命令

npx hardhat compile

要部署,请打开指向 hardhat-tutorial 目录的终端并执行此命令

npx hardhat run scripts/deploy.js --network goerli

将打印在终端上的白名单合约地址保存在记事本中,您将在教程中进一步使用它。

网站

为了开发网站,我们将使用 React 和 Next Js。 React 是一个用于制作网站的 javascript 框架,Next.js 是一个 React 框架,它还允许与前端一起编写后端 API 代码,因此您不需要两个单独的前端和后端服务。

首先,您需要创建一个新的下一个应用程序。要创建此下一个应用程序,请在终端指向 Whitelist-Dapp 文件夹并键入

npx create-next-app@latest

并按回车键所有问题

您的文件夹结构应该类似于

- Whitelist-Dapp
- hardhat-tutorial
- my-app

现在运行应用程序,在终端中执行这些命令

cd my-app
npm run dev

现在转到 http://localhost:3000,您的应用程序应该正在运行 🤘

现在让我们安装 Web3Modal 库。 Web3Modal 是一个易于使用的库,可帮助开发人员轻松地让他们的用户使用各种不同的钱包连接到您的 dApp。默认情况下,Web3Modal 库支持注入的提供程序,如(Metamask、Dapper、Gnosis Safe、Frame、Web3 浏览器等)和 WalletConnect,您还可以轻松配置库以支持 Portis、Fortmatic、Squarelink、Torus、Authereum、D'CENT 钱包和阿卡内。 (这是 Codesandbox.io 上的一个活生生的例子)

打开指向 atmy-app 目录的终端并执行此命令

npm install web3modal

在同一个终端也安装 ethers.js

npm install ethers

在您的 my-app/public 文件夹中,下载此图像并将其重命名为 crypto-devs.svg 现在转到您的样式文件夹并将 Home.modules.css 文件的所有内容替换为以下代码,这将添加一些样式你的 dapp:

.main {
min-height: 90vh;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
font-family: "Courier New", Courier, monospace;
}

.footer {
display: flex;
padding: 2rem 0;
border-top: 1px solid #eaeaea;
justify-content: center;
align-items: center;
}

.image {
width: 70%;
height: 50%;
margin-left: 20%;
}

.title {
font-size: 2rem;
margin: 2rem 0;
}

.description {
line-height: 1;
margin: 2rem 0;
font-size: 1.2rem;
}

.button {
border-radius: 4px;
background-color: blue;
border: none;
color: #ffffff;
font-size: 15px;
padding: 20px;
width: 200px;
cursor: pointer;
margin-bottom: 2%;
}
@media (max-width: 1000px) {
.main {
width: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
}
}

打开 pages 文件夹下的 index.js 文件并粘贴以下代码,代码解释可以在评论中找到。 如果您不熟悉 React 和 React Hooks、React Hooks 教程,请务必阅读它们。

import Head from "next/head";
import styles from "../styles/Home.module.css";
import Web3Modal from "web3modal";
import { providers, Contract } from "ethers";
import { useEffect, useRef, useState } from "react";
import { WHITELIST_CONTRACT_ADDRESS, abi } from "../constants";

export default function Home() {
// walletConnected keep track of whether the user's wallet is connected or not
const [walletConnected, setWalletConnected] = useState(false);
// joinedWhitelist keeps track of whether the current metamask address has joined the Whitelist or not
const [joinedWhitelist, setJoinedWhitelist] = useState(false);
// loading is set to true when we are waiting for a transaction to get mined
const [loading, setLoading] = useState(false);
// numberOfWhitelisted tracks the number of addresses's whitelisted
const [numberOfWhitelisted, setNumberOfWhitelisted] = useState(0);
// Create a reference to the Web3 Modal (used for connecting to Metamask) which persists as long as the page is open
const web3ModalRef = useRef();

/**
* Returns a Provider or Signer object representing the Ethereum RPC with or without the
* signing capabilities of metamask attached
*
* A `Provider` is needed to interact with the blockchain - reading transactions, reading balances, reading state, etc.
*
* A `Signer` is a special type of Provider used in case a `write` transaction needs to be made to the blockchain, which involves the connected account
* needing to make a digital signature to authorize the transaction being sent. Metamask exposes a Signer API to allow your website to
* request signatures from the user using Signer functions.
*
* @param {*} needSigner - True if you need the signer, default false otherwise
*/
const getProviderOrSigner = async (needSigner = false) => {
// Connect to Metamask
// Since we store `web3Modal` as a reference, we need to access the `current` value to get access to the underlying object
const provider = await web3ModalRef.current.connect();
const web3Provider = new providers.Web3Provider(provider);

// If user is not connected to the Goerli network, let them know and throw an error
const { chainId } = await web3Provider.getNetwork();
if (chainId !== 5) {
window.alert("Change the network to Goerli");
throw new Error("Change network to Goerli");
}

if (needSigner) {
const signer = web3Provider.getSigner();
return signer;
}
return web3Provider;
};

/**
* addAddressToWhitelist: Adds the current connected address to the whitelist
*/
const addAddressToWhitelist = async () => {
try {
// We need a Signer here since this is a 'write' transaction.
const signer = await getProviderOrSigner(true);
// Create a new instance of the Contract with a Signer, which allows
// update methods
const whitelistContract = new Contract(
WHITELIST_CONTRACT_ADDRESS,
abi,
signer
);
// call the addAddressToWhitelist from the contract
const tx = await whitelistContract.addAddressToWhitelist();
setLoading(true);
// wait for the transaction to get mined
await tx.wait();
setLoading(false);
// get the updated number of addresses in the whitelist
await getNumberOfWhitelisted();
setJoinedWhitelist(true);
} catch (err) {
console.error(err);
}
};

/**
* getNumberOfWhitelisted: gets the number of whitelisted addresses
*/
const getNumberOfWhitelisted = async () => {
try {
// Get the provider from web3Modal, which in our case is MetaMask
// No need for the Signer here, as we are only reading state from the blockchain
const provider = await getProviderOrSigner();
// We connect to the Contract using a Provider, so we will only
// have read-only access to the Contract
const whitelistContract = new Contract(
WHITELIST_CONTRACT_ADDRESS,
abi,
provider
);
// call the numAddressesWhitelisted from the contract
const _numberOfWhitelisted =
await whitelistContract.numAddressesWhitelisted();
setNumberOfWhitelisted(_numberOfWhitelisted);
} catch (err) {
console.error(err);
}
};

/**
* checkIfAddressInWhitelist: Checks if the address is in whitelist
*/
const checkIfAddressInWhitelist = async () => {
try {
// We will need the signer later to get the user's address
// Even though it is a read transaction, since Signers are just special kinds of Providers,
// We can use it in it's place
const signer = await getProviderOrSigner(true);
const whitelistContract = new Contract(
WHITELIST_CONTRACT_ADDRESS,
abi,
signer
);
// Get the address associated to the signer which is connected to MetaMask
const address = await signer.getAddress();
// call the whitelistedAddresses from the contract
const _joinedWhitelist = await whitelistContract.whitelistedAddresses(
address
);
setJoinedWhitelist(_joinedWhitelist);
} catch (err) {
console.error(err);
}
};

/*
connectWallet: Connects the MetaMask wallet
*/
const connectWallet = async () => {
try {
// Get the provider from web3Modal, which in our case is MetaMask
// When used for the first time, it prompts the user to connect their wallet
await getProviderOrSigner();
setWalletConnected(true);

checkIfAddressInWhitelist();
getNumberOfWhitelisted();
} catch (err) {
console.error(err);
}
};

/*
renderButton: Returns a button based on the state of the dapp
*/
const renderButton = () => {
if (walletConnected) {
if (joinedWhitelist) {
return (
<div className={styles.description}>
Thanks for joining the Whitelist!
</div>
);
} else if (loading) {
return <button className={styles.button}>Loading...</button>;
} else {
return (
<button onClick={addAddressToWhitelist} className={styles.button}>
Join the Whitelist
</button>
);
}
} else {
return (
<button onClick={connectWallet} className={styles.button}>
Connect your wallet
</button>
);
}
};

// useEffects are used to react to changes in state of the website
// The array at the end of function call represents what state changes will trigger this effect
// In this case, whenever the value of `walletConnected` changes - this effect will be called
useEffect(() => {
// if wallet is not connected, create a new instance of Web3Modal and connect the MetaMask wallet
if (!walletConnected) {
// Assign the Web3Modal class to the reference object by setting it's `current` value
// The `current` value is persisted throughout as long as this page is open
web3ModalRef.current = new Web3Modal({
network: "goerli",
providerOptions: {},
disableInjectedProvider: false,
});
connectWallet();
}
}, [walletConnected]);

return (
<div>
<Head>
<title>Whitelist Dapp</title>
<meta name="description" content="Whitelist-Dapp" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className={styles.main}>
<div>
<h1 className={styles.title}>Welcome to Crypto Devs!</h1>
<div className={styles.description}>
Its an NFT collection for developers in Crypto.
</div>
<div className={styles.description}>
{numberOfWhitelisted} have already joined the Whitelist
</div>
{renderButton()}
</div>
<div>
<img className={styles.image} src="./crypto-devs.svg" />
</div>
</div>

<footer className={styles.footer}>
Made with &#10084; by Crypto Devs
</footer>
</div>
);
}

现在在 my-app 文件夹下创建一个新文件夹并将其命名为常量。 在常量文件夹中创建一个文件 index.js 并粘贴以下代码。

export const WHITELIST_CONTRACT_ADDRESS = "YOUR_WHITELIST_CONTRACT_ADDRESS";
export const abi = YOUR_ABI;

将“YOUR_WHITELIST_CONTRACT_ADDRESS”替换为您部署的白名单合约的地址。 将“YOUR_ABI”替换为您的白名单合约的 ABI。 要为您的合约获取 ABI,请转到您的 hardhat-tutorial/artifacts/contracts/Whitelist.sol 文件夹,然后从您的 Whitelist.json 文件中获取标记在“abi”键下的数组(这将是一个巨大的数组,关闭 到 100 行(如果不是更多)。

现在在指向 my-app 文件夹的终端中,执行

npm run dev

您的白名单 dapp 现在应该可以正常运行了 🚀

推送到 github

确保在继续之前您已将所有代码推送到 github :)

部署 dApp

我们现在将部署您的 dApp,以便每个人都可以看到您的网站,并且您可以与所有 LearnWeb3 DAO 朋友分享它。

  • 转到 Vercel 并使用您的 GitHub 登录
  • 然后单击新建项目按钮,然后选择您的白名单 dApp 存储库
  • 配置新项目时,Vercel 将允许您自定义根目录
  • 单击根目录旁边的编辑并将其设置为 my-app
  • 选择框架为 Next.js
  • 单击部署
  • 现在,您可以通过转到仪表板、选择您的项目并从那里复制 URL 来查看您部署的网站!

在 Discord 中分享您的网站:D

确认

要验证此级别,请确保您已将合约中的某些地址列入白名单。 在智能合约验证框中输入您的合约地址,然后选择您部署的测试网络。

原文

参考