Web3开发学习笔记7----将合约集成到自己的前端应用【7/7】

707 阅读10分钟

“携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第7天,点击查看活动详情”

现现在是时候集成到自己的前端应用了。就以前面做好的helloworld 合约为例。

我们先选用一个前端框架,这里就用React.js。React 就不具体展开了,掘金上有太多优秀的教程和笔记了。

一共分12步完整的拆解。

1、创建一个前端工程,检查

npm install -g create-react-app

create-react-app helloworld

然后添加两个文件

然后检查启动环境OK。

npm start

2、 创建、HelloWorld.js 

接着我们将要处理一个要 React 组件。

都在 HelloWorld.js里

我们有几个导入语句是让我们的项目运行所必需的,包括 React 库、useEffect 和 useState hooks。(./util/interact.js)

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

import alchemylogo from "./img/logo.svg";

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

//先声明几个状态变量
const [walletAddress, setWallet] = useState("");
const [status, setStatus] = useState("");
const [message, setMessage] = useState("No connection to the network.");
const [newMessage, setNewMessage] = useState("");

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

walletAddress- 存储用户钱包地址的字符串

status- 存储有用消息的字符串,指导用户如何与 dApp 交互

message- 在智能合约中存储当前消息的字符串

new message- 存储将写入智能合约的新消息的字符串

在状态变量之后,您会看到五个未实现的函数:useEffect、addSmartContractListener、

connectWalletPressed和``onUpdatePressed

这些下面都会一一解释:

//仅调用一次
useEffect(async () => { //TODO: implement

}, []);

function addSmartContractListener() { //TODO: implement

}

function addWalletListener() { //TODO: implement

}

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

};

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

};

3、创建utils/interact.js (工具类)

我们需要一个单独的文件,其中包含我们所有的函数来管理 dApp 的逻辑、数据和规则,然后能够将这些函数导出到我们的前端组件HelloWorld.js里

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

interact.js

//export const helloWorldContract; 

export const loadCurrentMessage = async () => {

};

export const connectWallet = async () => {

};

const getCurrentWalletConnected = async () => {

};


export const updateMessage = async (message) => {

};

接着我们对四个未实现的函数做具体操作说明:

loadCurrentMessage-  处理加载存储在智能合约中的当前消息。

里将会用Alchmy Web3 API 对 Hello World 智能合约进行读取调用。

connectWallet- 这个函数会将我们的当前登录的 Metamask 账户,连接到我们的 dApp。

getCurrentWalletConnected- 此功能将在页面加载时检查钱包帐户是否已连接到我们的 dApp,并相应地通知我们的 UI组件。

updateMessage 此功能将更新存储在智能合约中的消息。它将对 Hello World 智能合约进行写调用,因此用户的 Metamask 钱包将必须签署以太坊交易的更新消息。。

4、用Alchmy Web3 API读取智能合约

要读取您的智能合约,您需要成功设置:

  • 与以太坊链的 API 连接

  • 您的智能合约的加载实例

  • 调用您的智能合约函数的函数

  • 当您从智能合约中读取的数据发生更改时,用于监视更新的侦听器

建立与以太坊链的 API 连接

还记得前天的教程么? 我们如何使用我们的Alchemy Web3 密钥来读取我们的智能合约吗?

在前端工程里也是一样的,我们安装 Alchemy Web3。

在终端中运行以下命令。

npm install @alch/alchemy-web3

[

Alchemy Web3

](github.com/alchemyplat…)

[

提供增强的 API 方法,让您作为 web3 开发人员的生活更轻松。

](web3js.readthedocs.io/en/v1.2.9/)

它旨在要求最少的配置,因此您可以立即在您的应用程序中开始使用它!

我们还需要在项目目中安装 dotenv (读取配置文件,因为我们会用到私钥,私钥这些信息尽量不写在代码里) Web3 API 以及其他一切敏感信息 都可以存放在.env 文件里。

npm install dotenv --save

我们将使用我们的 Websockets API 密钥

复制 websockets  密钥后,在项目的根目录中创建一个

.env

文件并将 Alchemy Websockets url 添加到其中。

.env

文件应如下所示:

REACT_APP_ALCHEMY_KEY = wss://eth-goerli.ws.alchemyapi.io/v2/

并在文件interact.js顶部添加以下代码:

require('dotenv').config();
const alchemyKey = process.env.REACT_APP_ALCHEMY_KEY;
const { createAlchemyWeb3 } = require("@alch/alchemy-web3");
const web3 = createAlchemyWeb3(alchemyKey);

//export const helloWorldContract;

准备好这个之后,就开始加载我们的智能合约了!

5、加载你的 Hello World 智能合约

要加载你的 Hello World 智能合约,你需要它的合约地址和 ABI,如果你完成了前面的教程,你可以remix 里面找到他。然后copy其内容 并在工程里创建 加contract-abi.josn (后面调用时会引用该文件)。

有了我们的合约地址、ABI 和 Alchemy Web3 的EndPoint,我们可以使用contract 方法来加载我们的智能合约的实例。

在interact.js  将您合约 ABI 导入,并添加合约地址。

interact.js 里代码如下

const contractABI = require("../contract-abi.json");
const contractAddress = "0x5f61076C4B174620B5018dBf090a8D967f07c3Bc";

把合约赋予给helloWorldContract变量,并使用我们的 Alchemy Web3 API加载读取合约,代码如下:

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

所以,interact.js 里的代码现在如下:

require('dotenv').config();
const alchemyKey = process.env.REACT_APP_ALCHEMY_KEY;
const { createAlchemyWeb3 } = require("@alch/alchemy-web3");
const web3 = createAlchemyWeb3(alchemyKey);

const contractABI = require('../contract-abi.json')
const contractAddress = "0x5f61076C4B174620B5018dBf090a8D967f07c3Bc";
export const helloWorldContract = new web3.eth.Contract(
  contractABI,
  contractAddress
);

6、在您的 interact.js 文件中实现 loadCurrentMessage

完成这些,调用合约的准备工作基本完成了。

接着,我们就进行一个简单的异步 web3 调用来读取我们的合约。

我们靠这个方法 loadCurrentMessage,来存储调用智能合约返回的消息:

更新interact.js 里的这个方法,代码如下:

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

由于我们要在 UI 中显示这个智能合约,所以让useEffect我们将HelloWorld.js 组件中的函数更新。

HelloWorld.js 代码如下:

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

注意:我们希望我们 loadCurrentMessage 在组件的第一次渲染期间被调用一次。

然后我们实现 addSmartContractListener 在智能合约中的消息更改后自动更新我们链接钱包的状态。

然后 npm instart

http://localhost:3000  (这里虽然调用了合约,但是钱包状态还更新)

7、实现 addSmartContractListener,实现合约中的方法。

回顾一下 HelloWorld.sol  这个合约,我们在里面实现了一个update的函数。 我们就在前端实现这个方法的调用。

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

helloWorld.js

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!");
    }
  });
}

最后,让我们在函数中调用我们的侦听器,以便在组件首次渲染useEffect时对其进行初始化。

HelloWorld.js 文件 代码如下:

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

8:设置钱包链接到UI

helloworld.js 文件,代码如下:

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: ({" "} 🦊{" "}您必须在浏览器中安装虚拟以太坊钱包 Metamask。), };
} };

这段逻辑是:它会检查您的浏览器是否已启用。

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

window.ethereum address status 如果 存在,那就得到 window.ethereum

9、将 connectWallet 函数添加到您的 HelloWorld.js UI 组件

导航到 中的 connectWalletPressed 函数

HelloWorld.js 方法代码如下:

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

请注意我们的大部分功能是如何 遵守 MVC 范式的!

HelloWorld.js

interact.js

在connectWalletPressed我们简单地对我们导入的

connectWallet

函数进行等待调用,并使用它的响应,

我们通过它们的状态钩子更新我们的 status 和变量walletAddress

现在,我们保存并测试UI了。

HelloWorld.js

interact.js

http://localhost:3000/

页面打开浏览器 ,然后点击页面右上角的“连接钱包”按钮。

如果您安装了 Metamask,系统会提示您将钱包连接到 dApp。 接受连接邀请。

您应该会看到钱包按钮现在反映您的地址已连接!

10、更新getCurrentWalletConnected方法

getCurrentWalletConnected 更新为以下内容:
interact.js

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: ({" "} 🦊{" "}您必须在浏览器中安装虚拟以太坊钱包 Metamask。), 
    };
} };

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

主要区别在于,我们调用不是 eth_requestAccounts打开 Metamask 连接钱包,而是调用eth_accounts方法,它只是返回一个数组,其中包含当前连接到我们的 dApp 的 Metamask 地址。

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

    const {address, status} = await getCurrentWalletConnected();
    setWallet(address);
    setStatus(status);

}, []);

请注意,我们调用getCurrentWalletConnected 来更新我们的 walletAddress 和 status 状态变量。

现在您已经添加了此代码,让我们尝试刷新我们的浏览器窗口。

11、实现addWalletListener

最后一步是实现钱包监听器,以便我们的 UI 在钱包状态发生变化时能立即更新,例如当用户断开连接或切换帐户时。

在 useEffect 函数中更新:

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

    const {address, status} = await getCurrentWalletConnected();
    setWallet(address)
    setStatus(status);

    addWalletListener();
}, []);

到这里,我们基本完善了钱包。接下去就是调用合约。

12、实现 合约中的 updateMessage 函数

看到这里,我们已经经到了最后一步!updateMessage (实现调用合约中的方法)

我们将执行以下操作:

interact.js file

1) 先做一个错误校验,确保我们的钱包链接是有效的

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.",
    };
  }
};

2)签署交易

将以下内容添加到updateMessage:

//set up transaction parameters
 const transactionParameters = {
    to: contractAddress, // Required except during contract publications.
    from: address, // must match user's active address.
    data: helloWorldContract.methods.update(message).encodeABI(),
  };

//sign the transaction
  try {
    const txHash = await window.ethereum.request({
      method: "eth_sendTransaction",
      params: [transactionParameters],
    });
    return {
      status: (

          ✅{" "}

            View the status of your transaction on Etherscan!


          ℹ️ Once the transaction is verified by the network, the message will
          be updated automatically.

      ),
    };
  } catch (error) {
    return {
      status: "😥 " + error.message,
    };
  }

transcation 里面有三个参数

to 指定收件人地址(我们的智能合约地址)

from 指定交易的签名者(就是我们操作的钱包子账户的address)

data 包含对我们的 Hello World 智能合约update 方法的调用,接收我们的``message字符串变量作为输入

完成的 updateMessage 函数应该是这样的:

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

  //input error handling
  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.",
    };
  }

  //set up transaction parameters
  const transactionParameters = {
    to: contractAddress, // Required except during contract publications.
    from: address, // must match user's active address.
    data: helloWorldContract.methods.update(message).encodeABI(),
  };

  //sign the transaction
  try {
    const txHash = await window.ethereum.request({
      method: "eth_sendTransaction",
      params: [transactionParameters],
    });
    return {
      status: (

          ✅{" "}

            View the status of your transaction on Etherscan!


          ℹ️ Once the transaction is verified by the network, the message will
          be updated automatically.

      ),
    };
  } catch (error) {
    return {
      status: "😥 " + error.message,
    };
  }
};

最后,我们需要将我们的功能 updateMessage 到我们的组件 HelloWorld.js

13、最后一步,updateMessage 连接到 HelloWorld.js 前端组件。

我们通过 onUpdatePressed 以反映我们的执行事务是成功还是失败:

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

这就完成了

很干净、很简单。 

让我们最后测试下!效果如下

这就完成了整个完整的的dApp 🚀🚀🚀🚀🚀🚀🚀🚀

Web3的大门,从此打开了!