Web3-React
Web3-React 是一个简单、高度可扩展且依赖最少的框架,用于构建现代以太坊 dApp 的连接组件。
快速开始
1.配置连接器:在 Web3-React 包中添加 BitgetWallet
import detectEthereumProvider from "@akkafinance/bitkeep-detect-provider";
import type {
Actions,
AddEthereumChainParameter,
Provider,
ProviderConnectInfo,
ProviderRpcError,
WatchAssetParameters,
} from "@web3-react/types";
import { Connector } from "@web3-react/types";
type BitGetWalletProvider = Provider & {
isBitGetWallet?: boolean;
isConnected?: () => boolean;
providers?: BitGetWalletProvider[];
};
export class NoBitGetWalletError extends Error {
public constructor() {
super("BitGetWallet not installed");
this.name = NoBitGetWalletError.name;
Object.setPrototypeOf(this, NoBitGetWalletError.prototype);
}
}
function parseChainId(chainId: string) {
return Number.parseInt(chainId, 16);
}
/**
* @param options - 传递给 `./detect-provider` 的选项
* @param onError - 处理从 eventListeners 抛出的错误的处理器。
*/
export interface BitGetWalletpConstructorArgs {
actions: Actions;
options?: Parameters<typeof detectEthereumProvider>[0];
onError?: (error: Error) => void;
}
export class BitGetWallet extends Connector {
/** {@inheritdoc Connector.provider} */
public provider?: BitGetWalletProvider;
private readonly options?: Parameters<typeof detectEthereumProvider>[0];
private eagerConnection?: Promise<void>;
constructor({ actions, options, onError }: BitGetWalletConstructorArgs) {
super(actions, onError);
this.options = options;
}
private async isomorphicInitialize(): Promise<void> {
if (this.eagerConnection) return;
return (this.eagerConnection = import(
"@akkafinance/bitkeep-detect-provider"
).then(async (m) => {
const provider = await m.default(this.options);
if (provider) {
this.provider = provider as unknown as BitGetWalletProvider;
if (this.provider.providers?.length) {
this.provider =
this.provider.providers.find((p) => p.isBitGetWallet) ??
this.provider.providers[0];
}
this.provider.on(
"connect",
({ chainId }: ProviderConnectInfo): void => {
this.actions.update({ chainId: parseChainId(chainId) });
}
);
this.provider.on("disconnect", (error: ProviderRpcError): void => {
this.actions.resetState();
this.onError?.(error);
});
this.provider.on("chainChanged", (chainId: string): void => {
this.actions.update({ chainId: parseChainId(chainId) });
});
this.provider.on("accountsChanged", (accounts: string[]): void => {
if (accounts.length === 0) {
// 通过断开连接来处理这种边缘情况
this.actions.resetState();
} else {
this.actions.update({ accounts });
}
});
}
}));
}
/** {@inheritdoc Connector.connectEagerly} */
public async connectEagerly(): Promise<void> {
const cancelActivation = this.actions.startActivation();
await this.isomorphicInitialize();
if (!this.provider) return cancelActivation();
return Promise.all([
this.provider.request({ method: "eth_chainId" }) as Promise<string>,
this.provider.request({ method: "eth_accounts" }) as Promise<string[]>,
])
.then(([chainId, accounts]) => {
if (accounts.length) {
this.actions.update({ chainId: parseChainId(chainId), accounts });
} else {
throw new Error("No accounts returned");
}
})
.catch((error) => {
console.debug("Could not connect eagerly", error);
// 我们应该能够在这里使用 `cancelActivation`,但在移动设备上,metamask 发出 'connect'
// 事件,意味着 chainId 被更新,而 cancelActivation 不起作用,因为发生了中间
// 更新,所以我们重置状态
this.actions.resetState();
});
}
/**
* 启动连接。
*
* @param desiredChainIdOrChainParameters - 如果定义,表示要连接的目标链。如果用户已经
* 连接到此链,则不会采取其他步骤。否则,如果满足以下两个条件之一,将提示用户切换
* 到该链:要么他们已经在扩展中添加了该链,要么参数是 AddEthereumChainParameter 类型,
* 在这种情况下,将首先提示用户使用指定参数添加链,然后再提示切换。
*/
public async activate(
desiredChainIdOrChainParameters?: number | AddEthereumChainParameter
): Promise<void> {
let cancelActivation: () => void;
if (!this.provider?.isConnected?.())
cancelActivation = this.actions.startActivation();
return this.isomorphicInitialize()
.then(async () => {
if (!this.provider) throw new NoBitGetWalletError();
return Promise.all([
this.provider.request({ method: "eth_chainId" }) as Promise<string>,
this.provider.request({ method: "eth_requestAccounts" }) as Promise<
string[]
>,
]).then(([chainId, accounts]) => {
const receivedChainId = parseChainId(chainId);
const desiredChainId =
typeof desiredChainIdOrChainParameters === "number"
? desiredChainIdOrChainParameters
: desiredChainIdOrChainParameters?.chainId;
// 如果没有目标链,或者它等于接收到的链,则更新
if (!desiredChainId || receivedChainId === desiredChainId)
return this.actions.update({ chainId: receivedChainId, accounts });
const desiredChainIdHex = `0x${desiredChainId.toString(16)}`;
// 如果我们在这里,我们可以尝试切换网络
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.provider!.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: desiredChainIdHex }],
})
.catch((error: ProviderRpcError) => {
if (
error.code === 4902 &&
typeof desiredChainIdOrChainParameters !== "number"
) {
// 如果我们在这里,我们可以尝试添加新网络
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.provider!.request({
method: "wallet_addEthereumChain",
params: [
{
...desiredChainIdOrChainParameters,
chainId: desiredChainIdHex,
},
],
});
}
throw error;
})
.then(() => this.activate(desiredChainId));
});
})
.catch((error) => {
cancelActivation?.();
throw error;
});
}
/** {@inheritdoc Connector.deactivate} */
public deactivate(): void {
this.actions.resetState();
}
public async watchAsset({
address,
symbol,
decimals,
image,
}: WatchAssetParameters): Promise<true> {
if (!this.provider) throw new Error("No provider");
return this.provider
.request({
method: "wallet_watchAsset",
params: {
type: "ERC20", // 最初只支持 ERC20,但最终会支持更多标准
options: {
address, // 代币合约的地址
symbol, // 代币符号或简称,最多 11 个字符
decimals, // 代币小数位数
image, // 显示在钱包中的代币图像的字符串 url
},
},
})
.then((success) => {
if (!success) throw new Error("Method not supported.");
return true;
});
}
}
2.Use this connector in your application:
import { Web3ReactHooks, Web3ReactProvider } from '@web3-react/core'
import { BitGetWallet } from './bitgetWallet'
import { hooks as metaMaskHooks, metaMask } from './metaMask'
const bitget = new BitGetWallet({
actions: bitgetHooks.useSelectedActions(connector),
onError: (error: Error) => {
console.debug(`web3-react error: ${error}`)
},
})
const connectors: [BitGetWallet, Web3ReactHooks][] = [
[bitget, bitgetHooks],
[metaMask, metaMaskHooks],
]
export default function App() {
return (
<Web3ReactProvider connectors={connectors}>
<div className='App'>
<header className='App-header'>
<h1>BitGet Wallet + Web3 React</h1>
</header>
<main>
<Profile />
</main>
</div>
</Web3ReactProvider>
)
}
3.Connect Wallet:
const { connector, hooks, store } = useSelectedConnector(connectors)
const { useSelectedAccount, useSelectedChainId, useSelectedIsActive } = hooks
const isActive = useSelectedIsActive(connector)
const account = useSelectedAccount(connector)
const chainId = useSelectedChainId(connector)
const handleToggleConnect = useCallback(() => {
if (isActive) {
if (connector?.deactivate) {
void connector.deactivate()
} else {
void connector.resetState()
}
} else if (!pendingConnection) {
setPendingConnection(true)
connector
.activate(1)
.then(() => setPendingConnection(false))
.catch(() => setPendingConnection(false))
}
}, [connector, isActive, pendingConnection])
连接钱包
使用 Hook
import { useWeb3React } from '@web3-react/core'
import { injected, walletconnect } from './connectors'
function WalletConnection() {
const { activate, deactivate, active, account, library, connector, error } =
useWeb3React()
const connectInjected = async () => {
try {
await activate(injected)
} catch (ex) {
console.log(ex)
}
}
const connectWalletConnect = async () => {
try {
await activate(walletconnect)
} catch (ex) {
console.log(ex)
}
}
const disconnect = () => {
try {
deactivate()
} catch (ex) {
console.log(ex)
}
}
if (active) {
return (
<div>
<p>已连接账户: {account}</p>
<p>连接器: {connector?.constructor.name}</p>
<button onClick={disconnect}>断开连接</button>
</div>
)
}
return (
<div>
<button onClick={connectInjected}>连接注入钱包</button>
<button onClick={connectWalletConnect}>连接 WalletConnect</button>
{error && <p>错误: {error.message}</p>}
</div>
)
}
获取账户信息
import { useWeb3React } from '@web3-react/core'
import { useEffect, useState } from 'react'
function AccountInfo() {
const { account, library } = useWeb3React()
const [balance, setBalance] = useState<string>('')
useEffect(() => {
if (account && library) {
library.getBalance(account).then((balance) => {
setBalance(library.utils.formatEther(balance))
})
}
}, [account, library])
if (!account) {
return <div>请先连接钱包</div>
}
return (
<div>
<p>地址: {account}</p>
<p>余额: {balance} ETH</p>
</div>
)
}
发送交易
import { useWeb3React } from '@web3-react/core'
import { parseEther } from '@ethersproject/units'
function SendTransaction() {
const { account, library } = useWeb3React()
const sendETH = async () => {
if (!account || !library) return
try {
const signer = library.getSigner(account)
const transaction = await signer.sendTransaction({
to: '0x...',
value: parseEther('0.01'),
})
console.log('交易哈希:', transaction.hash)
await transaction.wait()
console.log('交易确认')
} catch (error) {
console.error('交易失败:', error)
}
}
return (
<button onClick={sendETH} disabled={!account}>
发送 0.01 ETH
</button>
)
}
合约交互
import { useWeb3React } from '@web3-react/core'
import { Contract } from '@ethersproject/contracts'
import { useEffect, useState } from 'react'
const ERC20_ABI = [
'function balanceOf(address owner) view returns (uint256)',
'function transfer(address to, uint256 amount) returns (bool)',
'function symbol() view returns (string)',
'function decimals() view returns (uint8)',
]
function TokenContract() {
const { account, library } = useWeb3React()
const [contract, setContract] = useState<Contract | null>(null)
const [balance, setBalance] = useState<string>('')
const [symbol, setSymbol] = useState<string>('')
useEffect(() => {
if (library && account) {
const tokenContract = new Contract(
'0x...', // 代币合约地址
ERC20_ABI,
library.getSigner(account)
)
setContract(tokenContract)
}
}, [library, account])
useEffect(() => {
const fetchTokenInfo = async () => {
if (contract && account) {
try {
const [balance, symbol, decimals] = await Promise.all([
contract.balanceOf(account),
contract.symbol(),
contract.decimals(),
])
setBalance((balance / 10 ** decimals).toString())
setSymbol(symbol)
} catch (error) {
console.error('获取代币信息失败:', error)
}
}
}
fetchTokenInfo()
}, [contract, account])
const transferToken = async () => {
if (!contract) return
try {
const tx = await contract.transfer('0x...', parseEther('1'))
await tx.wait()
console.log('转账成功')
} catch (error) {
console.error('转账失败:', error)
}
}
return (
<div>
<p>
代币余额: {balance} {symbol}
</p>
<button onClick={transferToken}>转账 1 {symbol}</button>
</div>
)
}
签名消息
import { useWeb3React } from '@web3-react/core'
function SignMessage() {
const { account, library } = useWeb3React()
const signMessage = async () => {
if (!account || !library) return
try {
const signer = library.getSigner(account)
const message = 'Hello from Bitget Wallet!'
const signature = await signer.signMessage(message)
console.log('消息:', message)
console.log('签名:', signature)
} catch (error) {
console.error('签名失败:', error)
}
}
return (
<button onClick={signMessage} disabled={!account}>
签名消息
</button>
)
}
网络切换
import { useWeb3React } from '@web3-react/core'
function NetworkSwitcher() {
const { library, account } = useWeb3React()
const switchToPolygon = async () => {
if (!library || !account) return
try {
await library.provider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: '0x89' }], // Polygon
})
} catch (switchError: any) {
// 如果网络不存在,尝试添加
if (switchError.code === 4902) {
try {
await library.provider.request({
method: 'wallet_addEthereumChain',
params: [
{
chainId: '0x89',
chainName: 'Polygon',
nativeCurrency: {
name: 'MATIC',
symbol: 'MATIC',
decimals: 18,
},
rpcUrls: ['https://polygon-rpc.com/'],
blockExplorerUrls: ['https://polygonscan.com/'],
},
],
})
} catch (addError) {
console.error('添加网络失败:', addError)
}
}
}
}
return <button onClick={switchToPolygon}>切换到 Polygon</button>
}
监听事件
import { useWeb3React } from '@web3-react/core'
import { useEffect } from 'react'
function EventListeners() {
const { library, account, activate } = useWeb3React()
useEffect(() => {
const { ethereum } = window as any
if (ethereum) {
const handleAccountsChanged = (accounts: string[]) => {
console.log('账户变化:', accounts)
if (accounts.length === 0) {
// 用户断开连接
} else {
// 用户切换账户
activate(injected)
}
}
const handleChainChanged = (chainId: string) => {
console.log('网络变化:', chainId)
activate(injected)
}
ethereum.on('accountsChanged', handleAccountsChanged)
ethereum.on('chainChanged', handleChainChanged)
return () => {
ethereum.removeListener('accountsChanged', handleAccountsChanged)
ethereum.removeListener('chainChanged', handleChainChanged)
}
}
}, [activate])
return <div>事件监听器已激活</div>
}
完整示例
import React from 'react'
import { Web3ReactProvider, useWeb3React } from '@web3-react/core'
import { Web3Provider } from '@ethersproject/providers'
import { InjectedConnector } from '@web3-react/injected-connector'
const injected = new InjectedConnector({
supportedChainIds: [1, 3, 4, 5, 42, 56, 137],
})
function getLibrary(provider: any): Web3Provider {
const library = new Web3Provider(provider)
library.pollingInterval = 12000
return library
}
function DApp() {
const { activate, deactivate, active, account, library, error } =
useWeb3React()
const connect = async () => {
try {
await activate(injected)
} catch (ex) {
console.log(ex)
}
}
const disconnect = () => {
try {
deactivate()
} catch (ex) {
console.log(ex)
}
}
if (error) {
return <div>错误: {error.message}</div>
}
if (!active) {
return (
<div>
<button onClick={connect}>连接钱包</button>
</div>
)
}
return (
<div>
<p>已连接: {account}</p>
<button onClick={disconnect}>断开连接</button>
</div>
)
}
function App() {
return (
<Web3ReactProvider getLibrary={getLibrary}>
<DApp />
</Web3ReactProvider>
)
}
export default App
注意事项
- 连接器配置: 根据您的需求配置正确的连接器
- 错误处理: 始终处理连接和交易错误
- 事件监听: 监听账户和网络变化事件
- 性能优化: 合理设置轮询间隔和缓存策略
相关资源
Last updated on