• 教程:创建由以太坊支持的 Web3 聊天


    教程:创建由以太坊支持的 Web3 聊天

    未标题-3

    截屏2022-07-01 上午11.08.50

    在本文中,我们将学习如何将以太坊智能合约连接到React应用程序,并使用户能够与之交互。

    先决条件

    • 要在浏览器中安装MetaMask扩展
    • 一个代码编辑器
    • 关于以下主题的一些知识:以太坊,MetaMask, React, TypeScript

    在以太坊主网上工作要花真金白银!

    在本教程中,我假设的是你的MetaMask设置为使用Rinkeby。Rinkeby是一个复制主网的测试网络,允许我们免费部署和使用智能合约。

    项目

    我们将为这个基于区块链的聊天建立一个界面,如下所示:

    img

    • 左边的侧边栏包含一个按钮,用于连接到聊天或指示连接用户的地址。
    • 右侧的聊天框,显示消息和输入栏。

    在这篇文章中,我们不会关注如何让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;
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    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-收听新信息
    
    • 1
    • 2
    • 3
    • 4
    • 5

    A - 允许用户通过MetaMask连接到聊天

    要做到这一点,我们首先需要确保MetaMask扩展安装在了浏览器上。

    让我们创建一个Hook来实现这一点:

    const useIsMetaMaskInstalled = () => {
      const { ethereum } = window;
      return Boolean(ethereum && ethereum.isMetaMask);
    };
    
    export default useIsMetaMaskInstalled;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    ​ ./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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    ​ ./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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    ​ ./src/components/Sidebar.tsx

    现在,当用户单击 Connect With MetaMask 时,MetaMask 扩展程序将提示一个模式并询问要使用哪个帐户:

    img

    ​ MetaMask 要求我们连接到我们的聊天

    img

    ​ 现在已连接!

    侧边栏现在显示你的以太坊地址!

    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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    ​ ./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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    你是否注意到了,我们这里有一个错误,需要去做两件事情来解决问题:

    • 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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82

    Chat.tsx通过它的 props接收chatContract实例,我们可以用它来调用chatContract.getMessages()。通过接收到的消息,我们填充messages状态变量。

    如果你的聊天智能合约发布了消息,它们应该在聊天框中可见。否则,让我们继续允许用户发送消息。以下是目前为止你应该看到的:

    img

    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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91

    让我们继续,在textarea中输入一条消息并发送它!这应该会提示MetaMask,要求验证交易,继续:

    img

    我们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);
      }  //...}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    我们可以通过添加以下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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97

    接着,发送一条新消息,这一次,就应该不必重新加载页面来查看刚刚发布的消息。如果另一个用户发送消息,这显然也是有效的。

    img

    最终项目

    恭喜完成了本教程的学习。正如上面所承诺的,这里有一个最终项目的链接:

    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带来的落地机遇。敬请关注我们的微信公众号 “去中心化金融社区”

    img

  • 相关阅读:
    网络编程TP/IP (尹圣雨)(韩) 第二章 课后习题
    网红和主播们是用的什么美颜工具?深入剖析美颜sdk与美颜Api
    java基于ssm+jsp 多用户博客个人网站
    卷积神经网络(CNN):乳腺癌识别
    【单链表,循环链表和双向链表的时间效率比较,顺序表和链表的比较,有序表的合并------用顺序表实现,用链表实现】
    JVM GC垃圾回收
    postman的使用
    B站数据,怎样查看B站带货商品数据?
    生成对抗网络(GAN)
    ES6新特性详解
  • 原文地址:https://blog.csdn.net/chinadefi/article/details/125557171