前置知识
- 对以太坊的基本概念有初步认识
- 掌握
react
相关知识 - 了解
typescript
语法 - 了解
ethers.js
库 - 了解
web3js
库 - 了解
web3-react
库
为什么是 React
不是 Vue
?
区块链领域在 react
里面有一个很优秀的库叫 web3-react,还有一个很酷的连接钱包的react连接UI的库叫 web3modal,连接的过程不需要我们操作。这个两个库都已经在最大的交易网站上面使用了,除了这些优秀的库,由于 react
的生态在国外本身就是就发展得比其他框架更加蓬勃,所以导致所有知名的区块链行业代码都是使用 react
web3js ehthers web3-react 区别与联系
基本概念
web3js
Web3
是一个用于与以太坊区块链以及运行以太坊虚拟机的其他区块链进行通信的库,包括Avalanche
,Binance Smart chain
和Solana
。
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
返回网络ID
web3.*.net.getPeerCount
返回连接到节点的对等点数
web3-utils
:提供实用程序函数,这些函数可在以太坊去中心化应用以及其他 web3.js 模块中使用。实用程序函数可以重复使用,使代码编写更轻松,在 JavaScript 和其他编程语言中很常见。Web3-utils 包含实用程序函数,这些函数用于转换数字、验证值是否满足特定条件以及搜索数据集。web3.utils.toWei
将以太转换为Wei
web3.utils.hexToNumberString
将十六进制值转换为字符串web3.utils.isAddress
校验特定字符串是否为有效的以太坊地址
web3-react
web3-react
是流行的库 Web3
的一个很好的 React
实现
ehthers.js
Ethers.js 是一个 JavaScript 库,其作用是使开发者可以与以太坊区块链进行交互。该库包含 JavaScript
和 TypeScript
中的实用程序函数,以及以太坊钱包的所有功能。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.js 和web3.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
将为我们提供钱包地址的以太坊余额
为什么要引入这些库?
钱包连接其实也可以直接用 web3
和 meta mask
提供的方法写,但是这样有一个问题是需要考虑很多场景和多钱包,这样导致代码量很大并且问题可能很多。
区别
ehthers.js
与 web3.js
不同的是,ethers.js
在使用时不需要过多的回调函数,而且可以搭配 Hardhat
工具使的语法得到进一步的优化。
联系
三者 都是 JavaScript
库,其作用是使开发者可以与以太坊区块链交互。这两个库都很实用,都能满足大多数以太坊开发者的需求。web3-react
是 Web3js
的一个很好的 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-react
和 web3js
、ehthers.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.ethereum
是 MetaMask
插件注入到全局的一个对象,用于请求连接账户、获取用户连接的链的数据(如交易后的返回值和事件)、以及显示用户对交易的签名状态。 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
包改动也比较大,等正式版本发布了再更新一期最新的使用教程,至此,👋
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。