教程:建立由以太坊支援的 Web3 聊天

語言: CN / TW / HK

在本文中,我們將學習如何將以太坊智慧合約連線到React應用程式,並使使用者能夠與之互動。

先決條件

  • 要在瀏覽器中安裝MetaMask擴充套件
  • 一個程式碼編輯器
  • 關於以下主題的一些知識:以太坊,MetaMask, React, TypeScript

在以太坊主網上工作要花真金白銀!

在本教程中,我假設的是你的MetaMask設定為使用Rinkeby。Rinkeby是一個複製主網的測試網路,允許我們免費部署和使用智慧合約。

專案

我們將為這個基於區塊鏈的聊天建立一個介面,如下所示:

  • 左邊的側邊欄包含一個按鈕,用於連線到聊天或指示連線使用者的地址。
  • 右側的聊天框,顯示訊息和輸入欄。

在這篇文章中,我們不會關注如何讓UI更漂亮,我們的目標是關注如何用最直接的方式與智慧合約互動。

我已盡力使本教程易於理解,但如果有些東西還是不甚清晰,也不用灰心,你會在本文的最後找到一個包含已完成專案的 GitHub 儲存庫的連結。

智慧合約

首先,我們要連線到前端的智慧合約,如下所示:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.12;contract BlockchainChat {
  event NewMessage(address indexed from, uint timestamp, string message);  struct Message {
    address sender;
    string content;
    uint timestamp;
  }  Message[] messages;  function sendMessage(string calldata _content) public {
    messages.push(Message(msg.sender, _content, block.timestamp));
    emit NewMessage(msg.sender, block.timestamp, _content);
  }  function getMessages() view public returns (Message[] memory) {
    return messages;
  }
}

event ,`emit 這些東西是什麼?

event 用於通知外部使用者區塊鏈上發生的事情。

在我們的例子中,“外部使用者”是我們的前端應用程式,它將監聽傳送到智慧合約的新訊息,因此我們可以立即在我們的UI中顯示它們。

前端

我準備了一個樣板,這樣你就可以馬上開始編碼了。

以下是啟動專案的Github連結:

https://github.com/thmsgbrt/web3-chat-powered-by-ethereum-starter

一旦你克隆了專案,使用npm install安裝依賴項,並用npm start啟動了它,那麼花幾分鐘檢查幾個檔案以瞭解應用是如何構造的,也是有必要的。這是非常基本的React,就不在此贅述了。

以下是我們的行動計劃:

A-允許使用者通過MetaMask連線到聊天
B-在我們的前端例項化智慧合約
C-從我們的智慧合約中獲取訊息並顯示它們
D-允許使用者在聊天中釋出訊息
E-收聽新資訊

A – 允許使用者通過MetaMask連線到聊天

要做到這一點,我們首先需要確保MetaMask擴充套件安裝在了瀏覽器上。

讓我們建立一個Hook來實現這一點:

const useIsMetaMaskInstalled = () => {
  const { ethereum } = window;
  return Boolean(ethereum && ethereum.isMetaMask);
};
​
export default useIsMetaMaskInstalled;

./src/useIsMetaMaskInstalled.ts

解釋:

MetaMask在window.ethereum注入了一個全域性API。該API允許網站請求使用者的以太坊賬戶,從使用者連線的區塊鏈讀取資料,並建議使用者簽署訊息和交易。

現在我們已經準備好了Hook,轉向Sidebar.tsx,這樣我們就可以利用它:

import React from "react";
import useIsMetaMaskInstalled from "../useIsMetaMaskInstalled";
​
interface Props {
  setAccount: React.Dispatch<React.SetStateAction<string | undefined>>;
  account?: string;
}
​
const Sidebar = ({ setAccount, account }: Props) => {
  // Use our hook here
  const isMetaMaskInstalled = useIsMetaMaskInstalled();
​
  return (
    <div className="sidebar">
      {account && (
        <>
          <b>Connected as:</b>
          <br />
          <small>{account}</small>
        </>
      )}
      {!account && (
        <button disabled={!isMetaMaskInstalled}>Connect With MetaMask</button>
      )}
      {!isMetaMaskInstalled && <p>Please install MetaMask</p>}
    </div>
  );
};
​
export default Sidebar;

./src/components/Sidebar.tsx

以現在,我們有一種方法來檢測是否安裝了MetaMask,如果沒有安裝MetaMask,我們可以警告使用者,他們需要在瀏覽器上安裝MetaMask。

接下來,讓我們為“Connect With MetaMask”按鈕新增一個onClick處理程式:

import React from "react";
import { ethers } from "ethers";
import useIsMetaMaskInstalled from "../useIsMetaMaskInstalled";
​
interface Props {
  setAccount: React.Dispatch<React.SetStateAction<string | undefined>>;
  account?: string;
}
​
const Sidebar = ({ setAccount, account }: Props) => {
  const isMetaMaskInstalled = useIsMetaMaskInstalled();
​
  // Handle connection to MetaMask
  const handleOnConnect = () => {
    window.ethereum
      .request({ method: "eth_requestAccounts" })
      .then((accounts: string[]) => {
        setAccount(ethers.utils.getAddress(accounts[0]));
      })
      .catch((err: any) => console.log(err));
  };
​
  return (
    <div className="sidebar">
      {account && (
        <>
          <b>Connected as:</b>
          <br />
          <small>{account}</small>
        </>
      )}
      {!account && (
        // And don't forget to bind the onClick on our connection button
        <button onClick={handleOnConnect} disabled={!isMetaMaskInstalled}>
          Connect With MetaMask
        </button>
      )}
      {!isMetaMaskInstalled && <p>Please install MetaMask</p>}
    </div>
  );
};
​
export default Sidebar;

./src/components/Sidebar.tsx

現在,當用戶單擊 Connect With MetaMask 時,MetaMask 擴充套件程式將提示一個模式並詢問要使用哪個帳戶:

MetaMask 要求我們連線到我們的聊天

現在已連線!

側邊欄現在顯示你的以太坊地址!

B – 在我們的前端例項化智慧合約

為了能夠獲取資訊並使使用者能夠傳送訊息,我們需要有一種方法與我們的智慧合約進行通訊。

我們要使用ethers庫。

ethers是一個庫,可以幫助我們的前端與智慧合約進行對話。ethers通過提供商(在我們的例子中是MetaMask)連線到以太坊節點,它可以幫我們做很多事情。

讓我們建立另一個Hook,它將允許我們在ethers的幫助下與我們的智慧合約互動:

import { ethers } from "ethers";
import { useState, useEffect } from "react";
​
const useChatContract = (
  contractAddress: string,
  web3ChatAbi: ethers.ContractInterface,
  account?: string
): ethers.Contract | undefined => {
  const [signer, setSigner] = useState<ethers.providers.JsonRpcSigner>();
  const [webThreeProvider, setWebThreeProvider] =
    useState<ethers.providers.Web3Provider>();
  const { ethereum } = window;
​
  useEffect(() => {
    if (ethereum) {
      setWebThreeProvider(new ethers.providers.Web3Provider(window.ethereum));
    }
  }, [ethereum]);
​
  useEffect(() => {
    if (webThreeProvider && account) {
      setSigner(webThreeProvider.getSigner());
    }
  }, [account, webThreeProvider]);
​
  if (!contractAddress || !web3ChatAbi || !ethereum || !webThreeProvider)
    return;
​
  /**
   * Returns a new instance of the Contract.
   * By passing in a Provider, this will return a downgraded Contract which only has read-only access (i.e. constant calls).
   * By passing a signer (a logged in user), this will return a Contract with read and write access.
   */
  return new ethers.Contract(
    contractAddress,
    web3ChatAbi,
    signer || webThreeProvider
  );
};
​
export default useChatContract;

./src/useChatContract.ts

讓我們來分解一下:

  • 我們先檢查一下window.ethereum 是否存在並從中獲取了 Web3 Provider。
  • 如果已經定義了accountis,這意味著使用者點選了“Connect With MetaMask”按鈕,webThreeProvider.getSigner()會返回給我們他們的地址。
  • 最後,返回一個帶有新的ether . contract()的合約例項。

例項化我們的智慧合約

前往App.tsx,在那裡我們可以使用我們建立的hook:

import React, { useState } from "react";
import "./App.css";
import Chat from "./components/Chat";
import Sidebar from "./components/Sidebar";
import BlockchainChatArtifact from "./contract/BlockchainChat-artifact.json";
import useChatContract from "./useChatContract";
​
function App() {
  const contractAddress = "[CONTRACT_ADDRESS]";
  const [account, setAccount] = useState<string>();
  // use useChatContract here
  const chatContract = useChatContract(
    contractAddress,
    BlockchainChatArtifact.abi,
    account
  );
    
  return (
    <div className="App">
      <Sidebar setAccount={setAccount} account={account} />
      <Chat account={account} chatContract={chatContract} />
    </div>
  );
}
​
export default App;

你是否注意到了,我們這裡有一個錯誤,需要去做兩件事情來解決問題:

  • contractAddress不是合約地址。
  • ./contract/BlockchainChat-artifact.json是空的。

合約地址

這個地址告訴我們在哪裡找到區塊鏈上的區塊鏈聊天智慧合約。

你可以使用我為大家部署到 Rinkeby 的以下地址之一:

0x56cD072f27f06a58175aEe579be55601E82D8fcD
0xD99f113cAd1fe2eeebe0E7383415B586704DB5a3
0x23CAEEA0Bb03E6298C2eAaa76fBffa403c20984f

選擇其中任何一個,它們都是指向已部署的區塊鏈Chat智慧合約的地址。

合約的ABI

我們的Hook期望一個來自BlockchainChatArtifact的ABI。這是兩個新概念…

當你編譯一個智慧合約時,你會得到所謂的工件。

在Remix中(一個用於建立、編譯、測試和部署智慧合約的IDE),一旦你的智慧合約已經編譯完成,你將在合約/工件下找到工件。

這個工件包含庫的連結、位元組碼、部署的位元組碼、氣體估計、方法識別符號和ABI。它用於將庫地址連結到檔案。

現在,什麼是“ABI”:

ABI代表應用程式二進位制介面。ehters需要我們的BlockchainChat智慧合約的ABI,以便知道我們的智慧合約可以做什麼(方法、事件、錯誤等),併為我們的前端提供與它互動的一種方式。

如果你沒有自己部署智慧合約,仍然可以通過複製./contract/ blockchainchat – artifacts .json中的以下工件來繼續本文。

指向工件的Gist連結: https://gist.github.com/thmsgbrt/1db36bc688d6984070badb14652ed65c

很好,應用程式現在應該沒有錯誤!

C – 從我們的智慧合約中獲取訊息並顯示它們

現在我們已經在前端例項化了智慧合約,我們終於可以獲取訊息了。開啟 Chat.tsx 並新增以下 getMessages 函式:

import React, { useEffect, useState } from "react";
import { Message } from "../types";
import ChatBubble from "./ChatBubble";
import { ethers } from "ethers";
​
interface Props {
  account?: string;
  chatContract: ethers.Contract | undefined;
}
​
const Chat = ({ account, chatContract }: Props) => {
  const [textareaContent, setTextareaContent] = useState("");
  const [txnStatus, setTxnStatus] = useState<string | null>(null);
  const [messages, setMessages] = useState<Message[]>();
​
  // Add this function
  const getMessages = async () => {
    if (!chatContract || account) return;
​
    // Use our Contract Instance to call our Smart Contract's getMessages method
    const messages = await chatContract.getMessages();
    // Update our state with the received message
    setMessages(() => {
      return messages.map((w: any) => ({
        address: w.sender,
        date: w.timestamp._hex,
        content: w.content,
      }));
    });
  };
​
​
  useEffect(() => {
    // Let's call `getMessages` if there is an instance of the chatContract and that `message`is undefined
    if (!chatContract || messages) return;
    getMessages();
  }, [chatContract]);
​
  return (
    <div className="chat">
      <div className="chat__messages">
        {!chatContract && (
          <p className="state-message">
            Connect to the chat in order to see the messages!
          </p>
        )}
        {account && messages && messages.length === 0 && (
          <p className="state-message">There is no message to display</p>
        )}
        {messages &&
          messages.length > 0 &&
          messages.map((m, i) => (
            <ChatBubble
              key={i}
              ownMessage={m.address === account}
              address={m.address}
              message={m.content}
            />
          ))}
      </div>
      <div className="chat__actions-wrapper">
        {!account && (
          <p className="state-message">Connect With Metamask to chat!</p>
        )}
        <div className="chat__input">
          <textarea
            disabled={!!txnStatus || !account}
            value={textareaContent}
            onChange={(e) => {
              setTextareaContent(e.target.value);
            }}
          ></textarea>
          <button disabled={!!txnStatus || !account}>
            {txnStatus || "send message"}
          </button>
        </div>
      </div>
    </div>
  );
};
​
export default Chat;

Chat.tsx 通過它的 props接收 chatContract 例項,我們可以用它來呼叫 chatContract.getMessages() 。通過接收到的訊息,我們填充 messages 狀態變數。

如果你的聊天智慧合約釋出了訊息,它們應該在聊天框中可見。否則,讓我們繼續允許使用者傳送訊息。以下是目前為止你應該看到的:

D -允許使用者在聊天中釋出訊息

Chat.tsx 中,新增以下 sendMessage 函式來發布訊息:

import React, { useEffect, useState } from "react";
import { Message } from "../types";
import ChatBubble from "./ChatBubble";
import { ethers } from "ethers";
​
interface Props {
  account?: string;
  chatContract: ethers.Contract | undefined;
}
​
const Chat = ({ account, chatContract }: Props) => {
  const [textareaContent, setTextareaContent] = useState("");
  const [txnStatus, setTxnStatus] = useState<string | null>(null);
  const [messages, setMessages] = useState<Message[]>();
​
  const getMessages = async () => {
    // ...
  };
​
  // Our sendMessage function
  const sendMessage = async () => {
    if (!chatContract) return;
    try {
      // When the user clicks the button, change the status to "WAIT"
      setTxnStatus("WAIT");
      // This is when MetaMask prompts the user with the transaction to validate
      const messageTxn = await chatContract.sendMessage(textareaContent);
      // If the user validates the transaction, switch the status to "SENDING"
      setTxnStatus("SENDING");
      // Transaction being validated on the Blockchain
      await messageTxn.wait();
    } catch (e) {
      console.warn("Transaction failed with error", e);
    } finally {
      // When it's done, reset the content of the textarea
      setTextareaContent("");
      // Set the transaction status to its initial state
      setTxnStatus(null);
    }
  };
​
  useEffect(() => {
    if (!chatContract || messages) return;
    getMessages();
  }, [chatContract]);
​
  return (
    <div className="chat">
      <div className="chat__messages">
        {!chatContract && (
          <p className="state-message">
            Connect to the chat in order to see the messages!
          </p>
        )}
        {account && messages && messages.length === 0 && (
          <p className="state-message">There is no message to display</p>
        )}
        {messages &&
          messages.length > 0 &&
          messages.map((m, i) => (
            <ChatBubble
              key={i}
              ownMessage={m.address === account}
              address={m.address}
              message={m.content}
            />
          ))}
      </div>
      <div className="chat__actions-wrapper">
        {!account && (
          <p className="state-message">Connect With Metamask to chat!</p>
        )}
        <div className="chat__input">
          <textarea
            disabled={!!txnStatus || !account}
            value={textareaContent}
            onChange={(e) => {
              setTextareaContent(e.target.value);
            }}
          ></textarea>
          {/* Bind the onClick with our sendMessage handler */}
          <button onClick={sendMessage} disabled={!!txnStatus || !account}>
            {txnStatus || "send message"}
          </button>
        </div>
      </div>
    </div>
  );
};
​
export default Chat;

讓我們繼續,在textarea中輸入一條訊息併發送它!這應該會提示MetaMask,要求驗證交易,繼續:

我們UI中的“send message”按鈕有不同的狀態。它的內容根據交易狀態而變化:

  • “WAIT”表示交易需要使用者批准。
  • “SENDING”表示交易正在被驗證。

要檢視剛剛釋出的訊息,請重新載入頁面。它就應該會被新增。

但是在使用者體驗方面,必須重新載入頁面以檢視是否有新訊息釋出並不是非常友好的。

E -收聽新資訊

回到我們的智慧合約。正如你所看到的,當用戶釋出一條訊息時,會觸發一個事件:

contract BlockchainChat {
  event NewMessage(address indexed from, uint timestamp, string message);  // ...  function sendMessage(string calldata _content) public {
    messages.push(Message(msg.sender, _content, block.timestamp));
    emit NewMessage(msg.sender, block.timestamp, _content);
  }  //...}

我們可以通過新增以下setupMessageListener函式來監聽這個事件:

import React, { useEffect, useState } from "react";
import { Message } from "../types";
import ChatBubble from "./ChatBubble";
import { ethers } from "ethers";
​
interface Props {
  account?: string;
  chatContract: ethers.Contract | undefined;
}
​
const Chat = ({ account, chatContract }: Props) => {
  const [textareaContent, setTextareaContent] = useState("");
  const [txnStatus, setTxnStatus] = useState<string | null>(null);
  const [messages, setMessages] = useState<Message[]>();
​
  const getMessages = async () => {
    // ...
  };
​
  // Listen to new message posted
  const setupMessageListener = (): ethers.Contract | void => {
    if (!chatContract) return;
​
    // .on("EVENT_NAME", callback) to listen to an event
    const msgListener = chatContract.on(
      "NewMessage",
      (address, timestamp, content, _style) => {
        // When a new message is posted, update our "messages" state with "setMessages"
        setMessages((prev) => {
          const newMessage = {
            address,
            date: timestamp._hex,
            content,
          };
          return prev ? [...prev, newMessage] : [newMessage];
        });
      }
    );
​
    return msgListener;
  };
​
  const sendMessage = async () => {
    // ...
  };
​
  useEffect(() => {
    if (!chatContract || messages) return;
    getMessages();
    // Don't forget to call our listener here
    setupMessageListener();
  }, [chatContract]);
​
  return (
    <div className="chat">
      <div className="chat__messages">
        {!chatContract && (
          <p className="state-message">
            Connect to the chat in order to see the messages!
          </p>
        )}
        {account && messages && messages.length === 0 && (
          <p className="state-message">There is no message to display</p>
        )}
        {messages &&
          messages.length > 0 &&
          messages.map((m, i) => (
            <ChatBubble
              key={i}
              ownMessage={m.address === account}
              address={m.address}
              message={m.content}
            />
          ))}
      </div>
      <div className="chat__actions-wrapper">
        {!account && (
          <p className="state-message">Connect With Metamask to chat!</p>
        )}
        <div className="chat__input">
          <textarea
            disabled={!!txnStatus || !account}
            value={textareaContent}
            onChange={(e) => {
              setTextareaContent(e.target.value);
            }}
          ></textarea>
          <button onClick={sendMessage} disabled={!!txnStatus || !account}>
            {txnStatus || "send message"}
          </button>
        </div>
      </div>
    </div>
  );
};
​
export default Chat;

接著,傳送一條新訊息,這一次,就應該不必重新載入頁面來檢視剛剛釋出的訊息。如果另一個使用者傳送訊息,這顯然也是有效的。

最終專案

恭喜完成了本教程的學習。正如上面所承諾的,這裡有一個最終專案的連結:

https://github.com/thmsgbrt/web3-chat-powered-by-ethereum-finished-project

Source: https://betterprogramming.pub/create-a-web3-chat-powered-by-ethereum-6886824fad7a

關於

ChinaDeFi– ChinaDeFi.com 是一個研究驅動的DeFi創新組織,同時我們也是區塊鏈開發團隊。每天從全球超過500個優質資訊源的近900篇內容中,尋找思考更具深度、梳理更為系統的內容,以最快的速度同步到中國市場提供決策輔助材料。

Layer 2道友– 歡迎對Layer 2感興趣的區塊鏈技術愛好者、研究分析人與Gavin(微信: chinadefi)聯絡,共同探討Layer 2帶來的落地機遇。敬請關注我們的微信公眾號 “去中心化金融社群”