Web3 第一章 (DApp 开发初始化)

3,716 阅读9分钟

web3 大佬圈

我是 web3 小白,欢迎各大佬进群一起交流、进步。

image.png

转行 web3

  • 只要你会 web 开发,就会 web3 开发
  • Dapp 就是基于智能合约和钱包的前端应用

初始化项目

pnpx create-next-app
pnpm add antd @ant-design/web3 @ant-design/web3-wagmi wagmi @tanstack/react-query
  • @ant-design/web3 是一个 UI 组件库,它通过不同的适配器和不同的区块链连接。我们主要基于的是以太坊。对应的,我们也将使用以太坊的适配器来实现需求。
  • wagmi 是一个开源的服务以太坊的 React Hooks 库,并依赖 @tanstack/react-query。Ant Design Web3 的适配器 @ant-design/web3-wagmi 就是基于它实现的,如果没有特殊说明,那提到的适配器就是指 @ant-design/web3-wagmi

配置适配器


import { createConfig, http } from "wagmi";
import { mainnet } from "wagmi/chains";
import { WagmiWeb3ConfigProvider } from "@ant-design/web3-wagmi";
import { Address, NFTCard } from "@ant-design/web3";

const config = createConfig({
  chains: [mainnet],
  transports: {
    [mainnet.id]: http(),
  },
});

export default function Web3() {
  return (
    <WagmiWeb3ConfigProvider config={config}>
      <Address format address="0xEcd0D12E21805803f70de03B72B1C162dB0898d9" />
      <NFTCard address="0xEcd0D12E21805803f70de03B72B1C162dB0898d9" tokenId={641} />
    </WagmiWeb3ConfigProvider>
  );
};

其中引入的内容说明如下:

  • createConfig:wagmi 用来创建配置的方法。
  • http:wagmi 用来创建 HTTP JSON RPC 连接的方法,通过它你可以通过 HTTP 请求访问区块链。
  • mainnet:代表以太坊主网,除了 mainnet 以外还会有类似 goerli 的测速网和类似 bsc 和 base 的 EVM 兼容的其它公链,有的是和以太坊一样的 L1 公链,有的是 L2 公链,这里先暂不展开。
  • WagmiWeb3ConfigProvider:Ant Design Web3 用来接收 wagmi 配置的 Provider。

image.png

配置节点服务

什么是节点服务

  • 节点服务是 DApp 开发必不可少的服务。它是一个运行在区块链网络上的服务,它可以帮助你与区块链网络进行交互。在 DApp 开发中,我们需要通过节点服务来获取区块链的数据,发送交易等。

  • 在以太坊网络中,我们可以通过 ZANInfuraAlchemy 等服务来获取节点服务。这些服务都提供了免费的节点服务,当然,它们也提供了付费的服务,如果你的 DApp 需要更高的性能,你可以考虑使用它们的付费服务。

获取 key

这里以 ZAN 的节点服务为例,指引你如何配置节点服务。

首先注册并登录 zan.top 之后进入到节点服务的控制台 zan.top/service/api… 创建一个 Key,每个 Key 都有默认的免费额度,对于微型项目来说够用了,但是对于生产环境的项目来说,请结合实际情况购买节点服务。

image.png

配置节点服务

const config = createConfig({
  chains: [mainnet],
  transports: {
-    [mainnet.id]: http(),
+    [mainnet.id]: http('https://api.zan.top/node/v1/eth/mainnet/{YourZANApiKey}'),
  },
});

上面代码中的 YourZANApiKey 需要替换成你自己的 Key。另外在实际的项目中,为了避免你的 Key 被滥用,建议你将 Key 放到后端服务中,然后通过后端服务来调用节点服务,或者在 ZAN 的控制台中设置域名白名单来降低被滥用的风险。当然,在教程中你也可以继续直接使用 http(),使用 wagmi 内置的默认的实验性的节点服务。

连接钱包

  • 连接钱包是 DApp 中最重要的交互,通过 wagmi 和 Ant Design Web3 来实现连接钱包的功能。

  • 在 DApp 中,我们需要连接钱包来获取用户的钱包地址,以及进行一些需要用户签名的操作,比如发送交易、签名消息等。连接钱包有多种方式,通常在以太坊中有如下三种方式:

    • 通过浏览器插件建立连接。
    • 通过在钱包 App 中访问 DApp 建立连接。
    • 通过 WalletConnect 协议建立连接。

其中前面两种对于 DApp 来说都是通过钱包注入到浏览器运行环境中的接口来实现的,而 WalletConnect 则是通过服务端中转的方式来实现的。而钱包注入接口也有两种方式,一种是通过 EIP-1193 来实现的,另一种是通过 EIP-6963 来实现的。EIP-1193 是一个早期的协议,也比较简单,接下来我们先尝试用这种方式来和钱包建立连接。

配置钱包

我们以 MetaMask 为例,看一下如何和 MetaMask 钱包建立连接。

image.png

image.png

image.png

image.png

DApp 如何调用智能合约

DApp 的前端网站部分区别于传统 App 的地方在于,它需要和区块链进行交互。而区块链的交互主要是通过调用智能合约来实现的。在这一讲中,我们将会学习如何调用智能合约。

以 Ethereum 举例,当智能合约被部署到区块链上后,我们可以通过构建以太坊交易(Transaction),调用合约中的相应方法,前提是知道合约的 ABI 文件和合约的 HASH 地址。

ABI 是 Application Binary Interface 的缩写,是一种二进制接口标准,用于定义智能合约的函数和参数,合约 HASH 是合约在区块链上的地址,它们都可以在部署智能合约时获得。

DApp 调用合约方法一般有两种方式:通过钱包插件连接、通过节点 RPC 直接连接,我们主要介绍第一种方式。

通过 MetaMask

MetaMask 是目前以太坊生态中用户最多的钱包插件,它提供了一个简单的方式,让用户在浏览器中管理自己的以太坊资产,同时也是 DApp 与以太坊网络交互的桥梁。如果你还没有使用过,可以在这里下载安装,并参考官网的教程完成初始的配置。当然,你也可以使用其它钱包。比如 TokenPocketimToken 等。

MetaMask 钱包安装完成后,我们可以在浏览器的右上角看到 MetaMask 的图标,同时它也会给每一个页面注入 window.ethereum 对象,这个对象是 DApp 与以太坊网络交互的接口,我们可以通过它来调用以太坊的 API。比如我们可以可以发起一个 eth_chainId 的 RPC 请求,获取当前网络的 ID:

await window.ethereum.request({ method: "eth_chainId" }); // 0x1 代表以太坊主网

通过 RPC

从 DApp 编码的角度来说,调用一个合约方法通常需要以下几个步骤:

  • 构造交易数据
  • 通过唤起钱包授权将交易数据添加签名
  • 将签名后的交易数据通过节点服务发送到区块链网络

注:调用只读接口时,因为不需要向区块链写入数据,所以也不需要对交易签名,可以直接通过节点服务读取到链上数据。

读的方式调用合约方法

配置好节点服务后,我们就可以开始调用合约了。我们使用 wagmi 提供的 useReadContract Hook 来读取合约数据。示例代码如下:

const CallTest = () => {
  const { account } = useAccount();
  const { writeContract } = useWriteContract();
  const result = useReadContract({
    address: "0xEcd0D12E21805803f70de03B72B1C162dB0898d9",
    abi: [
      {
        type: "function",
        name: "balanceOf",
        stateMutability: "view",
        inputs: [{ name: "account", type: "address" }],
        outputs: [{ type: "uint256" }],
      },
    ],
    functionName: "balanceOf",
    args: [account?.address as `0x${string}`],
  });
  reutrn <div>{result.data.toString()}</div>
}

写的方式调用合约方法

仅仅是读取合约还不够,一个真正的 DApp,肯定会涉及到向智能合约写入数据。向智能合约写入数据通常都是通过在区块链上执行智能合约的方法,方法执行过程中会改写合约中的数据。

接下来我们尝试调用下合约的 mint 方法,mint 方法并不是 ERC721 规范中的方法,它是该合约自行定义的。在本合约中,调用 mint 方法需要消耗 GAS 以及至少 0.01ETH 的费用来获取 NFT。


import { createConfig, http, useReadContract, useWriteContract } from "wagmi";

const CallTest = () => {
  const { writeContract } = useWriteContract();
  const onClick = () => {
    writeContract(
      {
        abi: [
          {
            type: "function",
            name: "mint",
            stateMutability: "payable",
            inputs: [
              { internalType: "uint256", name: "quantity", type: "uint256" },
            ],
            outputs: [],
          },
        ],
        address: "0xEcd0D12E21805803f70de03B72B1C162dB0898d9",
        functionName: "mint",
        args: [BigInt(1)],
        value: parseEther("0.01"),
      },
      {
        onSuccess(data) {
          console.log("Success", data);
        },
        onError(err) {
          console.log("Error", err);
        },
      }
    );
  };
  
  return <Button onClick ={onClick}>Mint</Button>
} 

我们用到了 viem 这个库,它是 wagmi 底层依赖的一个库,你需要在项目中安装它:

pnpm add viem

这段代码中,我们实现了点击 mint 按钮后调用合约的 mint 方法,传入参数 1,在该合约的实现逻辑中,这代表要铸造一个 NFT。由于每个 NFT 铸造的价格是 0.01 ETH,所以我们还需要在交易中发送 0.01 ETH 的费用,这样才能成功铸造 NFT。所以上面会在调用合约中配置 value: parseEther("0.01")。在以太坊的合约方法执行中,合约并不能直接提取调用者的 ETH,所以我们需要在调用合约的时候主动发送 ETH 给合约,这是合约安全设计上的考虑。

合约调用成功和失败会有对应的提示,如果还未连接账号会抛出未连接账号的错误。所以你需要先点击我们在上一节课程中实现的连接按钮连接你的账户地址。

如果你的账户没有足够的 GAS,那么点击后会出现类似如下的错误:

image.png

如果你的账户有足够的 ETH,那么点击后会出现类似如下的授权弹窗: image.png

点击 拒绝 后,不会执行合约调用,不会消耗你的任何 ETH。下面,我们会指引你部署一个测试合约,在测试环境中体验完整的流程。当然如果你很富有,你也可以点击确认,这样就会执行合约调用,消耗你的 ETH,得到一个 NFT。

EIP1193 和 EIP6369

EIP1193

EIP1193 的规范地址在eips.ethereum.org/EIPS/eip-11…,它定义了在浏览器中如何通过 JavaScript 与钱包进行交互,有了这个规范钱包才能按照他提供相关接口,DApp 才能按照它调用钱包提供的接口。

这个定义很简单,其实就是约定了浏览器运行时的全局对象 window 上的 ethereum 对象的格式,定义了一些方法和事件。

对于 DApp 来说,它需要检查 window.ethereum 是否存在,如果存在,那么它就可以调用 window.ethereum 上的方法来和钱包进行交互。就同调用浏览器其他 API 类似,比如 window.localStorage

const ethereum = window.ethereum;

ethereum
  .request({ method: "eth_chainId" })
  .then((chainId) => {
    console.log(`hexadecimal string: ${chainId}`);
    console.log(`decimal number: ${parseInt(chainId, 16)}`);
  })
  .catch((error) => {
    console.error(`Error fetching chainId: ${error.code}: ${error.message}`);
  });

对于更多方法,也就对应修改 request 调用时的参数即可,支持的方法可以参考 JSON-RPC API。当然,对于一些钱包可能不支持的方法,你需要做好异常处理。也有一些钱包特有的一些方法或者一些已经约定俗成的方法,你需要查看钱包的文档。

通常来说,在 DApp 中你应该使用类似 web3.jsethersviem 这样的 SDK 来和钱包进行交互,这些 SDK 会帮你封装好一些方法,让你更方便的和钱包进行交互。

以上就是 EIP1193 的基本概念,但是 EIP1193 有一个主要的缺陷。就是 window.ethereum 对象只有一个,所以当用户安装了多个钱包时,用户只能选择一个钱包来使用。这样会导致钱包之间会争抢 window.ethereum 对象,一方面损害了用户体验,另外也不利于钱包之间的良性竞争。

在之前很长一段时间,针对这个问题钱包的做法一般是会注入自己独特的对象,比如 TokenPocket 会注入 window.tokenPocket。但是这样的做法并不是标准的,也不是一个好的解决方案。另外,这样的做法也会导致 DApp 需要适配很多钱包,增加了 DApp 的开发成本。

EIP6963

EIP6963 不再通过 window.ethereum 对象来和钱包进行交互,而是通过发送往 window 发送事件的方式来和钱包进行交互。这样就解决了 EIP1193 的问题,多个钱包可以和 DApp 进行交互,而不会争抢 window.ethereum 对象。

另外钱包也可以通过发送事件的方式主动告知 DApp 它的存在,这样 DApp 就可以知道用户安装了哪些钱包,然后根据用户的选择来和钱包进行交互。

技术上来讲其实就是通过浏览器的 window.addEventListener 来监听消息,通过 window.dispatchEvent 来发送消息。所有消息的 type 都有 eip6963: 前缀,具体的消息内容定义可以参考规范文档。

对于开发者来说,和 EIP1193 一样,你使用一些社区的库即可,这样可以免去对细节的关注。比如你如果使用 wagmi,那么通过配置 multiInjectedProviderDiscovery 即可接入 EIP6963。

如果你使用了 Ant Design Web3,通过配置 WagmiWeb3ConfigProvider 的 eip6963 即可在 DApp 中使用 EIP6963。它的连接钱包的弹窗会自动添加检测到的钱包。

export default function Web3() {
  return (
    <WagmiWeb3ConfigProvider
      config={config}
      wallets={[MetaMask()]}
+     eip6963={{
+       autoAddInjectedWallets: true,
+     }}
    >
      <Address format address="0xEcd0D12E21805803f70de03B72B1C162dB0898d9" />
      <NFTCard
        address="0xEcd0D12E21805803f70de03B72B1C162dB0898d9"
        tokenId={641}
      />
      <Connector>
        <ConnectButton />
      </Connector>
      <CallTest />
    </WagmiWeb3ConfigProvider>
  );
}

其中配置了 eip6963 使得使用通过 EIP6963 协议连接钱包,避免了多个钱包之间可能出现的冲突。另外添加了 autoAddInjectedWallets 配置使得自动添加检测到的钱包到 Ant Design Web3 的 UI 中,提升用户体验,让用户可以自由选择他已经安装的钱包。

总结

不管是 EIP1193 还是 EIP6963,它们都是通过浏览器的 JavaScript API 来和钱包进行交互的。它要求钱包可以向 DApp 的运行时注入对象或者发送事件,比如通过 Chrome 浏览器插件,或者你在钱包内置的浏览器中访问 DApp。

但是对于有的场景,用户没有安装插件,或者是在移动端浏览器访问 DApp,无法使用插件。又或者用户需要用其他手机安装的钱包客户端来连接 DApp。不管是 EIP1193 还是 EIP6963,都无法满足这些场景。所以,我们还需要其他的方式来连接钱包,比如 WalletConnect