乐趣区

关于javascript:web3-DApp-开发指南

前置常识

  1. 对以太坊的基本概念有初步意识
  2. 把握 react 相干常识
  3. 理解 typescript 语法
  4. 理解 ethers.js
  5. 理解 web3js
  6. 理解 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 个模块别离是:

  1. web3-eth : 使 web3.js 的用户能够与以太坊区块链进行交互,比方:

    1. web3.eth.getBalance 的作用是取得指定区块的某个地址的以太坊余额
    2. web3.eth.signTransaction 的作用是对交易签名
    3. web3.eth.sendSignedTransaction 的作用是将签名的交易发送到以太坊区块链。
  2. web3-shh : 使你能够与 Whisper 协定进行交互。Whisper 是一个音讯传输协定,其目标是轻松播送音讯以及进行低层异步通信。
  3. web3-bzz : 使你能够与 Swarm 交互。Swarm 是一个去中心化存储平台和内容散发服务,它能够用来为去中心化利用存储图片或视频等文件。
  4. web3-net : 使你能够与以太坊节点的网络属性进行交互。

    1. web3.*.net.getID 返回网络 ID
    2. web3.*.net.getPeerCount 返回连贯到节点的对等点数
  5. web3-utils:提供实用程序函数,这些函数可在以太坊去中心化利用以及其余 web3.js 模块中应用。实用程序函数能够重复使用,使代码编写更轻松,在 JavaScript 和其余编程语言中很常见。Web3-utils 蕴含实用程序函数,这些函数用于转换数字、验证值是否满足特定条件以及搜寻数据集。

    1. web3.utils.toWei 将以太转换为 Wei
    2. web3.utils.hexToNumberString 将十六进制值转换为字符串
    3. web3.utils.isAddress 校验特定字符串是否为无效的以太坊地址

web3-react

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

ehthers.js

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

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

  1. Ethers.provider : 封装与以太坊区块链的连贯。它能够用于签发查问和发送已签名的交易,这将扭转区块链的状态。

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

    注:web3.js 也有服务于此目标的提供商,位于 web3 根底模块中。Ethers.jsweb3.js 的组织形式截然不同,因而只管两个库的性能十分类似,但模块间并非总是能清晰对应。

  2. Ethers.contract : 部署智能合约并与它交互,该模块中的函数用于侦听从智能合约发射的事件、调用智能合约提供的函数、获取无关智能合约的信息,以及部署智能合约。
  3. Ethers.utils:提供用于格式化数据和解决用户输出的实用程序函数。Ethers.utils 的作用形式与 web3-utils 类似,可能简化去中心化利用的构建流程。

    1. ethers.utils.getContractAddress 从用于部署智能合约的交易中提取智能合约地址
    2. ethers.utils.computeAddress 通过传递与地址相干的公钥或私钥的函数来计算地址 ethers.utils.formatEther 将所传递的 Wei 金额转换为 Ether 十进制字符串格局
  4. Ethers.wallets : Ethers.wallet 提供的性能与咱们目前探讨过的其余模块截然不同。Ethers.wallet 的作用是使你能够与现有钱包(以太坊地址)建设连贯、创立新钱包以及对交易签名。

    1. ethers.wallet.createRandom 将创立随机新账户
    2. ethers.wallet.sign 将对交易签名并将已签名的交易返回为十六进制字符串的模式
    3. 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 成果展现:

连贯钱包:

切换网络:

拉起钱包签名成果展现:

总结

web3-react 这个库目前还在开发中,最新的 beta 版本反对了多种钱包的 Connector 连贯形式,core 包改变也比拟大,等正式版本公布了再更新一期最新的应用教程,至此,👋

退出移动版