1.区块链知识点(前端相关)
1.1 Provider、Connector、Abi
Provider:主要用于从区块链中获取数据。常用的有:INFURA、Alchemy、Etherscan等,或者也可以自定义一个RPC URL。
Connector:如果你想修改区块链中的数据,你需要发送一笔交易,发送交易的前提就是连接钱包,连接钱包就需要用到Connector,常用的有Injected,MetaMask(特殊的Injected)、WalletConnect(集成了很多种钱包)、Coinbase等等
Abi:一个普通的json文件。调用合约拿数据或者发送交易必须要知道两个东西,一个是调用的合约地址,另外一个就是调用的合约方法,这个合约方法就是在Abi文件中,它包含了方法的入参和出参,就类似于接口文档,但是这个实际执行的时候也是需要这个文件的,而不像接口文档只是用来看看的
1.2 Token
用的最多的就是ERC20 Token,了解它主要是为了获取Token的一些基本信息,如 decimals(精度)、balanceOf(余额)、approve(授权)、transfer转账、totalSupply(池子总供应量)等
(1).主币没有代币地址,所以没法调用这些方法
(2).关于授权,主币不需要授权,授权就是用户授权允许自己的钱包向某个合约转入一定数量代币,主要目的就是为了防止合约把你钱包的代码都弄走了,为了你的资金安全考虑的。一般如果涉及到想某个合约转入代币都需要先授权。授权调用approve方法,获取授权额度调用allowance方法。
1.3 签名
签名一般用于链下授权,主要是为了安全考虑,主要用到的是personal_sign和eth_signTypedData_v4,后者比较复杂一些,感兴趣可以去了解下
还有不少
jsonrpc api可以去参考下metamask官网, docs.metamask.io/wallet/refe…
2.前端工具链
目前比较流行的基础类库为ethers.js和web3.js,ethers.js后出来目前使用的人更多些。
不过这些类库只提供了与区块链交互的基本方法,如果希望与我们的前端框架配合开发更流畅、效率更高,还需要借助一些更上层的库,如web3-react、wagmi、useDApp,这三个库都是支持React,但是不支持Vue。所以如果你希望开发区块链,UI框架最好还是选择React,主要这玩意是老外搞出来的,所以相关生态对Vue的支持不是很好。
我个人用过web3-react和wagmi,web3-react目前比较知名的一些平台都用的它,如uniswap、aave等,wagmi现在比较火,目前的迭代也比较频繁,它自带了很多实用React Hooks,对于开发来说体验很好,提升了很多效率,不过兼容性和稳定性做的还不是特别好。不过我仍然还是推荐它,因为就是好用。
下面是wagmi官网上对三个框架的对比
3.代码实践
3.1 wagmi基础hooks
钱包连接断开
主要是useConnect、useDisconnect包含了连接、断开钱包等方法和状态
链信息或者切换链
useNetwork、useSwitchNetwork
交易信息获取与发送
useContractRead、useContractReads(批量获取)、useContractWrite
上面是一些常用的,然后还有一些大家自行去官网上看看,然后下面主要讲基于项目自定义的一些
3.2 自定义组件与hooks
useApprove.jsx (授权方法和一些状态)
import { useState, useCallback, useContext, useEffect } from 'react'
import { useNotification } from '@hooks/useNotification'
import { MaxUint256 } from '@ethersproject/constants'
import { useContractWriteWithNotification, useWaitForTransactionWithNotification } from '@hooks/useContractWithNotification'
import { useContractRead, usePrepareContractWrite, erc20ABI, useAccount } from 'wagmi'
import useTokenAllowance from './useTokenAllowance'
import { updateTokenAllowance } from '@state/tokenSlice'
import useTokenList from './useTokenList'
import { useBlockNumber } from 'wagmi'
import { NATIVE_TOKEN_ADDRESS } from '@constants/index'
const getIsNeedApprove = (allowance, spendAmount, address) => {
return address !== NATIVE_TOKEN_ADDRESS && ((!Number(spendAmount) && !allowance) || allowance < Number(spendAmount))
}
export function useApprove({ address, spenderAddress, spendAmount } = {}, { onSuccess, onReject } = {}) {
const { address: account } = useAccount()
const [allowancePending, setAllowancePending] = useState()
const [allowanceInsufficient, setAllowanceInsufficient] = useState(false)
const { data: blockNumber } = useBlockNumber({
watch: true,
})
const {
getAllowanceByAddress,
fetchAllowance,
isFetching: isAllowanceLoading,
} = useTokenAllowance([
{
address,
spenderAddress,
},
])
//获取allowance
const allowance = getAllowanceByAddress(address, spenderAddress)
useEffect(() => {
if (address && spenderAddress) {
fetchAllowance()
}
}, [account, blockNumber, address, spenderAddress])
console.log('allowance-spendAmount', allowance, Number(spendAmount))
//需要授权 不为主币 && ((allowance为0 && spendAmount也没有)|| allowance<spendAmount)
const isNeedApprove = getIsNeedApprove(allowance, spendAmount, address)
console.log('isNeedApprove', isNeedApprove)
//授权方法
const { config } = usePrepareContractWrite({
address,
abi: erc20ABI,
functionName: 'approve',
args: [spenderAddress, MaxUint256],
enabled: !!spenderAddress && isNeedApprove,
})
const { data, isLoading, isSuccess, isError, write, error, reset } = useContractWriteWithNotification({
...config,
onReject() {
onReject?.()
},
})
const waitForTransaction = useWaitForTransactionWithNotification({
hash: data?.hash,
onSuccess: async (data) => {
//授权成功,重新获取allowance
setAllowanceInsufficient(false) //重置授权不足标记
setAllowancePending(true)
const allowance = await fetchAllowance?.()
console.log('allowance777', allowance)
const curTokenAllowance = allowance.data[`${address}_${spenderAddress}`]
const curTokenIsNeedApprove = getIsNeedApprove(curTokenAllowance, spendAmount, address)
console.log('curTokenIsNeedApprove', curTokenIsNeedApprove)
//授权额度超过需要的额度才能执行回调,否则后续执行的交易会报错
if (!curTokenIsNeedApprove) {
onSuccess?.(data)
}
setAllowanceInsufficient(curTokenIsNeedApprove)
setAllowancePending(false)
},
})
const approveReset = useCallback(() => {
setAllowanceInsufficient(false)
reset()
}, [setAllowanceInsufficient, reset])
console.log('allowance', allowance, isAllowanceLoading)
return {
approveHandle: write,
approvePending: isLoading || waitForTransaction.isLoading,
approveStatus: {
approveLoading: isLoading,
approveWaitLoading: waitForTransaction.isLoading,
approveIsSuccess: isSuccess,
approveIsError: isError,
approveWaitIsSuccess: waitForTransaction.isSuccess,
approveWaitIsError: waitForTransaction.isError,
approveError: error,
approveWaitError: waitForTransaction.error,
allowancePending: allowancePending,
allowanceInsufficient: allowanceInsufficient,
},
approveReset,
allowancePending: isAllowanceLoading,
allowance,
isNeedApprove,
}
}
approveBtn.jsx (授权按钮组件)
import React, { useEffect, useMemo, useRef } from 'react'
import { Button, Alert } from 'antd'
import classNames from 'classnames/bind'
import styles from './styles/index.scss'
const cx = classNames.bind(styles)
import { useTranslation } from 'react-i18next'
import { useApprove } from '@hooks/useApprove'
export default ({ tokenName, tokenAddress, spendAmount = 0, spenderAddress, children, hideApproveBtn, hideApproveBtnWithFn, approveHandle, approvePending, isNeedApprove, ...restProps }) => {
const onApproveSuccessRef = useRef()
const { approveHandle: approveHandleSelf, approvePending: approvePendingSelf, isNeedApprove: isNeedApproveSelf } = useApprove({ address: tokenAddress, spenderAddress, spendAmount }, { onSuccess: onApproveSuccessRef.current })
const { t, i18n } = useTranslation()
const approveHandleResolved = approveHandle ?? approveHandleSelf
const approvePendingResolved = approvePending ?? approvePendingSelf
const isNeedApproveResolved = isNeedApprove ?? isNeedApproveSelf
//不显示授权按钮,直接在原本按钮上加授权功能,授权完成后直接执行原本按钮操作
if (hideApproveBtnWithFn) {
if (hideApproveBtn || children?.props?.disabled || !isNeedApproveResolved) {
return React.cloneElement(children, {
...children.props,
...restProps,
})
} else {
const { loading: originLoading, onClick: originOnClick, ...otherOriginProps } = children.props
onApproveSuccessRef.current = originOnClick
return React.cloneElement(children, {
...otherOriginProps,
...restProps,
loading: approvePendingResolved || originLoading,
onClick: () => {
approveHandleResolved?.()
},
})
}
}
return (
<>
{hideApproveBtn || children?.props?.disabled || !isNeedApproveResolved ? (
React.cloneElement(children, {
...children.props,
...restProps,
})
) : (
<Button type="primary" {...restProps} size={children.props?.size} loading={approvePendingResolved} className={cx('btn-full', children.props.className)} onClick={() => approveHandleResolved?.()}>
{t('index.total_assests.approve')}
{tokenName ? ' ' + tokenName : null}
</Button>
)}
</>
)
}
useSwitchNetwork (切换网络,主要添加了网络不存在添加网络逻辑)
import { useState, useEffect, useMemo } from 'react'
import { SUPPORTED_CHAINS } from '@constants/index'
import { useChainId } from './useChainId'
import { useSwitchNetwork as useWagmiSwitchNetWork } from 'wagmi'
export const useSwitchNetwork = () => {
const chainId = useChainId(false)
const [loading, setLoading] = useState(false)
const [currentChainId, setCurrentChainId] = useState()
const { switchNetworkAsync } = useWagmiSwitchNetWork({
onSuccess(data) {
console.log('Success', data)
window.location.reload()
},
})
const switchChain = async (selectedChainId) => {
if (selectedChainId === chainId) return
try {
setLoading(selectedChainId)
//连接钱包可以走wagmi这个切换网络
if (switchNetworkAsync) {
await switchNetworkAsync(selectedChainId)
} else {
await ethereum.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: '0x' + selectedChainId.toString(16) }],
})
}
setLoading(false)
setCurrentChainId(selectedChainId)
} catch (switchError) {
console.log('switchError', switchError)
if (switchError.code === 4902) {
//构造添加网络的参数
console.log('SUPPORTED_CHAINS', SUPPORTED_CHAINS)
const targetChain = SUPPORTED_CHAINS.find((item) => item.id === selectedChainId)
console.log('targetChain', targetChain)
const rpcUrls = [
...new Set(
Object.values(targetChain.rpcUrls)
.map((item) => item.http)
.flat()
),
]
const blockExplorerUrls = [...new Set(Object.values(targetChain.blockExplorers).map((item) => item.url))]
const addEthereumChainParameter = [
{
chainId: '0x' + targetChain.id.toString(16), // A 0x-prefixed hexadecimal string
chainName: targetChain.name,
nativeCurrency: targetChain.nativeCurrency,
rpcUrls: rpcUrls,
blockExplorerUrls: blockExplorerUrls,
},
]
console.log('addEthereumChainParameter', addEthereumChainParameter)
try {
await ethereum.request({
method: 'wallet_addEthereumChain',
params: addEthereumChainParameter,
})
window.location.reload()
} catch (addError) {
console.log(addError)
} finally {
setLoading(false)
}
} else {
setLoading(false)
}
}
}
return {
switchChain,
currentChainId,
loading,
}
}
useChainId.jsx(获取链id,主要处理没有连接钱包情况下获取链id)
import { useState, useEffect, useMemo } from 'react';
import detectEthereumProvider from '@metamask/detect-provider'
import { SUPPORTED_CHAIN_IDS, DEFAULT_CHAIN_ID } from '@constants/index'
import { useNetwork } from 'wagmi';
import { SUPPORTED_CHAINS } from '@constants/index'
export function useChainId (isUnSupportedSetFallback = true) {
const { chain } = useNetwork()
//CoinHub等小众钱包window.ethereum.chainId返回的是十进制数字
let initChainId = useMemo(() => {
let initChainId = DEFAULT_CHAIN_ID;
if (window.ethereum && window.ethereum.chainId) {
if (String(window.ethereum.chainId).indexOf('0x') > -1) {
initChainId = parseInt(window.ethereum.chainId, 16)
} else {
initChainId = window.ethereum.chainId
}
} else {
initChainId = DEFAULT_CHAIN_ID
}
return initChainId;
}, [window.ethereum && window.ethereum.chainId]);
if (isUnSupportedSetFallback && SUPPORTED_CHAIN_IDS.indexOf(initChainId) === -1) {
initChainId = DEFAULT_CHAIN_ID;
}
const [chainId, setChainId] = useState(initChainId)
useEffect(() => {
//如果连接了钱包同时是支持的网络则直接用wagmi的,否则就通过etherum检测
if (chain?.id) {
if(chain?.unsupported && isUnSupportedSetFallback){
setChainId(DEFAULT_CHAIN_ID)
}else{
setChainId(chain.id)
}
} else {
(async () => {
let provider = await detectEthereumProvider()
if (provider) {
console.log('Ethereum successfully detected!')
// From now on, this should always be true:
// provider === window.ethereum
// Access the decentralized web!
// Legacy providers may only have ethereum.sendAsync
const chainId = await provider.request({
method: 'eth_chainId'
})
if (chainId) {
if (isUnSupportedSetFallback && SUPPORTED_CHAIN_IDS.indexOf(parseInt(chainId, 16)) === -1) {
setChainId(DEFAULT_CHAIN_ID)
} else {
setChainId(parseInt(chainId, 16))
}
}
} else {
// if the provider is not detected, detectEthereumProvider resolves to null
// console.error('Please install MetaMask!')
}
})()
}
}, [window.ethereum, chain?.id, isUnSupportedSetFallback])
return chainId;
}
export const useConnectedChain = ()=>{
const chainId = useChainId();
console.log('SUPPORTED_CHAINS',SUPPORTED_CHAINS)
const connectedChain = SUPPORTED_CHAINS.find(item=>item.id === chainId)
console.log('connectedChain',connectedChain)
return connectedChain
}
useContractWithNotification.tsx (交易发送后添加侧边提示)
import React from 'react'
import { useContractWrite, UseContractWriteConfig, useWaitForTransaction } from 'wagmi'
import { useNotification } from '@hooks/useNotification'
import { addTransaction, finalizeTransaction, failedTransaction } from '@state/transactionSlice'
import { useDispatch } from 'react-redux'
import { BigNumber } from 'ethers'
import { calculateGasMargin } from '@utils/index'
export const useContractWriteWithNotification = function (config: any) {
const { onSuccess, onError, onReject, ...restConfig } = config
const dispatch = useDispatch()
const { showPendingTransaction, showTransactionReject, showFailedTransaction } = useNotification()
const configParams = {
onSuccess(data: any, ...restArgs: any[]) {
console.log('Success', data)
dispatch(addTransaction({ hash: data?.hash }))
showPendingTransaction(data?.hash)
onSuccess && onSuccess(data, ...restArgs)
},
onError(error: any, ...restArgs: any[]) {
const code = error?.code
if (code === 4001 || code === -32000) {
console.log('useContractWrite Error', error)
showTransactionReject()
onReject?.()
} else {
showFailedTransaction(undefined, undefined, error?.message)
}
onError && onError(error, ...restArgs)
},
...restConfig,
}
//修改估算的gasLimit
if (restConfig?.request && restConfig?.request?.gasLimit) {
configParams.request = {
...restConfig?.request,
gasLimit: calculateGasMargin(restConfig?.request?.gasLimit), // equivalent to a gasLimitMultiplier of 1.1
}
}
return useContractWrite(configParams)
}
export const useWaitForTransactionWithNotification = function (config: any): any {
const { onSuccess, onError, ...restConfig } = config
const dispatch = useDispatch()
const { showConfirmedTransaction, showFailedTransaction } = useNotification()
return useWaitForTransaction({
onSuccess(data) {
console.log('Success', data)
// dispatch(finalizeTransaction({ hash: data?.transactionHash, transactionReceipt: data }));
// showConfirmedTransaction(data?.transactionHash);
onSuccess && onSuccess(data)
},
onError(error: Error) {
// dispatch(failedTransaction({ hash: restConfig?.hash }));
// showFailedTransaction(restConfig?.hash)
onError && onError(error)
},
...restConfig,
})
}
transactionUpdater.tsx
交易完成后添加浮动通知,这个代码放在所有页面公用的js文件中,保证页面及时切换了也能监听到交易是否已完成
import React, { useEffect, useMemo, useRef } from 'react';
import useTokenBalance from '@hooks/useTokenBalance';
import { useAccount, useBlockNumber, useWaitForTransaction } from 'wagmi';
import { useDispatch, useSelector } from 'react-redux';
import { failedTransaction, finalizeTransaction } from '@state/transactionSlice';
import { useNotification } from '@hooks/useNotification';
import { AppState } from '@src/configStore';
import { TransactionStatus } from '@constants/index';
export default () => {
const dispatch = useDispatch()
const { showConfirmedTransaction, showFailedTransaction } = useNotification()
const transactions = useSelector<AppState, AppState['transactionSlice']>(state => state.transactionSlice)
const pendingTransactions = Object.values(transactions).filter(item => {
return item.status === TransactionStatus.Pending
}).sort((a, b) => b.addedTime - a.addedTime)
const watchTransaction = pendingTransactions[0]
useWaitForTransaction({
hash: watchTransaction?.hash,
onSuccess(data) {
console.log('Success', data)
dispatch(finalizeTransaction({ hash: data?.transactionHash, transactionReceipt: data }));
showConfirmedTransaction(data?.transactionHash);
},
onError(error: Error) {
dispatch(failedTransaction({ hash: watchTransaction?.hash }));
showFailedTransaction(watchTransaction?.hash)
},
})
return null
}