以太坊全栈开发完全指南
发布于 2021-05-13 13:50 ,所属分类:区块连和PHP开发学习资料
译文出自:登链翻译计划[1] 译者:翻译小组[2] 校对:Tiny 熊[3]
本项目的代码在这里[4]
我最近加入了Edge & Node[5],担任开发者关系工程师,并一直在深入研究以太坊的智能合约开发。我已经确定了我认为用 Solidity 构建全栈 dApps 的最佳技术栈:
▶︎ 客户端框架 - React
▶︎ 以太坊开发环境 - **Hardhat**[6]
▶︎ 以太坊 Web 客户端库 - **Ethers.js**[7]
▶︎ API 层 - The Graph Protocol[8]
在学习的过程中,我遇到的问题是,虽然每件事情都有相当好的文档,但对于如何将所有这些事情放在一起,并了解它们如何相互合作,却没有什么真正的文档。有一些非常好的项目模板,比如scaffold-eth[9](其中还包括 Ethers、Hardhat 和 The Graph),但对于刚入门的人来说,可能内容太多,难以拾掇。
我想要一个从前到后的完整指南,告诉我如何使用最新的资源、库和工具来构建全栈以太坊应用。
我感兴趣的内容有:
如何在本地、测试和主网上进行以太坊智能合约的创建、部署和测试。 如何在本地、测试和生产环境/网络之间切换。 如何从前端(如 React、Vue、Svelte 或 Angular)使用各种环境连接到合约并与之交互。
在花了一些时间来弄清楚所有这些事情,并且用我觉得真正满意的技术栈去做之后,我想写出如何使用这个技术栈来构建和测试一个全栈的以太坊应用,不仅是为了给其他可能对这个栈感兴趣的人,也是为了给我自己将来做参考。
组件背景
让我们来介绍一下将使用的主要组件,以及它们是如何融入到堆栈中的。
1. 以太坊开发环境
在构建智能合约时,你需要一种方法来部署你的合约,运行测试和调试 Solidity 代码,而无需处理真实的网络环境。
你还需要一种方法将你的 Solidity 代码编译成可以在客户端应用程序中运行的代码--在我们的例子中,就是一个 React 应用程序。
Hardhat 是一个专为全栈开发而设计的以太坊开发环境和框架,也是我将在本教程中使用的框架。
生态系统中其他类似的工具还有Ganache[10]和Truffle[11](见Truffle 中文文档[12])
2. 以太坊 Web 客户端库
在我们的 React 应用中,需要一种与已部署的智能合约进行交互的方式,我们需要一种方法来读取数据以及发送新的交易。
ethers.js[13]是一个一个完整而紧凑的库,用于从 React、Vue、Angular 或 Svelte 等 JavaScript 应用客户端中与以太坊区块链及其生态系统进行交互。我们将要使用这个代码库(见ethers.js 中文文档[14])。另一个流行的选择是web3.js[15](见web3.js 中文文档[16])
3. Metamask
Metamask[17]用来管理账户和将当前用户连接到区块链。MetaMask 使用户能够以几种不同的方式管理他们的账户和密钥,同时将密钥与网站环境隔离。
一旦用户连接了 MetaMask 钱包,作为开发者,你就可以与全局可用的以太坊 API(window.ethereum
)进行交互,该 API 可以识别与 web3 兼容浏览器的用户(比如 MetaMask 用户),每当你请求交易签名时,MetaMask 都会以尽可能可理解的方式提示用户。
4. React
React 是一个前端 JavaScript 库,用于构建 Web 应用、用户接口和 UI 组件。它是由 Facebook 和许多许多个人开发者和公司维护的。
React 有及其庞大生态系统,如Next.js[18]、Gatsby[19]、Redwood[20]、Blitz.js[21]等,可以实现所有类型的部署目标,包括传统的 SPA、静态网站生成器、服务器端渲染,以及三者的结合。React 似乎继续主导着前端领域,我认为至少在不久的将来依旧会继续。
5. The Graph
对于大多数建立在区块链(如以太坊)上的应用来说,直接从链上读取数据是很难的,也是很耗时的,所以你曾经看到有人和公司建立自己的中心化索引服务器,并从这些服务器上服务 API 请求。这需要大量的工程和硬件资源,并且打破了去中心化所需的安全属性。
The Graph 是一个用于查询区块链数据的索引协议,可以创建完全去中心化的应用程序,其暴露了一个可供应用程序使用的 GraphQL 查询层。在本指南中,我们不会为应用程序构建一个 subgraph,之后单独出一个教程。
我们将构建什么
在本教程中,我们将构建、部署并连接到几个基本的智能合约:
一个在以太坊区块链上创建和更新消息的合约。 铸造代币合约,然后允许合约的拥有者向他人发送代币并读取代币余额,新代币的拥有者也可以向他人发送代币。
我们还将构建出一个 React 前端,让用户:
阅读部署在区块链上的合约的问候语。 更新问候语 将新铸造的代币从他们的地址发送到另一个地址。 一旦有人收到了代币,允许他们也将自己的代币发送给其他人。 从部署到区块链的合约中读取代币余额。
安装依赖
在你的本地机器上安装 Node.js。 浏览器中安装的 Chrome 扩展程序 MetaMask[22]。
在本指南中,你不需要拥有任何以太坊,因为我们将在整个教程中在测试网络上使用测试(假的)以太币。
项目初始化
创建一个新的 React 应用程序:
npxcreate-react-appreact-dapp
接下来,换到新的目录下,使用NPM或Yarn安装`ethers.js`[23]和`hardhat`[24]。
npminstallethershardhat@nomiclabs/hardhat-waffleethereum-wafflechai@nomiclabs/hardhat-ethers
安装和配置以太坊开发环境
接下来,用 Hardhat 初始化一个新的以太坊开发环境。
npxhardhat
?Whatdoyouwanttodo?Createasampleproject
?Hardhatprojectroot:<Choosedefaultpath>
现在应该看到在根目录中为你创建了以下工件:
hardhat.config.js - Hardhat 设置的全部内容(即配置、插件和自定义任务)都包含在这个文件中。scripts - 文件夹中包含一个名为sample-script.js的脚本,在执行时会部署智能合约。test - 一个包含示例测试脚本的文件夹。contracts - 一个存放以太坊示例智能合约的文件夹。
由于MetaMask 配置问题[25],我们需要将 HardHat 配置中的链 ID 更新为1337。我们还需要更新artifacts[26]的位置,让我们编译的合约在 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
pragmasolidity^0.7.0;
import"hardhat/console.sol";
contractGreeter{
stringgreeting;
constructor(stringmemory_greeting){
console.log("DeployingaGreeterwithgreeting:",_greeting);
greeting=_greeting;
}
functiongreet()publicviewreturns(stringmemory){
returngreeting;
}
functionsetGreeting(stringmemory_greeting)public{
console.log("Changinggreetingfrom'%s'to'%s'",greeting,_greeting);
greeting=_greeting;
}
}
这是一个非常简单的智能合约,在部署时,设置了一个 Greeting 变量,并公开了一个返回问候语的函数(greet
)。
它还有一个允许用户更新问候语的函数(setGreeting
)。当部署到以太坊区块链后,用户可以和这些方法交互。
我们对智能合约做一个小小的修改。由于我们在hardhat.config.js中设置了编译器的 solidity 版本为 0.8.3
,所以也要确保更新合约,使用相同版本的 solidity。
//contracts/Greeter.sol
pragmasolidity^0.8.3;
对以太坊区块链进行读写
与智能合约的交互方式有两种,读或写(交易)。在我们的合约中,greet
可以认为是读,setGreeting
可以认为是写(交易)。
对于写入交易,必须为写入区块链交易付费(gas),如果只是从区块链中读取,则是免费的。读取调用的函数只由你所连接的节点来执行,所以你不需要付出任何 gas。
从我们的 React 应用中,与智能合约进行交互是使用ethers.js
库、合约地址和 从合约中创建的ABI[27]。
什么是 ABI?ABI 代表应用二进制接口。可以把它看作是客户端应用程序和以太坊区块链(智能合约部署的地方)之间的接口。
ABI 通常是由 HardHat 等开发框架从 Solidity 智能合约中编译出来的,经常可以在以太坊浏览器[28]上找到智能合约的 ABI。
编译出 ABI
现在我们有了基本的智能合约,知道了什么是 ABI,让我们为项目编译一个 ABI。
进入命令行并运行以下命令:
npxhardhatcompile
现在,你应该在src目录下看到一个名为artifacts的新文件夹。artifacts/contracts/Greeter.json文件包含 ABI 作为属性之一。当我们需要使用 ABI 时,可以从 JavaScript 文件中导入它:
importGreeterfrom'./artifacts/contracts/Greeter.sol/Greeter.json'
然后可以这样引用 ABI:
console.log("GreeterABI:",Greeter.abi)
请注意,Ethers.js 也可以启用友好可读 ABI 格式[29],但在本教程中不会涉及这个问题。
使用本地网络部署
接下来,让我们把智能合约部署到本地区块链上,这样就可以进行测试了。
要部署到本地网络,首先需要启动本地节点,打开 CLI 并运行以下命令:
npxhardhatnode
当运行这个命令时,你应该看到一个地址和私钥的列表:

hardhat 创建了 20 个测试账户,我们可以用来部署和测试智能合约。每个账户有 1 万个假的以太币。稍后,我们将学习如何将测试账户导入到 MetaMask 中,以便能够使用它。
接下来,需要将合约部署到测试网络中。首先将scripts/sample-script.js的名称更改为scripts/deploy.js。
现在可以运行 deploy 脚本,并给 CLI 提供部署网络参数:
npxhardhatrunscripts/deploy.js--networklocalhost
一旦这个脚本被执行,智能合约应该会被部署到本地测试网络,然后我们应该可以开始与它进行交互:
在部署合约时,它使用的是我们启动本地网络时创建的第一个账户。
如果你看一下 CLI 的输出,你应该可以看到类似的输出:
Greeterdeployedto:0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
这个是部署后的合约地址,将在客户端应用中用来与智能合约进行交互。
为了向智能合约发送交易,我们将需要使用之前npx hardhat node
创建的账户导入到 MetaMask 钱包,你应该看到了账号以及私钥:
➜react-defi-stackgit:(main)npxhardhatnode
StartedHTTPandWebSocketJSON-RPCserverathttp://127.0.0.1:8545/
Accounts
========
Account#0:0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266(10000ETH)
PrivateKey:0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
...
我们可以将这个账户导入到 MetaMask 中,以便使用账号中的 ETH。首先打开 MetaMask,更新网络到 Localhost 8545:

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

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

现在,我们已经部署了一个智能合约,并且账户也已经准备好了,我们可以在 React 应用中与它进行交互。
连接 React 客户端
在本教程中,我们不会去用 CSS 构建一个漂亮的 UI 之类的问题,而是 100%专注于核心功能,让你能用起来。如果你愿意,你可以把它变得好看。
回顾一下我们想要从 React 应用中获得的两个目标:
从智能合约中获取 greeting
的当前值。允许用户更新 greeting
的值。
我们如何实现这个目标呢?以下是我们需要做的事情:
创建一个输入字段和一些局部状态来管理输入的值(以更新 greeting
)。允许应用程序连接到用户的 MetaMask 账户以便签署交易。 创建对智能合约的读写函数。
要做到这一点,请打开src/App.js
,并用以下代码更新它,将greeterAddress
的值设置为你的智能合约的地址。
import'./App.css';
import{useState}from'react';
import{ethers}from'ethers'
importGreeterfrom'./artifacts/contracts/Greeter.sol/Greeter.json'
//UpdatewiththecontractaddressloggedouttotheCLIwhenitwasdeployed
constgreeterAddress="your-contract-address"
functionApp(){
//storegreetinginlocalstate
const[greeting,setGreetingValue]=useState()
//requestaccesstotheuser'sMetaMaskaccount
asyncfunctionrequestAccount(){
awaitwindow.ethereum.request({method:'eth_requestAccounts'});
}
//callthesmartcontract,readthecurrentgreetingvalue
asyncfunctionfetchGreeting(){
if(typeofwindow.ethereum!=='undefined'){
constprovider=newethers.providers.Web3Provider(window.ethereum)
constcontract=newethers.Contract(greeterAddress,Greeter.abi,provider)
try{
constdata=awaitcontract.greet()
console.log('data:',data)
}catch(err){
console.log("Error:",err)
}
}
}
//callthesmartcontract,sendanupdate
asyncfunctionsetGreeting(){
if(!greeting)return
if(typeofwindow.ethereum!=='undefined'){
awaitrequestAccount()
constprovider=newethers.providers.Web3Provider(window.ethereum);
constsigner=provider.getSigner()
constcontract=newethers.Contract(greeterAddress,Greeter.abi,signer)
consttransaction=awaitcontract.setGreeting(greeting)
awaittransaction.wait()
fetchGreeting()
}
}
return(
<divclassName="App">
<headerclassName="App-header">
<buttononClick={fetchGreeting}>FetchGreeting</button>
<buttononClick={setGreeting}>SetGreeting</button>
<inputonChange={e=>setGreetingValue(e.target.value)}placeholder="Setgreeting"/>
</header>
</div>
);
}
exportdefaultApp;
启动 React 服务器,测试一下:
npmstart
当应用程序加载时,你应该能够获取当前的问候语并打印到控制台。也应该可以通过 MetaMask 钱包签名交易来进行更新问候语。

部署和使用真实测试网络
有几个以太坊测试网络,如 Ropsten、Rinkeby 或 Kovan,我们也可以部署到这些网络上,以使合约有一个可公开访问的版本,而不必将其部署到主网。在本教程中,我们将部署到Ropsten测试网络中。
首先,先更新你的 MetaMask 钱包,连接到 Ropsten 网络。

接下来,通过访问本测试水龙头[30],给自己发送一些测试以太,以便在本教程的后面使用。
我们可以通过注册类似Infura[31]或Alchemy[32]这样的服务来访问 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",
};
请运行以下脚本进行部署:
npxhardhatrunscripts/deploy.js--networkropsten
一旦你的合约部署完毕,你应该可以开始与它进行交互。现在可以在Etherscan Ropsten Testnet Explorer[33]上查看合约。
创建代币
智能合约最常见的使用场景之一是创建代币,来看看如何做到这一点。由于我们对这些工作比较了解了,所以速度会更快一些。
在contracts目录下创建一个名为Token.sol的新文件,添加以下代码:
//SPDX-License-Identifier:Unlicense
pragmasolidity^0.8.3;
import"hardhat/console.sol";
contractToken{
stringpublicname="NaderDabitToken";
stringpublicsymbol="NDT";
uintpublictotalSupply=1000000;
addresspublicowner;
mapping(address=>uint)balances;
constructor(){
balances[msg.sender]=totalSupply;
owner=msg.sender;
}
functiontransfer(addressto,uintamount)external{
require(balances[msg.sender]>=amount,"Notenoughtokens");
balances[msg.sender]-=amount;
balances[to]+=amount;
}
functionbalanceOf(addressaccount)externalviewreturns(uint){
returnbalances[account];
}
}
请注意,该代币合约仅用于演示目的,不符合ERC20[34],关于 ERC20 代币的例子,请查看此合约[35]
该合约将创建一个名为 Nader Dabit Token
的新代币,并设置发行量为 1000000。
接下来,编译这份合约。
npxhardhatcompile
更新scripts/deploy.js的部署脚本,加入新的 Token 合约:
consthre=require("hardhat");
asyncfunctionmain(){
const[deployer]=awaithre.ethers.getSigners();
console.log(
"Deployingcontractswiththeaccount:",
deployer.address
);
constGreeter=awaithre.ethers.getContractFactory("Greeter");
constgreeter=awaitGreeter.deploy("Hello,World!");
constToken=awaithre.ethers.getContractFactory("Token");
consttoken=awaitToken.deploy();
awaitgreeter.deployed();
awaittoken.deployed();
console.log("Greeterdeployedto:",greeter.address);
console.log("Tokendeployedto:",token.address);
}
main()
.then(()=>process.exit(0))
.catch(error=>{
console.error(error);
process.exit(1);
});
现在,我们可以将这个新的合约部署到本地或 Ropsten 网络。
npxrunscripts/deploy.js--networklocalhost
一旦合约部署完毕,可以开始向其他地址发送这些代币。
为此,让我们更新一下我们需要的客户端代码,以使其工作:
import'./App.css';
import{useState}from'react';
import{ethers}from'ethers'
importGreeterfrom'./artifacts/contracts/Greeter.sol/Greeter.json'
importTokenfrom'./artifacts/contracts/Token.sol/Token.json'
constgreeterAddress="your-contract-address"
consttokenAddress="your-contract-address"
functionApp(){
const[greeting,setGreetingValue]=useState()
const[userAccount,setUserAccount]=useState()
const[amount,setAmount]=useState()
asyncfunctionrequestAccount(){
awaitwindow.ethereum.request({method:'eth_requestAccounts'});
}
asyncfunctionfetchGreeting(){
if(typeofwindow.ethereum!=='undefined'){
constprovider=newethers.providers.Web3Provider(window.ethereum)
console.log({provider})
constcontract=newethers.Contract(greeterAddress,Greeter.abi,provider)
try{
constdata=awaitcontract.greet()
console.log('data:',data)
}catch(err){
console.log("Error:",err)
}
}
}
asyncfunctiongetBalance(){
if(typeofwindow.ethereum!=='undefined'){
const[account]=awaitwindow.ethereum.request({method:'eth_requestAccounts'})
console.log({account})
constprovider=newethers.providers.Web3Provider(window.ethereum);
constsigner=provider.getSigner()
constcontract=newethers.Contract(tokenAddress,Token.abi,signer)
contract.balanceOf(account).then(data=>{
console.log("data:",data.toString())
})
}
}
asyncfunctionsetGreeting(){
if(!greeting)return
if(typeofwindow.ethereum!=='undefined'){
awaitrequestAccount()
constprovider=newethers.providers.Web3Provider(window.ethereum);
console.log({provider})
constsigner=provider.getSigner()
constcontract=newethers.Contract(greeterAddress,Greeter.abi,signer)
consttransaction=awaitcontract.setGreeting(greeting)
awaittransaction.wait()
fetchGreeting()
}
}
asyncfunctionsendCoins(){
if(typeofwindow.ethereum!=='undefined'){
awaitrequestAccount()
constprovider=newethers.providers.Web3Provider(window.ethereum);
constsigner=provider.getSigner()
constcontract=newethers.Contract(tokenAddress,Token.abi,signer)
contract.transfer(userAccount,amount).then(data=>console.log({data}))
}
}
return(
<divclassName="App">
<headerclassName="App-header">
<buttononClick={fetchGreeting}>FetchGreeting</button>
<buttononClick={setGreeting}>SetGreeting</button>
<inputonChange={e=>setGreetingValue(e.target.value)}placeholder="Setgreeting"/>
<br/>
<buttononClick={getBalance}>GetBalance</button>
<buttononClick={sendCoins}>SendCoins</button>
<inputonChange={e=>setUserAccount(e.target.value)}placeholder="AccountID"/>
<inputonChange={e=>setAmount(e.target.value)}placeholder="Amount"/>
</header>
</div>
);
}
exportdefaultApp;
接下来,运行应用程序:
npmstart
点击获取余额(Get Balance),看到我们的账户里有 100 万币打印在控制台。
也可以通过点击添加代币(Add Token),以便在 MetaMask 中查看它们:

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

接下来,让我们试着把这些硬币发送到另一个地址。
结论
本教程涵盖了很多, 希望你能学到很多东西。
如果你想在 MetaMask 之外支持多个钱包,请查看Web3Modal[36],它可以通过一个相当简单和可定制的配置,方便在你的应用程序中轻松实现对多个网络提供者的支持。
在我未来的教程和指南中,我会深入研究更复杂的智能合约开发,以及如何将其部署到Subgraph[37],使用 GraphQL API,实现分页和全文搜索等功能。
本翻译由 Cell Network[38] 赞助支持。
来源:https://dev.to/dabit3/the-complete-guide-to-full-stack-ethereum-development-3j13
参考资料
登链翻译计划: https://github.com/lbc-team/Pioneer
[2]翻译小组: https://learnblockchain.cn/people/412
[3]Tiny 熊: https://learnblockchain.cn/people/15
[4]这里: https://github.com/dabit3/full-stack-ethereum
[5]Edge & Node: https://twitter.com/edgeandnode
[6]Hardhat: https://hardhat.org/
[7]Ethers.js: https://docs.ethers.io/v5/
[8]The Graph Protocol: https://thegraph.com/
[9]scaffold-eth: https://github.com/austintgriffith/scaffold-eth
[10]Ganache: https://www.trufflesuite.com/ganache
[11]Truffle: https://www.trufflesuite.com/
[12]Truffle中文文档: https://learnblockchain.cn/docs/truffle/
[13]ethers.js: https://docs.ethers.io/v5/
[14]ethers.js中文文档: https://learnblockchain.cn/docs/ethers.js/
[15]web3.js: https://web3js.readthedocs.io/en/v1.3.4/
[16]web3.js中文文档: https://learnblockchain.cn/docs/web3.js/
[17]Metamask: https://metamask.io/download.html
[18]Next.js: https://nextjs.org/
[19]Gatsby: https://www.gatsbyjs.com/
[20]Redwood: https://redwoodjs.com/
[21]Blitz.js: https://blitzjs.com/
[22]MetaMask: https://metamask.io/
[23]ethers.js
: https://docs.ethers.io/v5/
hardhat
: https://github.com/nomiclabs/hardhat
MetaMask 配置问题: https://hardhat.org/metamask-issue.html
[26]artifacts: https://hardhat.org/guides/compile-contracts.html#artifacts
[27]ABI: https://learnblockchain.cn/docs/solidity/abi-spec.html
[28]以太坊浏览器: https://etherscan.io/
[29]友好可读ABI格式: https://blog.ricmoo.com/human-readable-contract-abis-in-ethers-js-141902f4d917
[30]本测试水龙头: https://faucet.ropsten.be/
[31]Infura: https://infura.io/dashboard/ethereum/cbdf7c5eee8b4e2b91e76b77ffd34533/settings
[32]Alchemy: https://alchemyapi.io/?r=7d60e34c-b30a-4ffa-89d4-3c4efea4e14b
[33]Etherscan Ropsten Testnet Explorer: https://ropsten.etherscan.io/
[34]ERC20: https://eips.ethereum.org/EIPS/eip-20
[35]此合约: https://solidity-by-example.org/app/erc20/
[36]Web3Modal: https://github.com/Web3Modal/web3modal
[37]Subgraph: https://thegraph.com/docs/define-a-subgraph
[38]Cell Network: https://www.cellnetwork.io/?utm_souce=learnblockchain
相关资源