在本文中,我们将学习如何将以太坊智能合约连接到React应用程序,并使用户能够与之交互。
在本文中,我们将学习如何将以太坊智能合约连接到React应用程序,并使用户能够与之交互。
在以太坊主网上工作要花真金白银!
在本教程中,我假设的是你的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-收听新信息
要做到这一点,我们首先需要确保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 要求我们连接到我们的聊天
现在已连接!
侧边栏现在显示你的以太坊地址!
为了能够获取信息并使用户能够发送消息,我们需要有一种方法与我们的智能合约进行通信。
我们要使用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
让我们来分解一下:
实例化我们的智能合约
前往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;
你是否注意到了,我们这里有一个错误,需要去做两件事情来解决问题:
合约地址
这个地址告诉我们在哪里找到区块链上的区块链聊天智能合约。
你可以使用我为大家部署到 Rinkeby 的以下地址之一:
0x56cD072f27f06a58175aEe579be55601E82D8fcD
0xD99f113cAd1fe2eeebe0E7383415B586704DB5a3
0x23CAEEA0Bb03E6298C2eAaa76fBffa403c20984f
选择其中任何一个,它们都是指向已部署的区块链Chat智能合约的地址。
合约的ABI
我们的Hook期望一个来自BlockchainChatArtifact的ABI。这是两个新概念…
当你编译一个智能合约时,你会得到所谓的工件。
在Remix中(一个用于创建、编译、测试和部署智能合约的IDE),一旦你的智能合约已经编译完成,你将在contracts/artifacts下找到工件。
这个工件包含库的链接、字节码、部署的字节码、gas估计、方法标识符和ABI。它用于将库地址链接到文件。
现在,什么是“ABI”:
ABI代表应用程序二进制接口。ethers需要我们的BlockchainChat智能合约的ABI,以便知道我们的智能合约可以做什么(方法、事件、错误等),并为我们的前端提供与它交互的一种方式。
如果你没有自己部署智能合约,仍然可以通过复制./contract/ blockchainchat - artifacts .json中的以下工件来继续本文。
指向工件的Gist链接:
https://gist.github.com/thmsgbrt/1db36bc688d6984070badb14652ed65c
应用程序现在应该没有错误了!
现在我们已经在前端实例化了智能合约,我们终于可以获取消息了。打开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
状态变量。
如果你的聊天智能合约发布了消息,它们应该在聊天框中可见。否则,让我们继续允许用户发送消息。以下是目前为止你应该看到的:
在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”按钮有不同的状态。它的内容根据交易状态而变化:
要查看刚刚发布的消息,请重新加载页面。它就应该会被添加。
但是在用户体验方面,必须重新加载页面以查看是否有新消息发布并不是非常友好的。
回到我们的智能合约。正如你所看到的,当用户发布一条消息时,会触发一个事件:
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篇内容中,寻找思考更具深度、梳理更为系统的内容,以最快的速度同步到中国市场提供决策辅助材料。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!