# 构建全栈 ERC-20 Dapp **Published by:** [元峰GPT](https://paragraph.com/@gpt-2/) **Published on:** 2023-02-06 **URL:** https://paragraph.com/@gpt-2/erc-20-dapp ## Content https://www.blocktrain.info/blog/build-a-full-stack-erc-20-dapp📖 你会学到什么?使用 Solidity 创建智能合约。利用 OpenZeppelin 创建 ERC20 合约使用 Hardhat 编译和部署智能合约。连接到 Metamask 钱包。Metamask 钱包重要方法和事件。通过 Etherjs 与来自 React/Next 应用程序的智能合约交互。👀 先决条件如果你以前使用过 Javascript 和 Reactjs,你会相处得很好。 你需要安装 Nodejs。 您还需要为 chrome 安装和设置 Metamask。⚒️ 我们在建造什么?我们正在建立一个网站,用户可以在其中连接到他们的以太坊钱包并铸造 ERC20 代币以换取一些 goerli 以太币。 我们将使用 Alchemy 节点将此智能合约部署在 Goerli 测试网上。一旦用户连接到钱包,他就可以铸造、转移甚至销毁他的 HKP 代币。 HKP 是我名字 Harsh Kumar Pandey 的缩写。我用它作为令牌的符号。您可以使用您的姓名或其他任何内容。Project-ERC20 项目预览🏃‍♀️ 启动并运行您的本地环境首先,您需要获取 node/npm。如果你没有它,请到这里 接下来,让我们前往航站楼。继续并 cd 到你想要工作的目录。一旦你在那里运行这些命令:mkdir project-erc20 cd project-erc20 npx create-next-app . npm install --save-dev hardhat 这里发生的是,你运行: 1. mkdir project-erc20 创建一个名为“project-erc20”的目录。 2. cd project-erc20 进入新建的目录。 3. npx create-next-app . 在当前目录中生成下一个应用程序模板。 4. npm install --save-dev hardhat 安装 Hardhat。 在运行最后一个命令并安装 Hardhat 后,您可能会看到有关漏洞的消息。每次从 NPM 安装某些东西时,都会进行安全检查,以查看您正在安装的任何软件包或库是否有任何已报告的漏洞。这更像是对你的警告,所以你要知道!⚙️初始项目设置太棒了,现在我们应该拥有 hardhat 了。 注意:如果您在 Windows 上使用 Git Bash 安装 hardhat,您可能会在这一步 (HH1) 遇到错误。如果遇到问题,您可以尝试使用 Windows CMD 执行 HardHat 安装。可以在此处找到其他信息。 选择创建基本示例项目的选项。对一切都说是。 示例项目将要求您安装 hardhat-waffle 和 hardhat-ethers。这些是我们稍后会用到的其他好东西。 这将生成一些文件夹和文件,我们将在后面详细介绍所有内容。 继续安装这些其他依赖项,以防它没有自动执行。npm install --save-dev @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ethers dotenv 您还需要安装 OpenZeppelin,这是另一个经常用于开发安全智能合约的库。我们将在下一节中详细了解它。现在,只需安装它 :)。npm install @openzeppelin/contracts ✍️ 关于智能合约的一些理论首先,我们需要让我们的本地以太坊网络正常工作。这就是我们编译和测试智能合约代码的方式。 现在,您只需要知道智能合约是一段存在于区块链上的代码。区块链是一个公共场所,任何人都可以通过收费安全地读写数据。 在合约内部,有变量和函数。变量存储数据(如数据库),函数帮助读取和写入这些变量。 这里的大局是: 我们要写一个智能合约。该合约具有围绕我们的 ERC20 的所有逻辑,即铸币、转让和销毁代币。📃 创建智能合约现在项目设置已经完成,我们可以开始为我们的应用程序编写智能合约。 在 contracts 文件夹中,创建一个名为 Token.sol 的新文件,并向其中添加以下代码。// SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; // This will import all the variables and methods of contracts mentioned after <is< // So we can accessing and override them according to our needs. contract HKPtoken is ERC20, ERC20Burnable, Ownable { // public keyword here creates a getter function which returns the value or the variable. // We can access these variable by simply writing variable name <tokenPrice< // But to access these vairables from inherited contracts, we need to call them <tokenPrice()<. uint public tokenPrice; uint public maxSupply; constructor() ERC20("HKPtoken", "HKP") { // In solidity, quantity of 1 means 1 * 10^18 // and 1 Ether means, 1 * 10^18 wei // Setting token price to 0.2 ether tokenPrice = 2000000000000000; // Max supply is 150 tokens maxSupply = 150000000000000000000; } function mint(uint amount) public payable{ // totalSupply means total number of Tokens already minted. // checking if totalSupply + requested amount is <= max allowed Supply(maxSupply) require( totalSupply() + amount <= maxSupply, "Exceeding max supply" ); // checking if ether sent by minter is in accordence with tokenPrice and amount. require( msg.value * 10 ** decimals() / amount >= tokenPrice, "Pay Ether according to Token Price" ); // This _mint() function is provided by OpenZeppelin ERC20 contract. There are generic codes that every ERC20 contract must have. OpenZeppelin helps us by removing the need to writing generic code and focus on requirement part. _mint(msg.sender, amount); } function withdrawEther() public onlyOwner { payable(owner()).transfer(address(this).balance); } // We will call this function to get these information to show in front-end application. // We are returning multiple things from single function here so that we won<t have to call each getter function one by one. function returnState() public view returns(uint _myBalance, uint _maxSupply, uint _totalSupply, uint _tokenPrice ){ return (balanceOf(msg.sender), maxSupply, totalSupply(), tokenPrice); } // Notice how we are calling <totalSupply()< to access.This is because this variable is inherited from ERC20 contract by OpenZeppelin. } 🚀 使用 Hardhat 部署智能合约让我们在 hardhat.config.js 更新 hardhat 配置。// We need to import these extensions in order for hardhat to function properly require("@nomiclabs/hardhat-waffle"); require("@nomiclabs/hardhat-ethers"); // We need hardhat-etherscan to upload(verify) aur smart contractto etherscan. require("@nomiclabs/hardhat-etherscan"); require("dotenv").config(); module.exports = { solidity: "0.8.9", networks: { goerli: { url: process.env.ALCHEMY_RPC_URL, accounts: [`0x${process.env.WALLET_PRIVATE_KEY}`], }, }, // We don<t need following ethescan block for deployment, but we need it for smart contract verification (upload). etherscan: { apiKey: process.env.NEXT_PUBLIC_ETHERSCAN_API_KEY, }, }; ALCHEMY_RPC_URL 是在我们的应用程序和区块链之间创建连接。 WALLET_PRIVATE_KEY 是将部署合约并将成为合约所有者的帐户。 需要 NEXT_PUBLIC_ETHERSCAN_API_KEY 来验证智能合约。这样我们也可以看到来自 etherscan 的智能合约并与之交互。让我们抓住 ALCHEMY_RPC_URL :1.在 alchemy.com 上创建一个帐户。 2.进入仪表板后,单击“创建应用程序”。3.为 App 命名和描述。4.保留链以太坊并选择 Goerli 作为网络。 5.创建 App 后,您将看到 Key、HTTPS Url 和 Websocket。6.复制 HTTPS Url 并将其粘贴到 .env 中,这是我们的 RPC URL让我们抓住 NEXT_PUBLIC_ETHERSCAN_API_KEY :在 etherscan.io 上创建一个帐户登录然后访问etherscan.io/myapikey单击“添加”,为应用程序命名,仅此而已。您将看到 api 密钥,复制并粘贴到 .env 中请注意,我们可以在免费计划中每秒进行 5 次 API 调用。我们之前在合约中创建的“getState()”函数将保存 api 调用并在一次调用中获取所有重要信息。让我们从 metamask 中获取 WALLET_PRIVATE_KEY :1.打开 MetaMask chrome 扩展。 2.解锁并点击右上角的3个点 3.单击“帐户详细信息”。4.单击“导出私钥”。 5.将私钥复制到 .env 中 现在你的 .env 文件应该是这样的。WALLET_PRIVATE_KEY = "6sadf43rtf7df3451f033453b7a293651096083e534rfcff48"; NEXT_PUBLIC_ETHERSCAN_API_KEY = "SD678FTGW7FU6GWEDFC6WE5FF88"; ALCHEMY_RPC_URL = "https://eth-goerli.g.alchemy.com/v2/SDFSDFSEFRR345T43R34RTFR"; 确保在推送到 github 之前将此文件添加到 .gitignore。 现在我们可以部署甚至验证我们的智能合约了。 继续并在 scripts 文件夹下创建一个名为 deploy.js 的文件。 将下面的代码复制粘贴到 deploy.js 文件。const main = async () => { // Here <hre< is injected automatically by hardhat, no need to import it explicitly. const DevToken = await hre.ethers.getContractFactory("HKPtoken"); // Any value passed to deploy() will be passed to contructor of our contract as parameters. const devToken = await DevToken.deploy(); // <deploy()< in previous line deploys the contract. // <deployed() in next line checks if contract is deployed. await devToken.deployed(); // Once deployed (in 20-30 seconds) you will see the contract address in console. You can also check the transaction on etherscan goerli network. console.log("Contract deployed to: ", devToken.address); }; main() .then(() => process.exit(0)) .catch((error) => { console.error(error); process.exit(1); }); 让我们编译我们的合约,在终端中输入这段代码。npx hardhat compile 这会在项目根目录中创建一个名为 artifacts 的新文件夹,并使用我们的令牌的编译版本填充它。 我们需要 artifacts 中的 HKPtoken.json 文件,以便稍后从前端与合约进行交互。 在我们部署我们的合约之前,我们需要在我们的账户中使用 goerli ether 来支付所有交易(部署,函数调用)的 gas 费用。 前往 Goerli Faucet 输入您的钱包地址,goerli eth将在20-30秒后转入您的钱包。 还有许多其他水龙头,如果您需要更多,请谷歌。 现在在终端中运行以下代码,通过运行我们之前创建的 deploy.js 文件来部署合约。npx hardhat run ./scripts/deploy.js --network goerli 运行几秒钟后,您将在终端中看到您部署的合约地址,如下所示npx hardhat run ./scripts/deploy.js --network goerli Contract deployed to: 0x3a7a068B3Bf32675F8C8dA60d34Cea7C02a5736b 复制此地址并将其保存在文件中,稍后我们将需要它以及 HKPtoken.json 文件来与合约进行交互。 现在,如果您转到 https://goerli.etherscan.io/address/0x3a7a068B3Bf32675F8C8dA60d34Cea7C02a5736b 并单击合同。 你会看到合约的字节码。现在让我们验证我们的智能合约。 在终端中输入以下代码。npx hardhat verify --network goerli 0x3a7a068B3Bf32675F8C8dA60d34Cea7C02a5736b 最后部署的合约地址 这段代码的执行完成后,再次转到 etherscan,合约选项卡。您将看到已部署合约的代码,以及所有继承的合约。 您还会在左上角看到这些按钮,读取合同和写入合同按钮。所以现在你也可以在连接到钱包后从 etherscan 读取和写入状态变量。💼 连接到钱包并处理事件。将 artifacts/contracts/HKPtoken.json 的内容复制粘贴到 helpers/abi.json 制作另一个文件 helpers/contants.js 并复制粘贴到下面的代码中。import abi from "./abi.json"; // Get abi array from abi.json file export const contractABI = abi.abi; // Deployed contract address export const contractAddress = "0x3a7a068B3Bf32675F8C8dA60d34Cea7C02a5736b"; 创建一个 context/walletContext.jsx 文件并添加以下代码。import { createContext, useContext, useEffect, useState } from "react"; import { contractABI, contractAddress } from "@/helpers/constants"; import { ethers } from "ethers"; export const walletContext = createContext(); export const useWalletContext = () => useContext(walletContext); function WalletProvider({ children }) { const [currentAccount, setCurrentAccount] = useState(null); const [contractState, setContractState] = useState(null); // Create a button labled "Connect Wallet" and on Click, call this function. const connectWallet = async () => { try { // If metamask extension is not installed, etherem will be undefined. const { ethereum } = window; if (!ethereum) return; const accounts = await ethereum.request({ // This opens metamask and ask to connect to the website. method: "eth_requestAccounts", }); // If connected, returns array of accounts(address) which you allow during request setCurrentAccount(accounts[0]); } catch (error) { console.log(error); } }; // This Function checks if website is connected to wallet or not. const checkConnection = async () => { try { // If metamask extension is not installed, etherem will be undefined. const { ethereum } = window; if (!ethereum) return; const accounts = await ethereum.request({ // this method check for any connection and returns array of accounts which are connected method: "eth_accounts", }); // First account in array is the active one. setCurrentAccount(accounts[0]); try { // If you are on mumbai network on metamask, this will ask you to switch to goerli network await window.ethereum.request({ method: "wallet_switchEthereumChain", // chainId must be in hexadecimal numbers. params: [{ chainId: `0x${Number(80001).toString(16)}` }], }); } catch (error) { console.log(error); // If there was error switching, code 4902 represents that goerli network is not added to metamask, so method below will pre-fill the network details and ask user to add the network. if (error.code === 4902) { addNetwork(); } if (error.code === -32002) { alert("Open Metamask"); } } // This function is declared at the bottom. // Will explain this with function declaration. getContractState(); } catch (error) { console.log(error); } }; const addNetwork = async () => { try { await window.ethereum.request({ // Learn more about this method here https://docs.metamask.io/guide/rpc-api.html#wallet-addethereumchain // To get the details of any chain, visit https://chainid.network/chains.json method: "wallet_addEthereumChain", params: [ { chainId: `0x${Number(80001).toString(16)}`, chainName: "Mumbai", nativeCurrency: { name: "MATIC", symbol: "MATIC", decimals: 18, }, rpcUrls: [ "https://matic-mumbai.chainstacklabcom", "https://rpc-mumbai.maticvigil.com", "https://matic-testnet-archive-rpbwarelabs.com", ], blockExplorerUrls: ["https://mumbapolygonscan.com"], }, ], }); } catch (error) { console.log(error); } } const getContract = () => { const { ethereum } = window; if (!ethereum) return; // This provider is connecting us to blockchain, there could be different types of web3 providers. // lern more here https://docs.ethers.io/v5/api/providers/ const provider = = new ethers.providers.Web3Provider(ethereum); // signer is required to call <write methods< of contracts. Every transaction is signed by this signer with the help of private key of your wallet, provided with ethereum object. const signer = provider.getSigner(); // We pass the contract address, abi and signer to get access to all the functions of the contract, and the ability to call setter functions. // We can also pass provider instead of signer in case user is not connected to wallet. In this user cannot call setter functions of contract, only getter functions could be called. const contractMethods = new ethers.Contract( contractAddress, contractABI, signer ); return contractMethods; }; useEffect(() => { if (!window.ethereum) return; // Metamask is installed, check connection and get contract state (tokenPrice, totalSupply, maxSupply, balance of user) checkConnection(); // Adding event listeners // In metamask, you can either change the active account(user), or change the active network (goerli, mumbai, kovan, etc.) ethereum.on("chainChanged", handleChainChanged); ethereum.on("accountsChanged", handleDisconnect); // Cleanup of listener on unmount return () => { ethereum.removeListener("chainChanged", handleChainChanged); ethereum.removeListener("accountsChanged", handleDisconnect); }; }, []); const handleDisconnect = (accounts) => { if (accounts.length == 0) { setCurrentAccount(""); } else { setCurrentAccount(accounts[0]); } }; const handleChainChanged = (chainId) => { // If the chain is changed to goerli network, don<t do anything. if (chainId == "0x13881") return; // chain id is received in hexadecimal // chain is changed to any other network, reload the page. // On reload, checkConnection will run due to useEffect. // Inside of that function, we are asking user to switch to goerli network. window.location.reload(); }; const contextValue = { connectWallet, currentAccount, contractState, getContract, }; return ( <walletContext.Provider value={contextValue}> {children} </walletContext.Provider> ); } export default WalletProvider; 上面的代码解释了有关连接钱包和处理事件的所有内容。 但是记住我已经为孟买测试网编码了 addNetwork() ,你必须找出并填写goerli 测试网的详细信息。资源https://docs.metamask.io/guide/rpc-api.html#wallet-addethereumchain😬 与合约互动现在让我们调用智能合约的函数并获取我们或 OpenZeppelin 声明的状态变量的值。 将下面的代码添加到 walletContext.jsx获取合约状态变量const getContractState = async () => { try { // contractMethods will have all the functions of our smart contract as well as the inherited smart contract. const contractMethods = getContract(); // We declared this function to get the values of our state variables. We were returning multiple values from this function, so we<ll recieve them in array. const state = await contractMethods.returnState(); // Accessing them by index and storing in react state. // Remember 1 in JS = 1*10^18 in solidity? Values returned by contract are not supported in JS, so ether gives us util function to format / parse them. // so we are formatting the values using ethers.utils.formatEther(). setContractState({ myBalance: state[0] ? parseFloat(ethers.utils.formatEther(state[0])) : 0, maxSupply: parseFloat(ethers.utils.formatEther(state[1])), totalSupply: parseFloat(ethers.utils.formatEther(state[2])), tokenPrice: ethers.utils.formatEther(state[3]), }); } catch (error) { console.log(error); } }; Mint tokens 铸币代币现在让我们了解如何通过向合约的 mint() 函数发送以太币和预期参数来铸造代币。const handleMint = async () => { // mintAmount is a react state hooked with an number input. if (mintAmount == 0) return; // Getting all the methods of contract. const contractMethods = createEthereumContract(); // Declaring value (number of ether) that has to be sent with function call (Transaction). // Calculating how munch ether user need to send based on tokenPrice to get mintAmount number of token. // Like I<ve mentioned earlier, 1 in JS = 1*10^18 in Solidity, so we are converting JS value to Solidity supported value using ethers.utils.parseEther(). const options = { value: ethers.utils.parseEther( (mintAmount * contractState.tokenPrice).toString() ), }; // ethers.utils.parseEther() only accepts string, so we need to convert mintAmount to String. // Passing our option object as the LAST PARAMETER to mint() function of contract which determines how much ether to be sent. try { const txn = await contractMethods.mint( ethers.utils.parseEther(mintAmount.toString()), options ); // Once transaction is initiated, we can wait for the transaction be get mined. await txn.wait(); // Once transaction is mined, Fetch updated states of contract. getContractStates(); } catch (error) { console.log(error); } }; Transfer tokens. 转移代币既然您已经了解了代币的铸造,那么转移和销毁它们就是简单的函数调用,就像铸造一样。const handleTransfer = async () => { // amount is react state hooked with number input to determine how much tkoen to transfer. if (amount == 0) return; // Fetching functions of contract. const contractMethods = createEthereumContract(); try { // Passing the wallet adrress of receiver and amount after parsing it. const txn = await contractMethods.transfer( to, ethers.utils.parseEther(amount.toString()) ); // Waiting for transaction to be mined. await txn.wait(); // Once mined, fetching updated contract states. getContractStates(); } catch (error) { // Handle Common Errors. if (error.reason === "invalid address") alert("Invalid Address"); else if ( error.reason === "execution reverted: ERC20: transfer amount exceeds balance" ) alert("Transfer amount exceeds balance"); else alert(error.reason); } }; 使用 console.log(contractMethods) 检查 const contractMethods = createEthereumContract(); 之后有哪些可用方法。 您会发现带有一个参数的 burn() 方法。尝试自己实现 handleBurn() 方法。这些红色标记是从我们合约的 returnState() 方法返回的合约状态。😍 你做到了超级令人兴奋,你能走到最后。相当重要! 感谢您通过学习这些东西为 Web3 的未来做出贡献。事实上,您知道它是如何工作的以及如何对其进行编码是一种超能力。明智地使用你的力量。 链游 /Dapp /NFT数藏 /公链 /交易所等区块链技术开发业务对接。 开启区块链元宇宙之旅,拥抱数字未来。 业务咨询:15829049609 ## Publication Information - [元峰GPT](https://paragraph.com/@gpt-2/): Publication homepage - [All Posts](https://paragraph.com/@gpt-2/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@gpt-2): Subscribe to updates - [Twitter](https://twitter.com/Metafeng09): Follow on Twitter