8

以太坊全栈开发完全指南

 3 years ago
source link: https://learnblockchain.cn/article/2383
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
以太坊全栈开发完全指南 | 登链社区 | 深入浅出区块链技术

以太坊全栈开发完全指南

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

本项目的代码在这里

我最近加入了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是一个专为全栈开发而设计的以太坊开发环境和框架,也是我将在本教程中使用的框架。

生态系统中其他类似的工具还有GanacheTruffle(见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.jsGatsbyRedwoodBlitz.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: <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账号

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 (
    <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网络。

Ropsten网络

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

我们可以通过注册类似InfuraAlchemy这样的服务来访问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 (
    <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

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

显示代币

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

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

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

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


本翻译由 Cell Network 赞助支持。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK