以太坊全栈开发完全指南

用React、Ethers.js、Solidity和Hardhat构建全栈dApps。

> * 原文:https://dev.to/dabit3/the-complete-guide-to-full-stack-ethereum-development-3j13 > * 译文出自:[登链翻译计划](https://github.com/lbc-team/Pioneer) > * 译者:[翻译小组](https://learnblockchain.cn/people/412) > * 校对:[Tiny 熊](https://learnblockchain.cn/people/15) > * 本文永久链接:[learnblockchain.cn/article…](https://learnblockchain.cn/article/2383) 本项目的代码在[这里](https://github.com/dabit3/full-stack-ethereum) 我最近加入了[Edge & Node](https://twitter.com/edgeandnode),担任开发者关系工程师,并一直在深入研究以太坊的智能合约开发。 我已经确定了我认为用Solidity构建全栈dApps的最佳技术栈: ▶︎ 客户端框架 - **React** ▶︎ 以太坊开发环境 - [**Hardhat**](https://hardhat.org/) ▶︎ 以太坊 Web客户端库 - [**Ethers.js**](https://docs.ethers.io/v5/) ▶︎ API层 - [The Graph Protocol](https://thegraph.com/) 在学习的过程中,我遇到的问题是,虽然每件事情都有相当好的文档,但对于如何将所有这些事情放在一起,并了解它们如何相互合作,却没有什么真正的文档。 有一些非常好的项目模板,比如[scaffold-eth](https://github.com/austintgriffith/scaffold-eth)(其中还包括Ethers、Hardhat和The Graph),但对于刚入门的人来说,可能内容太多,难以拾掇。 我想要一个从前到后的完整指南,告诉我如何使用最新的资源、库和工具来构建全栈以太坊应用。 我感兴趣的内容有: 1. 如何在本地、测试和主网上进行以太坊智能合约的创建、部署和测试。 2. 如何在本地、测试和生产环境/网络之间切换。 3. 如何从前端(如React、Vue、Svelte或Angular)使用各种环境连接到合约并与之交互。 在花了一些时间来弄清楚所有这些事情,并且用我觉得真正满意的技术栈去做之后,我想写出如何使用这个技术栈来构建和测试一个全栈的以太坊应用,不仅是为了给其他可能对这个栈感兴趣的人,也是为了给我自己将来做参考。 ## 组件背景 让我们来介绍一下将使用的主要组件,以及它们是如何融入到堆栈中的。 ### 1. 以太坊开发环境 在构建智能合约时,你需要一种方法来部署你的合约,运行测试和调试Solidity代码,而无需处理真实的网络环境。 你还需要一种方法将你的Solidity代码编译成可以在客户端应用程序中运行的代码--在我们的例子中,就是一个React应用程序。 Hardhat是一个专为全栈开发而设计的以太坊开发环境和框架,也是我将在本教程中使用的框架。 生态系统中其他类似的工具还有[Ganache](https://www.trufflesuite.com/ganache)和[Truffle](https://www.trufflesuite.com/)(见[Truffle中文文档](https://learnblockchain.cn/docs/truffle/)) ### 2. 以太坊 Web客户端库 在我们的React应用中,需要一种与已部署的智能合约进行交互的方式,我们需要一种方法来读取数据以及发送新的交易。 [ethers.js](https://docs.ethers.io/v5/)是一个一个完整而紧凑的库,用于从React、Vue、Angular或Svelte等JavaScript应用客户端中与以太坊区块链及其生态系统进行交互。 我们将要使用这个代码库(见[ethers.js中文文档](https://learnblockchain.cn/docs/ethers.js/))。另一个流行的选择是[web3.js](https://web3js.readthedocs.io/en/v1.3.4/)(见[web3.js中文文档](https://learnblockchain.cn/docs/web3.js/)) ### 3. Metamask [Metamask](https://metamask.io/download.html)用来管理账户和将当前用户连接到区块链。 MetaMask使用户能够以几种不同的方式管理他们的账户和密钥,同时将密钥与网站环境隔离。 一旦用户连接了MetaMask钱包,作为开发者,你就可以与全局可用的以太坊 API(`window.ethereum`)进行交互,该API可以识别与web3兼容浏览器的用户(比如MetaMask用户),每当你请求交易签名时,MetaMask都会以尽可能可理解的方式提示用户。 ### 4. React React是一个前端JavaScript库,用于构建Web应用、用户接口和UI组件。 它是由Facebook和许多许多个人开发者和公司维护的。 React有及其庞大生态系统,如[Next.js](https://nextjs.org/)、[Gatsby](https://www.gatsbyjs.com/)、[Redwood](https://redwoodjs.com/)、[Blitz.js](https://blitzjs.com/)等,可以实现所有类型的部署目标,包括传统的SPA、静态网站生成器、服务器端渲染,以及三者的结合。 React似乎继续主导着前端领域,我认为至少在不久的将来依旧会继续。 ### 5. The Graph 对于大多数建立在区块链(如以太坊)上的应用来说,直接从链上读取数据是很难的,也是很耗时的,所以你曾经看到有人和公司建立自己的中心化索引服务器,并从这些服务器上服务API请求。 这需要大量的工程和硬件资源,并且打破了去中心化所需的安全属性。 The Graph是一个用于查询区块链数据的索引协议,可以创建完全去中心化的应用程序,其暴露了一个可供应用程序使用的GraphQL查询层。 在本指南中,我们不会为应用程序构建一个subgraph,之后单独出一个教程。 ## 我们将构建什么 在本教程中,我们将构建、部署并连接到几个基本的智能合约: 1. 一个在以太坊区块链上创建和更新消息的合约。 2. 铸造代币合约,然后允许合约的拥有者向他人发送代币并读取代币余额,新代币的拥有者也可以向他人发送代币。 我们还将构建出一个React前端,让用户: 1. 阅读部署在区块链上的合约的问候语。 2. 更新问候语 3. 将新铸造的代币从他们的地址发送到另一个地址。 4. 一旦有人收到了代币,允许他们也将自己的代币发送给其他人。 5. 从部署到区块链的合约中读取代币余额。 ### 安装依赖 1. 在你的本地机器上安装Node.js。 2. 浏览器中安装的Chrome扩展程序 [MetaMask](https://metamask.io/)。 在本指南中,你不需要拥有任何以太坊,因为我们将在整个教程中在测试网络上使用测试(假的)以太币。 ## 项目初始化 创建一个新的React应用程序: ``` npx create-react-app react-dapp ``` 接下来,换到新的目录下,使用**NPM**或**Yarn**安装[`ethers.js`](https://docs.ethers.io/v5/)和[`hardhat`](https://github.com/nomiclabs/hardhat)。 ``` npm install ethers hardhat @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers ``` ### 安装和配置以太坊开发环境 接下来,用Hardhat初始化一个新的以太坊开发环境。 ``` npx hardhat ? What do you want to do? Create a sample project ? Hardhat project root: <Choose default path> ``` 现在应该看到在根目录中为你创建了以下工件: **hardhat.config.js** - Hardhat设置的全部内容(即配置、插件和自定义任务)都包含在这个文件中。 **scripts** - 文件夹中包含一个名为**sample-script.js**的脚本,在执行时会部署智能合约。 **test** - 一个包含示例测试脚本的文件夹。 **contracts** - 一个存放以太坊示例智能合约的文件夹。 由于[MetaMask 配置问题](https://hardhat.org/metamask-issue.html),我们需要将HardHat配置中的链ID更新为**1337**。 我们还需要更新[artifacts](https://hardhat.org/guides/compile-contracts.html#artifacts)的位置,让我们编译的合约在React应用的**src**目录下。 要进行这些更新,请打开**hardhat.config.js**,并将`module.exports`更新成这样: ``` module.exports = { solidity: "0.8.3", paths: { artifacts: './src/artifacts', }, networks: { hardhat: { chainId: 1337 } } }; ``` ## 智能合约 接下来,来看看给我们的合约示例:**contracts/Greeter.sol** ```javascript //SPDX-License-Identifier: Unlicense pragma solidity ^0.7.0; import "hardhat/console.sol"; contract Greeter { string greeting; constructor(string memory _greeting) { console.log("Deploying a Greeter with greeting:", _greeting); greeting = _greeting; } function greet() public view returns (string memory) { return greeting; } function setGreeting(string memory _greeting) public { console.log("Changing greeting from '%s' to '%s'", greeting, _greeting); greeting = _greeting; } } ``` 这是一个非常简单的智能合约,在部署时,设置了一个Greeting变量,并公开了一个返回问候语的函数(`greet`)。 它还有一个允许用户更新问候语的函数(`setGreeting`)。 当部署到以太坊区块链后,用户可以和这些方法交互。 我们对智能合约做一个小小的修改。 由于我们在**hardhat.config.js**中设置了编译器的solidity版本为 `0.8.3`,所以也要确保更新合约,使用相同版本的solidity。 ``` // contracts/Greeter.sol pragma solidity ^0.8.3; ``` ### 对以太坊区块链进行读写 与智能合约的交互方式有两种,读或写(交易)。 在我们的合约中,`greet `可以认为是读,`setGreeting `可以认为是写(交易)。 对于写入交易,必须为写入区块链交易付费(gas),如果只是从区块链中读取,则是免费的。读取调用的函数只由你所连接的节点来执行,所以你不需要付出任何gas。 从我们的React应用中,与智能合约进行交互是使用`ethers.js`库、合约地址和 从合约中创建的[ABI](https://learnblockchain.cn/docs/solidity/abi-spec.html)。 > 什么是ABI? ABI代表应用二进制接口。 可以把它看作是客户端应用程序和以太坊区块链(智能合约部署的地方)之间的接口。 ABI通常是由HardHat等开发框架从Solidity智能合约中编译出来的,经常可以在[以太坊浏览器](https://etherscan.io/)上找到智能合约的ABI。 ### 编译出 ABI 现在我们有了基本的智能合约,知道了什么是ABI,让我们为项目编译一个ABI。 进入命令行并运行以下命令: ``` npx hardhat compile ``` 现在,你应该在**src**目录下看到一个名为**artifacts**的新文件夹。 **artifacts/contracts/Greeter.json**文件包含ABI作为属性之一。 当我们需要使用ABI时,可以从JavaScript文件中导入它: ``` import Greeter from './artifacts/contracts/Greeter.sol/Greeter.json' ``` 然后可以这样引用ABI: ``` console.log("Greeter ABI: ", Greeter.abi) ``` > 请注意,Ethers.js也可以启用[友好可读ABI格式](https://blog.ricmoo.com/human-readable-contract-abis-in-ethers-js-141902f4d917),但在本教程中不会涉及这个问题。 ### 使用本地网络部署 接下来,让我们把智能合约部署到本地区块链上,这样就可以进行测试了。 要部署到本地网络,首先需要启动本地节点,打开CLI并运行以下命令: ``` npx hardhat node ``` 当运行这个命令时,你应该看到一个地址和私钥的列表: ![Hardhat账号](https://img.learnblockchain.cn/pics/20210414225228.jpeg) hardhat 创建了20个测试账户,我们可以用来部署和测试智能合约。 每个账户有1万个假的以太币。 稍后,我们将学习如何将测试账户导入到MetaMask中,以便能够使用它。 接下来,需要将合约部署到测试网络中。 首先将**scripts/sample-script.js**的名称更改为**scripts/deploy.js**。 现在可以运行deploy脚本,并给CLI提供部署网络参数: ``` npx hardhat run scripts/deploy.js --network localhost ``` 一旦这个脚本被执行,智能合约应该会被部署到本地测试网络,然后我们应该可以开始与它进行交互: > 在部署合约时,它使用的是我们启动本地网络时创建的第一个账户。 如果你看一下CLI的输出,你应该可以看到类似的输出: ``` Greeter deployed to: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0 ``` 这个是部署后的合约地址,将在客户端应用中用来与智能合约进行交互。 为了向智能合约发送交易,我们将需要使用之前`npx hardhat node`创建的账户导入到MetaMask钱包,你应该看到了**账号**以及**私钥**: ``` react-defi-stack git:(main) npx hardhat node Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/ Accounts ======== Account #0: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 (10000 ETH) Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 ... ``` 我们可以将这个账户导入到MetaMask中,以便使用账号中的 ETH。 首先打开MetaMask,更新网络到Localhost 8545: ![网络](https://img.learnblockchain.cn/pics/20210414225711.jpeg) 接下来,在MetaMask中点击账户菜单中的**导入账户**: ![帐户](https://img.learnblockchain.cn/pics/20210414225859.jpeg) 复制然后粘贴一个**私钥**,点击**导入**。 账户导入后,你应该可以看到账户中的Eth: ![导入账号](https://img.learnblockchain.cn/pics/20210414225944.jpeg) 现在,我们已经部署了一个智能合约,并且账户也已经准备好了,我们可以在React应用中与它进行交互。 ### 连接React客户端 在本教程中,我们不会去关注用CSS构建一个漂亮的UI之类的问题,而是 100%专注于核心功能,让你能用起来。 如果你愿意,你可以把它变得好看。 回顾一下我们想要从React应用中获得的两个目标: 1. 从智能合约中获取 `greeting `的当前值。 2. 允许用户更新 `greeting `的值。 我们如何实现这个目标呢? 以下是我们需要做的事情: 1. 创建一个输入字段和一些局部状态来管理输入的值(以更新 `greeting `)。 2. 允许应用程序连接到用户的MetaMask账户以便签署交易。 3. 创建对智能合约的读写函数。 要做到这一点,请打开`src/App.js`,并用以下代码更新它,将`greeterAddress`的值设置为你的智能合约的地址。 ```javascript import './App.css'; import { useState } from 'react'; import { ethers } from 'ethers' import Greeter from './artifacts/contracts/Greeter.sol/Greeter.json' // Update with the contract address logged out to the CLI when it was deployed const greeterAddress = "your-contract-address" function App() { // store greeting in local state const [greeting, setGreetingValue] = useState() // request access to the user's MetaMask account async function requestAccount() { await window.ethereum.request({ method: 'eth_requestAccounts' }); } // call the smart contract, read the current greeting value async function fetchGreeting() { if (typeof window.ethereum !== 'undefined') { const provider = new ethers.providers.Web3Provider(window.ethereum) const contract = new ethers.Contract(greeterAddress, Greeter.abi, provider) try { const data = await contract.greet() console.log('data: ', data) } catch (err) { console.log("Error: ", err) } } } // call the smart contract, send an update async function setGreeting() { if (!greeting) return if (typeof window.ethereum !== 'undefined') { await requestAccount() const provider = new ethers.providers.Web3Provider(window.ethereum); const signer = provider.getSigner() const contract = new ethers.Contract(greeterAddress, Greeter.abi, signer) const transaction = await contract.setGreeting(greeting) await transaction.wait() fetchGreeting() } } return ( <div className="App"> <header className="App-header"> <button onClick={fetchGreeting}>Fetch Greeting</button> <button onClick={setGreeting}>Set Greeting</button> <input onChange={e => setGreetingValue(e.target.value)} placeholder="Set greeting" /> </header> </div> ); } export default App; ``` 启动React服务器,测试一下: ``` npm start ``` 当应用程序加载时,你应该能够获取当前的问候语并打印到控制台。 也应该可以通过MetaMask钱包签名交易来进行更新问候语。 ![设置和获取问候值](https://img.learnblockchain.cn/pics/20210414230117.jpeg) ### 部署和使用真实测试网络 有几个以太坊测试网络,如Ropsten、Rinkeby或Kovan,我们也可以部署到这些网络上,以使合约有一个可公开访问的版本,而不必将其部署到主网。 在本教程中,我们将部署到**Ropsten**测试网络中。 首先,先更新你的MetaMask钱包,连接到Ropsten网络。 ![Ropsten网络](https://img.learnblockchain.cn/pics/20210415110605.jpeg) 接下来,通过访问[本测试水龙头](https://faucet.ropsten.be/),给自己发送一些测试以太,以便在本教程的后面使用。 我们可以通过注册类似[Infura](https://infura.io/dashboard/ethereum/cbdf7c5eee8b4e2b91e76b77ffd34533/settings)或[Alchemy](https://alchemyapi.io/?r=7d60e34c-b30a-4ffa-89d4-3c4efea4e14b)这样的服务来访问Ropsten(或其他任何测试网络),本教程我使用的是Infura。 一旦你在Infura或Alchemy中创建了应用程序,你会得到一个类似于这样的节点URL: ``` https://ropsten.infura.io/v3/your-project-id ``` 请确保在Infura或Alchemy应用程序配置中设置**ALLOWLIST ETHEREUM ADDRESSES**,包括你的钱包地址。 要部署到测试网络,我们需要在hardhat配置中添加额外的网络信息,以及设置部署账号的钱包私钥。 可以从MetaMask中导出私钥: ![导出私钥](https://img.learnblockchain.cn/pics/20210415111208.png) > 我建议不要在应用程序中硬编码私钥,而是把它设置为环境变量之类的东西。 接下来,添加一个`networks`属性,配置如下: ``` module.exports = { defaultNetwork: "hardhat", paths: { artifacts: './src/artifacts', }, networks: { hardhat: {}, ropsten: { url: "https://ropsten.infura.io/v3/your-project-id", accounts: [`0x${your-private-key}`] } }, solidity: "0.7.3", }; ``` 请运行以下脚本进行部署: ``` npx hardhat run scripts/deploy.js --network ropsten ``` 一旦你的合约部署完毕,你应该可以开始与它进行交互。 现在可以在[Etherscan Ropsten Testnet Explorer](https://ropsten.etherscan.io/)上查看合约。 ## 创建代币 智能合约最常见的使用场景之一是创建代币,来看看如何做到这一点。 由于我们对这些工作比较了解了,所以速度会更快一些。 在**contracts**目录下创建一个名为**Token.sol**的新文件,添加以下代码: ```javascript //SPDX-License-Identifier: Unlicense pragma solidity ^0.8.3; import "hardhat/console.sol"; contract Token { string public name = "Nader Dabit Token"; string public symbol = "NDT"; uint public totalSupply = 1000000; address public owner; mapping(address => uint) balances; constructor() { balances[msg.sender] = totalSupply; owner = msg.sender; } function transfer(address to, uint amount) external { require(balances[msg.sender] >= amount, "Not enough tokens"); balances[msg.sender] -= amount; balances[to] += amount; } function balanceOf(address account) external view returns (uint) { return balances[account]; } } ``` > 请注意,该代币合约仅用于演示目的,不符合[ERC20](https://eips.ethereum.org/EIPS/eip-20),关于ERC20代币的例子,请查看[此合约](https://solidity-by-example.org/app/erc20/) 该合约将创建一个名为 `Nader Dabit Token `的新代币,并设置发行量为1000000。 接下来,编译这份合约。 ``` npx hardhat compile ``` 更新**scripts/deploy.js**的部署脚本,加入新的Token合约: ```javascript const hre = require("hardhat"); async function main() { const [deployer] = await hre.ethers.getSigners(); console.log( "Deploying contracts with the account:", deployer.address ); const Greeter = await hre.ethers.getContractFactory("Greeter"); const greeter = await Greeter.deploy("Hello, World!"); const Token = await hre.ethers.getContractFactory("Token"); const token = await Token.deploy(); await greeter.deployed(); await token.deployed(); console.log("Greeter deployed to:", greeter.address); console.log("Token deployed to:", token.address); } main() .then(() => process.exit(0)) .catch(error => { console.error(error); process.exit(1); }); ``` 现在,我们可以将这个新的合约部署到本地或Ropsten网络。 ``` npx run scripts/deploy.js --network localhost ``` 一旦合约部署完毕,可以开始向其他地址发送这些代币。 为此,让我们更新一下我们需要的客户端代码,以使其工作: ```javascript import './App.css'; import { useState } from 'react'; import { ethers } from 'ethers' import Greeter from './artifacts/contracts/Greeter.sol/Greeter.json' import Token from './artifacts/contracts/Token.sol/Token.json' const greeterAddress = "your-contract-address" const tokenAddress = "your-contract-address" function App() { const [greeting, setGreetingValue] = useState() const [userAccount, setUserAccount] = useState() const [amount, setAmount] = useState() async function requestAccount() { await window.ethereum.request({ method: 'eth_requestAccounts' }); } async function fetchGreeting() { if (typeof window.ethereum !== 'undefined') { const provider = new ethers.providers.Web3Provider(window.ethereum) console.log({ provider }) const contract = new ethers.Contract(greeterAddress, Greeter.abi, provider) try { const data = await contract.greet() console.log('data: ', data) } catch (err) { console.log("Error: ", err) } } } async function getBalance() { if (typeof window.ethereum !== 'undefined') { const [account] = await window.ethereum.request({ method: 'eth_requestAccounts' }) console.log({ account }) const provider = new ethers.providers.Web3Provider(window.ethereum); const signer = provider.getSigner() const contract = new ethers.Contract(tokenAddress, Token.abi, signer) contract.balanceOf(account).then(data => { console.log("data: ", data.toString()) }) } } async function setGreeting() { if (!greeting) return if (typeof window.ethereum !== 'undefined') { await requestAccount() const provider = new ethers.providers.Web3Provider(window.ethereum); console.log({ provider }) const signer = provider.getSigner() const contract = new ethers.Contract(greeterAddress, Greeter.abi, signer) const transaction = await contract.setGreeting(greeting) await transaction.wait() fetchGreeting() } } async function sendCoins() { if (typeof window.ethereum !== 'undefined') { await requestAccount() const provider = new ethers.providers.Web3Provider(window.ethereum); const signer = provider.getSigner() const contract = new ethers.Contract(tokenAddress, Token.abi, signer) contract.transfer(userAccount, amount).then(data => console.log({ data })) } } return ( <div className="App"> <header className="App-header"> <button onClick={fetchGreeting}>Fetch Greeting</button> <button onClick={setGreeting}>Set Greeting</button> <input onChange={e => setGreetingValue(e.target.value)} placeholder="Set greeting" /> <br /> <button onClick={getBalance}>Get Balance</button> <button onClick={sendCoins}>Send Coins</button> <input onChange={e => setUserAccount(e.target.value)} placeholder="Account ID" /> <input onChange={e => setAmount(e.target.value)} placeholder="Amount" /> </header> </div> ); } export default App; ``` 接下来,运行应用程序: ``` npm start ``` 点击**获取余额(Get Balance)**,看到我们的账户里有100万币打印在控制台。 也可以通过点击**添加代币(Add Token)**,以便在MetaMask中查看它们: ![Add Token](https://img.learnblockchain.cn/pics/20210415112240.png) 接下来点击**自定义代币(**Custom Token**)**,输入代币合约地址,然后**添加代币**。 现在,你的钱包里应该有代币了。 ![显示代币](https://img.learnblockchain.cn/pics/20210415113232.jpeg) 接下来,让我们试着把这些硬币发送到另一个地址。 ## 结论 本教程涵盖了很多, 希望你能学到很多东西。 如果你想在MetaMask之外支持多个钱包,请查看[Web3Modal](https://github.com/Web3Modal/web3modal),它可以通过一个相当简单和可定制的配置,方便在你的应用程序中轻松实现对多个网络提供者的支持。 在我未来的教程和指南中,我会深入研究更复杂的智能合约开发,以及如何将其部署到[Subgraph](https://thegraph.com/docs/define-a-subgraph),使用 GraphQL API,实现分页和全文搜索等功能。 本项目的代码在[这里](https://github.com/dabit3/full-stack-ethereum) 下一篇[在以太坊上构建 GraphQL API](https://learnblockchain.cn/article/2566) --- 本翻译由 [Cell Network](https://www.cellnetwork.io/?utm_souce=learnblockchain) 赞助支持。

  • 原文:https://dev.to/dabit3/the-complete-guide-to-full-stack-ethereum-development-3j13
  • 译文出自:登链翻译计划
  • 译者:翻译小组
  • 校对:Tiny 熊
  • 本文永久链接:learnblockchain.cn/article…

本项目的代码在这里

我最近加入了Edge & Node,担任开发者关系工程师,并一直在深入研究以太坊的智能合约开发。 我已经确定了我认为用Solidity构建全栈dApps的最佳技术栈:

▶︎ 客户端框架 - React ▶︎ 以太坊开发环境 - Hardhat ▶︎ 以太坊 Web客户端库 - Ethers.js ▶︎ API层 - The Graph Protocol

在学习的过程中,我遇到的问题是,虽然每件事情都有相当好的文档,但对于如何将所有这些事情放在一起,并了解它们如何相互合作,却没有什么真正的文档。 有一些非常好的项目模板,比如scaffold-eth(其中还包括Ethers、Hardhat和The Graph),但对于刚入门的人来说,可能内容太多,难以拾掇。

我想要一个从前到后的完整指南,告诉我如何使用最新的资源、库和工具来构建全栈以太坊应用。

我感兴趣的内容有:

  1. 如何在本地、测试和主网上进行以太坊智能合约的创建、部署和测试。
  2. 如何在本地、测试和生产环境/网络之间切换。
  3. 如何从前端(如React、Vue、Svelte或Angular)使用各种环境连接到合约并与之交互。

在花了一些时间来弄清楚所有这些事情,并且用我觉得真正满意的技术栈去做之后,我想写出如何使用这个技术栈来构建和测试一个全栈的以太坊应用,不仅是为了给其他可能对这个栈感兴趣的人,也是为了给我自己将来做参考。

组件背景

让我们来介绍一下将使用的主要组件,以及它们是如何融入到堆栈中的。

1. 以太坊开发环境

在构建智能合约时,你需要一种方法来部署你的合约,运行测试和调试Solidity代码,而无需处理真实的网络环境。

你还需要一种方法将你的Solidity代码编译成可以在客户端应用程序中运行的代码--在我们的例子中,就是一个React应用程序。

Hardhat是一个专为全栈开发而设计的以太坊开发环境和框架,也是我将在本教程中使用的框架。

生态系统中其他类似的工具还有Ganache和Truffle(见Truffle中文文档)

2. 以太坊 Web客户端库

在我们的React应用中,需要一种与已部署的智能合约进行交互的方式,我们需要一种方法来读取数据以及发送新的交易。

ethers.js是一个一个完整而紧凑的库,用于从React、Vue、Angular或Svelte等JavaScript应用客户端中与以太坊区块链及其生态系统进行交互。 我们将要使用这个代码库(见ethers.js中文文档)。另一个流行的选择是web3.js(见web3.js中文文档)

3. Metamask

Metamask用来管理账户和将当前用户连接到区块链。 MetaMask使用户能够以几种不同的方式管理他们的账户和密钥,同时将密钥与网站环境隔离。

一旦用户连接了MetaMask钱包,作为开发者,你就可以与全局可用的以太坊 API(window.ethereum)进行交互,该API可以识别与web3兼容浏览器的用户(比如MetaMask用户),每当你请求交易签名时,MetaMask都会以尽可能可理解的方式提示用户。

4. React

React是一个前端JavaScript库,用于构建Web应用、用户接口和UI组件。 它是由Facebook和许多许多个人开发者和公司维护的。

React有及其庞大生态系统,如Next.js、Gatsby、Redwood、Blitz.js等,可以实现所有类型的部署目标,包括传统的SPA、静态网站生成器、服务器端渲染,以及三者的结合。 React似乎继续主导着前端领域,我认为至少在不久的将来依旧会继续。

5. The Graph

对于大多数建立在区块链(如以太坊)上的应用来说,直接从链上读取数据是很难的,也是很耗时的,所以你曾经看到有人和公司建立自己的中心化索引服务器,并从这些服务器上服务API请求。 这需要大量的工程和硬件资源,并且打破了去中心化所需的安全属性。

The Graph是一个用于查询区块链数据的索引协议,可以创建完全去中心化的应用程序,其暴露了一个可供应用程序使用的GraphQL查询层。 在本指南中,我们不会为应用程序构建一个subgraph,之后单独出一个教程。

我们将构建什么

在本教程中,我们将构建、部署并连接到几个基本的智能合约:

  1. 一个在以太坊区块链上创建和更新消息的合约。
  2. 铸造代币合约,然后允许合约的拥有者向他人发送代币并读取代币余额,新代币的拥有者也可以向他人发送代币。

我们还将构建出一个React前端,让用户:

  1. 阅读部署在区块链上的合约的问候语。
  2. 更新问候语
  3. 将新铸造的代币从他们的地址发送到另一个地址。
  4. 一旦有人收到了代币,允许他们也将自己的代币发送给其他人。
  5. 从部署到区块链的合约中读取代币余额。

安装依赖

  1. 在你的本地机器上安装Node.js。
  2. 浏览器中安装的Chrome扩展程序 MetaMask。

在本指南中,你不需要拥有任何以太坊,因为我们将在整个教程中在测试网络上使用测试(假的)以太币。

项目初始化

创建一个新的React应用程序:

npx create-react-app react-dapp

接下来,换到新的目录下,使用NPMYarn安装ethers.jshardhat

npm install ethers hardhat @nomiclabs/hardhat-waffle ethereum-waffle chai @nomiclabs/hardhat-ethers

安装和配置以太坊开发环境

接下来,用Hardhat初始化一个新的以太坊开发环境。

npx hardhat

? What do you want to do? Create a sample project
? Hardhat project root: &lt;Choose default path>

现在应该看到在根目录中为你创建了以下工件:

hardhat.config.js - Hardhat设置的全部内容(即配置、插件和自定义任务)都包含在这个文件中。 scripts - 文件夹中包含一个名为sample-script.js的脚本,在执行时会部署智能合约。 test - 一个包含示例测试脚本的文件夹。 contracts - 一个存放以太坊示例智能合约的文件夹。

由于MetaMask 配置问题,我们需要将HardHat配置中的链ID更新为1337。 我们还需要更新artifacts的位置,让我们编译的合约在React应用的src目录下。

要进行这些更新,请打开hardhat.config.js,并将module.exports更新成这样:

module.exports = {
  solidity: "0.8.3",
  paths: {
    artifacts: './src/artifacts',
  },
  networks: {
    hardhat: {
      chainId: 1337
    }
  }
};

智能合约

接下来,来看看给我们的合约示例:contracts/Greeter.sol

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

import "hardhat/console.sol";

contract Greeter {
  string greeting;

  constructor(string memory _greeting) {
    console.log("Deploying a Greeter with greeting:", _greeting);
    greeting = _greeting;
  }

  function greet() public view returns (string memory) {
    return greeting;
  }

  function setGreeting(string memory _greeting) public {
    console.log("Changing greeting from '%s' to '%s'", greeting, _greeting);
    greeting = _greeting;
  }
}

这是一个非常简单的智能合约,在部署时,设置了一个Greeting变量,并公开了一个返回问候语的函数(greet)。

它还有一个允许用户更新问候语的函数(setGreeting)。 当部署到以太坊区块链后,用户可以和这些方法交互。

我们对智能合约做一个小小的修改。 由于我们在hardhat.config.js中设置了编译器的solidity版本为 0.8.3,所以也要确保更新合约,使用相同版本的solidity。

// contracts/Greeter.sol
pragma solidity ^0.8.3;

对以太坊区块链进行读写

与智能合约的交互方式有两种,读或写(交易)。 在我们的合约中,greet可以认为是读,setGreeting可以认为是写(交易)。

对于写入交易,必须为写入区块链交易付费(gas),如果只是从区块链中读取,则是免费的。读取调用的函数只由你所连接的节点来执行,所以你不需要付出任何gas。

从我们的React应用中,与智能合约进行交互是使用ethers.js库、合约地址和 从合约中创建的ABI。

什么是ABI? ABI代表应用二进制接口。 可以把它看作是客户端应用程序和以太坊区块链(智能合约部署的地方)之间的接口。

ABI通常是由HardHat等开发框架从Solidity智能合约中编译出来的,经常可以在以太坊浏览器上找到智能合约的ABI。

编译出 ABI

现在我们有了基本的智能合约,知道了什么是ABI,让我们为项目编译一个ABI。

进入命令行并运行以下命令:

npx hardhat compile

现在,你应该在src目录下看到一个名为artifacts的新文件夹。 artifacts/contracts/Greeter.json文件包含ABI作为属性之一。 当我们需要使用ABI时,可以从JavaScript文件中导入它:

import Greeter from './artifacts/contracts/Greeter.sol/Greeter.json'

然后可以这样引用ABI:

console.log("Greeter ABI: ", Greeter.abi)

请注意,Ethers.js也可以启用友好可读ABI格式,但在本教程中不会涉及这个问题。

使用本地网络部署

接下来,让我们把智能合约部署到本地区块链上,这样就可以进行测试了。

要部署到本地网络,首先需要启动本地节点,打开CLI并运行以下命令:

npx hardhat node

当运行这个命令时,你应该看到一个地址和私钥的列表:

hardhat 创建了20个测试账户,我们可以用来部署和测试智能合约。 每个账户有1万个假的以太币。 稍后,我们将学习如何将测试账户导入到MetaMask中,以便能够使用它。

接下来,需要将合约部署到测试网络中。 首先将scripts/sample-script.js的名称更改为scripts/deploy.js

现在可以运行deploy脚本,并给CLI提供部署网络参数:

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

一旦这个脚本被执行,智能合约应该会被部署到本地测试网络,然后我们应该可以开始与它进行交互:

在部署合约时,它使用的是我们启动本地网络时创建的第一个账户。

如果你看一下CLI的输出,你应该可以看到类似的输出:

Greeter deployed to: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0

这个是部署后的合约地址,将在客户端应用中用来与智能合约进行交互。

为了向智能合约发送交易,我们将需要使用之前npx hardhat node创建的账户导入到MetaMask钱包,你应该看到了账号以及私钥

  react-defi-stack git:(main) npx hardhat node
Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/

Accounts
========
Account #0: 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 (10000 ETH)
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

...

我们可以将这个账户导入到MetaMask中,以便使用账号中的 ETH。 首先打开MetaMask,更新网络到Localhost 8545:

接下来,在MetaMask中点击账户菜单中的导入账户

复制然后粘贴一个私钥,点击导入。 账户导入后,你应该可以看到账户中的Eth:

现在,我们已经部署了一个智能合约,并且账户也已经准备好了,我们可以在React应用中与它进行交互。

连接React客户端

在本教程中,我们不会去关注用CSS构建一个漂亮的UI之类的问题,而是 100%专注于核心功能,让你能用起来。 如果你愿意,你可以把它变得好看。

回顾一下我们想要从React应用中获得的两个目标:

  1. 从智能合约中获取 greeting的当前值。
  2. 允许用户更新 greeting的值。

我们如何实现这个目标呢? 以下是我们需要做的事情:

  1. 创建一个输入字段和一些局部状态来管理输入的值(以更新 greeting)。
  2. 允许应用程序连接到用户的MetaMask账户以便签署交易。
  3. 创建对智能合约的读写函数。

要做到这一点,请打开src/App.js,并用以下代码更新它,将greeterAddress的值设置为你的智能合约的地址。

import './App.css';
import { useState } from 'react';
import { ethers } from 'ethers'
import Greeter from './artifacts/contracts/Greeter.sol/Greeter.json'

// Update with the contract address logged out to the CLI when it was deployed 
const greeterAddress = "your-contract-address"

function App() {
  // store greeting in local state
  const [greeting, setGreetingValue] = useState()

  // request access to the user's MetaMask account
  async function requestAccount() {
    await window.ethereum.request({ method: 'eth_requestAccounts' });
  }

  // call the smart contract, read the current greeting value
  async function fetchGreeting() {
    if (typeof window.ethereum !== 'undefined') {
      const provider = new ethers.providers.Web3Provider(window.ethereum)
      const contract = new ethers.Contract(greeterAddress, Greeter.abi, provider)
      try {
        const data = await contract.greet()
        console.log('data: ', data)
      } catch (err) {
        console.log("Error: ", err)
      }
    }    
  }

  // call the smart contract, send an update
  async function setGreeting() {
    if (!greeting) return
    if (typeof window.ethereum !== 'undefined') {
      await requestAccount()
      const provider = new ethers.providers.Web3Provider(window.ethereum);
      const signer = provider.getSigner()
      const contract = new ethers.Contract(greeterAddress, Greeter.abi, signer)
      const transaction = await contract.setGreeting(greeting)
      await transaction.wait()
      fetchGreeting()
    }
  }

  return (
    &lt;div className="App">
      &lt;header className="App-header">
        &lt;button onClick={fetchGreeting}>Fetch Greeting&lt;/button>
        &lt;button onClick={setGreeting}>Set Greeting&lt;/button>
        &lt;input onChange={e => setGreetingValue(e.target.value)} placeholder="Set greeting" />
      &lt;/header>
    &lt;/div>
  );
}

export default App;

启动React服务器,测试一下:

npm start

当应用程序加载时,你应该能够获取当前的问候语并打印到控制台。 也应该可以通过MetaMask钱包签名交易来进行更新问候语。

部署和使用真实测试网络

有几个以太坊测试网络,如Ropsten、Rinkeby或Kovan,我们也可以部署到这些网络上,以使合约有一个可公开访问的版本,而不必将其部署到主网。 在本教程中,我们将部署到Ropsten测试网络中。

首先,先更新你的MetaMask钱包,连接到Ropsten网络。

接下来,通过访问本测试水龙头,给自己发送一些测试以太,以便在本教程的后面使用。

我们可以通过注册类似Infura或Alchemy这样的服务来访问Ropsten(或其他任何测试网络),本教程我使用的是Infura。

一旦你在Infura或Alchemy中创建了应用程序,你会得到一个类似于这样的节点URL:

https://ropsten.infura.io/v3/your-project-id

请确保在Infura或Alchemy应用程序配置中设置ALLOWLIST ETHEREUM ADDRESSES,包括你的钱包地址。

要部署到测试网络,我们需要在hardhat配置中添加额外的网络信息,以及设置部署账号的钱包私钥。

可以从MetaMask中导出私钥:

我建议不要在应用程序中硬编码私钥,而是把它设置为环境变量之类的东西。

接下来,添加一个networks属性,配置如下:

module.exports = {
  defaultNetwork: "hardhat",
  paths: {
    artifacts: './src/artifacts',
  },
  networks: {
    hardhat: {},
    ropsten: {
      url: "https://ropsten.infura.io/v3/your-project-id",
      accounts: [`0x${your-private-key}`]
    }
  },
  solidity: "0.7.3",
};

请运行以下脚本进行部署:

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

一旦你的合约部署完毕,你应该可以开始与它进行交互。 现在可以在Etherscan Ropsten Testnet Explorer上查看合约。

创建代币

智能合约最常见的使用场景之一是创建代币,来看看如何做到这一点。 由于我们对这些工作比较了解了,所以速度会更快一些。

contracts目录下创建一个名为Token.sol的新文件,添加以下代码:

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

import "hardhat/console.sol";

contract Token {
  string public name = "Nader Dabit Token";
  string public symbol = "NDT";
  uint public totalSupply = 1000000;
  address public owner;
  mapping(address => uint) balances;

  constructor() {
    balances[msg.sender] = totalSupply;
    owner = msg.sender;
  }

  function transfer(address to, uint amount) external {
    require(balances[msg.sender] >= amount, "Not enough tokens");
    balances[msg.sender] -= amount;
    balances[to] += amount;
  }

  function balanceOf(address account) external view returns (uint) {
    return balances[account];
  }
}

请注意,该代币合约仅用于演示目的,不符合ERC20,关于ERC20代币的例子,请查看此合约

该合约将创建一个名为 Nader Dabit Token的新代币,并设置发行量为1000000。

接下来,编译这份合约。

npx hardhat compile

更新scripts/deploy.js的部署脚本,加入新的Token合约:

const hre = require("hardhat");

async function main() {
  const [deployer] = await hre.ethers.getSigners();

  console.log(
    "Deploying contracts with the account:",
    deployer.address
  );

  const Greeter = await hre.ethers.getContractFactory("Greeter");
  const greeter = await Greeter.deploy("Hello, World!");

  const Token = await hre.ethers.getContractFactory("Token");
  const token = await Token.deploy();

  await greeter.deployed();
  await token.deployed();

  console.log("Greeter deployed to:", greeter.address);
  console.log("Token deployed to:", token.address);
}

main()
  .then(() => process.exit(0))
  .catch(error => {
    console.error(error);
    process.exit(1);
  });

现在,我们可以将这个新的合约部署到本地或Ropsten网络。

npx run scripts/deploy.js --network localhost

一旦合约部署完毕,可以开始向其他地址发送这些代币。

为此,让我们更新一下我们需要的客户端代码,以使其工作:

import './App.css';
import { useState } from 'react';
import { ethers } from 'ethers'
import Greeter from './artifacts/contracts/Greeter.sol/Greeter.json'
import Token from './artifacts/contracts/Token.sol/Token.json'

const greeterAddress = "your-contract-address"
const tokenAddress = "your-contract-address"

function App() {
  const [greeting, setGreetingValue] = useState()
  const [userAccount, setUserAccount] = useState()
  const [amount, setAmount] = useState()

  async function requestAccount() {
    await window.ethereum.request({ method: 'eth_requestAccounts' });
  }

  async function fetchGreeting() {
    if (typeof window.ethereum !== 'undefined') {
      const provider = new ethers.providers.Web3Provider(window.ethereum)
      console.log({ provider })
      const contract = new ethers.Contract(greeterAddress, Greeter.abi, provider)
      try {
        const data = await contract.greet()
        console.log('data: ', data)
      } catch (err) {
        console.log("Error: ", err)
      }
    }    
  }

  async function getBalance() {
    if (typeof window.ethereum !== 'undefined') {
      const [account] = await window.ethereum.request({ method: 'eth_requestAccounts' })
      console.log({ account })
      const provider = new ethers.providers.Web3Provider(window.ethereum);
      const signer = provider.getSigner()
      const contract = new ethers.Contract(tokenAddress, Token.abi, signer)
      contract.balanceOf(account).then(data => {
        console.log("data: ", data.toString())
      })
    }
  }

  async function setGreeting() {
    if (!greeting) return
    if (typeof window.ethereum !== 'undefined') {
      await requestAccount()
      const provider = new ethers.providers.Web3Provider(window.ethereum);
      console.log({ provider })
      const signer = provider.getSigner()
      const contract = new ethers.Contract(greeterAddress, Greeter.abi, signer)
      const transaction = await contract.setGreeting(greeting)
      await transaction.wait()
      fetchGreeting()
    }
  }

  async function sendCoins() {
    if (typeof window.ethereum !== 'undefined') {
      await requestAccount()
      const provider = new ethers.providers.Web3Provider(window.ethereum);
      const signer = provider.getSigner()
      const contract = new ethers.Contract(tokenAddress, Token.abi, signer)
      contract.transfer(userAccount, amount).then(data => console.log({ data }))
    }
  }

  return (
    &lt;div className="App">
      &lt;header className="App-header">
        &lt;button onClick={fetchGreeting}>Fetch Greeting&lt;/button>
        &lt;button onClick={setGreeting}>Set Greeting&lt;/button>
        &lt;input onChange={e => setGreetingValue(e.target.value)} placeholder="Set greeting" />

        &lt;br />
        &lt;button onClick={getBalance}>Get Balance&lt;/button>
        &lt;button onClick={sendCoins}>Send Coins&lt;/button>
        &lt;input onChange={e => setUserAccount(e.target.value)} placeholder="Account ID" />
        &lt;input onChange={e => setAmount(e.target.value)} placeholder="Amount" />
      &lt;/header>
    &lt;/div>
  );
}

export default App;

接下来,运行应用程序:

npm start

点击获取余额(Get Balance),看到我们的账户里有100万币打印在控制台。

也可以通过点击添加代币(Add Token),以便在MetaMask中查看它们:

接下来点击自定义代币(Custom Token),输入代币合约地址,然后添加代币。 现在,你的钱包里应该有代币了。

接下来,让我们试着把这些硬币发送到另一个地址。

结论

本教程涵盖了很多, 希望你能学到很多东西。

如果你想在MetaMask之外支持多个钱包,请查看Web3Modal,它可以通过一个相当简单和可定制的配置,方便在你的应用程序中轻松实现对多个网络提供者的支持。

在我未来的教程和指南中,我会深入研究更复杂的智能合约开发,以及如何将其部署到Subgraph,使用 GraphQL API,实现分页和全文搜索等功能。

本项目的代码在这里

下一篇在以太坊上构建 GraphQL API

本翻译由 Cell Network 赞助支持。

  • 发表于 2021-04-15 11:33
  • 阅读 ( 5208 )
  • 学分 ( 131 )
  • 分类:以太坊

评论