本教程详细介绍了如何在以太坊区块链上使用Hardhat和Ethers.js创建、运行、编译和部署智能合约。通过使用合约代码示例以及连接React前端的步骤,读者将能够创建一个简单的代币合约并将其结合到一个去中心化的应用程序中。
在以太坊区块链上构建智能合约时,新开发人员通常会联系truffle和web3.js等工具来构建他们的智能合约。本教程将介绍如何使用Hardhat和Ether.js,这些工具现在正成为构建智能合约的标准。
在本教程中,我们将创建、运行、编译并部署使用Hardhat以太坊开发环境制作的智能合约。
Node.js 已安装。
有一些Solidity知识
对React有工作知识
在这里,我们将查看如何设置我们的项目以及一些必要的包,需要安装的包。要初始化和配置我们的项目,我们需要创建一个目录来容纳我们的dApp。由于我们将使用React作为我们的UI库,我们可以使用create-react-app来初始化一个模板项目。
npx create-react-app moDappTut
然后我们将逐步添加更多包,并构建我们的dApp。接下来,我们将安装Hardhat、Ethers.js(确保你使用的是版本5.7)和其他重要库来创建我们的dApp。以下是我们将要安装的包的列表。
npm install ethers@5.7 hardhat @nomiclabs/hardhat-waffle ethereum-waffle @nomiclabs/hardhat-ethers chai
查看上面的部分,我们已经知道Ethers和Hardhat包将用于什么。我们还有来自 @nomiclabs 的支持包,这些包在构建我们的dApp时允许顺利的开发流程;然后,还有chai库用于测试我们的智能合约。在下一部分中,我们将创建我们的示例Hardhat项目。
要创建一个带有所有需要的Hardhat配置的基本设置,我们在终端中运行 npx hardhat 命令。
npx hardhat
在提示时选择以下设置:
你想做什么? 选择 创建一个基本示例项目
Hardhat项目根目录 按Enter键将当前目录设置为根目录
你想添加.gitignore吗?(Y/n) n
现在我们可以创建一个基本的示例项目,这将默认Hardhat配置设置在我们的当前目录 moDappTut 中。在我们的编辑器中,我们会看到Hardhat配置、测试、脚本文件和合约文件夹已添加到我们的项目中。
注意: 你可以通过Hardhat配置文件在项目中进行不同的配置,例如更改默认的部署网络或自定义测试或工件的路径。有关hardhat配置的更多信息,请参阅 这里。让我们深入到下一部分以创建我们的智能合约。
在这一部分,我们将分析Hardhat提供的默认合约,并创建我们将在本教程后面用于交易的自定义代币。
我们可以在合约文件夹中的Greeter.sol文件中看到Hardhat提供的一个已编写的合约。
在Greeter合约中,我们有两个函数。一个函数返回问候语,而另一个函数设置一个新的问候语,这将改变当前问候语的值。我们将向我们的合约文件夹添加一个新合约。合约将是一个命名为MDToken的代币合约。
//MDToken.sol
//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.7.0;
contract Token {
string public name = "Madfinger Token";
string public symbol = "MHT";
// 存储在无符号整数类型变量中的固定代币数量。
uint256 public totalSupply = 1000000;
// 使用地址类型变量来存储以太坊账户。
address public owner;
// 映射是键/值映射。在这里,我们存储每个账户余额。
mapping(address => uint256) balances;
/**
* 合约初始化。
*
* 在合约创建时只执行一次`constructor`。
* `public`修饰符使函数可从外部合约调用。
*/
constructor() {
// 将totalSupply分配给交易发送者,即部署合约的账户。
balances[msg.sender] = totalSupply;
owner = msg.sender;
}
/**
* 转移代币的函数。
*
* `external`修饰符使函数**只能**从外部合约调用。
*/
function transfer(address to, uint256 amount) external {
// 检查交易发送者是否有足够的代币。
// 如果`require`的第一个参数计算为`false`,则交易将会回退。
require(balances[msg.sender] >= amount, "Not enough tokens");
// 转移金额。
balances[msg.sender] -= amount;
balances[to] += amount;
}
/**
* 只读函数以检索给定账户的代币余额。
*
* `view`修饰符指示它不修改合约的状态,这允许我们在不执行交易的情况下调用它。
*/
function balanceOf(address account) external view returns (uint256) {
return balances[account];
}
}
在我们上述的MDToken合约中,我们创建了一个可以用于交易的代币,以及在部署后发送和接收该代币。如果你仍然对Solidity不熟悉,合约中的注释解释了每段代码的作用。接下来,我们需要做的是部署我们的合约。
在这一部分中,我们将把代币部署到Hardhat提供的本地节点。要启动本地节点,我们只需运行以下命令:
npx hardhat node
运行节点后,Hardhat提供了20个具有虚拟资金的账户,我们可以用于交易。以下是我们终端的情况:
从上面的图片中可以看到,我们的本地测试网络运行在 http://127.0.0.1:8545/。
每个账户都有一个钱包账户ID和一个私钥。我们将使用它们稍后连接到MetaMask。
在我们的脚本文件夹中,创建一个 deploy.js 文件,在其中编写部署合约的脚本。Hardhat为我们提供了一个用于部署Greeter合约的示例脚本 sample-script.js。我们可以根据自己的需求调整合约,并部署MDToken合约。以下是我们的部署脚本:
//deploy.js
const hre = require("hardhat");
async function main() {
// ethers在全局范围内可用
const [deployer] = await hre.ethers.getSigners();
console.log(
"Deploying the contracts with the account:",
await deployer.getAddress()
);
console.log("Account balance:", (await deployer.getBalance()).toString());
const Token = await hre.ethers.getContractFactory("Token");
const token = await Token.deploy();
await token.deployed();
console.log("Token address:", token.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
当我们部署我们的合约时,Hardhat会自动创建一个工件文件夹,并将其存储在项目目录的根文件夹中。此工件文件夹包含我们智能合约所需的所有信息,以便与我们的前端连接。我们希望将工件文件夹放在src目录中,因此可以从Hardhat配置文件进行配置。
//hardhat.config.js
module.exports = {
paths: {
artifacts: './src/artifacts',
},
solidity: "0.8.4",
};
现在,最后要部署我们的合约,我们运行以下命令:
npx hardhat run --network localhost scripts/deploy.js
我们的合约现在已部署在测试网络上,我们可以从终端看到有关部署的信息。Hardhat提供的第一个账户是我们用来部署合约的账户,并且部署此合约会收取一个Gas费用。
现在我们的合约已部署,让我们把我们的MetaMask账户连接到它。
现在我们的合约已部署,让我们把我们的MetaMask账户连接到它。
本部分将介绍设置MetaMask账户和添加我们的本地Hardhat网络的步骤。
安装 MetaMask扩展 并设置你的账户。要设置我们的MetaMask账户,我们必须创建一个新钱包并备份我们的短语。在设置完MetaMask钱包后,如果我们单击MetaMask图标,我们会看到我们在以太坊主网。我们想要做的是将我们的本地Hardhat网络连接到MetaMask。我们可以这样做:
确保你的测试网络设置为可见。导航到你的MetaMask设置,转到高级设置,并将显示测试网络设置为开启。
转到你的设置并选择网络设置,然后单击添加新网络。
填写所需信息并保存,你现在已设置好本地Hardhat网络。
以下是设置网络所需提供的信息的截图:
现在让我们导入我们用于部署Token合约的账户。单击导入账户,并粘贴你从Hardhat提供的第一个账户使用的私钥,该账户用于部署合约。你会注意到,我们获得的10000 ethers由于我们支付给部署合约的Gas费用有所减少。现在是时候将我们创建的智能合约连接到我们的React前端。
在本部分中,我们将本地运行我们的React应用程序,将我们的合约与ethers库连接,并编写代码以测试一切是否正常工作。
首先,通过运行以下命令启动react应用程序:
npm start
为了使我们的dApp完整,我们需要将智能合约与React前端连接。首先,我们导入ethers库以与我们的智能合约数据进行通信,然后从React中使用useState来管理我们的应用状态。我们需要导入TokenArtifacts ABI,它包含我们所需的合约数据,然后将我们的代币地址设置为我们的合约部署到的地址,也就是合约地址。
我们使用react useState在状态中存储三个变量:
tokenData: 这是一个对象,用来存储有关代币合约的信息,包括名称和符号
amount: 我们将使用amount来存储我们将要发送到另一个地址的MDToken数量
userAccountId: 这将存储我们将要发送MDToken到的用户的账户地址。amount和userAccountId将从我们提供给用户UI的表单输入中设置。
// src/app.js
import './App.css';
import {ethers} from 'ethers'
import { useState } from 'react';
import TokenArtifact from "./artifacts/contracts/Token.sol/Token.json"
const tokenAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3"
function App() {
const [tokenData, setTokenData] = useState({})
const [amount, setAmount] = useState()
const [userAccountId, setUserAccountId] = useState()
async function requestAccount() {
await window.ethereum.request({ method: 'eth_requestAccounts' });
}
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
async function _intializeContract(init) {
// 我们首先通过使用window.ethereum创建一个提供者来初始化ethers
// 然后,我们使用该提供者和代币的工件初始化合约。你可以使用一般方式处理你的合约。
const contract = new ethers.Contract(
tokenAddress,
TokenArtifact.abi,
init
);
return contract
}
async function _getTokenData() {
const contract = await _intializeContract(signer)
const name = await contract.name();
const symbol = await contract.symbol();
const tokenData = {name, symbol}
setTokenData(tokenData);
}
async function sendMDToken() {
if (typeof window.ethereum !== 'undefined') {
await requestAccount()
const contract = await _intializeContract(signer)
const transaction = await contract.transfer(userAccountId, amount);
await transaction.wait();
console.log(`${amount} MDToken has been sent to ${userAccountId}`);
}
}
async function getBalance() {
if (typeof window.ethereum !== 'undefined') {
const contract = await _intializeContract(signer)
const [account] = await window.ethereum.request({ method: 'eth_requestAccounts' })
const balance = await contract.balanceOf(account);
console.log("Account Balance: ", balance.toString());
}
}
return (
<div className="App">
<header className="App-header">
<button onClick={_getTokenData}>获取代币数据</button>
<h1>{tokenData.name}</h1>
<h1>{tokenData.symbol}</h1>
<button onClick={getBalance}>获取余额</button>
<button onClick={sendMDToken}>发送MDToken</button>
<input onChange={e => setUserAccountId(e.target.value)} placeholder="账户ID" />
<input onChange={e => setAmount(e.target.value)} placeholder="金额" />
</header>
</div>
);
}
export default App;
我们将编写的前两个函数将是_requestAccount_函数,它将允许我们连接到用户的MetaMask钱包,以及_initializationContract_函数,该函数将初始化并提供我们的合约供我们使用。
async function requestAccount() {
await window.ethereum.request({ method: 'eth_requestAccounts' });
}
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
async function _intializeContract(init) {
// 我们首先通过使用window.ethereum创建一个提供者来初始化ethers
// 然后,我们使用该提供者和代币的工件初始化合约。你可以使用一般方式处理你的合约。
const contract = new ethers.Contract(
tokenAddress,
TokenArtifact.abi,
init
);
return contract
}
_requestAccount_函数是一个异步函数,调用window.ethereum.request方法并允许用户连接到其MetaMask钱包。为了使我们能够使用_initializeContract_函数,我们需要使用提供者和签名者。提供者是从我们导入的ethers库中获得的,并使用Web3Provider。
在_initializeContract函数中,我们将创建一个使用Ethers库中Contract方法的合约变量,该方法接受三个参数:代币地址、代币ABI和签名者或提供者。然后返回合约。稍后我们将看到如何使用_initializeContract函数。
这两个函数将允许我们在我们的dApp中执行我们想要的主要任务,具体为:
显示我们的MDToken信息。
获取我们的MDToken余额。
将MDToken发送给用户。
我们dApp中实现上述任务的三个函数是_getTokenData、getBalance和sendMDToken。
在我们的UI中,我们有三个按钮调用_getTokenData、getBalance和sendMDToken函数。然后是两个输入,用户可以在其中提供发送代币的账户地址以及要发送的金额输入。
return (
<div className="App">
<header className="App-header">
<button onClick={_getTokenData}>获取代币数据</button>
<h1>{tokenData.name}</h1>
<h1>{tokenData.symbol}</h1>
<button onClick={getBalance}>获取余额</button>
<button onClick={sendMDToken}>发送MDToken</button>
<input onChange={e => setUserAccountId(e.target.value)} placeholder="账户ID" />
<input onChange={e => setAmount(e.target.value)} placeholder="金额" />
</header>
</div>
);
现在我们在dApp将执行的内容有了一个概述,让我们深入了解事情如何运行。
当用户单击获取代币数据按钮时,将调用_getTokenData函数。
在我们的_getTokenData函数中,我们运行我们的_initializeContract函数并传入我们的签名者。该函数返回一个合约,我们将其分配给我们创建的合约变量。现在我们可以使用合约获取我们的MDToken名称和符号,并在我们的代币合约中将其标记为公共。然后我们调用setTokenData,该函数接受包含我们名称和符号的代币数据对象并更新tokenData状态。
async function _getTokenData() {
const contract = await _intializeContract(signer)
const name = await contract.name();
const symbol = await contract.symbol();
const tokenData = {name, symbol}
setTokenData(tokenData);
}
_getBalance_函数首先检查window.ethereum对象是否存在。如果存在,则意味着我们已安装MetaMask并且拥有钱包。然后我们初始化我们的合约。初始化合约后,我们获取连接到我们的MetaMask的账户,并利用合约来获取该账户的余额,使用来自我们合约的方法balanceOf,然后记录下账户余额。
async function getBalance() {
if (typeof window.ethereum !== 'undefined') {
const contract = await _intializeContract(signer)
const [account] = await window.ethereum.request({ method: 'eth_requestAccounts' })
const balance = await contract.balanceOf(account);
console.log("账户余额: ", balance.toString());
}
}
对于最后一个方法sendMDToken,我们首先检查window.ethereum对象是否存在,然后运行requestAccount函数并初始化我们的合约。我们使用合约的转移方法,它接受userAccountId和amount作为参数,然后我们等待交易发生并记录成功消息。
async function sendMDToken() {
if (typeof window.ethereum !== 'undefined') {
await requestAccount()
const contract = await _intializeContract(signer)
const transaction = await contract.transfer(userAccountId, amount);
await transaction.wait();
console.log(`${amount} MDToken已发送至${userAccountId}`);
}
}
注意: 要发送MDToken,我们必须先从MetaMask导入代币。为此,我们单击MetaMask,点击导入代币,选择自定义代币,然后粘贴代币地址或合约地址,你会看到我们的自定义MDToken已成功导入并带有可用的代币。
现在我们可以获取我们的代币信息,查看我们MDToken余额,该余额记录在控制台中,并在输入中提供要发送的账户ID和金额时传输MDToken。
你应确保不会将真实资金发送到任何提供的Hardhat账户。如果你将实际资金转移到本教程中使用的任何账户中,你的资金将永远丢失。
我们终于结束了本教程。在本教程中,我们看到了如何创建、运行、编译和部署使用Hardhat以太坊开发环境创建的智能合约。希望这能让你开始构建惊人产品的旅程!
来源:
订阅我们的 时事通讯 以获取更多关于以太坊的文章和指南。如果你有任何反馈,请通过 Twitter 联系我们。你也可以随时在我们的 Discord 社区服务器与我们聊天,那里有一些你将见到的最酷的开发人员 :)
- 原文链接: quicknode.com/guides/eth...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!