全栈以太坊和 EVM 开发完整指南
使用 React、Ethers.js、Solidity 和 Hardhat 构建全栈 dApp
2022年9月11日更新
在本教程中,您将学习一个 web3 技术栈,它允许您利用以太坊虚拟机 (EVM) 在数十个区块链网络上构建全栈应用程序,包括以太坊、Polygon、Avalanche、Celo 等。
该项目的代码位于此处。本教程的视频课程位于此处。另请参阅定义 web3 堆栈
自从我开始涉足区块链领域以来,我一直在深入研究使用 Solidity 和 EVM 进行智能合约开发。我找到了一个我认为适合使用 Solidity 构建全栈 dApp 的优秀技术栈:
▶︎ 客户端框架 - React
▶︎ 以太坊开发环境 - Hardhat
▶︎ 以太坊 Web 客户端库 - Ethers.js
▶︎ API 层 - The Graph 协议
然而,我在学习过程中遇到的问题是,虽然针对每个组件都有相当完善的文档,但却没有真正介绍如何将所有这些组件组合在一起并理解它们如何相互协作的文档。虽然有一些非常好的样板,例如scaffold-eth(也包含 Ethers、Hardhat 和 The Graph),但对于刚入门的人来说,可能太难掌握了。
我想要一个端到端的指南来向我展示如何使用最新的资源、库和工具构建全栈以太坊应用程序。
我感兴趣的事情是这样的:
- 如何在本地、测试和主网上创建、部署和测试以太坊智能合约
- 如何在本地、测试和生产环境/网络之间切换
- 如何使用 React、Vue、Svelte 或 Angular 等前端的各种环境来连接合约并与之交互
在花了一些时间弄清楚所有这些,并开始使用我真正满意的堆栈之后,我觉得有必要写一篇关于如何使用这个堆栈构建和测试一个全栈以太坊应用程序的文章,这不仅对其他可能对此堆栈感兴趣的人有用,也为我自己将来的参考。这就是我的参考资料。
碎片
让我们回顾一下我们将要使用的主要部件以及它们如何融入堆栈。
1. 以太坊开发环境
在构建智能合约时,您需要一种方法来部署合约、运行测试和调试 Solidity 代码,而无需处理实时环境。
您还需要一种方法将 Solidity 代码编译成可在客户端应用程序(在本例中为 React 应用程序)中运行的代码。稍后我们将详细了解其工作原理。
Hardhat 是一个专为全栈开发而设计的以太坊开发环境和框架,也是我将在本教程中使用的框架。
生态系统中的其他类似工具有Ganache、Truffle和Foundry。
2. 以太坊 Web 客户端库
在我们的 React 应用中,我们需要一种与已部署的智能合约进行交互的方式。我们需要一种读取数据以及发送新交易的方式。
ethers.js旨在成为一个完整而紧凑的库,用于通过 React、Vue、Angular 或 Svelte 等客户端 JavaScript 应用程序与以太坊区块链及其生态系统进行交互。我们将使用这个库。
生态系统中的另一个流行选择是web3.js
3. Metamask
Metamask有助于处理账户管理并将当前用户连接到区块链。MetaMask 允许用户以几种不同的方式管理他们的账户和密钥,同时将它们与网站环境隔离。
一旦用户连接了他们的 MetaMask 钱包,您作为开发人员就可以与全球可用的以太坊 API(window.ethereum
)进行交互,该 API 可识别兼容 web3 浏览器的用户(如 MetaMask 用户),并且每当您请求交易签名时,MetaMask 都会以尽可能易于理解的方式提示用户。
4. React
React 是一个用于构建 Web 应用程序、用户界面和 UI 组件的前端 JavaScript 库。它由 Facebook 以及众多个人开发者和公司维护。
React 及其庞大的元框架生态系统(例如Next.js、Gatsby、Redwood、Blitz.js等)支持所有类型的部署目标,包括传统的 SPA、静态站点生成器、服务器端渲染以及三者的组合。React 似乎继续在前端领域占据主导地位,我认为至少在不久的将来还会继续保持这种势头。
5.图表
对于大多数基于以太坊等区块链构建的应用来说,直接从链上读取数据非常困难且耗时,因此过去常常看到个人和公司构建自己的中心化索引服务器,并通过这些服务器处理 API 请求。这需要大量的工程和硬件资源,并且破坏了去中心化所需的安全性。
Graph 是一个用于查询区块链数据的索引协议,它能够创建完全去中心化的应用程序,并解决了这个问题,它提供了一个丰富的 GraphQL 查询层供应用程序使用。在本指南中,我们不会为我们的应用程序构建子图,但会在以后的教程中实现。
要了解如何使用 The Graph 构建区块链 API,请查看在以太坊上构建 GraphQL API。
我们将要构建什么
在本教程中,我们将构建、部署和连接几个基本的智能合约:
- 在以太坊区块链上创建和更新消息的合约
- 铸造代币的合约,然后允许合约所有者将代币发送给其他人并读取代币余额,并且允许新代币的所有者将其发送给其他人。
我们还将构建一个 React 前端,允许用户:
- 读取部署到区块链的合约的问候语
- 更新问候语
- 将新铸造的代币从其地址发送到另一个地址
- 一旦有人收到代币,允许他们也将其代币发送给其他人
- 从部署到区块链的合约中读取代币余额
先决条件
- 安装在本地机器上的 Node.js
- 您的浏览器中安装了MetaMask Chrome 扩展程序
您不需要拥有任何以太坊来学习本指南,因为我们将在整个教程中在测试网络上使用虚假/测试以太币。
入门
首先,我们将创建一个新的 React 应用程序:
npx create-react-app react-dapp
接下来,切换到新目录并使用NPM或Yarnethers.js
安装: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 JavaScript project
? Hardhat project root: <Choose default path>
如果您收到有关 README.md 文件的错误,请删除 README.md 文件并重新运行该命令。
现在您应该在根目录中看到为您创建的以下工件:
hardhat.config.js - 您的整个 Hardhat 设置(即您的配置、插件和自定义任务)都包含在此文件中。
scripts - 包含名为sample-script.js 的脚本的文件夹,该脚本将在执行时部署您的智能合约
test - 包含示例测试脚本的文件夹
contract - 包含示例 Solidity 智能合约的文件夹
由于MetaMask 配置问题,我们需要将 HardHat 配置中的链 ID 更新为1337。我们还需要将已编译合约的工件位置更新为React 应用的src目录。
要进行这些更新,请打开hardhat.config.js并更新module.exports
如下内容:
module.exports = {
solidity: "0.8.9",
paths: {
artifacts: './src/artifacts',
},
networks: {
hardhat: {
chainId: 1337
}
}
};
我们的智能合约
接下来,让我们看一下contract/Greeter.sol中提供的示例合同:
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
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
)的函数。部署到以太坊区块链后,这些方法将可供用户交互。
读取和写入以太坊区块链
与智能合约交互有两种方式:读取或写入/交易。在我们的合约中,greet
可以视为读取,也setGreeting
可以视为写入/交易。
在编写或初始化交易时,您需要支付将交易写入区块链的费用。为了实现这一点,您需要支付Gas,即在以太坊区块链上成功执行交易并执行合约所需的费用或价格。
只要您只是读取区块链数据,而不更改或更新任何内容,就无需执行交易,也无需支付任何 Gas 或其他费用。您调用的函数仅由您连接的节点执行,因此您无需支付任何 Gas,读取操作也是免费的。
从我们的 React 应用程序中,我们与智能合约交互的方式是使用ethers.js
库、合约地址和由 hardhat 从合约创建的ABI的组合。
什么是 ABI?ABI 代表应用程序二进制接口 (ABI)。您可以将其视为客户端应用程序与部署了您要交互的智能合约的以太坊区块链之间的接口。
ABI 通常由 HardHat 等开发框架从 Solidity 智能合约编译而来。您也可以在Etherscan上找到智能合约的 ABI。
编译 ABI
现在我们已经了解了基本的智能合约并知道了什么是 ABI,让我们为我们的项目编译一个 ABI。
为此,请转到命令行并运行以下命令:
npx hardhat compile
如果您遇到任何与依赖性错误有关的问题,请查看此处的
hardhat-toolbox
安装说明。
现在,您应该在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
当我们运行此命令时,您应该会看到地址和私钥的列表。
这是为我们创建的 20 个测试账户和地址,可用于部署和测试我们的智能合约。每个账户还预存了 10,000 个虚拟以太币。稍后我们将学习如何将测试账户导入 MetaMask 以便使用。
接下来,使用以下代码更新scripts/deploy.jsGreeter
来部署合约:
const hre = require("hardhat");
async function main() {
const Greeter = await hre.ethers.getContractFactory("Greeter");
const greeter = await Greeter.deploy("Hello World");
await greeter.deployed();
console.log(
`contract successfully deployed to ${greeter.address}`
);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
现在我们可以运行部署脚本并向 CLI 发出我们想要部署到本地网络的标志:
npx hardhat run scripts/deploy.js --network localhost
一旦执行此脚本,智能合约就应该部署到本地测试网络,然后我们就可以开始与其交互。
当合约部署时,它使用我们启动本地网络时创建的第一个帐户。
如果您查看 CLI 的输出,您应该能够看到类似这样的内容:
Greeter deployed to: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
我们将在客户端应用程序中使用此地址与智能合约通信。请保留此地址,因为我们在从客户端应用程序连接时需要用到它。
要将交易发送到智能合约,我们需要使用运行 时创建的账户之一连接我们的 MetaMask 钱包npx hardhat node
。在 CLI 注销的合约列表中,您应该看到账号和私钥:
➜ react-dapp 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 中,从帐户菜单中单击“导入帐户” :
复制并粘贴CLI 注销的私钥之一,然后单击“导入”。导入帐户后,您应该会在帐户中看到 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'
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 钱包签署合约并使用伪造的以太币来更新问候语。
部署并使用实时测试网络
我们也可以部署到多个以太坊测试网络,例如 Ropsten、Rinkeby 或 Kovan,这样就可以获得一个可公开访问的合约版本,而无需将其部署到主网。在本教程中,我们将部署到Ropsten测试网络。
首先,更新您的 MetaMask 钱包以连接到 Ropsten 网络。
接下来,通过访问此或其他测试水龙头,向自己发送一些测试以太币,以供在本教程的其余部分使用。
我们可以通过注册Infura或Alchemy等服务来访问 Ropsten(或任何其他测试网络)(我在本教程中使用 Infura)。
在 Infura 或 Alchemy 中创建应用程序后,您将获得一个如下所示的端点:
https://ropsten.infura.io/v3/your-project-id
请确保在 Infura 或 Alchemy 应用程序配置中设置ALLOWLIST ETHEREUM ADDRESSES,以包含您将从中部署的帐户的钱包地址。
要部署到测试网络,我们需要更新安全帽配置,添加一些额外的网络信息。其中一项需要设置的内容是部署时使用的钱包的私钥。
要获取私钥,您可以从 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.8.9",
};
要部署,请运行以下脚本:
npx hardhat run scripts/deploy.js --network ropsten
合约部署完成后,您就可以开始与其交互了。现在,您可以在Etherscan Ropsten 测试网浏览器上查看实时合约。
铸造代币
智能合约最常见的用例之一是创建代币,让我们来看看如何创建代币。由于我们对智能合约的工作原理了解得比较多,所以我们会讲得更快一些。
在主合约目录中创建一个名为Token.sol的新文件。
接下来,使用以下智能合约更新Token.sol :
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "hardhat/console.sol";
contract Token {
string public name = "Nader Dabit Token";
string public symbol = "NDT";
uint public totalSupply = 1000000;
mapping(address => uint) balances;
constructor() {
balances[msg.sender] = totalSupply;
}
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 hardhat 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' })
const provider = new ethers.providers.Web3Provider(window.ethereum);
const contract = new ethers.Contract(tokenAddress, Token.abi, provider)
const balance = await contract.balanceOf(account);
console.log("Balance: ", balance.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);
const transation = await contract.transfer(userAccount, amount);
await transation.wait();
console.log(`${amount} Coins successfully sent to ${userAccount}`);
}
}
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
我们应该能够点击“获取余额”并看到我们的帐户中有 1,000,000 个硬币并已注销到控制台。
您还可以通过单击导入令牌在 MetaMask 中查看它们:
接下来单击“自定义代币”并输入代币合约地址,然后单击“添加自定义代币”。 (如果要求输入代币小数,请选择 0)现在,代币应该可以在您的钱包中使用:
接下来,让我们尝试将这些硬币发送到另一个地址。
为此,请复制另一个帐户的地址,并使用更新后的 React UI 将其发送到该地址。当您检查代币金额时,它应该等于原始金额减去您发送到该地址的金额。
ERC20代币
ERC20 代币标准定义了一套适用于所有 ERC20 代币的规则,使它们能够轻松地相互交互。ERC20 使得人们能够轻松铸造自己的代币,并与以太坊区块链上的其他代币实现互操作性。
让我们看看如何使用 ERC20 标准构建我们自己的代币。
首先,安装OpenZepplin智能合约库,我们将在其中导入基本代ERC20
币:
npm install @openzeppelin/contracts
接下来,我们将通过扩展(或继承)ERC20
合同来创建我们的代币:
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract NDToken is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
_mint(msg.sender, 100000 * (10 ** 18));
}
}
构造函数允许您设置令牌名称和符号,函数_mint
允许您铸造令牌并设置数量。
默认情况下,ERC20 将小数位数设置为 18,因此在我们的函数中,我们将 100,000 乘以 10 的 18 次方,以铸造总共 100,000 个代币,每个代币都有 18 个小数位(类似于 1 Eth 由 10 的 18 次方wei_mint
组成)。
为了部署,我们需要传入构造函数值(name
和symbol
),因此我们可能在部署脚本中执行如下操作:
const NDToken = await hre.ethers.getContractFactory("NDToken");
const ndToken = await NDToken.deploy("Nader Dabit Token", "NDT");
通过扩展原始 ERC20 代币,您的代币将继承以下所有功能和功能:
function name() public view returns (string)
function symbol() public view returns (string)
function decimals() public view returns (uint8)
function totalSupply() public view returns (uint256)
function balanceOf(address _owner) public view returns (uint256 balance)
function transfer(address _to, uint256 _value) public returns (bool success)
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success)
function approve(address _spender, uint256 _value) public returns (bool success)
function allowance(address _owner, address _spender) public view returns (uint256 remaining)
部署完成后,您可以使用其中任何函数与新的智能合约进行交互。有关 ERC20 代币的另一个示例,请查看 Solidity 示例 ( https://solidity-by-example.org/app/erc20/ )
结论
好的,我们这里讲了很多,但对我来说,这算是入门这个技术栈的入门基础/核心,也是我不仅作为一个正在学习这些东西的人所希望拥有的,而且也是我未来需要参考时所需要的。希望你学到了很多。
如果您除了 MetaMask 之外还想支持多个钱包,请查看Web3Modal,它可以通过相当简单且可定制的配置轻松地在您的应用程序中实现对多个提供商的支持。
在我未来的教程和指南中,我将深入研究更复杂的智能合约开发,以及如何将它们部署为子图以在它们之上公开 GraphQL API 并实现分页和全文搜索等功能。
我还将介绍如何使用 IPFS 和 Web3 数据库等技术以分散的方式存储数据。
如果您对未来的教程有任何问题或建议,请在此处发表评论并告诉我。
文章来源:https://dev.to/dabit3/the-complete-guide-to-full-stack-ethereum-development-3j13