区块链 Web3 钱包 Provider是如何实现的, 定制Provider, 实例 + js 拆解代码

602 阅读4分钟

前言介绍

前端应该调用哪个库好?

个人推荐etherjs, 因为现在大多数都是使用这个库来跟链上交互, 更新相对比较快, 社区活跃.

如果你还是无法确定使用哪个,可以看看一下官网的介绍以及api, 也可以混搭一起使用

web3js 与 etherjs

web3js: 允许开发人员通过 HTTP、IPC 或 WebSocket 连接与本地或远程以太坊节点(或任何与 EVM 兼容的区块链)进行交互

etherjs: 实现了读写分离, 是目前更流行, 更安全的连接以太坊方式, 查看etherjs v5文档

Web3.jsEthers.js 是两个常用于与以太坊区块链进行交互的JavaScript库

Ethers.js 更加轻量、模块化、易用,适合前端和轻量级应用。

Web3.js 功能更为全面,但相对复杂,适合需要大量以太坊交互功能的场景。

EIP 标准协议

EIP 代表 Ethereum Improvement Proposal(以太坊改进提案)。

它是以太坊平台上推荐使用的标准和协议的统称。 它所包含的具体标准和协议涉及以太坊的核心协议、客户端API、智能合约标准等。 每一个EIP包含对某个标准或协议的定义。

provider

provider: 一个用于连接以太坊网络的抽象类,提供了只读形式来访问区块链网络和获取链上状态, dapp中的一个常见约定是钱包通过网页中一个JavaScript对象公开其API。该对象称为“provider”。

背景: 过去由于不同钱包提供的接口方法各不相同, dapp需要对接不同钱包的api非常麻烦, 比如要签名时, 需要针对不同钱包写多套签名过程

因此, EIP-1193 标准化了钱包Provider API, 使得dapp可以操作不同的钱包, 对钱包抽象化

由于业务需求需要通过应用层数据跟区块链信息进行交互, 在调用provider时不仅跟链上交互, 还需要跟应用层进行数据的交互. 因此我们根据标准重写签名方法, 这样在使用不同provider既可以使用我们的提供的provider, 也不会影响其他提供者的provider

Provider设计方案

大部分provider封装的方法应基于标准化的以太坊json-rpc

常见的 provider 有JsonRpcProvider 和 IpcProvider 允许连接到控制或可以访问的以太坊节点。

综合以上所述, 我们选择JsonRpcProvider进行重写和定制.

重写其中api的思路: 调用eth_signTypedData_v4时, 对多签进行分片, 分片之后的数据需要存储和读取, 这时候就需要嵌入接口来存分片.

etherjs提供了多种provider的基底, 根据情况进行选择

这里帮大家找出各自库提供的provider.

etherjs - provider
web3js - provider

如果你不熟悉如何使用 provider, 这里有个入门教程 WTF - Provoder

image.png

代码实现

参考案例: (看看大佬是怎么实现的)

这里有两个例子, 挑个重点代码片段解读一下.

例1: cloud-cryptographic-wallet 是一组用于将加密库与各种云服务的密钥管理系统连接起

image.png

按照这个思路, 我们可以找到一个合适的provider作为基底, 再根据我们需要重写的api写入新的provider, 再将这个新的provider丢进基底形成新的provider, 既满足业务场景的需求, 又符合市面上provider规范.

基底: new ethers.providers.Web3Provider( externalProvider [ , network ] ), externalProvider 就是我们需要实现代码逻辑

import { EventEmitter } from 'events'
import { BigNumber, ethers } from 'ethers'

class NewProvider extends EventEmitter {
  constructor(rpcUrl) {
    super()
    this.wallet = null

    // Initialize with JsonRpcProvider using the custom RPC URL
    this.ethersProvider = new ethers.providers.JsonRpcProvider(rpcUrl)
    // Placeholder for the current account and chain ID
    this.accounts = []
    this.currentAccount = null
    this.chainId = null

    // Bind this context to methods
    this.request = this.request.bind(this)


  }

  // EIP-1193 request method
  // eslint-disable-next-line complexity
  async request({ method, params }) {
    switch (method) {
      case 'eth_accounts':
        return this.accounts || []
      case 'eth_requestAccounts':
        await this.connect()
        return this.accounts || []
      case 'eth_chainId':
        return this.handleEthChainId()
      case 'eth_getBalance':
        return this.getBalance(params)
      case 'eth_signTransaction':
        return this.signTransaction(params)
      case 'dtt_multiSignTransaction':
        return this.multiSignTransaction(params)
      case 'eth_sendTransaction':
        return this.sendTransaction(params)
      case 'eth_signTypedData_v4':
        return this.signTypedData(params)
      default:
        if (this.ethersProvider[method]) {
          this.ethersProvider.send(method, params)
        } else {
          throw new Error(`Unsupported method: ${method}`)
        }
    }
  }

  // Method to trigger events
  triggerEvent(eventName, data) {
    this.emit(eventName, data)
  }
  // Method to connect to the provider
  async connect() {
    try {
      this.chainId = await this.handleEthChainId()
      this.wallet = {} // { ... }
      this.accounts = [] // [...]
      this.currentAccount = this.accounts[0]
      this.triggerEvent('accountsChanged', this.accounts)
      this.triggerEvent('chainChanged', this.chainId)
    } catch (error) {
      this.triggerEvent('disconnect', { error })
    }
  }
  // 返回节点所控制的账户列表
  async listAccounts() {
    return await this.ethersProvider.listAccounts()
  }
  // Implementations of various eth_ methods
  async handleEthChainId() {
    const network = await this.ethersProvider.getNetwork()
    const chainId = `0x${network.chainId.toString(16)}`
    return chainId
  }

  async getGasPrice() {
    const gasPriceMultiplier = 1
    const gasPrice = await this.ethersProvider.getGasPrice()
    const baseGasPrice = Math.pow(10, 9)
    return Math.ceil(Math.max(baseGasPrice, gasPrice.toNumber()) * gasPriceMultiplier).toString()
  }


  async getBalance([address, blockTag]) {
    const balance = await this.ethersProvider.getBalance(address, blockTag)
    return balance.toString()
  }

  async signTransaction(tx) {}

  async signTypedData() {}
}

export default NewProvider

  • gasPrice、estimateGas、chainId 、getBalance、getTransactionCount等与节点交互

    这类交互直接复用JsonRpcProvider方法,如有需要在此基础上进行进制转换等简单操作

  • signTransaction: 基础签名方法,对交易进行签名

  • signTypedData: 特有配合多钱使用的签名方法

    对typeData进行签名,在Provider层不对typeData的终签及非终签进行区分

ethers已经提供了 Web3Provider 且是EIP-1193, 我们就不用实现更多复杂的逻辑,直接套娃

把定制NewProvider丢进Web3Provider

async connect() {
  this.provider = new NewProvider(this.rpcUrl)
  this.web3Provider = new ethers.providers.Web3Provider(this.provider)
  this.signer = this.web3Provider.getSigner()
  await this.provider.connect()
  this.provider.on('disconnect', (val) => {
    Message.error(val.error.message)
    throw val.error
  })
}

以上只是一个雏形Provider, 还有类似v, r, s需要根据业务场景来实现.

参考文档

ethers.js 中文文档 (learnblockchain.cn)