意见箱
恒创运营部门将仔细参阅您的意见和建议,必要时将通过预留邮箱与您保持联络。感谢您的支持!
意见/建议
提交建议

web3 DApp 开发指南

来源:恒创科技 编辑:恒创科技编辑部
2024-01-28 07:45:59
前置知识对以太坊的基本概念有初步认识掌握 react 相关知识了解 typescript 语法了解 ethers.js 库了解 web3js 库了解 web3-react 库为什么是 React 不是 Vue ?

区块链领域在 react 里面有一个很优秀的库叫 web3-react,还有一个很酷的连接钱包的react连接UI的库叫 web3modal,连接的过程不需要我们操作。这个两个库都已经在最大的交易网站上面使用了,除了这些优秀的库,由于 react 的生态在国外本身就是就发展得比其他框架更加蓬勃,所以导致所有知名的区块链行业代码都是使用 react

web3js ehthers web3-react 区别与联系基本概念web3js

Web3 是一个用于与以太坊区块链以及运行以太坊虚拟机的其他区块链进行通信的库,包括AvalancheBinance Smart chainSolana

web3.js 有一个主类,称为 web3。在该类中可以找到该库的大多数功能。组成 web3js 的另外 5 个模块分别是:

web3-eth : 使 web3.js 的用户可以与以太坊区块链进行交互,比如:

web3.eth.getBalance 的作用是获得指定区块的某个地址的以太坊余额web3.eth.signTransaction 的作用是对交易签名web3.eth.sendSignedTransaction 的作用是将签名的交易发送到以太坊区块链。web3-shh : 使你可以与 Whisper 协议进行交互。Whisper 是一个消息传输协议,其目的是轻松广播消息以及进行低层异步通信。web3-bzz : 使你可以与 Swarm 交互。Swarm 是一个去中心化存储平台和内容分发服务,它可以用来为去中心化应用存储图片或视频等文件。

web3-net : 使你可以与以太坊节点的网络属性进行交互。

web3.*.net.getID 返回网络 IDweb3.*.net.getPeerCount 返回连接到节点的对等点数

web3-utils :提供实用程序函数,这些函数可在以太坊去中心化应用以及其他 web3.js 模块中使用。实用程序函数可以重复使用,使代码编写更轻松,在 JavaScript 和其他编程语言中很常见。Web3-utils 包含实用程序函数,这些函数用于转换数字、验证值是否满足特定条件以及搜索数据集。

web3.utils.toWei 将以太转换为 Weiweb3.utils.hexToNumberString 将十六进制值转换为字符串web3.utils.isAddress 校验特定字符串是否为有效的以太坊地址web3-react

web3-react 是流行的库 Web3 的一个很好的 React 实现

ehthers.js

Ethers.js 是一个 JavaScript 库,其作用是使开发者可以与以太坊区块链进行交互。该库包含 JavaScriptTypeScript 中的实用程序函数,以及以太坊钱包的所有功能。Ethers.js 是通过 Ethers 创建的,是采用 MIT 许可证的开放源。

web3.js 相似,ethers.js 有四个模块,构成应用程序编程界面 (API)。

Ethers.provider : 封装与以太坊区块链的连接。它可以用于签发查询和发送已签名的交易,这将改变区块链的状态。

ethers.providers.InfuraProvider 的作用是使你可以与 Infura 托管的以太坊节点网络建立连接ethers.provider.getBalance 将为你获取区块链中某个地址或区块的以太坊余额ethers.provider.resolve 将解析传递到以太坊地址的以太坊名称服务 (ENS) 名称

注:web3.js 也有服务于此目的的提供商,位于 web3 基础模块中。Ethers.jsweb3.js 的组织方式截然不同,因此尽管两个库的功能非常相似,但模块间并非总是能清晰对应。

Ethers.contract : 部署智能合约并与它交互,该模块中的函数用于侦听从智能合约发射的事件、调用智能合约提供的函数、获取有关智能合约的信息,以及部署智能合约。

Ethers.utils :提供用于格式化数据和处理用户输入的实用程序函数。Ethers.utils 的作用方式与 web3-utils 相似,能够简化去中心化应用的构建流程。

ethers.utils.getContractAddress 从用于部署智能合约的交易中提取智能合约地址ethers.utils.computeAddress 通过传递与地址相关的公钥或私钥的函数来计算地址 ethers.utils.formatEther 将所传递的 Wei 金额转换为 Ether 十进制字符串格式

Ethers.wallets : Ethers.wallet 提供的功能与我们目前讨论过的其他模块截然不同。Ethers.wallet 的作用是使你可以与现有钱包(以太坊地址)建立连接、创建新钱包以及对交易签名。

ethers.wallet.createRandom 将创建随机新账户ethers.wallet.sign 将对交易签名并将已签名的交易返回为十六进制字符串的形式ethers.wallet.getBalance 将为我们提供钱包地址的以太坊余额为什么要引入这些库?

钱包连接其实也可以直接用 web3meta mask 提供的方法写,但是这样有一个问题是需要考虑很多场景和多钱包,这样导致代码量很大并且问题可能很多。

区别

ehthers.jsweb3.js不同的是,ethers.js 在使用时不需要过多的回调函数,而且可以搭配 Hardhat 工具使的语法得到进一步的优化。

联系

三者 都是 JavaScript 库,其作用是使开发者可以与以太坊区块链交互。这两个库都很实用,都能满足大多数以太坊开发者的需求。web3-reactWeb3js 的一个很好的 React 实现。

基础能力封装WrappedWeb3ReactProvider

全局注入 web3 实例,在组件里通过 liabrary 获取实例

import { Web3Provider } from '@ethersproject/providers'
import { Web3ReactProvider } from '@web3-react/core'
import React from 'react'

// 获取web3实例的library
function getLibrary(provider: any): Web3Provider {
  const library = new Web3Provider(provider)
  // library.pollingInterval = 12000
  return library
}

function WrappedWeb3ReactProvider({ children }: { children: JSX.Element }) {
  return (
    <Web3ReactProvider getLibrary={getLibrary}>{children}</Web3ReactProvider>
  )
}

export default WrappedWeb3ReactProvider
Connector
import { InjectedConnector } from '@web3-react/injected-connector'

export const injected = new InjectedConnector({
  // 支持的链ID
  // supportedChainIds: [56]
})
Contract
import Web3 from 'web3'

/**
 * usage
 const contract = getContract(library, abi, address)
 contract.methods
 .exit()
 .send({
    from: account,
    })
 .on('transactionHash', (hash) => {
  })
 */

// ethers.Contract(address, abi, library.provider.singer)
export const getContract = (library: any, abi: any, address: string) => {
  const web3 = new Web3(library.provider)
  return new web3.eth.Contract(abi, address)
}
Switch network

下例中仅支持 BSC

import Web3 from 'web3'

const BSC_CHAIN_ID = 56

export const changeToBscNetwork = async (
  library: any,
  onError?: () => void
) => {
  try {
    await library.provider.request({
      method: 'wallet_switchEthereumChain',
      params: [{ chainId: Web3.utils.toHex(BSC_CHAIN_ID) }]
    })
  } catch (error: any) {
    if (error.code === 4902) {
      try {
        library.provider.request({
          jsonrpc: '2.0',
          method: 'wallet_addEthereumChain',
          params: [
            {
              chainId: '0x38',
              chainName: 'Binance Smart Chain Mainnet',
              rpcUrls: ['https://bsc-dataseed.binance.org/'],
              nativeCurrency: {
                name: 'BNB',
                symbol: 'BNB',
                decimals: 18
              },
              blockExplorerUrls: ['https://bscscan.com']
            }
          ],
          id: 0
        })
      } catch (e) {
        console.error('changeNetwork addEthereumChain error', e)
      }
    }
    onError?.()
    console.error('changeNetwork error', error)
  }
}
常见 API

文档链接

wallet_addEthereumChain

添加网络,切换网络时,错误返回 code 为4902时表示该网络未添加,下面以添加 bsc 到钱包网络中为例:

library.provider.request({
  jsonrpc: '2.0',
  method: 'wallet_addEthereumChain',
  params: [
    {
      chainId: '0x38',
      chainName: 'Binance Smart Chain Mainnet',
      rpcUrls: ['https://bsc-dataseed.binance.org/'],
      nativeCurrency: {
        name: 'BNB',
        symbol: 'BNB',
        decimals: 18
      },
      blockExplorerUrls: ['https://bscscan.com']
    }
  ],
  id: 0
})
networkChanged

监听网络变化

library?.provider.on('networkChanged', (e: any) => {
  // 切换网络后,尝试连接
  console.log('networkChanged', e)
})
wallet_switchEthereumChain

切换网络,代码参考基础能力封装的 Switch network 部分代码

eth_sendTransaction

拉起钱包签名交易

const params = [
  {
    from: account,
    to: CONTRACT,
    gas: web3.utils.toHex('76597'),
    gasPrice: web3.utils.toHex(web3.utils.toWei('5', 'gwei')),
    value: web3.utils.toHex(web3.utils.toWei(String(value))),
    data
  }
]

library.provider
  .request({
  method: 'eth_sendTransaction',
  params
})
  .then((result: any) => {
  console.log('Tx:', result)
})
  .catch((error: any) => {
  console.error(error.message)
})
  .finally(() => {
  setLoading(false)
})
项目实战项目介绍

模拟 PancakeSwap 实现一个连接 MetaMask 钱包并能实现在 BSC 链上交易的功能。

新建react项目
$ create-react-app web3-dapp-demo --template typescript
$ cd web3-dapp-demo
$ yarn start
配置环境

引入 web3-reactweb3jsehthers.js 等库后会报类似于 Uncaught ReferenceError: process is not defined 的错误,视情况配置,如果没报错则忽略此配置

// config-overrides.js
// 先安装对应的依赖 url、fs、assert...

webpack: override(
  (config, env) => {
    config.resolve.fallback = {
      url: require.resolve('url'),
      fs: require.resolve('fs'),
      assert: require.resolve('assert'),
      crypto: require.resolve('crypto-browserify'),
      http: require.resolve('stream-http'),
      https: require.resolve('https-browserify'),
      os: require.resolve('os-browserify/browser'),
      buffer: require.resolve('buffer'),
      stream: require.resolve('stream-browserify')
    }
    config.plugins.push(
      new webpack.ProvidePlugin({
        process: 'process/browser',
        Buffer: ['buffer', 'Buffer']
      })
    )
    return config
  }
配置项目入口文件

如果项目用到了 antv ,使用 react18 提供的 ReactDOM.createRoot方式挂在 App 会让 antv 重复渲染两个一模一样的图表。

ReactDOM.render(
  <React.StrictMode>
    <Router>
      <Suspense fallback={<HomePage />}>
        <WrappedWeb3ReactProvider>
          <App />
        </WrappedWeb3ReactProvider>
      </Suspense>
    </Router>
  </React.StrictMode>,
  document.getElementById('root')
)

/**
 * react18 这种挂载方式会让antv重复渲染两个一模一样的图表,待官方更新,暂时先用回react17的方式渲染页面
 */
// root.render(
//   <React.StrictMode>
//     <Router>
//       <Suspense fallback={<HomePage />}>
//         <WrappedWeb3ReactProvider>
//           <App />
//         </WrappedWeb3ReactProvider>
//       </Suspense>
//     </Router>
//   </React.StrictMode>
// )
编写HomePage视图页面

因为我们在入口文件已经把web3 实例注入到 Provider 了,所以在项目任一组件里都能拿到它,如需获取 window.ethereum 全局 API 可以使用 从liabrary.provider 代替,window.ethereumMetaMask 插件注入到全局的一个对象,用于请求连接账户、获取用户连接的链的数据(如交易后的返回值和事件)、以及显示用户对交易的签名状态。 Provider 的存在可以显示以太坊用户的对象。

import { useEffect, useState, type FC } from 'react'
import { useWeb3React } from '@web3-react/core'

import { BSC_CHAIN_ID } from '@common/constants'

import CheckNetwork from './components/CheckNetwork'
import SwapForm from './components/SwapForm'
import styles from './index.module.scss'

const HomePage: FC = () => {
  const [visible, setVisible] = useState(false)
  const { library } = useWeb3React()

  useEffect(() => {
    library?.provider.on('networkChanged', (e: any) => {
      // 切换网络后,尝试连接
      console.log('networkChanged', e, e === String(BSC_CHAIN_ID))
      setVisible(e !== String(BSC_CHAIN_ID))
    })
  }, [])

  return (
    <div className={styles.container}>
      {/* 交易Swap表单ui */}
      <SwapForm />
      {/* 检查当前网络是否是bsc,如果不是则断开连接或者switch到bsc */}
      <CheckNetwork visible={visible} onClose={() => setVisible(false)} />
    </div>
  )
}

export default HomePage
Swap 表单

BNB 兑换 USDT 为 1 : 5000

USDT 是我们自己部署的一个合约,只是名字跟他一样,此用于开发测试用

big.js 用于大数据的比较、计算使用

import { useWeb3React } from '@web3-react/core'
import Big from 'big.js'
import { type ChangeEvent, type FC, useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import Web3 from 'web3'

import arrowDown from '@assets/images/arrow-down.png'
import contractAbi from '@common/abi/contractABI.json'
import { BSC_CHAIN_ID, CONTRACT } from '@common/constants'
import {
  changeToBscNetwork,
  cutZero,
  getContract,
  injected
} from '@common/utils'
import { useBalance } from '@common/hooks'

import styles from './index.module.scss'

const SwapForm: FC = () => {
  const [loading, setLoading] = useState(false)
  const { active, activate, library, account, chainId, deactivate } =
    useWeb3React()

  const web3 = new Web3(library?.provider)

  const {
    register,
    setValue,
    getValues,
    handleSubmit,
    formState: { errors }
  } = useForm()

  const balance = useBalance()

  // 监听网络变换,不是bsc就切换到bsc,否则断开连接
  useEffect(() => {
    if (chainId && chainId !== BSC_CHAIN_ID) {
      try {
        changeToBscNetwork(library, () => {
          deactivate()
        })
      } catch (ex) {
        console.log(ex)
      }
    }
  }, [chainId])

  const onBnbChange = (e: ChangeEvent<HTMLInputElement>) => {
    if (e.target.value) {
      if (new Big(e.target.value).mul(10e8).lt(10)) {
        setValue('usdt', null)
      } else {
        const val = cutZero(new Big(e.target.value).mul(5000).toFixed(8))
        setValue('usdt', !Number.isNaN(val) ? val : null)
      }
    } else {
      setValue('usdt', null)
    }
  }
  const onUsdtChange = (e: ChangeEvent<HTMLInputElement>) => {
    if (e.target.value) {
      if (new Big(e.target.value).div(5000).mul(10e8).lt(10)) {
        setValue('bnb', null)
      } else {
        const val = cutZero(new Big(e.target.value).div(5000).toFixed(8))
        setValue('bnb', !Number.isNaN(val) ? val : null)
      }
    } else {
      setValue('bnb', null)
    }
  }
  const run = async (value: number) => {
    setLoading(true)
    const myContract = getContract(library, contractAbi, CONTRACT)
    /**
     * 交易数据大部分都是写死的,参考给的示例: https://bscscan.com/tx/0x97af78f98cb6106314d19822c3e3782eba0e1945e0d45fb2193d0bfea5471094
     */
    const pairAddress = [
      '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c',
      '0x55d398326f99059fF775485246999027B3197955'
    ]
    const deadline = Math.floor(Date.now() / 1000) + 60 * 10 // this represents 10 mins of deadline
    // 调用合约,读取数据
    const data = await myContract.methods
      .swapExactETHForTokens(0, pairAddress, account, deadline)
      .encodeABI()

    const params = [
      {
        from: account,
        to: CONTRACT,
        gas: web3.utils.toHex('76597'),
        gasPrice: web3.utils.toHex(web3.utils.toWei('5', 'gwei')),
        value: web3.utils.toHex(web3.utils.toWei(String(value))),
        data
      }
    ]

    // 拉起钱包签名交易
    library.provider
      .request({
        method: 'eth_sendTransaction',
        params
      })
      .then((result: any) => {
        console.log('Tx:', result)
      })
      .catch((error: any) => {
        console.error(error.message)
      })
      .finally(() => {
        setLoading(false)
      })
  }

  const onCollect = (e: any) => {
    e.preventDefault()
    try {
      activate(injected)
    } catch (e) {
      console.log(e)
    }
  }

  const onSwap = () => {
    run(getValues().bnb)
  }

  return (
    <div className={styles.container}>
      <div className={styles.header}>
        <div className={styles.title}>Swap</div>
        <div className={styles.subTitle}>Trade tokens in an instant</div>
      </div>
      <form
        onSubmit={handleSubmit(() => {
          onSwap()
        })}
      >
        <label>
          <img
            src="https://assets.blocksec.com/image/1663207399693-2.png"
            alt=""
          />
          <span>BNB</span>
        </label>
        <input
          {...register('bnb', {
            required: true,
            validate: (val: any) => {
              if (/^(0|[1-9]\d*)(\.\d+)?$/.test(val)) {
                if (new Big(val).gt(new Big(balance ?? 0))) {
                  return 'Insufficient BNB balance'
                }
                return true
              }
              return false
            }
          })}
          onChange={onBnbChange}
        />
        {errors.bnb && (
          <p className={styles.errorValidTip}>
            {(errors.bnb as any)?.message || 'Can not be empty'}
          </p>
        )}
        <img src={arrowDown} alt="" className={styles.arrowDownIcon} />
        <label>
          <img
            src="https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xdAC17F958D2ee523a2206206994597C13D831ec7/logo.png"
            alt=""
          />
          <span>USDT</span>
        </label>
        <input
          {...register('usdt', {
            required: true,
            pattern: {
              value: /^(0|[1-9]\d*)(\.\d+)?$/,
              message: 'Please input the number'
            }
          })}
          onChange={onUsdtChange}
        />
        {errors.usdt && (
          <p className={styles.errorValidTip}>
            {(errors.usdt as any)?.message || 'Can not be empty'}
          </p>
        )}
        <div className={styles.displayItem}>
          <div className={styles.label}>Price</div>
          <div className={styles.value}>0.0002 BNB per USDT</div>
        </div>
        <div className={styles.displayItem}>
          <div className={styles.label}>Slippage Tolerance</div>
          <div className={styles.value}>0.5%</div>
        </div>
        {active ? (
          <button
            disabled={loading}
            type="submit"
            className={styles.primaryBtn}
          >
            <p>Swap</p>
            <p>(Current Solution)</p>
          </button>
        ) : (
          <div className={styles.primaryBtn} onClick={onCollect}>
            Connect Wallet
          </div>
        )}
      </form>
    </div>
  )
}

export default SwapForm
项目成果

ui效果展示:

image-20220918160739000

连接钱包:

image-20220918161021422

切换网络:

image-20220918161224538

拉起钱包签名效果展示:

image-20220918160854568

总结

web3-react 这个库目前还在开发中,最新的 beta 版本支持了多种钱包的 Connector 连接方式,core 包改动也比较大,等正式版本发布了再更新一期最新的使用教程,至此,

上一篇: 前端周刊第三十六期 下一篇: 手机怎么远程登录云服务器?