跳转至主要内容

针对初学者的 Hello World 智能合约指南 - 全栈

solidityhardhatalchemy智能合约deploying区块链浏览器前端交易
初学者
nstrike2
2021年10月25日
66 分钟阅读 minute read

如果你是区块链开发的新手,不知道从哪里开始或者如何部署并与智能合约交互,那么这个指南就是为你准备的。 我们将演示如何使用 MetaMask(opens in a new tab)Solidity(opens in a new tab)安全帽(opens in a new tab)以及 Alchemy(opens in a new tab),在 Goerli 测试网络上创建和部署一个简单的智能合约。

你将会需要一个 Alchemy 帐户来完成这个教程。 注册一个免费帐户(opens in a new tab)

如果你有任何问题,请随时在 Alchemy Discord(opens in a new tab) 中提出!

第一部分 - 使用安全帽创建和部署你的智能合约

连接以太坊网络

有多种方法可以向以太坊链发起连接请求。 简单起见,我们将会使用一个 Alchemy 上的免费帐户。Alchemy 是一个区块链开发者平台和应用程序接口,让我们在不用自己运行节点的情况下与以太坊链进行通信。 Alchemy 还有着用于监控和分析的开发者工具;我们将在本教程中利用这些工具来深入了解我们智能合约部署中的情况。

创建你的应用程序和应用程序接口密钥

当你创建了一个 Alchemy 帐户后,你可以通过创建应用程序来生成应用程序接口密钥。 这将允许你向 Goerli 测试网发送请求。 如果你不熟悉测试网,你可以阅读 Alchemy 指南来选择一个网络(opens in a new tab)

在 Alchemy 仪表板上,找到位于导航栏的 Apps 下拉菜单并点击 Create App

创建应用程序 Hello world

给你的应用程序命名为“Hello World”并写一个简短的描述。 选择 Staging 作为你的环境以及 Goerli 作为你的网络。

创建应用程序视图 hello world

注:请确保选择 Goerli,否则本教程将不适用。

点击 Create app。 你的应用程序应该会出现在下面的表中。

创建一个以太坊帐户

你需要一个以太坊帐户来发送和接受交易。 我们将会使用 MetaMask,这是一个浏览器中的虚拟钱包,可供用户管理他们的以太坊帐户地址。

你可以在这里(opens in a new tab)免费下载并创建一个 MetaMask 帐户。 When you are creating an account, or if you already have an account, make sure to switch over to the “Goerli Test Network” in the upper right (so that we’re not dealing with real money).

步骤 4:从水龙头添加以太币

为了将你的智能合约部署到测试网络,你需要一些虚拟以太币。 为了获得 Goerli 网络上的以太币,请前往 Goerli 水龙头并输入你的 Goerli 帐户地址。 注意 Goerli 水龙头最近可能不太可靠 - 请查看测试网络页面以了解可以尝试的选项列表:

注:由于网络拥塞,这可能需要一些时间。

步骤 5:查看帐户余额

为了核查钱包中的以太币,我们使用 Alchemy 的 Composer 工具(opens in a new tab)来发送 eth_getBalance(opens in a new tab) 请求。 这将返回我们钱包中的以太币金额。 想要了解更多请查看 Alchemy 关于如何使用 Composer 工具的简短教程(opens in a new tab)

输入你的 MetaMask 帐户地址并点击 Send Request。 你将会看到类似以下代码片段的响应。

1{ "jsonrpc": "2.0", "id": 0, "result": "0x2B5E3AF16B1880000" }
复制

注:此结果以 wei 为单位,而非 ETH。 Wei is used as the smallest denomination of ether.

Phew! 这里显示了我们所有的虚拟货币。

步骤 6:初始化我们的项目

首先,需要为我们的项目创建一个文件夹。 导航到你的命令行并输入以下内容。

1mkdir hello-world
2cd hello-world

现在我们进入了项目文件夹,我们将使用 npm init 来初始化项目。

如果你尚未安装 npm,请按照这些说明来安装 Node.js 和 npm(opens in a new tab)

对于本教程而言,你如何回答初始化问题并不重要。 以下是我们的参考操作方式:

1package name: (hello-world)
2version: (1.0.0)
3description: hello world smart contract
4entry point: (index.js)
5test command:
6git repository:
7keywords:
8author:
9license: (ISC)
10
11About to write to /Users/.../.../.../hello-world/package.json:
12
13{
14 "name": "hello-world",
15 "version": "1.0.0",
16 "description": "hello world smart contract",
17 "main": "index.js",
18 "scripts": {
19 "test": "echo \"Error: no test specified\" && exit 1"
20 },
21 "author": "",
22 "license": "ISC"
23}
显示全部

批准 package.json,我们就可以进行下一步了!

步骤 7:下载安全帽

安全帽是一个用于编译、部署、测试和调试以太坊软件的开发环境。 它帮助开发者在本地构建智能合约和去中心化应用程序并部署到实时链上。

在我们的 hello-world 项目中运行:

1npm install --save-dev hardhat

查看此页面,了解更多有关安装说明(opens in a new tab)的详细信息。

步骤 8:创建安全帽项目

在我们的 hello-world 项目文件夹中,运行:

1npx hardhat

然后应该能看到一条欢迎消息和选项,用于选择你想要做的事情。 选择“创建一个空的 hardhat.config.js”:

1888 888 888 888 888
2888 888 888 888 888
3888 888 888 888 888
48888888888 8888b. 888d888 .d88888 88888b. 8888b. 888888
5888 888 "88b 888P" d88" 888 888 "88b "88b 888
6888 888 .d888888 888 888 888 888 888 .d888888 888
7888 888 888 888 888 Y88b 888 888 888 888 888 Y88b.
8888 888 "Y888888 888 "Y88888 888 888 "Y888888 "Y888
9
10👷 Welcome to Hardhat v2.0.11 👷‍
11
12What do you want to do? …
13Create a sample project
14❯ Create an empty hardhat.config.js
15Quit
显示全部

这将会在项目中生成一个 hardhat.config.js 文件。 我们稍后将在教程中使用它来为我们的项目指定设置。

步骤 9:添加项目文件夹

为了使项目有条理,我们将创建两个新的文件夹。 在命令行中,导航到你的 hello-world 项目的根目录中并输入:

1mkdir contracts
2mkdir scripts
  • contracts/ 是保存我们的 hello world 智能合约代码文件的位置
  • scripts/ 是我们存放脚本的位置,用于部署我们的合约和与之交互。

步骤 10:编写合约

你可能在问自己,到底什么时候才能写代码? 就是现在!

在你最喜爱的编辑器中打开 hello-world 项目。 智能合约通常使用 Solidity 来编写,我们也将使用它来编写智能合约。

  1. 导航到 contracts 文件夹并创建一个名为 HelloWorld.sol 的新文件。
  2. 下面是我们将在本教程中使用的示例 Hello World 智能合约。 将下面的内容复制到 HelloWorld.sol 文件中。

注:务必阅读注释以理解此合约的内容。

1// Specifies the version of Solidity, using semantic versioning.
2// Learn more: https://solidity.readthedocs.io/en/v0.5.10/layout-of-source-files.html#pragma
3pragma solidity >=0.7.3;
4
5// Defines a contract named `HelloWorld`.
6// 一个合约是函数和数据(其状态)的集合。 Once deployed, a contract resides at a specific address on the Ethereum blockchain. Learn more: https://solidity.readthedocs.io/en/v0.5.10/structure-of-a-contract.html
7contract HelloWorld {
8
9 //Emitted when update function is called
10 //Smart contract events are a way for your contract to communicate that something happened on the blockchain to your app front-end, which can be 'listening' for certain events and take action when they happen.
11 event UpdatedMessages(string oldStr, string newStr);
12
13 // Declares a state variable `message` of type `string`.
14 // 状态变量是其值永久存储在合约存储中的变量。 The keyword `public` makes variables accessible from outside a contract and creates a function that other contracts or clients can call to access the value.
15 string public message;
16
17 // Similar to many class-based object-oriented languages, a constructor is a special function that is only executed upon contract creation.
18 // 构造器用于初始化合约的数据。 Learn more:https://solidity.readthedocs.io/en/v0.5.10/contracts.html#constructors
19 constructor(string memory initMessage) {
20
21 // Accepts a string argument `initMessage` and sets the value into the contract's `message` storage variable).
22 message = initMessage;
23 }
24
25 // A public function that accepts a string argument and updates the `message` storage variable.
26 function update(string memory newMessage) public {
27 string memory oldMsg = message;
28 message = newMessage;
29 emit UpdatedMessages(oldMsg, newMessage);
30 }
31}
显示全部

这是一个在创建时存储一条消息的基础智能合约。 可以通过调用 update 函数来更新该合约。

步骤 11:将 Metamask 和 Alchemy 连接至你的项目

我们创建了 MetaMask 钱包、Alchemy 帐户,并且编写了一个智能合约,现在是将这三者连起来的时候了。

从你的钱包发出的每一笔交易都需要使用你独有私钥的签名。 为了给我们的程序提供此权限,我们可以安全地将私钥存储在一个环境文件中。 我们也会在此存储一个 Alchemy 的应用程序接口密钥。

如需了解更多关于发送交易的信息,请查看关于使用 web3 发送交易的教程(opens in a new tab)

首先,在项目目录中安装 dotenv 软件包:

1npm install dotenv --save

然后,在项目根目录下创建一个 .env 文件。 在文件中添加你的 MetaMask 私钥和 HTTP Alchemy 应用程序接口 URL。

你的环境文件必须以 .env 命名,否则不会被识别为环境文件。

不要命名为 process.env.env-custom 或其他名称。

你的 .env 文件应该类似:

1API_URL = "https://eth-goerli.alchemyapi.io/v2/your-api-key"
2PRIVATE_KEY = "your-metamask-private-key"

为了将这些变量和代码连接,我们将在步骤 13 中调用 hardhat.config.js 文件中的这些变量。

步骤 12:安装 Ethers.js

Ethers.js 是一个程序库,通过以更加方便用户的方法打包标准 JSON RPC 方法(opens in a new tab),从而更容易与以太坊互动,以及向以太坊提出请求。

安全帽可用于集成插件(opens in a new tab)以获取额外的工具和扩展功能。 我们将利用 Ethers 插件(opens in a new tab)完成合约部署。

在你的项目目录中输入:

npm install --save-dev @nomiclabs/hardhat-ethers "ethers@^5.0.0"

步骤 13:更新 hardhat.config.js

到目前为止,我们已经添加了几个依赖库和插件,现在我们需要更新 hardhat.config.js,以便项目使用所有这些新的组件。

按如下所示更新你的 hardhat.config.js 代码:

1/**
2 * @type import('hardhat/config').HardhatUserConfig
3 */
4
5require("dotenv").config()
6require("@nomiclabs/hardhat-ethers")
7
8const { API_URL, PRIVATE_KEY } = process.env
9
10module.exports = {
11 solidity: "0.7.3",
12 defaultNetwork: "goerli",
13 networks: {
14 hardhat: {},
15 goerli: {
16 url: API_URL,
17 accounts: [`0x${PRIVATE_KEY}`],
18 },
19 },
20}
显示全部

步骤 14:编写合约

为了确保一切正常,我们来编译一下合约。 compile 任务是安全帽的内部任务之一。

在命令行中运行:

npx hardhat compile

你可能会看到关于 SPDX license identifier not provided in source file 的警告,但不用担心,希望其他方面看起来一切正常! 如果遇到问题,你可以随时在 Alchemy cord(opens in a new tab) 社区中发消息询问。

步骤 15:编写部署脚本

合约已经写完,配置文件也准备妥当,现在是写合约部署脚本的时候了。

转到 scripts/ 文件夹,创建一个名为 deploy.js 的新文件,在其中添加以下内容:

1async function main() {
2 const HelloWorld = await ethers.getContractFactory("HelloWorld")
3
4 // Start deployment, returning a promise that resolves to a contract object
5 const hello_world = await HelloWorld.deploy("Hello World!")
6 console.log("Contract deployed to address:", hello_world.address)
7}
8
9main()
10 .then(() => process.exit(0))
11 .catch((error) => {
12 console.error(error)
13 process.exit(1)
14 })
显示全部

安全帽在合约教程(opens in a new tab)中对这些代码的每一行均提供了很好的解释,我们在这里直接引用他们的解释。

1const HelloWorld = await ethers.getContractFactory("HelloWorld")

ethers.js 中的 ContractFactory 是用于部署新智能合约的抽象对象。因此这里的 HelloWorld 是我们 hello world 合约实例的工厂(opens in a new tab)。 使用 hardhat-ethers 插件时,ContractFactoryContract 实例默认与第一个签名者(所有者)相连。

1const hello_world = await HelloWorld.deploy()

调用 ContractFactory 代码中的 deploy() 函数会启动合约部署,然后返回解析为 Contract 对象的 Promise。 这个对象包括我们智能合约中每个函数的对应调用方法。

步骤 16:部署合约

我们终于准备好部署我们的智能合约啦! 导航到命令行后运行:

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

你会看到类似以下所示的信息:

Contract deployed to address: 0x6cd7d44516a20882cEa2DE9f205bF401c0d23570

请保存这个地址。 之后在教程中我们会用到这个地址。

如果我们在 Goerli etherscan(opens in a new tab) 搜索我们的合约地址,我们应能够看到它已经成功部署。 交易将类似以下:

From 地址应该匹配你的 MetaMask 帐户地址,To 地址将是合约创建。 如果我们点击进入交易,我们将在 To 字段中看到我们的合约地址。

恭喜! 你刚刚在以太坊测试网上部署了一个智能合约。

为了更深入了解到底发生了什么,我们转到 Alchemy 仪表板(opens in a new tab)中的 Explorer 选项卡。 如果你有多个 Alchemy 应用程序,请确保按应用程序筛选,然后选择 Hello World

在这里,你会看到一系列 JSON-RPC 方法,当我们调用 .deploy() 函数时,安全帽/Ethers 会在后端完成这些方法。 在这里有两个重要的方法,eth_sendRawTransaction(opens in a new tab) 是用于把我们的合约写入 Goerli 链的请求,eth_getTransactionByHash(opens in a new tab) 是根据哈希读取交易信息的请求。 如需了解更多关于发送交易的信息,请查看我们关于使用 Web3 发送交易的教程

第二部分 - 和你的智能合约交互

现在我们已经成功地将智能合约部署到 Goerli 网络,让我们学习如何与它交互。

创建 interact.js 文件

这是我们将在其中编写交互脚本的文件。 我们将使用在第一部分中安装的 Ethers.js 库。

scripts/ 文件夹中,新建一个文件,命名为 interact.js,添加以下代码:

1// interact.js
2
3const API_KEY = process.env.API_KEY
4const PRIVATE_KEY = process.env.PRIVATE_KEY
5const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS

更新 .env 文件

我们将使用新的环境变量,因此需要在我们之前创建的 .env 文件中定义这些变量。

我们需要为 Alchemy API_KEY 和部署你的智能合约的 CONTRACT_ADDRESS 添加定义。

你的 .env 文件应该如下所示:

# .env
API_URL = "https://eth-goerli.alchemyapi.io/v2/<your-api-key>"
API_KEY = "<your-api-key>"
PRIVATE_KEY = "<your-metamask-private-key>"
CONTRACT_ADDRESS = "0x<your contract address>"

获取你的合约应用程序二进制接口

我们的合约 是与我们的智能合约交互的接口。 安全帽自动生成应用程序二进制接口,并将其保存在 HelloWorld.json 中。 为了使用该接口,我们需要通过在我们的 interact.js 文件中添加以下代码行来解析内容:

1// interact.js
2const contract = require("../artifacts/contracts/HelloWorld.sol/HelloWorld.json")

如果你想查看应用程序二进制接口,你可以将其发送到你的控制台:

1console.log(JSON.stringify(contract.abi))

要查看输出到控制台的应用程序二进制接口,请导航至你的终端并运行:

npx hardhat run scripts/interact.js

创建合约的实例

为了与我们的合约进行交互,我们需要在代码中创建一个合约实例。 要使用 Ethers.js 实现,我们需要使用三个概念:

  1. 提供者 - 为你提供区块链读写访问权限的节点提供者。
  2. 签名者 - 代表可以给交易签名的以太坊帐户
  3. 合约 - 代表部署在链上的特定合约的 Ethers.js对象

我们将使用上一步中的合约应用程序二进制接口来创建我们的合约实例:

1// interact.js
2
3// Provider
4const alchemyProvider = new ethers.providers.AlchemyProvider(
5 (network = "goerli"),
6 API_KEY
7)
8
9// Signer
10const signer = new ethers.Wallet(PRIVATE_KEY, alchemyProvider)
11
12// Contract
13const helloWorldContract = new ethers.Contract(
14 CONTRACT_ADDRESS,
15 contract.abi,
16 signer
17)
显示全部

ethers.js 文档(opens in a new tab)获取更多关于提供者、签名者和合约的信息。

阅读 init 消息

还记得我们用 initMessage = "Hello world!" 部署合约时的情况吗? 我们现在要读取存储在智能合约中的消息,并将其输出到控制台。

在 JavaScript 中,与网络交互时会使用异步函数。 要了解更多关于异步函数的信息,请阅读这篇 Medium 文章(opens in a new tab)

使用下面的代码来调用智能合约中的 message 函数,并读取 init 消息:

1// interact.js
2
3// ...
4
5async function main() {
6 const message = await helloWorldContract.message()
7 console.log("The message is: " + message)
8}
9main()
显示全部

在终端使用 npx hardhat run scripts/interact.js 运行文件后,我们应该看到如下响应:

1The message is: Hello world!

恭喜! 你刚刚成功从以太坊区块链读取了智能合约数据,好样的!

更新消息

除了读取消息,我们还可以使用 update 函数更新保存在我们智能合约中的消息! 很酷,对吗?

要更新消息,我们可以直接在实例化的合约对象上调用 update 函数:

1// interact.js
2
3// ...
4
5async function main() {
6 const message = await helloWorldContract.message()
7 console.log("The message is: " + message)
8
9 console.log("Updating the message...")
10 const tx = await helloWorldContract.update("This is the new message.")
11 await tx.wait()
12}
13main()
显示全部

请注意,在第 11 行,我们对返回的交易对象调用了 .wait()。 这确保了脚本在退出函数前等待交易在区块链上完成挖掘。 如果不包含 .wait() 调用,脚本可能不会看到合约中更新后的 message 值。

读取新消息

你可以重复前面的步骤来读取更新后的 message 值。 花点时间,看看是否可以进行必要的更改以输出新值!

如果你需要提示,你的 interact.js 文件现在应如下所示:

1// interact.js
2
3const API_KEY = process.env.API_KEY
4const PRIVATE_KEY = process.env.PRIVATE_KEY
5const CONTRACT_ADDRESS = process.env.CONTRACT_ADDRESS
6
7const contract = require("../artifacts/contracts/HelloWorld.sol/HelloWorld.json")
8
9// provider - Alchemy
10const alchemyProvider = new ethers.providers.AlchemyProvider(
11 (network = "goerli"),
12 API_KEY
13)
14
15// signer - you
16const signer = new ethers.Wallet(PRIVATE_KEY, alchemyProvider)
17
18// contract instance
19const helloWorldContract = new ethers.Contract(
20 CONTRACT_ADDRESS,
21 contract.abi,
22 signer
23)
24
25async function main() {
26 const message = await helloWorldContract.message()
27 console.log("The message is: " + message)
28
29 console.log("Updating the message...")
30 const tx = await helloWorldContract.update("this is the new message")
31 await tx.wait()
32
33 const newMessage = await helloWorldContract.message()
34 console.log("The new message is: " + newMessage)
35}
36
37main()
显示全部

现在只需运行脚本,你应该能够看到旧消息、更新状态和输出到终端的新消息!

npx hardhat run scripts/interact.js --network goerli

1The message is: Hello World!
2Updating the message...
3The new message is: This is the new message.

当运行那条脚本时,你可能会发现 Updating the message... 步骤需要一段时间才能加载完成,然后再加载新消息。 这是由挖矿过程导致的;如果你希望在对交易进行挖矿的同时追踪交易,可以访问 Alchemy 内存池(opens in a new tab)查看交易的状态。 如果交易被丢弃,可以访问 Goerli Etherscan(opens in a new tab) 并搜索你的交易哈希值。

第三部分:将你的智能合约发布到 Etherscan

你已经完成了实现智能合约相关的所有艰苦工作,现在是时候与世界分享了!

通过在 Etherscan 上验证你的智能合约,任何人都可以查看其源代码,并与智能合约进行交互。 让我们开始吧!

步骤 1:在你的 Etherscan 帐户上生成应用程序接口密钥

需要 Etherscan 应用程序接口密钥来验证你是否拥有你正在尝试发布的智能合约。

如果你还没有 Etherscan 帐户,请注册一个帐户(opens in a new tab)

登录后,在导航栏中找到你的用户名,将鼠标悬停在用户名上,然后选择 My profile 按钮。

在你的个人资料页面上,应该可以看到一个侧边导航栏。 从侧边导航栏中,选择 API Key。 接下来,按“Add”按钮创建一个新的应用程序接口密钥,将你的应用程序命名为 hello-world,然后按 Create New API Key 按钮。

你的新应用程序接口密钥应出现在应用程序接口密钥表中。 将应用程序接口复制到剪贴板。

接下来,我们需要将 Etherscan 应用程序接口添加到 .env 文件中。

添加完成后,你的 .env 文件应如下所示:

1API_URL = "https://eth-goerli.alchemyapi.io/v2/your-api-key"
2PUBLIC_KEY = "your-public-account-address"
3PRIVATE_KEY = "your-private-account-address"
4CONTRACT_ADDRESS = "your-contract-address"
5ETHERSCAN_API_KEY = "your-etherscan-key"

使用安全帽部署智能合约

安装 hardhat-etherscan

使用安全帽将你的合约发布到 Etherscan 非常简单。 首先需要安装 hardhat-etherscan 插件。 hardhat-etherscan 会在 Etherscan 上自动验证智能合约的源代码和应用程序二进制接口。 为了添加此插件,你需要在 hello-world 目录中运行:

1npm install --save-dev @nomiclabs/hardhat-etherscan

安装完成后,在你的 hardhat.config.js 文件中添加下面的语句,并添加 Etherscan 配置选项:

1// hardhat.config.js
2
3require("dotenv").config()
4require("@nomiclabs/hardhat-ethers")
5require("@nomiclabs/hardhat-etherscan")
6
7const { API_URL, PRIVATE_KEY, ETHERSCAN_API_KEY } = process.env
8
9module.exports = {
10 solidity: "0.7.3",
11 defaultNetwork: "goerli",
12 networks: {
13 hardhat: {},
14 goerli: {
15 url: API_URL,
16 accounts: [`0x${PRIVATE_KEY}`],
17 },
18 },
19 etherscan: {
20 // Your API key for Etherscan
21 // Obtain one at https://etherscan.io/
22 apiKey: ETHERSCAN_API_KEY,
23 },
24}
显示全部

在 Etherscan 验证你的智能合约

确认所有文件都已保存,且所有 .env 变量都已正确配置。

运行 verify 任务,传入合约地址以及要部署到的网络:

1npx hardhat verify --network goerli DEPLOYED_CONTRACT_ADDRESS 'Hello World!'

确保 DEPLOYED_CONTRACT_ADDRESS 是你在 Goerli 测试网络上部署的智能合约的地址。 此外,最后一个参数 ('Hello World!') 必须是你在第一部分的部署步骤中使用的相同字符串。

如果一切正常,你会在终端中看到以下消息:

1Successfully submitted source code for contract
2contracts/HelloWorld.sol:HelloWorld at 0xdeployed-contract-address
3for verification on Etherscan. Waiting for verification result...
4
5
6Successfully verified contract HelloWorld on Etherscan.
7https://goerli.etherscan.io/address/<contract-address>#contracts

恭喜! 你的智能合约代码已经在 Etherscan 部署!

在 Etherscan 查看你的智能合约!

当你进入终端中给出的链接时,你应该会看到你的智能合约代码和应用程序二进制接口已在 Etherscan 上发布!

哇 - 你成功了! 现在所有人都可以调用或写入你的智能合约! 我们已经等不及看你接下来会做什么了!

第 4 部分 - 将智能合约与前端集成

本教程结束时,你将知道如何:

  • 将 MetaMask 钱包连接到你的去中心化应用程序
  • 使用 Alchemy Web3(opens in a new tab) 应用程序接口从你的智能合约中读取数据
  • 使用 MetaMask 对以太坊交易签名

对于此去中心化应用程序,我们会使用 React(opens in a new tab) 作为前端框架;然而,需要注意的是,我们不会花很多时间来分解其基本内容,而是会聚焦于将 Web3 功能引入我们的项目。

作为前提条件,你需要对 React 有基本的了解。 否则,建议你完成官方的 React 入门教程(opens in a new tab)

克隆启动文件

首先,到 hello-world-part-four GitHub 存储库(opens in a new tab)中获取项目的初始文件,并将此存储库克隆到你的本地计算机中。

在本地打开克隆的存储库。 你会注意到它包含两个文件夹:starter-filescompleted

  • starter-files - 我们会在这个目录中工作,我们会将用户界面连接到你的以太坊钱包以及我们在第三部分中发布到 Etherscan 的智能合约。
  • completed 包含已完成的整个教程,并只应用作出现问题时的参考。

下面,用你喜欢的代码编辑器打开 starter-files,然后进入 src 文件夹。

我们编写的所有代码都将保存在 src 文件夹下。 我们将通过编辑 HelloWorld.js 组件并编写 util/interact.js JavaScript 文件,为我们的项目提供 Web3 功能。

查看初始文件

在我们开始编写之前,让我们看看初始文件为我们提供了什么。

让你的 react 项目运行起来

首先在浏览器中运行 React 项目。 React 的美妙之处在于,一旦我们的项目在浏览器中运行,保存的任何更改都会在浏览器中实时更新。

要让项目运行,浏览到 starter-files 文件夹的根目录,然后在终端运行 npm install 以安装项目的依赖项:

cd starter-files
npm install

依赖项安装完成后,在终端运行 npm start

npm start

这样做应该会在你的浏览器中打开 http://localhost:3000/(opens in a new tab),在这里你会看到我们项目的前端界面。 它应该包含一个字段 (一个更新存储在智能合约中的消息的地方),一个“Connect Wallet”按钮,以及一个“Update”按钮。

如果你试图点击这些按钮,你会发现它们都不起作用—这是因为我们仍然需要对其功能进行编程。

HelloWorld.js组件

让我们在编辑器中返回 src 文件夹并打开 HelloWorld.js 文件。 理解该文件中的所有内容非常重要,因为它是我们将要处理的主要 React 组件。

在该文件开头,你会发现我们有几条 import 语句,这些语句是我们项目运行所必须的,它们包括 React 程序库,useEffect 和 useState 钩子,一些来自 ./util/interact.js 的项(我们之后还会更详细的说明它们!)以及 Alchemy 徽标。

1// HelloWorld.js
2
3import React from "react"
4import { useEffect, useState } from "react"
5import {
6 helloWorldContract,
7 connectWallet,
8 updateMessage,
9 loadCurrentMessage,
10 getCurrentWalletConnected,
11} from "./util/interact.js"
12
13import alchemylogo from "./alchemylogo.svg"
显示全部

下面,我们来定义将在特定事件后更新的状态变量。

1// HelloWorld.js
2
3//State variables
4const [walletAddress, setWallet] = useState("")
5const [status, setStatus] = useState("")
6const [message, setMessage] = useState("No connection to the network.")
7const [newMessage, setNewMessage] = useState("")

以下是每个变量的含义:

  • walletAddress — 存储用户钱包地址的字符串
  • status — 用于存储有用消息,指导用户如何与去中心化应用程序交互的字符串
  • message — 存储智能合约中当前消息的字符串
  • newMessage — 存储将要写入智能合约中的新消息的字符串

在状态变量后,你会发现五个还未实现的函数:useEffectaddSmartContractListeneraddWalletListenerconnectWalletPressed 以及 onUpdatePressed。 我们会在下面解释它们的作用:

1// HelloWorld.js
2
3//called only once
4useEffect(async () => {
5 //TODO: implement
6}, [])
7
8function addSmartContractListener() {
9 //TODO: implement
10}
11
12function addWalletListener() {
13 //TODO: implement
14}
15
16const connectWalletPressed = async () => {
17 //TODO: implement
18}
19
20const onUpdatePressed = async () => {
21 //TODO: implement
22}
显示全部
  • useEffect(opens in a new tab) — 这是会在你的组件被渲染后调用的 React 钩子。 因为向它传入了一个空的数组 [] 属性 \(见第 4 行\),它只会在组件的第一次渲染时被调用。 在这里我们会加载智能合约中存储的当前消息,调用智能合约和钱包监听器,并更新用户界面来反映钱包是否已连接。
  • addSmartContractListener — 这个函数设置了一个监听器,这个监听器会监视 HelloWorld 合约中的 UpdatedMessages 事件,并在智能合约中的消息变化时更新用户界面。
  • addWalletListener — 这个函数设置了一个监听器,来检测用户的 MetaMask 钱包的状态变化,比如用户断开他们的钱包或切换钱包地址时。
  • connectWalletPressed — 这个函数用于将用户的 MetaMask 钱包连接到我们的去中心化应用程序。
  • onUpdatePressed — 这个函数会在用户更新智能合约中存储的消息时被调用。

在接近该文件末尾处,我们获得我们组件的用户界面。

1// HelloWorld.js
2
3//the UI of our component
4return (
5 <div id="container">
6 <img id="logo" src={alchemylogo}></img>
7 <button id="walletButton" onClick={connectWalletPressed}>
8 {walletAddress.length > 0 ? (
9 "Connected: " +
10 String(walletAddress).substring(0, 6) +
11 "..." +
12 String(walletAddress).substring(38)
13 ) : (
14 <span>Connect Wallet</span>
15 )}
16 </button>
17
18 <h2 style={{ paddingTop: "50px" }}>Current Message:</h2>
19 <p>{message}</p>
20
21 <h2 style={{ paddingTop: "18px" }}>New Message:</h2>
22
23 <div>
24 <input
25 type="text"
26 placeholder="Update the message in your smart contract."
27 onChange={(e) => setNewMessage(e.target.value)}
28 value={newMessage}
29 />
30 <p id="status">{status}</p>
31
32 <button id="publishButton" onClick={onUpdatePressed}>
33 Update
34 </button>
35 </div>
36 </div>
37)
显示全部

如果你细致地检查这段代码,就会发现我们在用户界面中的哪里使用了各种状态变量:

  • 在第 6-12 行,如果用户的钱包已连接 (walletAddress.length > 0),我们就在 ID 为“walletButton;”的按钮中显示用户 walletAddress 的截短版,否则我们就只显示“连接钱包”。
  • 在第 17 行,我们显示在 message 字符串中获取的智能合约中存储的当前消息。
  • 在第 23-26 行,我们使用一个受控组件,(opens in a new tab)以用于在文本字段中输入更改时更新 newMessage 状态变量。

除了状态变量之外,你还将看到,在分别单击 ID 为 publishButtonwalletButton 的按钮时,会调用 connectWalletPressedonUpdatePressed

最后,我们来看看 HelloWorld.js 组件是在哪里添加的。

如果你打开 App.js 文件,将会看到我们的 HelloWorld.js 组件是在第 7 行添加的。此文件是 React 中的主要组件,作为所有其他组件的容器。

最后,让我们再看看另一个为你提供的文件,interact.js

interact.js文件

因为我们建议采用 M-V-C(opens in a new tab) 规范,我们需要一个单独的文件包含用来管理我们的去中心化应用程序的逻辑、数据和规则的所有函数,然后我们就能将这些函数导出到前端 (HelloWorld.js 组件)

👆🏽这就是 interact.js 文件的确切目的!

进入 src 目录中的 util 文件夹,然后你就会发现我们引入了 interact.js 文件,它包含所有的智能合约交互以及钱包函数和变量。

1// interact.js
2
3//export const helloWorldContract;
4
5export const loadCurrentMessage = async () => {}
6
7export const connectWallet = async () => {}
8
9const getCurrentWalletConnected = async () => {}
10
11export const updateMessage = async (message) => {}
显示全部

你会发现在文件的开头,我们注释了 helloWorldContract 对象。 在之后的教程里,我们会删除这个对象的注释,并且在这个变量中实例化智能合约,然后我们可以将其导出到 HelloWorld.js 组件。

helloWorldContract 对象之后的四个未实现函数会发挥以下作用:

  • loadCurrentMessage — 这个函数处理加载智能合约中存储的信息的逻辑。 它会使用 Alchemy Web3 API(opens in a new tab),向 Hello World 智能合约发起一个读取调用。
  • connectWallet — 这个函数会将用户的 MetaMask 连接到我们的去中心化应用程序。
  • getCurrentWalletConnected — 这个函数会在页面加载时检查是否已经有以太坊帐户连接到我们的去中心化应用程序,并且相应更新我们的用户界面。
  • updateMessage — 这个函数会更新智能合约中存储的消息。 它会向 Hello World 智能合约发起一个写入调用,所以用户的 MetaMask 钱包需要签名一个以太坊交易来更新此消息。

现在我们已经了解所操作的对象,让我们看看如何从智能合约中读取吧!

第三步:从你的智能合约中读取

为了从智能合约中读取,你需要成功设置以下内容:

  • 一个到以太坊链的应用程序接口连接
  • 一个已加载的智能合约实例
  • 一个用来调用智能合约函数的函数
  • 一个监听器,用于监听智能合约中你正读取的数据出现变化时的更新

听起来步骤可能很多,但是不要担心! 我们会引导你逐步完成这些步骤! :)

建立一个到以太坊链的应用程序接口连接

还记得我们在教程的第二部分怎么用我们的 Alchemy Web3 密钥从我们的智能合约中读取(opens in a new tab)吗? 你在去中心化应用程序中也需要一个 Alchemy Web3 密钥来从链上读取。

如果你还没有提前准备好,首先,我们来安装 Alchemy Web3(opens in a new tab),导航到 starter-files 的根目录,然后在你的终端运行以下内容:

1npm install @alch/alchemy-web3

Alchemy Web3(opens in a new tab)Web3.js(opens in a new tab) 的包装器,提供增强的应用程序接口方法和其他重要优势,让 Web3 开发者的工作更轻松。 它设计成只需经过最少的配置即可使用,因此你可以直接在你的应用程序中开始使用它!

然后,在你的项目目录中安装 dotenv(opens in a new tab) 程序包,我们就有了一个在获取我们应用程序接口密钥后安全存储它的地方。

1npm install dotenv --save

对于我们的去中心化应用程序,将不再使用 HTTP 应用程序接口密钥,而是改用 Websockets 应用程序接口密钥,因为它会允许我们设置一个监测智能合约中消息变化的监听器。

在你获得应用程序接口密钥后,在你的根目录中创建 .env 文件,并在其中添加你的 Alchemy Websockets URL。 在这之后,你的 .env 文件应该如下所示:

1REACT_APP_ALCHEMY_KEY = wss://eth-goerli.ws.alchemyapi.io/v2/<key>

现在,我们已经准备好在我们的去中心化应用程序中设置 Alchemy Web3 端点了! 让我们回到嵌套在 util 文件夹中的 interact.js,并在文件的开头加上以下代码:

1// interact.js
2
3require("dotenv").config()
4const alchemyKey = process.env.REACT_APP_ALCHEMY_KEY
5const { createAlchemyWeb3 } = require("@alch/alchemy-web3")
6const web3 = createAlchemyWeb3(alchemyKey)
7
8//export const helloWorldContract;

在上面,我们先从 .env 文件中导入了 Alchemy 密钥,然后将 alchemyKey 传递给 createAlchemyWeb3 以建立 Alchemy Web3 端点。

准备好此端点后,我们就可以加载智能合约了!

加载你的 Hello World 智能合约

要加载你的 Hello World 智能合约,将需要其合约地址和应用程序二进制接口,如果你完成了这个教程的第三部分,这两者都可以在 Etherscan 上找到。

如何从 Etherscan 获取合约的应用程序二进制接口

如果你跳过了这个教程的第三部分,你可以使用 HelloWorld 合约,其地址为 0x6f3f635A9762B47954229Ea479b4541eAF402A6A(opens in a new tab)。 它的应用程序二进制接口可在这里(opens in a new tab)找到。

在指定合约将要调用的函数,以及确保函数以你期望的格式返回数据时,合约的应用程序二进制接口必不可少。 复制合约应用程序二进制接口后,让我们将其保存到 src 目录,保存为文件名为 contract-abi.json 的 JSON 文件。

你的 contract-abi.json 文件应存储在 src 文件夹。

有了合约地址、应用程序二进制接口和 Alchemy Web3 端点,我们就可以使用合约方法(opens in a new tab)来加载智能合约的实例。 将你的合约应用程序二进制接口导入 interact.js 文件,然后添加你的合约地址。

1// interact.js
2
3const contractABI = require("../contract-abi.json")
4const contractAddress = "0x6f3f635A9762B47954229Ea479b4541eAF402A6A"

现在,我们终于可以删除 helloWorldContract 变量的注释,并用 AlchemyWeb3 端点加载智能合约了:

1// interact.js
2export const helloWorldContract = new web3.eth.Contract(
3 contractABI,
4 contractAddress
5)

概括一下,你的 interact.js 文件的前 12 行应该如下所示:

1// interact.js
2
3require("dotenv").config()
4const alchemyKey = process.env.REACT_APP_ALCHEMY_KEY
5const { createAlchemyWeb3 } = require("@alch/alchemy-web3")
6const web3 = createAlchemyWeb3(alchemyKey)
7
8const contractABI = require("../contract-abi.json")
9const contractAddress = "0x6f3f635A9762B47954229Ea479b4541eAF402A6A"
10
11export const helloWorldContract = new web3.eth.Contract(
12 contractABI,
13 contractAddress
14)
显示全部

我们现在已经加载了合约,可以实现 loadCurrentMessage 函数了!

interact.js 文件中实现 loadCurrentMessage

这个函数非常简单。 我们将通过一个简单的异步 Web3 调用来从我们的合约中读取信息。 我们的函数会返回智能合约中存储的消息:

interact.js 文件中的 loadCurrentMessage 更新为如下:

1// interact.js
2
3export const loadCurrentMessage = async () => {
4 const message = await helloWorldContract.methods.message().call()
5 return message
6}

因为我们希望在用户界面中显示该智能合约,让我们将 HelloWorld.js 组件中的 useEffect 函数更新如下:

1// HelloWorld.js
2
3//called only once
4useEffect(async () => {
5 const message = await loadCurrentMessage()
6 setMessage(message)
7}, [])

注意,我们只希望在组件第一次渲染时调用 loadCurrentMessage 函数一次。 我们很快会实现 addSmartContractListener 以在智能合约中的消息变化后自动更新用户界面。

在我们深入研究监听器之前,让我们看看现在已经做了什么! 保存你的 HelloWorld.jsinteract.js 文件,然后访问 http://localhost:3000/(opens in a new tab)

你会发现当前的消息不再是“No connection to the network.” 相反,它反映了智能合约中存储的消息。 酷!

你的用户界面现在应该反映了智能合约中存储的消息

说到监听器……

实现 addSmartContractListener

回想我们在这个教程的第 1 部分(opens in a new tab)编写的 HelloWorld.sol,你会记得有一个名为 UpdatedMessages 的智能合约事件,这是在调用我们的智能合约的 update 函数后触发的 \(参见第 9 行和第 27 行\):

1// HelloWorld.sol
2
3// Specifies the version of Solidity, using semantic versioning.
4// Learn more: https://solidity.readthedocs.io/en/v0.5.10/layout-of-source-files.html#pragma
5pragma solidity ^0.7.3;
6
7// Defines a contract named `HelloWorld`.
8// 一个合约是函数和数据(其状态)的集合。 Once deployed, a contract resides at a specific address on the Ethereum blockchain. Learn more: https://solidity.readthedocs.io/en/v0.5.10/structure-of-a-contract.html
9contract HelloWorld {
10
11 //Emitted when update function is called
12 //Smart contract events are a way for your contract to communicate that something happened on the blockchain to your app front-end, which can be 'listening' for certain events and take action when they happen.
13 event UpdatedMessages(string oldStr, string newStr);
14
15 // Declares a state variable `message` of type `string`.
16 // 状态变量是其值永久存储在合约存储中的变量。 The keyword `public` makes variables accessible from outside a contract and creates a function that other contracts or clients can call to access the value.
17 string public message;
18
19 // Similar to many class-based object-oriented languages, a constructor is a special function that is only executed upon contract creation.
20 // 构造器用于初始化合约的数据。 Learn more:https://solidity.readthedocs.io/en/v0.5.10/contracts.html#constructors
21 constructor(string memory initMessage) {
22
23 // Accepts a string argument `initMessage` and sets the value into the contract's `message` storage variable).
24 message = initMessage;
25 }
26
27 // A public function that accepts a string argument and updates the `message` storage variable.
28 function update(string memory newMessage) public {
29 string memory oldMsg = message;
30 message = newMessage;
31 emit UpdatedMessages(oldMsg, newMessage);
32 }
33}
显示全部

智能合约事件是你的合约向你的前端应用程序传达区块链上发生的事情 \(即:有一个事件\)的一种方式,你的前端应用程序可以“监听”特定事件,并在事件发生时采取行动。

addSmartContractListener 函数将专门监听我们 Hello World 智能合约的 UpdatedMessages 事件,并更新用户界面来显示新消息。

addSmartContractListener 修改为如下内容:

1// HelloWorld.js
2
3function addSmartContractListener() {
4 helloWorldContract.events.UpdatedMessages({}, (error, data) => {
5 if (error) {
6 setStatus("😥 " + error.message)
7 } else {
8 setMessage(data.returnValues[1])
9 setNewMessage("")
10 setStatus("🎉 Your message has been updated!")
11 }
12 })
13}
显示全部

让我们分析一下当监听器检测到事件时会发生什么:

  • 如果事件触发时出现错误,它将通过我们的 status 状态变量反映在用户界面中。
  • 反之,我们就可以使用返回的 data 对象。 data.returnValues 是一个以 0 为起始索引的数组,其中数组的第一个元素保存的是之前的消息,而第二个元素则保存了更新后的新消息。 总体来说,在成功触发事件的情况下,我们会将 message 字符串设置为更新后的消息,同时清除 newMessage 字符串,并且更新 status 状态变量以表明已在智能合约上发布一条新的消息。

最终,我们应在 useEffect 钩子函数中调用这个监听器,确保它在 HelloWorld.js 组件初次渲染时即被初始化。 综上所述,我们的 useEffect 钩子函数应当如下所示:

1// HelloWorld.js
2
3useEffect(async () => {
4 const message = await loadCurrentMessage()
5 setMessage(message)
6 addSmartContractListener()
7}, [])

现在我们已经掌握了从智能合约读取消息的方法,那么接下来了解如何向智能合约中写入消息那就太棒了! 然而,要向我们的去中心化应用执行写入,我们首先必须有一个连接到该去中心化应用程序的以太坊钱包。

接下来,我们将着手设置以太坊钱包 (MetaMask),然后将其连接到我们的去中心化应用程序!

第四步:设置你的以太坊钱包

为了向以太坊链上写入任何数据,用户必须使用其虚拟钱包的私钥来签署交易。 在本教程中,我们将使用浏览器中的虚拟钱包 MetaMask(opens in a new tab) 来管理你的以太坊帐户地址,因为它让终端用户签署交易变得极其简单。

如果你想了解更多关于以太坊交易的运作方式,请查看以太坊基金会的这个页面

下载 MetaMask

你可以在这里(opens in a new tab)免费下载并创建一个 MetaMask 帐户。 在你创建帐户时,或者如果你已经有帐户,请确保切换到右上角的“Goerli 测试网络”(这样我们就不会使用实际货币进行交易)。

通过水龙头中添加以太币

为了在以太坊区块链上签署交易,我们需要一些虚拟以太币。 要获取以太币,你可以前往 FaucETH(opens in a new tab) 并输入你的 Goerli 帐户地址,单击“Request funds”,然后在下拉菜单中选择“Ethereum Testnet Goerli”,最后再次单击“Request funds”按钮。 你应该会很快在你的 MetaMask 帐户中看到以太币!

查看你的余额

为了核查我们帐户中有余额,我们使用 Alchemy composer 工具(opens in a new tab)发出 eth_getBalance(opens in a new tab) 请求。 这将返回我们钱包中的以太币数量。 输入你的 Metamask 帐户地址并单击“发送请求”后,你应该会看到这样的响应:

1{"jsonrpc": "2.0", "id": 0, "result": "0xde0b6b3a7640000"}

注意:结果以 wei 为单位,而非 ETH。 Wei is used as the smallest denomination of ether. 从 wei 到 eth 的转换是:1 eth = 10¹⁸ wei。 因此,如果我们将 0xde0b6b3a7640000 转换为十进制,我们会得到 1*10¹⁸,它等于 1 eth。

Phew! 我们的虚拟以太币都在那里了! 🤑

第五步:将 MetaMask 连接到你的用户界面

既然我们的 MetaMask 钱包已经设置好了,我们将我们的去中心化应用程序与之连接!

connectWallet函数

在我们的 interact.js 文件中,我们实现了 connectWallet 函数,然后我们可以在 HelloWorld.js 组件中调用它。

让我们将 connectWallet 函数修改为以下形式:

1// interact.js
2
3export const connectWallet = async () => {
4 if (window.ethereum) {
5 try {
6 const addressArray = await window.ethereum.request({
7 method: "eth_requestAccounts",
8 })
9 const obj = {
10 status: "👆🏽 Write a message in the text-field above.",
11 address: addressArray[0],
12 }
13 return obj
14 } catch (err) {
15 return {
16 address: "",
17 status: "😥 " + err.message,
18 }
19 }
20 } else {
21 return {
22 address: "",
23 status: (
24 <span>
25 <p>
26 {" "}
27 🦊 <a target="_blank" href={`https://metamask.io/download.html`}>
28 You must install MetaMask, a virtual Ethereum wallet, in your
29 browser.
30 </a>
31 </p>
32 </span>
33 ),
34 }
35 }
36}
显示全部

那么,这一大段代码究竟是做什么的呢?

首先,该函数会检查你的浏览器是否启用了 window.ethereum

window.ethereum 是一个由 MetaMask 和其他钱包提供商注入的全局应用程序接口,它允许网站请求用户的以太坊帐户。 如果被批准,它可以读取用户连接的区块链上的数据,并建议用户签署消息和交易。 参阅 MetaMask 文档(opens in a new tab)了解更多信息!

如果 window.ethereum 不存在,则表示未安装 MetaMask。 这会导致返回一个 JSON 对象,其中返回的 address 是一个空字符串,而 status JSX 对象指示用户必须安装 MetaMask。

现在如果 window.ethereum 存在,那么事情就会变得有趣。

使用 try/catch 循环,我们将尝试通过调用 window.ethereum.request({ method: "eth_requestAccounts" });(opens in a new tab) 连接到 MetaMask。 调用此函数将在浏览器中打开 MetaMask,提示用户将他们的钱包连接到你的去中心化应用程序。

  • 如果用户选择连接,method: "eth_requestAccounts" 将返回一个数组,其中包含连接到去中心化应用程序的用户的所有帐户地址。 总之,我们的 connectWallet 函数将返回一个 JSON 对象,其中包含此数组中的第一个 address \(见第 9 行\),并返回一条 status 信息,提示用户向智能合约写入信息。
  • 如果用户拒绝连接,则 JSON 对象将包含返回的 address 的空字符串和反映用户拒绝连接的 status 信息。

现在我们已经编写了这个 connectWallet 函数,下一步是调用它到我们的 HelloWorld.js 组件中。

connectWallet 函数添加到你的 HelloWorld.js用户界面组件中

导航到 HelloWorld.js 文件中的 connectWalletPressed 函数,并将其更新为以下内容:

1// HelloWorld.js
2
3const connectWalletPressed = async () => {
4 const walletResponse = await connectWallet()
5 setStatus(walletResponse.status)
6 setWallet(walletResponse.address)
7}

注意我们的大部分函数是如何从 interact.js 文件中的 HelloWorld.js 组件中抽象出来的? 这就是我们遵守 M-V-C 规范的原因!

connectWalletPressed 中,我们只需对导入的 connectWallet 函数进行 await 调用,并使用其响应,通过变量的状态挂钩更新我们的 statuswalletAddress 变量。

现在,让我们保存两个文件 (HelloWorld.jsinteract.js) ,并测试一下我们目前的用户界面。

打开浏览器,在地址栏中输入 localhost:3000(opens in a new tab),然后按页面右上角的“Connect Wallet”按钮。

如果你安装了 MetaMask,系统会提示你将钱包连接到去中心化应用程序。 接受邀请并连接。

你会看到钱包按钮现在反映你的地址已连接! 太棒了 🔥 !

接下来,尝试刷新页面......有点儿奇怪。 我们的钱包按钮提示我们连接 MetaMask,尽管它已经连接......

然而,不要害怕! 我们可以通过实现 getCurrentWalletConnected 轻松解决这个问题(明白了吗?),它将检查一个地址是否已经连接到我们的去中心化应用程序,并相应地更新我们的用户界面!

getCurrentWalletConnected函数

interact.js 文件中的 getCurrentWalletConnected 函数更新如下:

1// interact.js
2
3export const getCurrentWalletConnected = async () => {
4 if (window.ethereum) {
5 try {
6 const addressArray = await window.ethereum.request({
7 method: "eth_accounts",
8 })
9 if (addressArray.length > 0) {
10 return {
11 address: addressArray[0],
12 status: "👆🏽 Write a message in the text-field above.",
13 }
14 } else {
15 return {
16 address: "",
17 status: "🦊 Connect to MetaMask using the top right button.",
18 }
19 }
20 } catch (err) {
21 return {
22 address: "",
23 status: "😥 " + err.message,
24 }
25 }
26 } else {
27 return {
28 address: "",
29 status: (
30 <span>
31 <p>
32 {" "}
33 🦊 <a target="_blank" href={`https://metamask.io/download.html`}>
34 You must install MetaMask, a virtual Ethereum wallet, in your
35 browser.
36 </a>
37 </p>
38 </span>
39 ),
40 }
41 }
42}
显示全部

这段代码与我们刚刚在上一步中编写的 connectWallet 函数 非常相似

主要区别在于,这里我们调用了 eth_accounts 方法,它只是返回一个数组,其中包含当前连接到我们的去中心化应用程序的 MetaMask 地址,而不是调用 eth_requestAccounts 方法来打开 MetaMask 以供用户连接他们的钱包。

为了看看这个函数在实际应用中的效果,让我们在 HelloWorld.js 组件的 useEffect 函数中调用它:

1// HelloWorld.js
2
3useEffect(async () => {
4 const message = await loadCurrentMessage()
5 setMessage(message)
6 addSmartContractListener()
7
8 const { address, status } = await getCurrentWalletConnected()
9 setWallet(address)
10 setStatus(status)
11}, [])
显示全部

请注意,我们使用调用 getCurrentWalletConnected 的响应来更新我们的 walletAddressstatus 状态变量。

现在我们已经添加了这段代码,让我们刷新浏览器窗口,看看最新的效果如何。

很不错! 按钮应显示你已连接,并显示已连接钱包地址的预览 — 即使在你刷新后也是如此!

实现 addWalletListener

我们的去中心化应用程序钱包设置的最后一步是实现钱包监听器,以便我们的用户界面在钱包状态发生变化时更新,例如当用户断开或切换帐户时。

在你的 HelloWorld.js 文件中,按照以下方式修改你的 addWalletListener 函数:

1// HelloWorld.js
2
3function addWalletListener() {
4 if (window.ethereum) {
5 window.ethereum.on("accountsChanged", (accounts) => {
6 if (accounts.length > 0) {
7 setWallet(accounts[0])
8 setStatus("👆🏽 Write a message in the text-field above.")
9 } else {
10 setWallet("")
11 setStatus("🦊 Connect to MetaMask using the top right button.")
12 }
13 })
14 } else {
15 setStatus(
16 <p>
17 {" "}
18 🦊 <a target="_blank" href={`https://metamask.io/download.html`}>
19 You must install MetaMask, a virtual Ethereum wallet, in your browser.
20 </a>
21 </p>
22 )
23 }
24}
显示全部

我敢打赌,到了这一步你可能已经无需我们帮助就能理解这里发生的情况了,但为了确保详尽无遗,我们还是快速梳理一下:

  • 首先,我们的函数检查是否启用了 window.ethereum \(即 MetaMask 已安装\)。
    • 如果未启用,我们只需将 status 状态变量设置为提示用户安装 MetaMask 的 JSX 字符串。
    • 如果启用,我们会在第 3 行设置监听器 window.ethereum.on("accountsChanged") 监听 MetaMask 钱包中的状态变化,变化包括用户将其他帐户连接到去中心化应用程序、切换帐户或断开帐户。 如果至少连接了一个帐户,walletAddress 状态变量将更新为监听器返回的 accounts 数组中的第一个帐户。 否则,walletAddress 设置为空字符串。

最后,但同样重要的一点是,我们必须在 useEffect 函数中调用它:

1// HelloWorld.js
2
3useEffect(async () => {
4 const message = await loadCurrentMessage()
5 setMessage(message)
6 addSmartContractListener()
7
8 const { address, status } = await getCurrentWalletConnected()
9 setWallet(address)
10 setStatus(status)
11
12 addWalletListener()
13}, [])
显示全部

就是这样! 我们已经成功完成了所有钱包功能的编程! 现在,我们来完成最后一个任务:更新智能合约中存储的消息!

第六步:实现 updateMessage函数

好嘞,伙计们,我们已经来到最后阶段了! 在 interact.js 文件中的 updateMessage 函数中,我们将执行以下操作:

  1. 确保我们想要在智能合约中发布的消息有效
  2. 使用 MetaMask 钱包签署每项交易
  3. HelloWorld.js 前端组件调用这个函数

这不会太耗时;我们把这个去中心化应用程序做完!

输入错误处理

显然,我们在函数开头加入一些输入错误处理代码是有意义的做法。

如果未安装 MetaMask 扩展,或者钱包尚未连接 \(即传入的 address 为空字符串\),亦或是 message 为空字符串,我们希望函数能够提前返回。 让我们在 updateMessage 函数中添加以下错误处理代码:

1// interact.js
2
3export const updateMessage = async (address, message) => {
4 if (!window.ethereum || address === null) {
5 return {
6 status:
7 "💡 Connect your MetaMask wallet to update the message on the blockchain.",
8 }
9 }
10
11 if (message.trim() === "") {
12 return {
13 status: "❌ Your message cannot be an empty string.",
14 }
15 }
16}
显示全部

现在,我们已经实现了正确的输入错误处理,接下来就是通过 MetaMask 来签署交易的时候了!

签署交易

如果你已经对传统的 web3 以太坊交易驾轻就熟,那么接下来我们要编写的代码将会非常熟悉。 在输入错误处理代码下方,向 updateMessage 函数添加以下内容:

1// interact.js
2
3//set up transaction parameters
4const transactionParameters = {
5 to: contractAddress, // Required except during contract publications.
6 from: address, // must match user's active address.
7 data: helloWorldContract.methods.update(message).encodeABI(),
8}
9
10//sign the transaction
11try {
12 const txHash = await window.ethereum.request({
13 method: "eth_sendTransaction",
14 params: [transactionParameters],
15 })
16 return {
17 status: (
18 <span>
19{" "}
20 <a target="_blank" href={`https://goerli.etherscan.io/tx/${txHash}`}>
21 View the status of your transaction on Etherscan!
22 </a>
23 <br />
24 ℹ️ Once the transaction is verified by the network, the message will be
25 updated automatically.
26 </span>
27 ),
28 }
29} catch (error) {
30 return {
31 status: "😥 " + error.message,
32 }
33}
显示全部

让我们来详细解析下这些代码的工作原理。 首先,我们设置了交易参数,具体内容如下:

  • to 指定接收者地址\(我们的智能合约\)
  • from 指定交易的签名者,即我们传入函数的 address 变量
  • data 包含对我们的“Hello World”智能合约中 update 方法的调用,其中将 message 字符串变量作为输入

接下来,我们进行对 window.ethereum.request 进行异步调用,请求 MetaMask 对交易进行签名。 请注意,在第 11 和 12 行中,我们指定了以太坊方法 eth_sendTransaction,并传入了我们的 transactionParameters

此时,MetaMask 将在浏览器中打开,并提示用户签署或拒绝交易。

  • 如果交易成功,该函数将返回一个 JSON 对象,其中 status JSX 字符串会提示用户前往 Etherscan 查看更多关于他们交易的信息。
  • 如果交易失败,该函数将返回一个 JSON 对象,其中 status 字符串会传递错误消息。

综上所述,我们的 updateMessage 函数应如下所示:

1// interact.js
2
3export const updateMessage = async (address, message) => {
4 //input error handling
5 if (!window.ethereum || address === null) {
6 return {
7 status:
8 "💡 Connect your MetaMask wallet to update the message on the blockchain.",
9 }
10 }
11
12 if (message.trim() === "") {
13 return {
14 status: "❌ Your message cannot be an empty string.",
15 }
16 }
17
18 //set up transaction parameters
19 const transactionParameters = {
20 to: contractAddress, // Required except during contract publications.
21 from: address, // must match user's active address.
22 data: helloWorldContract.methods.update(message).encodeABI(),
23 }
24
25 //sign the transaction
26 try {
27 const txHash = await window.ethereum.request({
28 method: "eth_sendTransaction",
29 params: [transactionParameters],
30 })
31 return {
32 status: (
33 <span>
34{" "}
35 <a target="_blank" href={`https://goerli.etherscan.io/tx/${txHash}`}>
36 View the status of your transaction on Etherscan!
37 </a>
38 <br />
39 ℹ️ Once the transaction is verified by the network, the message will
40 be updated automatically.
41 </span>
42 ),
43 }
44 } catch (error) {
45 return {
46 status: "😥 " + error.message,
47 }
48 }
49}
显示全部

最后但同样重要的是,我们需要将 updateMessage 函数与我们的 HelloWorld.js 组件进行连接。

updateMessage 函数连接到 HelloWorld.js前端

我们的 onUpdatePressed 函数应当通过异步调用导入的 updateMessage 函数,并根据交易成功或失败的结果来修改 status 状态变量:

1// HelloWorld.js
2
3const onUpdatePressed = async () => {
4 const { status } = await updateMessage(walletAddress, newMessage)
5 setStatus(status)
6}

这个实现非常干净且简单。 你猜怎么着... 你的去中心化应用程序终于完工了!

现在就去测试一下 Update 按钮吧!

开发你的去中心化应用程序

哇哦,你成功完成了本教程的全部内容! 回顾一下,你已经学习了如何:

  • 使用 MetaMask 钱包连接你的去中心化应用程序项目
  • 使用 Alchemy Web3(opens in a new tab) 应用程序接口从你的智能合约中读取数据
  • 使用 MetaMask 对以太坊交易签名

现在,你已经完全掌握本教程中的技能,可以着手开发属于自己的个性化去中心化应用程序项目了! 一如既往,如果你有任何问题,欢迎随时在 Alchemy Discord(opens in a new tab) 频道联系我们寻求帮助。 🧙‍♂️

完成本教程后,请在 Twitter 上标记我们 @alchemyplatform(opens in a new tab),告诉我们你的体验如何,或者你是否有任何反馈!

上次修改时间: @wackerow(opens in a new tab), 2024年5月7日

本教程对你有帮助吗?