# 初级用例 **Published by:** [icepy](https://paragraph.com/@icepy/) **Published on:** 2022-03-17 **URL:** https://paragraph.com/@icepy/fS76qrBgwhiPziIip7oJ ## Content 这是一个关于如何配置 hardhat,创建前端工程,编写智能合约,部署智能合约,使用 ethers.js 与之交互的用例。 说明:必须先安装 nodejs,接下来我们所有的编程都会依赖和使用到它。 准备阶段 在本地终端上使用命令创建一个空目录,并且快速生成 package.json 文件,package.json 是 nodejs 用于管理项目和模块的文件: $ mkdir useweb3 $ cd useweb3 $ npm init -y 安装 Hardhat $ npm install hardhat --save-dev 让我们对 package.json 做一些适当的修改,scripts 字段上: "scripts": { "init": "hardhat", "node": "hardhat node", "test": "hardhat test", "compile": "hardhat compile" } 在终端处运行 npm run init,且选择 create a basic sample project 来初始化项目。 现在我们得到了一个有如下目录结构的项目: contracts 编写合同的目录 scripts 编写部署脚本的目录 test 编写测试脚本的目录 hardhat.config.js hardhat 的配置文件 接下来,我们可以利用 create-react-app 来创建前端工程 说明:你也可以选择 Next.js $ npx create-react-app client 最后我们需要完善 hardhat 的配置和部署脚本,在根目录中创建一个.env文件,并安装 dotenv: $ npm install dotenv --save-dev ⚠️⚠️⚠️ .env 文件由于存放了私钥,千万不要提交!!! 我们主要将合约部署在本地和 rinkeby 测试网络,所以 .env 文件中,只配置这两项。 ALCHEMY_API_KEY_URL="" LOCALHOST_URL="" PRIVATE_KEY="" LOCALHOST_PRIVATE_KEY="" 在 https://www.alchemy.com/ 上创建一个 rinkeby 项目: 点击 VIEW KEY 来获取 URL 粘贴到 .env 文件的 ALCHEMY_API_KEY_URL 字段 私钥可在 metamask 上获取,粘贴到 .env 文件的 PRIVATE_KEY 字段 接下来我们需要配置本地网络,在终端上使用 npm run node 启动节点,hardhat 会自动创建 20 个地址,每一个地址上都有 10000 个 ETH,随便获取其中一个的私钥: $ npm run node 粘贴到 .env 文件的 LOCALHOST_PRIVATE_KEY 字段 本地网络 http://localhost:8545 粘贴到 .env 文件 的 LOCALHOST_URL 字段 链 ID 修改为 31337 现在我们需要对 hardhat.config.js 做一些修改: require("@nomiclabs/hardhat-waffle"); require("dotenv").config({path: '.env'}); // This is a sample Hardhat task. To learn how to create your own go to // https://hardhat.org/guides/create-task.html task("accounts", "Prints the list of accounts", async (taskArgs, hre) => { const accounts = await hre.ethers.getSigners(); for (const account of accounts) { console.log(account.address); } }); // You need to export an object to set up your config // Go to https://hardhat.org/config/ to learn more const ALCHEMY_API_KEY_URL = process.env.ALCHEMY_API_KEY_URL; const LOCALHOST_URL = process.env.LOCALHOST_URL; const PRIVATE_KEY = process.env.PRIVATE_KEY; const LOCALHOST_PRIVATE_KEY = process.env.LOCALHOST_PRIVATE_KEY; /** * @type import('hardhat/config').HardhatUserConfig */ module.exports = { solidity: "0.8.4", networks: { rinkeby: { url: ALCHEMY_API_KEY_URL, accounts: [PRIVATE_KEY], }, localhost: { url: LOCALHOST_URL, accounts: [LOCALHOST_PRIVATE_KEY] } } }; 最后在 scripts 目录中创建 deploy.js 文件,并修改 package.json 在 scripts 中增加两个配置: { "rinkeby": "hardhat run scripts/deploy.js --network rinkeby", "localhost": "hardhat run scripts/deploy.js --network localhost" } deploy.js 因为还是一个空文件,所以并不急于测试。 编写一个简单的合约 在 contracts 目录中创建一个 Hello 合约,在初始化时将 Hello useweb3 传递给它,并且实现一个函数可返回这个字符串 ! hardhat/console.sol 是 hardhat 提供可以打印日志的模块 // SPDX-License-Identifier: SEE LICENSE IN LICENSE pragma solidity ^0.8.4; import "hardhat/console.sol"; contract Hello { string text; constructor(string memory _text) { console.log("%s", _text); text = _text; } function say() public view returns (string memory) { return text; } } 现在我们需要在deploy.js 文件中编写我们的部署脚本: const hre = require("hardhat"); async function main(){ const Hello = await hre.ethers.getContractFactory("Hello"); const hello = await Hello.deploy("hello useweb3"); hello.deployed(); console.log("Hello Contract Address: ", hello.address); } main() .then(() => process.exit(0)) .catch((error) => { console.error(error); process.exit(1); }); 现在,我们如果想将合约部署到本地网络可运行: $ npm run localhost 如果我们想将合约部署到rinkeby测试网络可运行: $ npm run rinkeby 通过在终端上输出的合约地址,在 https://rinkeby.etherscan.io/ 处查看。 最后,我们需要在 test 目录创建一个 Hello.js 用于测试 const { expect } = require("chai"); const { ethers } = require("hardhat"); describe("Hello", function () { it("测试 say 函数的返回值", async function () { const value = "hello useweb3"; const Hello = await ethers.getContractFactory("Hello"); const hello = await Hello.deploy(value); await hello.deployed(); expect(await hello.say()).to.equal(value); }); }); 编写一个简单的可交互界面 说明:后续的开发与测试都在本地网络上进行 我们需要将合约进行一些修改,将状态存储到区块链上,并且通过钱包的交互来改变状态。 // SPDX-License-Identifier: SEE LICENSE IN LICENSE pragma solidity ^0.8.4; import "hardhat/console.sol"; contract Hello { string text; uint256 count; constructor(string memory _text) { console.log("%s", _text); text = _text; count = 0; } function addition() public{ count += 1; } function getCount() public view returns (uint256) { return count; } function say() public view returns (string memory) { return text; } } 重新运行 npm run localhost 来部署合约。 在进入之前又需要普及一个知识点:ABI(Application Binary Interface)和 ethers.js。 ABI是合约接口的说明,内容包括合约的接口列表、接口名称、参数名称、参数类型、返回类型等,这些信息以 JSON 格式保存,可以在 solidity 文件编译时由合约编译器生成。 ethers.js 是目前前端用来与合约进行交互的基础库,提供只读(Provider)和可读可写(Signer)的能力 在 client/src 目录中创建一个 abi 目录并创建一个 Hello.json 文件,现在我们需要将之前部署好的合约生成的对应 JSON 复制过来。 说明:由于只支持 metamask 钱包进行交互,因此直接依赖 Web3Provider import React, { useEffect, useState } from 'react'; import { ethers } from 'ethers'; import HelloJSON from './abi/Hello.json'; import './Hello.css' const HELLO_CONTRACT_ADDRESS = "0xB581C9264f59BF0289fA76D61B2D0746dCE3C30D"; const abi = HelloJSON.abi; const getWeb3Provider = () => { let web3Provider = window.web3Provider; if (!web3Provider){ web3Provider = new ethers.providers.Web3Provider(window.ethereum); window.web3Provider = web3Provider; } return web3Provider; } export default function Hello(){ const [address, setAddress] = useState(''); const [count, setCount] = useState(0); useEffect(() => { if (!address){ const web3Provider = getWeb3Provider(); const selectedAddress = web3Provider.provider.selectedAddress; if (!selectedAddress){ web3Provider.provider.request({ method: 'eth_requestAccounts' }).then((accounts) => { const account = accounts[0]; setAddress(account); }); } else { setAddress(selectedAddress); } } }, [address]); const onGetCount = async () => { const web3Provider = getWeb3Provider(); const signer = web3Provider.getSigner(); const helloContract = new ethers.Contract(HELLO_CONTRACT_ADDRESS, abi, signer); const result = await helloContract.getCount(); setCount(result.toNumber()); } const onAddition = async () => { const web3Provider = getWeb3Provider(); const signer = web3Provider.getSigner(); const helloContract = new ethers.Contract(HELLO_CONTRACT_ADDRESS, abi, signer); await helloContract.addition(); } return ( <div className='helloContainer'> <div>address: {address}</div> <div className='helloButtons'> <button onClick={onGetCount}>Update Count</button> <button onClick={onAddition}>Addition</button> </div> <div>count: {count}</div> </div> ); } 点击 Addition 来修改区块链的状态 点击 Update Count 来更新 count 的值 ## Publication Information - [icepy](https://paragraph.com/@icepy/): Publication homepage - [All Posts](https://paragraph.com/@icepy/): More posts from this publication - [RSS Feed](https://api.paragraph.com/blogs/rss/@icepy): Subscribe to updates - [Twitter](https://twitter.com/i_icepy): Follow on Twitter