区块链Dapp前端开发常用技能点整理

前端开发者如何开发区块链DAPP

Posted by Warden_Gfs on August 17, 2022

区块链技术的快速发展带来了去中心化应用(DApp)的广泛应用,而作为前端开发者,掌握与区块链交互的技能显得尤为重要。本篇文章将系统性地整理区块链DApp开发中常用的前端技术点,涵盖以太坊生态的核心工具及标准,例如 ethers.js、web3.js、EIP-4361 和 EIP-1193 等。文章旨在帮助开发者快速上手区块链开发,并掌握与钱包交互、调用智能合约等关键技能点。

在开发过程中,我们会遇到诸如如何连接钱包、如何签名、如何调用智能合约等场景。通过本文的内容,希望能让大家从基础到进阶,逐步掌握这些技能,为开发高效、安全的DApp奠定基础。

JS库选型

这里主要对比web3.js和ethers.js,web3.js比较基础,很多地方都可以使用,不仅仅是前端,比如一些插件,工具等,调用较为繁琐,在我看来ethers.js更加容易上手一点。如果作为前端的我们只需要配合区块链开发人员完成DAPP的话,熟练掌握连接钱包,发起交易,调用合约基本上就够了(如果对自己没有更高要求的话:p)。选择ethers.js的优点如下:ethers.js更加简洁明了,或者说调用方式对前端更加友好;文档和生态也更加丰富;体积也相对较小等。如果你已经熟悉这里面的来龙去脉了,我推荐使用web3-react这个库,把一些常用的钱包操作封装得更加简单了,我准备下一期再和大家聊一聊。

连接钱包

说是连接钱包,但我们实际上应该是说连接以太坊比较正确。在开发时我们主要是用了ethers.js的各种API,同时我们会借助@metamask提供的一些帮助来参与开发。

provider

在dapp的开发过程中我们会经常看到这个词,其面向只读操作,翻译成英文叫做提供者,充当以太坊网络的匿名连接,主要用于查询、检查状态或发送更改状态的交易。我们可以选择不同的provider来连接网络。常用的有JsonRpcProvider、Web3Provider、BaseProvider等,以及其他的还有 EtherscanProvider、InfuraProvider、FallbackProvider、IpcProvider、HttpProvider等,我们在使用不同的provider之前需要安装引入依赖,这会导致构建结束的包增大,因此我们需要考虑是否有必要支持更多的provider。

示例代码

import {
  JsonRpcProvider,
  Web3Provider,
  BaseProvider,
} from "@ethersproject/providers";

// metamask提供的,可以直接使用detectEthereumProvider
import detectEthereumProvider from "@metamask/detect-provider";
const currentProvider = await detectEthereumProvider();
...
let currentProvider: JsonRpcProvider | BaseProvider;
// getDefaultProvider() 这是默认推荐的连接以太坊网络的方法
if (ChainInfo.mainnet.network) {
  currentProvider = ethers.getDefaultProvider(ChainInfo.mainnet.network);
} else {
// JsonRpcProvider() 这是通过节点 urlorInfo 的 JSON-RPC API URL 进行连接
  currentProvider = new ethers.providers.JsonRpcProvider(
    ChainInfo.mainnet.rpc
  );
}

上面提到的JsonRpcProvider是实际开发中比较常用的,它需要传入一个rpc地址,那什么是rpc?全称是Remote Procedure Calls, 他是一种远程过程调用协议,简单来说,就是从一台机器(客户端)传一些参数来调用另一台机器(服务端)上的函数或方法并取得结果。一般有两种,XML-RPC和JSON-RPC,在这里我们经常采用JSON-RPC的方式,JsonRpcProvider的参数需要传入一个rpc地址:https://bsc-dataseed.binance.org/ 例如币安智能链rpc。

signer

什么是signer?它是以太坊账户的一个抽象,可以用于签署消息和交易,并将签名交易发送到以太坊网络以执行状态更改操作。现在我们已经创建了Web3Provider实例了,它可以应付一般的只读操作。我们在DAPP的项目中经常会需要发起交易或者签名,比如区块链团队之前做的yolofox或者metalist等。这时我们需要通过 provider.getSigner() 获取singer来完成剩下的操作,如连接合约。 获取signer示例代码

 // xxx
const provider: Web3Provider = new ethers.providers.Web3Provider(window.ethereum);
const signer: Signer = provider.getSigner();
return signer;
// xxx

如何连接合约,使用合约

使用new ethers.Contract()来连接合约,对于合约变量的访问有两种方式,只读和读写的方式,示例代码,区别在于,一个传provider,一个传signer。

// 只读
contract = new ethers.Contract(contractAddress,contractAbi,provider)
// 读写
contract = new ethers.Contract(contractAddress,contractAbi,signer)

连接到合约之后,我们就可以调用合约内的方法,对,就像你熟悉的调接口一样调用,这里需要和我们的合约开发人员约定好,需要传的参数,怎么调用等等。代码示例:

xxx
// 先连接一波合约
 const contract = new ethers.Contract(
      ChainInfo.mainnet.contractAddress,
      TomCatABI.abi,
      signer
    );
// 一个查询剩余数量的合约方法
const availableCount = await contract.numberMinted(account);
// 一个调用合约上交易的方法
const tx = await contract.buy(amount, { value: ethers.utils.parseEther(currentPrice) })
xxx

我们还可以监听合约上的一些事件,比如用户购买了token,合约上会触发一个事件记录一个log,这个log,比如说就是我们要监听的Transfer事件,事件里有描述谁买了多少个。我们监听这个事件,发现有变化就去重新调合约拉去剩余的token数量,代码大致如下

const handleOnEvent = () => {
    try {
      contract.on("Transfer", () => {
        toDoSomething();
      });
    } catch (error) {
      console.log(error);
    }
  };

上述连接合约时需要传一个叫ABI的东西(上述代码中:contractAbi),没有DAPP开发经验的同学可能不太清楚,那这个ABI是什么呢?他就是应用程序二进制接口,有点类似于我们常见的API-应用程序接口,API 是代码接口的可读表示。ABI 定义了用于与二进制合约交互的方法和结构,就像 API 一样,但在较低的级别上。 ABI长这样

xxx
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": true,
          "internalType": "address",
          "name": "from",
          "type": "address"
        },
        {
          "indexed": true,
          "internalType": "address",
          "name": "to",
          "type": "address"
        },
        {
          "indexed": true,
          "internalType": "uint256",
          "name": "tokenId",
          "type": "uint256"
        }
      ],
      "name": "Transfer",
      "type": "event"
    },
xxx

在连接合约的过程中,合约、RPC、节点之间有什么关系?我简单画了个图,大致如下。

合约、RPC、节点之间有什么关系

合约部署是一笔交易,交易会被矿工节点广播到所有节点,交易被打包入块,块里的所有交易会在所有节点上执行一遍来改变本节点记录的状态。于是,一笔成功执行的合约部署交易,就可以在所有节点上访问合约的状态,我们前端在开发的过程中可以通过RPC来调用合约内的方法。

如何获取钱包地址和链ID

这里我们主要讲一下常用的window.ethereum,这是metamask注入的全局的,我们可以通过它获取我们想要的东西,比如我们可以通过window.ethereum.request()来进行一些操作

// 获取当前的区块链地址
const accounts = await window.ethereum.request({ method: "eth_requestAccounts", });
// 获取当前的链Id
const chainId = await window.ethereum.request({  method: "eth_chainId", });
// 切链
await window.ethereum.request({
        method: "wallet_switchEthereumChain",
        params: [{ chainId: "0x1" }],
      });
// 签名,这里签名需要传一个msg,是一个字符串,在签名的弹窗里面会展示
await window.ethereum.request({
        method: "personal_sign",
        params: [msg, account],
      })
// 添加一个链
await window.ethereum.request({
        method: "wallet_addEthereumChain",
        params: [AddChainList[chainId]],
      });

目前开发时主要用到的应该就这几个

上述签名时传入的msg,具体的生成规范可以参阅EIP-4361: Sign-In with Ethereum,我在具体开发的过程中参考了该规范,可以有所不同。这里的EIP 指的是Ethereum Improvement Proposals,他是以太坊改进的提案,描述了以太坊的标准,在一次次的提案中我们可以看到这个东西其实是一直在进步的。

在具体的调用过程中,我们可能会遇见一些常见的错误,我们可以在try catch里面捕获这些错误,我们可以捕获这些错误做一些友好的提示 错误可能是:

4001User Rejected Request -The user rejected the request.
用户拒绝了请求
4100Unauthorized-The requested method and/or account has not been authorized by the user.
请求的方法或账户未获得用户授权
4200Unsupported Method-The Provider does not support the requested method.
提供的程序不支持请求的方法
4900Disconnected-The Provider is disconnected from all chains.
提供程序已端口与所有链的连接
4901Chain Disconnected-The Provider is not connected to the requested chain.
提供程序未连接到请求的链

我们还可以通过 ethereum.isMetaMask 来判断当前的钱包的类型,例如:

// 判断 MetaMask 的环境
export function isMetaMask() {
  return Boolean(window.ethereum && window.ethereum.isMetaMask);
}
// 判断 TokenPocket 环境
export function isTokenPocket() {
  return Boolean(window.ethereum && window.ethereum.isTokenPocket);
}

那么我们可以试着打印window.ethereum来看看到底有什么东西

window.ethereum

更多内容可以去EIP-1193: Ethereum Provider JavaScript API看看

监听事件

  • on,对特定事件添加监听器,如果没有移除事件监听器,会继续监听后续事件。
    ethereum.on(chainChanged, handleChainChanged);
    ethereum.on(accountsChanged, handleAccountsChanged);
    
  • off,移除某事件全部监听器。
  • once,添加事件监听器,并且在事件处理完成后自动移除。它和 on 的区别在于:once 处理完当前事件之后,不再监听,对后续到来的事件不再处理;

大数的概念

在做区块链交易的时候,我们会遇到大数的概念,什么是大数:BigNumber它是一个对象,它可以安全的允许对任何数量级的数字进行数学运算。为什么要使用大数,这里就涉及到JS对于数字的处理方式,它采用的是IEEE-754 64位双精度浮点数存储,这个数字范围大概是-1.79E+308 ~ +1.79E+308,这种模式在处理高精度的运算时,会出现精度丢失的问题,比如0.1+0.2=0.30000000000000004。而在区块链交易中,经常会出现非常小非常小的数,所以引入了BigNumber这个概念。 因为以太坊的精度是18,所以我们可以通过ethers.utils.parseEther(xxx)来处理合约希望我们传的数据为精度18的数。 查看TS源码我们可以看到formatUnits()方法,所以如果有需要,我们可以通过ethers.utils.parseUnit(“0.02”, 10)来自定义精度

export function formatEther(wei: BigNumberish): string {
    return formatUnits(wei, 18);
}
export function parseEther(ether: string): BigNumber {
    return parseUnits(ether, 18);
}

如果觉得麻烦,我们还可以通过引入decimal.js来处理精度的问题,示例代码:

 const computedPrice = () => {
    let currentAmount = 1;
    if (amount) currentAmount = amount;
    if (unitPrice) {
      const price = new Decimal(unitPrice);
      const current = price.mul(currentAmount);
      setCurrentPrice(`${current}`);
    }
  };

后记

区块链技术可以帮我们解决很多问题,上面只是很小的一部分。在后续的开发过程中,警醒自己还是要持续保持学习,敬畏的心态,与大家共勉,后面我会继续分享整理区块链+前端相关的内容,祝好。

参考文献