使用 Hardhat、ethers.js、Next.js 和 TailwindCSS 构建全栈 NFT 铸造 DApp
先决条件
要成功使用本指南,您必须具备以下条件:
资源
-
Solidity实例详解:通过简单示例介绍Solidity
建造空间
Buildspace是一家初创公司,旨在帮助人们免费学习构建与加密货币相关的项目。
该项目基于 buildspace 项目Mint 你自己的 NFT 收藏,并发布一个 Web3 应用程序来展示它们。
Buildspace 上还有很多类似的项目,我强烈建议你去看看。完成项目后你还可以获得很酷的 NFT。这是我完成这个项目后获得的 NFT——
关于本项目
在这篇文章中,我们将使用Solidity、Hardhat、ethers.js、Next.js和TailwindCSS构建一个全栈 NFT 铸造 dapp 。
项目设置
首先,我们需要创建一个安全帽项目。为此,请打开终端。创建一个新的空目录或切换到该目录,然后运行以下命令:
npm install ethers hardhat @nomiclabs/hardhat-waffle \
ethereum-waffle chai @nomiclabs/hardhat-ethers \
@openzeppelin/contracts dotenv
这将安装设置安全帽项目所需的依赖项以及该项目的其他一些依赖项。
接下来,从项目根目录初始化一个新的 Hardhat 开发环境。为此,请在终端中运行以下命令:
npx hardhat
输出结果将类似于下方所示。选择此项Create a basic sample project可在您的目录中创建一个新的安全帽项目。
What do you want to do? …
Create a basic sample project
Create an advanced sample project
. . .
现在您应该能在根目录中看到以下创建的文件和文件夹:
hardhat.config.js -您的 Hardhat 设置(即您的配置、插件和自定义任务)全部包含在此文件中。
scripts -一个包含名为 sample-script.js 的脚本的文件夹,该脚本将在执行时部署您的智能合约。
test -包含示例测试脚本的文件夹。
合约 -一个包含 Solidity 智能合约示例的文件夹。
现在,我们需要为 dapp 的前端创建一个新的 Next.js 项目。为此,请在终端中运行以下命令:
npx create-next-app -e with-tailwindcss client
这将创建一个新的 Next 项目,使用 Tailwind CSS 进行样式设置,项目位于名为“client”的文件夹中。
完成上述步骤后,请在文件夹内安装前端所需的依赖项client。为此,请在终端中运行以下命令:
cd client
npm install axios ethers react-loader-spinner
使用 Alchemy 创建以太坊 API 密钥
Alchemy 是一个专注于简化区块链开发的区块链开发者平台。他们构建了一套开发者工具、增强的 API 和卓越的节点基础设施,使构建和运行区块链应用程序变得无缝衔接。
要创建 API 密钥,请观看以下视频。
注意事项:
- 选择网络为 rinkeby。
- 在 Alchemy 上创建应用程序后,复制 HTTP 密钥。
接下来,创建一个.env文件来存储你Alchemy key和你的Account Private Key
ALCHEMY_RINKEBY_URL = "ALCHEMY_HTTP_API_KEY"
ACCOUNT_KEY = "YOUR_ACCOUNT_PRIVATE_KEY
重要提示:请勿将该.env文件推送到 GitHub,因为它包含您的私人数据。
正在更新 hardhat.config.js
之后,请使用以下内容更新 hardhat.config.js 中的配置:
require('@nomiclabs/hardhat-waffle')
require('dotenv').config()
module.exports = {
solidity: '0.8.3',
networks: {
rinkeby: {
url: process.env.ALCHEMY_RINKEBY_URL,
accounts: [process.env.ACCOUNT_KEY],
},
},
}
创建智能合约逻辑
接下来,我们将创建智能合约!我们将创建一个用于生成 NFT 资产的 NFT 合约。
在合约目录中创建一个名为 `.nft.src` 的新文件EternalNFT.sol。在此处添加以下代码:
您可以在EternalNFT.sol查看概要。
//SPDX-License-Identifier: MIT
pragma solidity 0.8.3;
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import { Base64 } from "./libraries/Base64.sol";
contract EternalNFT is ERC721URIStorage {
using Counters for Counters.Counter;
Counters.Counter private _tokenId;
string public collectionName;
string public collectionSymbol;
string baseSvg = "<svg xmlns='http://www.w3.org/2000/svg' preserveAspectRatio='xMinYMin meet' viewBox='0 0 350 350'><style>.base { fill: white; font-family: serif; font-size: 24px; }</style><rect width='100%' height='100%' fill='black' /><text x='50%' y='50%' class='base' dominant-baseline='middle' text-anchor='middle'>";
string[] element = [
'Fire',
'Wind',
'Wave',
'Earth',
'Thunder',
'Space',
'Time'
];
string[] weapon = [
'Sword',
'Spear',
'Shield',
'Hammer',
'Saber',
'Axe',
'Bow'
];
string[] rank = [
'Lord',
'King',
'Emperor',
'Venerable',
'Ancestor',
'Saint',
'God'
];
constructor() ERC721("EternalNFT", "ENFT") {
collectionName = name();
collectionSymbol = symbol();
}
function random(string memory _input) internal pure returns(uint256) {
return uint256(keccak256(abi.encodePacked(_input)));
}
function pickFirstWord(uint256 tokenId) public view returns(string memory) {
uint256 rand = random(string(abi.encodePacked("element", Strings.toString(tokenId))));
rand = rand % element.length;
return element[rand];
}
function pickSecondWord(uint256 tokenId) public view returns(string memory) {
uint256 rand = random(string(abi.encodePacked("weapon", Strings.toString(tokenId))));
rand = rand % weapon.length;
return weapon[rand];
}
function pickThirdWord(uint256 tokenId) public view returns(string memory) {
uint256 rand = random(string(abi.encodePacked("rank", Strings.toString(tokenId))));
rand = rand % rank.length;
return rank[rand];
}
function createEternalNFT() public returns(uint256) {
uint256 newItemId = _tokenId.current();
string memory first = pickFirstWord(newItemId);
string memory second = pickSecondWord(newItemId);
string memory third = pickThirdWord(newItemId);
string memory combinedWord = string(abi.encodePacked(first,second,third));
string memory finalSvg = string(abi.encodePacked(baseSvg, first, second, third, "</text></svg>"));
string memory json = Base64.encode(
bytes(
string(
abi.encodePacked(
'{"name": "',
combinedWord,
'", "description": "A highly acclaimed collection Eternal Warriors", "image": "data:image/svg+xml;base64,',
Base64.encode(bytes(finalSvg)),
'"}'
)
)
)
);
string memory finalTokenURI = string(abi.encodePacked(
"data:application/json;base64,", json
));
_safeMint(msg.sender, newItemId);
_setTokenURI(newItemId, finalTokenURI);
_tokenId.increment();
return newItemId;
}
}
在本合约中,我们继承了OpenZeppelin实现的ERC721URIStorage.sol和Counters.sol。
对于合约继承的Base64 库libraries,请在 contracts 文件夹内创建一个文件夹。在 libraries 文件夹内创建一个Base64.sol文件,并添加以下代码:
您可以在Base64.sol查看 gist。
/**
*Submitted for verification at Etherscan.io on 2021-09-05
*/
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/// [MIT License]
/// @title Base64
/// @notice Provides a function for encoding some bytes in base64
/// @author Brecht Devos <brecht@loopring.org>
library Base64 {
bytes internal constant TABLE =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/// @notice Encodes some bytes to the base64 representation
function encode(bytes memory data) internal pure returns (string memory) {
uint256 len = data.length;
if (len == 0) return "";
// multiply by 4/3 rounded up
uint256 encodedLen = 4 * ((len + 2) / 3);
// Add some extra buffer at the end
bytes memory result = new bytes(encodedLen + 32);
bytes memory table = TABLE;
assembly {
let tablePtr := add(table, 1)
let resultPtr := add(result, 32)
for {
let i := 0
} lt(i, len) {
} {
i := add(i, 3)
let input := and(mload(add(data, i)), 0xffffff)
let out := mload(add(tablePtr, and(shr(18, input), 0x3F)))
out := shl(8, out)
out := add(
out,
and(mload(add(tablePtr, and(shr(12, input), 0x3F))), 0xFF)
)
out := shl(8, out)
out := add(
out,
and(mload(add(tablePtr, and(shr(6, input), 0x3F))), 0xFF)
)
out := shl(8, out)
out := add(
out,
and(mload(add(tablePtr, and(input, 0x3F))), 0xFF)
)
out := shl(224, out)
mstore(resultPtr, out)
resultPtr := add(resultPtr, 4)
}
switch mod(len, 3)
case 1 {
mstore(sub(resultPtr, 2), shl(240, 0x3d3d))
}
case 2 {
mstore(sub(resultPtr, 1), shl(248, 0x3d))
}
mstore(result, encodedLen)
}
return string(result);
}
}
测试智能合约
现在智能合约代码和环境已经完成,我们可以尝试进行测试了。
为此,我们可以创建一个本地测试来运行大部分功能,例如检查代币的名称、符号和地址、铸造代币等。
要创建测试,请打开 test/sample-test.js 文件,并使用以下代码对其进行更新:
您可以在sample-test.js中查看要点。
const { assert } = require('chai')
describe('EternalNFT Contract', async () => {
let nft
let nftContractAddress
let tokenId
// Deploys the EternalNFT contract and the EternalMarket contract before each test
beforeEach('Setup Contract', async () => {
const EternalNFT = await ethers.getContractFactory('EternalNFT')
nft = await EternalNFT.deploy()
await nft.deployed()
nftContractAddress = await nft.address
})
// Tests address for the EternalNFT contract
it('Should have an address', async () => {
assert.notEqual(nftContractAddress, 0x0)
assert.notEqual(nftContractAddress, '')
assert.notEqual(nftContractAddress, null)
assert.notEqual(nftContractAddress, undefined)
})
// Tests name for the token of EternalNFT contract
it('Should have a name', async () => {
// Returns the name of the token
const name = await nft.collectionName()
assert.equal(name, 'EternalNFT')
})
// Tests symbol for the token of EternalNFT contract
it('Should have a symbol', async () => {
// Returns the symbol of the token
const symbol = await nft.collectionSymbol()
assert.equal(symbol, 'ENFT')
})
// Tests for NFT minting function of EternalNFT contract using tokenID of the minted NFT
it('Should be able to mint NFT', async () => {
// Mints a NFT
let txn = await nft.createEternalNFT()
let tx = await txn.wait()
// tokenID of the minted NFT
let event = tx.events[0]
let value = event.args[2]
tokenId = value.toNumber()
assert.equal(tokenId, 0)
// Mints another NFT
txn = await nft.createEternalNFT()
tx = await txn.wait()
// tokenID of the minted NFT
event = tx.events[0]
value = event.args[2]
tokenId = value.toNumber()
assert.equal(tokenId, 1)
})
})
要运行测试,请在项目根目录的终端中运行以下命令:
npx hardhat test
将合约部署到 Rinkeby 网络
创建项目时,Hardhat 创建了一个示例部署脚本scripts/sample-script.js。
为了明确此脚本的目的,请删除scripts/sample-script.js并创建scripts/deploy.js。
要部署合约,请在代码中添加以下代码deploy.js:
const main = async () => {
const nftContractFactory = await ethers.getContractFactory('EternalNFT')
const nftContract = await nftContractFactory.deploy()
await nftContract.deployed()
console.log('Contract deployed to:', nftContract.address)
}
const runMain = async () => {
try {
await main()
process.exit(0)
} catch (error) {
console.log(error)
process.exit(1)
}
}
runMain()
要将合约部署到 Rinkeby 网络,请在终端中运行以下命令:
npx hardhat run scripts/deploy.js --network rinkeby
这将把合约部署到 rinkeby 网络,并在终端中输出合约部署的地址。
要将您的合约部署到任何其他网络
- 在 Alchemy 上更新已注册 dApp 中的网络。
- 在文件中添加所需的网络
hardhat.config.js,并指定 Alchemy 网络 URL。
例如,如果您想将智能合约部署到kovan网络中
- 将 Alchemy dApp 中的网络更新为
kovan。 - 在网络对象中添加以下代码
hardhat.config.js,以添加rinkeby用于部署智能合约的网络。
kovan: {
url: process.env.ALCHEMY_KOVAN_URL,
accounts: [process.env.ACCOUNT_KEY],
}
然后,要将合约部署到网络,请在终端中运行以下命令:
npx hardhat run scripts/deploy.js --network <network>
在“网络”一栏中,只需输入您要将智能合约部署到的网络名称即可。
构建前端
现在智能合约已经运行并准备就绪,我们可以开始构建用户界面了。
首先,我们需要将前端连接到智能合约,以便它可以使用智能合约中的函数与区块链中的数据进行交互。
为此,我们需要执行以下操作:
utils在文件夹内创建一个文件夹client,然后将文件复制粘贴artifacts/contracts/EternalNFT.sol/EternalNFT.json到该utils文件夹内。config.js在文件夹内创建一个文件client,并将以下代码添加到该文件中。
export const nftContractAddress = "DEPLOYED_CONTRACT_ADDRES"
部署智能合约时,请将 替换DEPLOYED_CONTRACT_ADDRES为终端中已部署的合约地址。
接下来,要设置前端,请转到client/pages/index.js并使用以下代码更新它:
您可以在index.js查看要点。
import { useState, useEffect } from 'react'
import { nftContractAddress } from '../config.js'
import { ethers } from 'ethers'
import axios from 'axios'
import Loader from 'react-loader-spinner'
import NFT from '../utils/EternalNFT.json'
const mint = () => {
const [mintedNFT, setMintedNFT] = useState(null)
const [miningStatus, setMiningStatus] = useState(null)
const [loadingState, setLoadingState] = useState(0)
const [txError, setTxError] = useState(null)
const [currentAccount, setCurrentAccount] = useState('')
const [correctNetwork, setCorrectNetwork] = useState(false)
// Checks if wallet is connected
const checkIfWalletIsConnected = async () => {
const { ethereum } = window
if (ethereum) {
console.log('Got the ethereum obejct: ', ethereum)
} else {
console.log('No Wallet found. Connect Wallet')
}
const accounts = await ethereum.request({ method: 'eth_accounts' })
if (accounts.length !== 0) {
console.log('Found authorized Account: ', accounts[0])
setCurrentAccount(accounts[0])
} else {
console.log('No authorized account found')
}
}
// Calls Metamask to connect wallet on clicking Connect Wallet button
const connectWallet = async () => {
try {
const { ethereum } = window
if (!ethereum) {
console.log('Metamask not detected')
return
}
let chainId = await ethereum.request({ method: 'eth_chainId' })
console.log('Connected to chain:' + chainId)
const rinkebyChainId = '0x4'
const devChainId = 1337
const localhostChainId = `0x${Number(devChainId).toString(16)}`
if (chainId !== rinkebyChainId && chainId !== localhostChainId) {
alert('You are not connected to the Rinkeby Testnet!')
return
}
const accounts = await ethereum.request({ method: 'eth_requestAccounts' })
console.log('Found account', accounts[0])
setCurrentAccount(accounts[0])
} catch (error) {
console.log('Error connecting to metamask', error)
}
}
// Checks if wallet is connected to the correct network
const checkCorrectNetwork = async () => {
const { ethereum } = window
let chainId = await ethereum.request({ method: 'eth_chainId' })
console.log('Connected to chain:' + chainId)
const rinkebyChainId = '0x4'
const devChainId = 1337
const localhostChainId = `0x${Number(devChainId).toString(16)}`
if (chainId !== rinkebyChainId && chainId !== localhostChainId) {
setCorrectNetwork(false)
} else {
setCorrectNetwork(true)
}
}
useEffect(() => {
checkIfWalletIsConnected()
checkCorrectNetwork()
}, [])
// Creates transaction to mint NFT on clicking Mint Character button
const mintCharacter = async () => {
try {
const { ethereum } = window
if (ethereum) {
const provider = new ethers.providers.Web3Provider(ethereum)
const signer = provider.getSigner()
const nftContract = new ethers.Contract(
nftContractAddress,
NFT.abi,
signer
)
let nftTx = await nftContract.createEternalNFT()
console.log('Mining....', nftTx.hash)
setMiningStatus(0)
let tx = await nftTx.wait()
setLoadingState(1)
console.log('Mined!', tx)
let event = tx.events[0]
let value = event.args[2]
let tokenId = value.toNumber()
console.log(
`Mined, see transaction: https://rinkeby.etherscan.io/tx/${nftTx.hash}`
)
getMintedNFT(tokenId)
} else {
console.log("Ethereum object doesn't exist!")
}
} catch (error) {
console.log('Error minting character', error)
setTxError(error.message)
}
}
// Gets the minted NFT data
const getMintedNFT = async (tokenId) => {
try {
const { ethereum } = window
if (ethereum) {
const provider = new ethers.providers.Web3Provider(ethereum)
const signer = provider.getSigner()
const nftContract = new ethers.Contract(
nftContractAddress,
NFT.abi,
signer
)
let tokenUri = await nftContract.tokenURI(tokenId)
let data = await axios.get(tokenUri)
let meta = data.data
setMiningStatus(1)
setMintedNFT(meta.image)
} else {
console.log("Ethereum object doesn't exist!")
}
} catch (error) {
console.log(error)
setTxError(error.message)
}
}
return (
<div className='flex flex-col items-center pt-32 bg-[#0B132B] text-[#d3d3d3] min-h-screen'>
<div className='trasition hover:rotate-180 hover:scale-105 transition duration-500 ease-in-out'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='60'
height='60'
fill='currentColor'
viewBox='0 0 16 16'
>
<path d='M8.186 1.113a.5.5 0 0 0-.372 0L1.846 3.5 8 5.961 14.154 3.5 8.186 1.113zM15 4.239l-6.5 2.6v7.922l6.5-2.6V4.24zM7.5 14.762V6.838L1 4.239v7.923l6.5 2.6zM7.443.184a1.5 1.5 0 0 1 1.114 0l7.129 2.852A.5.5 0 0 1 16 3.5v8.662a1 1 0 0 1-.629.928l-7.185 2.874a.5.5 0 0 1-.372 0L.63 13.09a1 1 0 0 1-.63-.928V3.5a.5.5 0 0 1 .314-.464L7.443.184z' />
</svg>
</div>
<h2 className='text-3xl font-bold mb-20 mt-12'>
Mint your Eternal Domain NFT!
</h2>
{currentAccount === '' ? (
<button
className='text-2xl font-bold py-3 px-12 bg-black shadow-lg shadow-[#6FFFE9] rounded-lg mb-10 hover:scale-105 transition duration-500 ease-in-out'
onClick={connectWallet}
>
Connect Wallet
</button>
) : correctNetwork ? (
<button
className='text-2xl font-bold py-3 px-12 bg-black shadow-lg shadow-[#6FFFE9] rounded-lg mb-10 hover:scale-105 transition duration-500 ease-in-out'
onClick={mintCharacter}
>
Mint Character
</button>
) : (
<div className='flex flex-col justify-center items-center mb-20 font-bold text-2xl gap-y-3'>
<div>----------------------------------------</div>
<div>Please connect to the Rinkeby Testnet</div>
<div>and reload the page</div>
<div>----------------------------------------</div>
</div>
)}
<div className='text-xl font-semibold mb-20 mt-4'>
<a
href={`https://rinkeby.rarible.com/collection/${nftContractAddress}`}
target='_blank'
>
<span className='hover:underline hover:underline-offset-8 '>
View Collection on Rarible
</span>
</a>
</div>
{loadingState === 0 ? (
miningStatus === 0 ? (
txError === null ? (
<div className='flex flex-col justify-center items-center'>
<div className='text-lg font-bold'>
Processing your transaction
</div>
<Loader
className='flex justify-center items-center pt-12'
type='TailSpin'
color='#d3d3d3'
height={40}
width={40}
/>
</div>
) : (
<div className='text-lg text-red-600 font-semibold'>{txError}</div>
)
) : (
<div></div>
)
) : (
<div className='flex flex-col justify-center items-center'>
<div className='font-semibold text-lg text-center mb-4'>
Your Eternal Domain Character
</div>
<img
src={mintedNFT}
alt=''
className='h-60 w-60 rounded-lg shadow-2xl shadow-[#6FFFE9] hover:scale-105 transition duration-500 ease-in-out'
/>
</div>
)}
</div>
)
}
export default mint
我们来讨论一下添加到index.js文件中的代码。
代码包含以下函数:
-
checkIfWalletIsConnected:此函数在加载时检查钱包是否已连接到 dapp。 -
connectWallet当用户点击Connect Wallet前端按钮时,此功能会将钱包连接到 dapp。 -
checkCorrectNetwork此函数检查钱包是否已连接到rinkeby网络。如果未连接,前端会提示用户连接到rinkeby网络并重新加载页面。 -
mintCharacter:此功能会在用户点击按钮时创建铸造新 NFT 的交易Mint Character。 -
getMintedNFT:此函数检索新铸造的 NFT 的数据,并在前端显示它。
要在浏览器中测试 dapp,请在终端中运行以下命令:
cd client
npm run dev
后续步骤
恭喜!您已将全栈 NFT 铸造 dapp 部署到以太坊。
成功部署 dapp 后,您可以将其托管在Vercel或Netlify等服务上。
希望您喜欢这篇文章!如果您有任何问题或评论,欢迎在下方留言或联系我。
文章来源:https://dev.to/abhinavxt/building-an-nft-minting-dapp-using-hardhat-ethersjs-nextjs-and-tailwindcss-lp