将智能合约与前端集成
Web

将智能合约与前端集成

Flying
2023-11-09 / 0 评论 / 81 阅读 / 正在检测是否收录...

本教程我们将教你如何通过使用 Metamask 和 Web3 工具将你的 Hello World 智能合约连接到一个 React 前端,从而创建一个全栈的 dApp。

你需要在开始下面的第 4 部分之前完成第 1 部分:使用 Hardhat 创建部署智能合约、第 2 部分:与智能合约交互 和第 3 部分:向 Etherscan 提交智能合约

🎉哇!你终于来到了这个教程系列的最后部分:通过将你的 Hello World 智能合约与前端项目连接并与之交互,创建一个全栈去中心化应用程序(dApp)。

在本教程结束时,你将学会如何:

  • 将 Metamask 钱包连接到你的 dApp 项目
  • 使用 Alchemy Web3 API 从你的智能合约中读取数据
  • 使用 Metamask 签署 Ethereum 交易

在这个 dApp 中,我们将使用 React 作为我们的前端框架;然而,重要的是要注意,我们不会花太多时间来解释它的基础知识,因为我们将主要专注于为我们的项目引入 Web3 功能。

React 理解
作为先决条件,你应该对 React 有初级了解——了解组件、props、useState/useEffect 以及基本的函数调用原理。如果你之前从未听说过这些术语,我们建议你查看这个React 入门教程。对于更喜欢视觉学习的人,我们强烈推荐这个出色的 Net Ninja 现代 React 完整教程视频系列。

我们在这里做什么呢?让我们开始吧!😎

步骤 1:克隆起始文件

首先,访问hello-world-part-four github 仓库获取这个项目的起始文件。将这个仓库克隆到你的本地环境中。

仓库克隆指南
不知道如何克隆一个仓库吗?查看 GitHub 的指南

当你打开这个克隆的 hello-world-part-four 仓库时,你会注意到它包含两个文件夹:starter-filescompleted

  • starter-files 包含这个项目的起始文件(基本上是 React UI)。在这个教程中,我们将在这个目录中工作,你将学习如何通过连接到你的以太坊钱包和你在Etherscan Part 3中发布的 Hello World 智能合约,使这个 UI 变得有活力。
  • completed 包含整个完成的教程,如果你遇到问题,可以作为参考

接下来,打开你的 starter-files 拷贝到你喜欢的代码编辑器中(在 Alchemy,我们非常喜欢VSCode),然后进入你的 src 文件夹:

src-folder.png
代码库中的 src 文件夹

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

步骤 2:查看起始文件

在我们开始编码之前,弄清楚起始文件中已经为我们提供了什么非常重要。

让你的 React 项目运行起来

让我们从在浏览器中运行 React 项目开始。React 的美妙之处在于,一旦我们在浏览器中运行了项目,我们保存的任何更改都将在浏览器中实时更新。要运行该项目,请导航到 starter-files 文件夹的根目录

cd starter-files

然后复制以下 package.json 文件并替换掉 starter-files 目录中的文件。这是因为前一个 package.json 文件中的大多数依赖项已被弃用。下面的文件是所有依赖项的适当更新。

然后在终端中运行 npm install 以安装项目的依赖项:

npm install

接下来,将一些新安装的依赖项包含在你的 webpack.config.js 文件中。找到你的 node_modules/react-scripts 目录下的文件,即 webpack.config.js。将以下内容添加到文件中的 resolve 对象中,如下所示:

fallback: { "http": require.resolve("stream-http"),
  "https": require.resolve("https-browserify"), "zlib": require.resolve("browserify-zlib")  },

下面的图像也描述了这一点:

fallback.png

一旦模块安装完成并且你已经更新了 webpack.config.js 文件,运行终端中的 npm start

npm start

这样应该会在你的浏览器中打开http://localhost:3000/,你将看到我们项目的前端。它应该包含一个字段(用于更新存储在你的智能合约中的消息的地方),一个 “Connect Wallet” 按钮和一个"Update"按钮。

ui.png
UI 应该是什么样子的

如果尝试点击 “Connect Wallet” 或 “Update” 按钮,你会发现它们不起作用,因为我们仍然需要编写它们的功能!

HelloWorld.js 组件

❗️注意 :
确保你在 starter-files 文件夹中而不是 completed 文件夹中!

让我们回到我们的编辑器中的 src 文件夹,并打开 HelloWorld.js 文件。我们必须了解这个文件中的所有内容,因为它是我们将要工作的主要 React 组件。

在这个文件的顶部,你会注意到我们有几个 import 语句,这些语句是必需的,以便让我们的项目运行起来,包括 React 库、useEffect 和 useState hooks,还有一些来自 ./util/interact.js(我们稍后会更详细地描述它们!),以及 Alchemy 的标志。🧙‍♂️

import React from "react";
import { useEffect, useState } from "react";
import {
  helloWorldContract,
  connectWallet,
  updateMessage,
  loadCurrentMessage,
  getCurrentWalletConnected,
} from "./util/interact.js";

import alchemylogo from "./alchemylogo.svg";

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

// State variables
const [walletAddress, setWallet] = useState("");
const [status, setStatus] = useState("");
const [message, setMessage] = useState("No connection to the network.");
const [newMessage, setNewMessage] = useState("");
React Hooks
从未听说过 React 状态变量或状态钩子?查看这些文档。

下面是每个变量代表的内容:

  • walletAddress - 一个字符串,用于存储用户的钱包地址
  • status - 一个字符串,存储一个有用的消息,指导用户如何与 dApp 交互
  • message - 一个字符串,存储智能合约中当前的消息
  • newMessage - 一个字符串,存储将要写入智能合约的新消息

在状态变量之后,你会看到五个未实现的函数:_useEffect_,_addSmartContractListener_,_addWalletListener_,_connectWalletPressed_ 和 _onUpdatePressed_。我们将解释它们分别是做什么的:

// called only once
useEffect(() => { //TODO: implement

}, []);

function addSmartContractListener() { //TODO: implement

}

function addWalletListener() { //TODO: implement

}

const connectWalletPressed = async () => { //TODO: implement

};

const onUpdatePressed = async () => { //TODO: implement

};
  • useEffect- 这是一个 React hook,它在你的组件被呈现后调用。因为它有一个空数组[]的 prop 传递给它(见第 4 行),它只会在组件的第一次渲染时被调用。在这里,我们将加载我们智能合约中存储的当前消息,调用我们的智能合约和钱包监听器,并更新我们的 UI 以反映钱包是否已经连接。
  • addSmartContractListener- 这个函数设置一个监听器,将监听我们 HelloWorld 合约的 UpdatedMessages 事件,并在我们的智能合约中的消息发生变化时更新我们的 UI。
  • addWalletListener- 这个函数设置一个监听器,检测用户的 Metamask 钱包状态的变化,比如用户断开钱包连接或切换地址。
  • connectWalletPressed- 这个函数将被调用以连接用户的 Metamask 钱包到我们的 dApp。
  • onUpdatePressed - 当用户想要更新存储在智能合约中的消息时,将调用这个函数。

在这个文件的末尾附近,我们有我们组件的 UI。

//the UI of our component
return (
  <div id="container">
    <img id="logo" src={alchemylogo}></img>
    <button id="walletButton" onClick={connectWalletPressed}>
      {walletAddress.length > 0 ? (
        "Connected: " +
        String(walletAddress).substring(0, 6) +
        "..." +
        String(walletAddress).substring(38)
      ) : (
        <span>Connect Wallet</span>
      )}
    </button>

    <h2 style={{ paddingTop: "50px" }}>Current Message:</h2>
    <p>{message}</p>

    <h2 style={{ paddingTop: "18px" }}>New Message:</h2>

    <div>
      <input
        type="text"
        placeholder="Update the message in your smart contract."
        onChange={(e) => setNewMessage(e.target.value)}
        value={newMessage}
      />
      <p id="status">{status}</p>

      <button id="publishButton" onClick={onUpdatePressed}>
        Update
      </button>
    </div>
  </div>
);

如果你仔细检查这段代码,你会注意到我们在 UI 中如何使用各种状态变量:

  • 在 6-12 行,如果用户的钱包已连接 (即,walletAddress.length > 0),我们在 ID 为 “walletButton” 的按钮中显示用户 walletAddress 的缩写版本;否则,它只是显示 “Connect Wallet”
  • 在第 17 行,我们显示存储在智能合约中的当前消息,这个消息被保存在 message 字符串中。
  • 在 23-26 行,我们使用受控组件来在文本字段中的输入发生变化时更新我们的 newMessage 状态变量。

除了我们的状态变量之外,你还会看到当具有 ID 为 publishButtonwalletButton 的按钮被点击时,connectWalletPressedonUpdatePressed 函数被调用。

最后,让我们弄清楚 HelloWorld.js 组件是在哪里添加的。

如果你打开 App.js 文件,它是 React 中的主要组件,充当所有其他组件的容器,你会看到我们的 HelloWorld.js 组件在第 7 行被注入。

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

interact.js 文件

因为我们想要遵循M-V-C(模型 - 视图 - 控制器)范例,我们希望有一个单独的文件,其中包含所有管理我们 dApp 的逻辑、数据和规则的函数,然后能够将这些函数导出到我们的前端(我们的 HelloWorld.js 组件)。

👆🏽这正是我们的 interact.js 文件的目的!

在你的 src 目录下的 util 文件夹中导航,你会注意到我们包含了一个名为 interact.js 的文件,其中包含所有我们智能合约交互和钱包功能和变量的函数。

// interact.js
//export const helloWorldContract;

export const loadCurrentMessage = async () => {

};

export const connectWallet = async () => {

};

const getCurrentWalletConnected = async () => { 

};


export const updateMessage = async (message) => {

};

在文件的顶部,你会注意到我们已经将 helloWorldContract 对象注释掉了。在本教程的后面部分,我们将取消注释这个对象,并在这个变量中实例化我们的智能合约,然后将它导出到我们的 HelloWorld.js 组件中。

我们的 helloWorldContract 对象后面的四个未实现的函数分别执行以下操作:

函数函数描述
loadCurrentMessage这个函数处理加载存储在智能合约中的当前消息的逻辑。它将使用 Alchemy Web3 API 发出对 Hello World 智能合约的读取调用。
connectWallet这个函数将连接用户的 Metamask 到我们的 dApp。
getCurrentWalletConnected这个函数将检查页面加载时是否已经连接了以太坊账户到我们的 dApp,并相应地更新我们的 UI。
updateMessage这个函数将更新存储在智能合约中的消息。它将对 Hello World 智能合约发出写入调用,

现在我们了解了我们要处理的内容,让我们看看如何从我们的智能合约中读取数据!

步骤 3:从你的智能合约中读取数据

要从你的智能合约中读取数据,你需要成功地设置:

  • 与以太坊链的 API 连接。
  • 你的智能合约的已加载实例。
  • 调用你的智能合约函数的功能。
  • 一个监听器,在数据从智能合约中读取时进行更新。

这听起来可能是很多步骤,但不用担心!我们将逐步指导你如何完成每一步!

在以太坊链上建立 API 连接

还记得在这个教程的第二部分中,我们使用了Alchemy Web3 密钥从智能合约中读取数据 吗?你还需要在 dApp 中使用 Alchemy Web3 密钥来从链上读取数据。

如果你还没有,首先在你的终端中导航到你的 starter-files 根目录,并运行以下命令来安装 Alchemy Web3

const alchemyKey = "wss://eth-sepolia.g.alchemy.com/v2/<YOUR-API-KEY>"
const { createAlchemyWeb3 } = require("@alch/alchemy-web3");
const web3 = createAlchemyWeb3(alchemyKey); 

//export const helloWorldContract;
Web3
Alchemy Web3 是 Web3.js 的封装,提供增强的 API 方法和其他关键优势,使得你作为 web3 开发人员的生活更轻松。它设计得需要很少的配置,所以你可以立即在你的应用中使用它!

对于我们的 dApp,我们将使用我们的 Websockets API 密钥,而不是我们的 HTTP API 密钥,因为它将允许我们设置一个监听器,当存储在智能合约中的消息发生变化时检测到。

connect.png

像上面显示的那样,复制 WebSockets 密钥,并将其保管在一个安全的地方,因为你将在你的 interact.js 文件中使用它作为 alchemyKey 的值。

现在,我们已经准备好在我们的 dApp 中设置 Alchemy Web3 端点了!让我们回到我们的 interact.js,它位于 util 文件夹内,然后在文件的顶部添加以下代码:

在上面的代码中,我们包含了 Alchemy WebSockets 密钥的值,然后将我们的 alchemyKey 传递给 createAlchemyWeb3 以建立我们的 Alchemy Web3 端点。

有了这个端点准备好,现在是时候加载我们的智能合约了!

加载你的 Hello World 智能合约

要加载你的 Hello World 智能合约,你需要它的合约地址和 ABI,这两者都可以在 Etherscan 上找到,如果你完成了本教程的第三部分 的话。

👍合约 ABI
如果你跳过了本教程的第三部分,你可以使用合约地址 0x6f3f635A9762B47954229Ea479b4541eAF402A6A 的 HelloWorld 合约。

合约 ABI 是指定合约将调用哪个函数并确保函数将以你期望的格式返回数据的必要条件。一旦我们复制了我们的合约 ABI,让我们将其保存为一个名为 contract-abi.json 的 JSON 文件,放在你的 src 目录中。

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

拥有了我们的合约地址、ABI 和 Alchemy Web3 端点,我们可以使用合约方法来加载我们的智能合约的实例。将你的合约 ABI 导入到 interact.js 文件中,并添加你的合约地址。

const contractABI = require("../contract-abi.json");
const alchemyKey = "wss://eth-sepolia.g.alchemy.com/v2/<YOUR-API-KEY>"
const contractAddress = <Your-Contract-Address>;

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

export const helloWorldContract = new web3.eth.Contract(
  contractABI,
  contractAddress
);

回顾一下,你的 interact.js 的前 12 行现在应该像这样:

const alchemyKey = "wss://eth-sepolia.g.alchemy.com/v2/<YOUR-API-KEY>"
const { createAlchemyWeb3 } = require("@alch/alchemy-web3");
const web3 = createAlchemyWeb3(alchemyKey); 
const contractABI = require('../contract-abi.json')
const contractAddress = <Your-Contract-Address>;

export const helloWorldContract = new web3.eth.Contract(
  contractABI,
  contractAddress
);

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

在你的 interact.js 文件中实现 loadCurrentMessage

这个函数非常简单。就像我们在本教程系列的第二部分中所做的那样,我们将进行一个简单的异步 web3 调用,从我们的合约中读取数据。我们的函数将返回存储在智能合约中的消息:

将你的 interact.js 文件中的 loadCurrentMessage 更新为以下内容:

export const loadCurrentMessage = async () => { 
  const message = await helloWorldContract.methods.message().call(); 
  return message;
};

由于我们想在我们的 UI 中显示这个智能合约,让我们更新我们的 HelloWorld.js 组件中的 useEffect 函数为以下内容:

//called only once
useEffect(() => {
  async function fetchMessage() {
    const message = await loadCurrentMessage();
    setMessage(message);
  }
  fetchMessage();
}, []);

请注意,我们只希望在组件的第一次渲染期间调用我们的 _loadCurrentMessage_。我们将很快实现 addSmartContractListener,以在智能合约中的消息更改后自动更新 UI。

在我们深入研究监听器之前,让我们看看我们到目前为止做了什么!保存你的 HelloWorld.jsinteract.js 文件,然后转到 http://localhost:3000/

你会注意到当前的消息不再显示 “没有与网络的连接。” 而是反映了存储在智能合约中的消息。太棒了!

ui-update.png
你的 UI 现在应该反映了存储在智能合约中的消息。

现在,说到监听器 ...

实现 addSmartContractListener

如果你回想一下我们在本教程系列的第一部分中编写的 HelloWorld.sol 文件,你会记得有一个名为 UpdatedMessages 的智能合约事件,该事件在我们的智能合约的更新函数被调用后被触发(参见第 9 行和第 27 行):

// SPDX-License-Identifier: UNLICENSED
// Specifies the version of Solidity, using semantic versioning.
// Learn more: https://solidity.readthedocs.io/en/v0.5.10/layout-of-source-files.html#pragma
pragma solidity ^0.7.3;

// Defines a contract named `HelloWorld`.
// A contract is a collection of functions and data (its state). 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
contract HelloWorld {

  //Emitted when update function is called
  //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.
  event UpdatedMessages(string oldStr, string newStr);

  // Declares a state variable `message` of type `string`.
  // State variables are variables whose values are permanently stored in contract storage. 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.
  string public message;

  // Similar to many class-based object-oriented languages, a constructor is a special function that is only executed upon contract creation.
  // Constructors are used to initialize the contract's data. Learn more:https://solidity.readthedocs.io/en/v0.5.10/contracts.html#constructors
  constructor(string memory initMessage) {

    // Accepts a string argument `initMessage` and sets the value into the contract's `message` storage variable).
    message = initMessage;
  }

  // A public function that accepts a string argument and updates the `message` storage variable.
  function update(string memory newMessage) public {
    string memory oldMsg = message;
    message = newMessage;
    emit UpdatedMessages(oldMsg, newMessage);
  }
}
智能合约事件
智能合约事件是你的合约与前端应用程序之间通信的一种方式,它可以通知前端应用程序在区块链上发生了什么(即发生了事件)。

addSmartContractListener 函数将专门监听我们的 Hello World 智能合约的 UpdatedMessages 事件,并更新我们的 UI 以显示新消息。将 addSmartContractListener 修改为以下内容:

function addSmartContractListener() {
  helloWorldContract.events.UpdatedMessages({}, (error, data) => {
    if (error) {
      setStatus("😥 " + error.message);
    } else {
      setMessage(data.returnValues[1]);
      setNewMessage("");
      setStatus("🎉 Your message has been updated!");
    }
  });
}

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

  • 如果在事件被触发时发生错误,它将通过我们的状态变量 status 在 UI 中反映出来。
  • 否则,我们将使用返回的 data 对象。_data.returnValues_ 是一个数组,索引为 0 的元素存储先前的消息,索引为 1 的元素存储更新后的消息。在成功的事件中,我们将把我们的 message 字符串设置为更新后的消息,清除 newMessage 字符串,并更新我们的 status 状态变量,以反映在我们的智能合约中发布了新消息。

最后,在我们的 useEffect 函数中调用我们的监听器,以在 HelloWorld.js 组件的第一次渲染时初始化它。最终,你的 useEffect 函数应该像这样:

useEffect(() => {
  async function fetchMessage() {
    const message = await loadCurrentMessage();
    setMessage(message);  
  }
  fetchMessage();
  addSmartContractListener();
}, []);

现在我们可以从我们的智能合约中读取数据了,现在是时候弄清楚如何向其中写入数据了!但是,要向我们的 dApp 中写入数据,我们必须先有一个连接到它的以太坊钱包。

所以下一步是设置我们的以太坊钱包(Metamask)然后将它连接到我们的 dApp!

步骤 4:设置你的以太坊钱包

要在以太坊链上写任何东西,用户必须使用他们虚拟钱包的私钥签署交易。在本教程中,我们将使用 Metamask,这是一个在浏览器中管理你的以太坊账户地址的虚拟钱包,因为它使得用户签署交易非常容易。

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

下载 Metamask

你可以免费下载并创建一个 Metamask 账户在这里。在创建一个账户时,或者如果你已经有一个账户了,请切换到右上角的“Sepolia Test Network”(这样我们就不会使用真正的钱了)。

metamask
Metamask 仪表板

从水龙头获取 ether

要在以太坊区块链上签署交易,我们将需要一些假的 ETH。为了获得 ETH,你可以前往 Sepolia 水龙头,输入你的钱包地址,然后点击 “Send Me ETH”。你应该很快就会在你的 Metamask 账户中看到 ETH!

为了将我们的智能合约部署到测试网络,我们将需要一些假的 ETH。要获取 ETH,你可以前往Sepolia Faucet,输入你的 Metamask 地址,然后点击“Send Me Eth”。你应该很快就会在你的 Metamask 账户中看到 ETH!

检查你的余额

为了确保我们的余额在那里,让我们使用 Alchemy的创作工具 发送一个 eth_getBalance 请求。这将返回我们钱包中的 ETH 金额。在输入你的 Metamask 账户地址并点击“Send Request”后,你应该会看到类似下面的响应:

{"jsonrpc": "2.0", "id": 0, "result": "0xde0b6b3a7640000"}
注意:
这个结果是以 Wei 为单位的,而不是 ETH。Wei 被用作以太的最小单位。从 Wei 到 ETH 的转换是 1 ETH = 10¹⁸ Wei。所以如果我们将 0xde0b6b3a7640000 转换成十进制,我们得到 1*10¹⁸,这相当于 1 ETH。

哇!我们的虚拟货币都在那里了!🤑

步骤 5:将 Metamask 连接到你的 UI

既然我们的 Metamask 钱包已经设置好了,让我们将它连接到我们的 dApp!

connectWallet 函数

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

让我们将 connectWallet 修改为以下内容:

export const connectWallet = async () => {
  if (window.ethereum) {
    try {
      const addressArray = await window.ethereum.request({
        method: "eth_requestAccounts",
      });
      const obj = {
        status: "👆🏽 Write a message in the text-field above.",
        address: addressArray[0],
      };
      return obj;
    } catch (err) {
      return {
        address: "",
        status: "😥 " + err.message,
      };
    }
  } else {
    return {
      address: "",
      status: (
        <span>
          <p>
            {" "}
            🦊{" "}
            <a target="_blank" href={`https://metamask.io/download`}>
              You must install Metamask, a virtual Ethereum wallet, in your
              browser.
            </a>
          </p>
        </span>
      ),
    };
  }
};

那么,这个庞大的代码块到底做了什么呢?

首先,它检查你的浏览器中是否启用了 _window.Ethereum_。

什么是 window.Ethereum
window.Ethereum 是由 Metamask 和其他钱包提供商注入的全局 API,允许网站请求用户的以太坊账户。如果获得授权,它可以从用户连接的区块链中读取数据,并建议用户签署消息和交易。查看 Metamask 文档 以获取更多信息!

如果 window.Ethereum 不存在,那意味着 Metamask 没有安装。这会返回一个 JSON 对象,其中的地址为空字符串,并且 status JSX 对象提示用户安装 Metamask。

现在,如果 window.Ethereum 存在,那就有点有趣了。

使用一个 try/catch 循环,我们将尝试通过调用 window.ethereum.request({ method: "eth_requestAccounts" }); 连接到 Metamask。调用这个函数将在浏览器中打开 Metamask,用户将被提示将他们的钱包连接到你的 dApp。

  • 如果用户选择连接,_method: "eth_requestAccounts"_ 将返回一个包含所有用户账户地址的数组,这些地址连接到了 dApp。最终,我们的 connectWallet 函数将返回一个包含这个数组中的第一个地址(见第 9 行)的 JSON 对象,以及一个提示用户向智能合约写入消息的 status 消息。
  • 如果用户拒绝连接,JSON 对象将包含一个空字符串的 address_,并且 _status 消息反映用户拒绝了连接。

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

在HelloWorld.js UI 组件中添加 connectWallet 函数

HelloWorld.js 文件中,找到 connectWalletPressed 函数,并将它更新为以下内容:

const connectWalletPressed = async () => {
  const walletResponse = await connectWallet();
  setStatus(walletResponse.status);
  setWallet(walletResponse.address);
};

注意我们的大部分功能现在都被抽象到了我们的 HelloWorld.js 组件中,而不是 interact.js 文件中。这样做是为了遵循 M-V-C(模型 - 视图 - 控制器)范式!

connectWalletPressed 中,我们调用导入的 connectWallet 函数并使用它的响应来通过它们的状态 hook 更新我们的 statuswalletAddress 变量。

现在,让我们保存这两个文件(HelloWorld.jsinteract.js),然后测试我们的 UI。

在浏览器中打开 http://localhost:3000/ 页面,然后点击页面右上角的 “Connect Wallet” 按钮。

如果你已经安装了 Metamask,你应该会看到提示将你的钱包连接到你的 dApp。接受邀请进行连接。

你应该看到钱包按钮现在显示你的地址已连接!太棒了 🔥

接着,试着刷新页面... 这有点奇怪。即使你已经连接了钱包,我们的钱包按钮还是提示我们连接 Metamask...

ui-connected.gif

不过,不用担心!我们可以通过实现 getCurrentWalletConnected 来解决这个问题,这个函数会检查地址是否已经连接到我们的 dApp,并相应地更新我们的 UI!

getCurrentWalletConnected 函数

interact.js 文件中,将你的 getCurrentWalletConnected 函数更新为以下内容:

export const getCurrentWalletConnected = async () => {
  if (window.ethereum) {
    try {
      const addressArray = await window.ethereum.request({
        method: "eth_accounts",
      });
      if (addressArray.length > 0) {
        return {
          address: addressArray[0],
          status: "👆🏽 Write a message in the text-field above.",
        };
      } else {
        return {
          address: "",
          status: "🦊 Connect to Metamask using the top right button.",
        };
      }
    } catch (err) {
      return {
        address: "",
        status: "😥 " + err.message,
      };
    }
  } else {
    return {
      address: "",
      status: (
        <span>
          <p>
            {" "}
            🦊{" "}
            <a target="_blank" href={`https://metamask.io/download`}>
              You must install Metamask, a virtual Ethereum wallet, in your
              browser.
            </a>
          </p>
        </span>
      ),
    };
  }
};

这段代码与我们刚刚编写的 connectWallet 函数非常相似。

主要的区别在于,这次我们不是调用 eth_requestAccounts 方法,它会打开 Metamask,让用户连接他们的钱包,而是调用 eth_accounts 方法,它会返回当前连接到我们的 dApp 的 Metamask 地址的数组。

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

useEffect(() => {
  async function fetchMessage() {
    const message = await loadCurrentMessage();
    setMessage(message);
  }
  fetchMessage();
  addSmartContractListener();

  async function fetchWallet() {
    const {address, status} = await getCurrentWalletConnected();
    setWallet(address);
    setStatus(status); 
  }
  fetchWallet();
}, []);

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

现在你已经添加了这段代码,让我们尝试刷新浏览器窗口。

refreshing.gif

太好了!按钮应该显示你已经连接,并显示你连接的钱包地址的预览 - 即使在刷新后也是如此!

实现 addWalletListener

我们的 dApp 钱包设置的最后一步是实现钱包监听器,这样当钱包状态发生变化时,比如用户断开连接或切换账户时,我们的 UI 就会更新。

在你的 HelloWorld.js 文件中,将你的 addWalletListener 函数修改为以下内容:

function addWalletListener() {
  if (window.ethereum) {
    window.ethereum.on("accountsChanged", (accounts) => {
      if (accounts.length > 0) {
        setWallet(accounts[0]);
        setStatus("👆🏽 Write a message in the text-field above.");
      } else {
        setWallet("");
        setStatus("🦊 Connect to Metamask using the top right button.");
      }
    });
  } else {
    setStatus(
      <p>
        {" "}
        🦊{" "}
        <a target="_blank" href={`https://metamask.io/download`}>
          You must install Metamask, a virtual Ethereum wallet, in your
          browser.
        </a>
      </p>
    );
  }
}

我敢打赌你现在甚至不需要我们的帮助就能理解这里发生了什么 😉,但是为了完整起见,让我们快速分解一下:

  • 首先,我们的函数检查 window.Ethereum 是否启用(即 Metamask 是否安装)。
  • 如果没有启用,我们将我们的 status 状态变量设置为一个 JSX 字符串,提示用户安装 Metamask。
  • 如果启用了,我们设置监听器 window.ethereum.on("accountsChanged")_(第 3 行),用于监听 Metamask 钱包的状态变化,比如用户连接了额外的账户到 dApp、切换账户或断开了账户。如果至少连接了一个账户,_walletAddress 状态变量会被更新为监听器返回的账户数组中的第一个账户。否则,_walletAddress_ 将被设置为空字符串。

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

useEffect(() => {
  async function fetchMessage() {
    const message = await loadCurrentMessage();
    setMessage(message);
  }
  fetchMessage();
  addSmartContractListener();

  async function fetchWallet() {
    const {address, status} = await getCurrentWalletConnected();
    setWallet(address)
    setStatus(status); 
  }
  fetchWallet();
  addWalletListener(); 
}, []);

这就是全部!我们成功地完成了所有的钱包功能编程!现在,我们进行最后的任务:更新我们的智能合约中存储的消息!

步骤 6:实现 updateMessage 函数

🏃🏽‍♀️好了,亲爱的朋友们,我们终于来到了最后的阶段!在你的 interact.js 文件中的 updateMessage 函数中,我们将做以下事情:

  • 确保我们希望在智能合约中发布的消息是有效的
  • 使用 Metamask 签署我们的交易
  • 从我们的 HelloWorld.js 前端组件中调用这个函数

这不会花费太长时间,让我们完成这个 dApp!

输入错误处理

当然,在函数的开始部分进行一些输入错误处理是有意义的。

如果没有安装 Metamask 扩展、没有连接钱包(即传入的 address 是一个空字符串)或者 message 是一个空字符串,我们希望函数能够尽早返回。让我们将以下错误处理添加到 updateMessage 中:

export const updateMessage = async (address, message) => {
  if (!window.ethereum || address === null) {
    return {
      status:
        "💡 Connect your Metamask wallet to update the message on the blockchain.",
    };
  }

  if (message.trim() === "") {
    return {
      status: "❌ Your message cannot be an empty string.",
    };
  }
};

现在它有了适当的输入错误处理,现在是时候通过 Metamask 签署交易了!

签署我们的交易

如果你已经熟悉传统的 Web3 以太坊交易,我们接下来的代码将会非常熟悉。在你的输入错误处理代码下面,将以下代码添加到 _updateMessage_:

// 设置交易参数
 const transactionParameters = {
  to: contractAddress, // 除非在合约发布时需要
  from: address, // 必须匹配用户当前的地址
  data: helloWorldContract.methods.update(message).encodeABI(),
};

// 签署交易
try {
  const txHash = await window.ethereum.request({
    method: "eth_sendTransaction",
    params: [transactionParameters],
  });
  return {
    status: (
      <span>
        ✅{" "}
        <a target="_blank" href={`https://sepolia.etherscan.io/tx/${txHash}`}>
          View the status of your transaction on Etherscan!
        </a>
        <br />
        ℹ️ Once the transaction is verified by the network, the message will
        be updated automatically.
      </span>
    ),
  };
} catch (error) {
  return {
    status: "😥 " + error.message,
  };
}

让我们分解一下发生了什么。首先,我们设置了我们的 transactions 参数,其中:

  • to 指定了接收地址(我们的智能合约)
  • from 指定了交易的签名者,也就是我们传入函数的地址变量
  • data 包含对我们的 Hello World 智能合约的更新方法的调用,将我们的消息字符串变量作为输入。

然后,我们进行了一个等待调用,_window.Ethereum.request_,我们要求 Metamask 签署交易。请注意,第 11 和第 12 行指定了我们的 eth 方法,_eth_sendTransaction_ 并传入我们的 _transactionParameters_。

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

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

最终,我们的 updateMessage 函数应该如下所示:

export const updateMessage = async (address, message) => {

  // 输入错误处理
  if (!window.ethereum || address === null) {
    return {
      status:
        "💡 Connect your Metamask wallet to update the message on the blockchain.",
    };
  }

  if (message.trim() === "") {
    return {
      status: "❌ Your message cannot be an empty string.",
    };
  }

  // 设置交易参数
  const transactionParameters = {
    to: contractAddress, // 除非在合约发布时需要
    from: address, // 必须匹配用户当前的地址
    data: helloWorldContract.methods.update(message).encodeABI(),
  };

  // 签署交易
  try {
    const txHash = await window.ethereum.request({
      method: "eth_sendTransaction",
      params: [transactionParameters],
    });
    return {
      status: (
        <span>
          ✅{" "}
          <a target="_blank" href={`https://sepolia.etherscan.io/tx/${txHash}`}>
            View the status of your transaction on Etherscan!
          </a>
          <br />
          ℹ️ Once the transaction is verified by the network, the message will
          be updated automatically.
        </span>
      ),
    };
  } catch (error) {
    return {
      status: "😥 " + error.message,
    };
  }
};

最后但同样重要的是,我们需要将它连接到我们的 HelloWorld.js 组件中。

updateMessage 连接到前端的 HelloWorld.js

我们的 onUpdatePressed 函数应该调用导入的 updateMessage 函数,并通过状态 hook 修改 status 状态变量,以反映我们的交易是成功还是失败:

const onUpdatePressed = async () => {
  const { status } = await updateMessage(walletAddress, newMessage);
  setStatus(status);
};

这非常简洁和简单。😌 而且猜猜... 你的 DApp 完成了!!!让我们来测试一下“Update”按钮!

finished.gif

步骤 7:制作你自己的自定义 dApp 🚀

哇哦,你终于完成了本教程!回顾一下,你学会了如何:

  • 连接 Metamask 钱包到你的 dApp 项目
  • 使用 Alchemy Web3 API 从你的智能合约中读取数据
  • 使用 Metamask 签署以太坊交易

现在,你已经完全具备了将这个教程中的技能应用到构建自己自定义 dApp 项目中的能力!如果你有任何问题,别犹豫,随时在 Alchemy Discord 中联系我们寻求帮助。

2

评论 (0)

取消