让DAPP实现钱包保持链接
钱包在有效期内自动连接,然后使用hooks函数获取对应的信息和之后跟智能合约交互做准备。钱包连接其实也可以直接用web3和meta mask提供的方法写,但是这样有一个问题是需要考虑很多场景和多钱包,这样导致代码量很大并且问题可能很多。于是我们应该寻找一些现成的方案来实现。
在react里面有一个很优秀的库叫web3-react
还有一个很酷的连接钱包的react连接UI的库叫web3modal,连接的过程不需要我们操作。这个两个库都已经在最大的交易网站上面使用了,不怕出问题。
而因为这些优秀的库,所以导致所有知名的区块链行业代码都是使用react
下面多代码预警,但是核心就一个,设置Providers
,一个是web3-react需要的,一个是自定义管理web3连接的Providers
。基于这个目的,我们需要编写一些hooks等代码。
下面直接附上代码。(解析uniswap出来的,可放心食用)
web3-react
配置常量
/constants/index
import { InjectedConnector } from '@web3-react/injected-connector'
import NetworkConnector from 'modules/web3/utils/netWork'
const MainChaid = 56
export const NetworkContextName = 'NETWORK'
export const connectorLocalStorageKey = 'connectorId'
export const injected = new InjectedConnector({
supportedChainIds: [MainChaid],
})
export const RPC = {
56: 'https://bsc-dataseed4.binance.org',
}
export const NETWORK_CHAIN_ID: number = parseInt(process.env.REACT_APP_CHAIN_ID ?? MainChaid.toString())
export const network = new NetworkConnector({
urls: {
[NETWORK_CHAIN_ID]: "https://bsc-dataseed1.defibit.io",
},
})
全局基本注入钩子
/Providers.tsx
const Web3ProviderNetwork = createWeb3ReactRoot(NetworkContextName)
const Providers: React.FC = ({ children }) => {
return (<Web3ReactProvider getLibrary={getLibrary}>
<Web3ProviderNetwork getLibrary={getLibrary}>
{children}
</Web3ProviderNetwork>
</Web3ReactProvider>)
}
再设置管理的Provider, 下次自动连接而不需要重新重新签名
Web3ReactManager.tsx
import React, { useState, useEffect } from 'react'
import { useWeb3React } from '@web3-react/core'
import { network, NetworkContextName } from '../../constants'
import { useEagerConnect, useInactiveListener } from 'modules/web3/hooks'
export default function Web3ReactManager ({ children }: { children: JSX.Element }) {
const { active } = useWeb3React()
const { active: networkActive, error: networkError, activate: activateNetwork } = useWeb3React(NetworkContextName)
// try to eagerly connect to an injected provider, if it exists and has granted access already
const triedEager = useEagerConnect()
// after eagerly trying injected, if the network connect ever isn't active or in an error state, activate itd
useEffect(() => {
if (triedEager && !networkActive && !networkError && !active) {
activateNetwork(network)
}
}, [triedEager, networkActive, networkError, activateNetwork, active])
// when there's no account connected, react to logins (broadly speaking) on the injected provider, if it exists
useInactiveListener(!triedEager)
// handle delayed loader state
const [showLoader, setShowLoader] = useState(false)
useEffect(() => {
const timeout = setTimeout(() => {
setShowLoader(true)
}, 600)
return () => {
clearTimeout(timeout)
}
}, [])
// on page load, do nothing until we've tried to connect to the injected connector
if (!triedEager) {
return null
}
// if the account context isn't active, and there's an error on the network context, it's an irrecoverable error
if (!active && networkError) {
return (
<div>unknownError</div>
)
}
// if neither context is active, spin
if (!active && !networkActive) {
return showLoader ? (<div>Loader</div>) : null
}
return children
}
hooks.ts
import { useEffect, useState } from 'react'
import { useWeb3React as useWeb3ReactCore } from '@web3-react/core'
import { connectorLocalStorageKey, injected } from 'constants/index'
import { isMobile } from 'web3modal'
export function useEagerConnect () {
const { activate, active } = useWeb3ReactCore() // specifically using useWeb3ReactCore because of what this hook does
const [tried, setTried] = useState(false)
useEffect(() => {
injected.isAuthorized().then((isAuthorized) => {
const hasSignedIn = window.localStorage.getItem(connectorLocalStorageKey)
if (isAuthorized && hasSignedIn) {
activate(injected, undefined, true).catch(() => {
setTried(true)
})
} else if (isMobile() && window.ethereum && hasSignedIn) {
activate(injected, undefined, true).catch(() => {
setTried(true)
})
} else {
setTried(true)
}
})
}, [activate]) // intentionally only running on mount (make sure it's only mounted once :))
// if the connection worked, wait until we get confirmation of that to flip the flag
useEffect(() => {
if (active) {
setTried(true)
}
}, [active])
return tried
}
/**
* Use for network and injected - logs user in
* and out after checking what network theyre on
*/
export function useInactiveListener (suppress = false) {
const { active, error, activate } = useWeb3ReactCore() // specifically using useWeb3React because of what this hook does
useEffect(() => {
const { ethereum } = window
if (ethereum && ethereum.on && !active && !error && !suppress) {
const handleChainChanged = () => {
// eat errors
activate(injected, undefined, true).catch((e) => {
console.error('Failed to activate after chain changed', e)
})
}
const handleAccountsChanged = (accounts: string[]) => {
if (accounts.length > 0) {
// eat errors
activate(injected, undefined, true).catch((e) => {
console.error('Failed to activate after accounts changed', e)
})
}
}
ethereum.on('chainChanged', handleChainChanged)
ethereum.on('accountsChanged', handleAccountsChanged)
return () => {
if (ethereum.removeListener) {
ethereum.removeListener('chainChanged', handleChainChanged)
ethereum.removeListener('accountsChanged', handleAccountsChanged)
}
}
}
return undefined
}, [active, error, suppress, activate])
}
netWork.ts
import { ConnectorUpdate } from '@web3-react/types'
import { AbstractConnector } from '@web3-react/abstract-connector'
import invariant from 'tiny-invariant'
interface NetworkConnectorArguments {
urls: { [chainId: number]: string }
defaultChainId?: number
}
// taken from ethers.js, compatible interface with web3 provider
type AsyncSendable = {
isMetaMask?: boolean
host?: string
path?: string
sendAsync?: (request: any, callback: (error: any, response: any) => void) => void
send?: (request: any, callback: (error: any, response: any) => void) => void
}
class RequestError extends Error {
constructor(message: string, public code: number, public data?: unknown) {
super(message)
}
}
interface BatchItem {
request: { jsonrpc: '2.0'; id: number; method: string; params: unknown }
resolve: (result: any) => void
reject: (error: Error) => void
}
class MiniRpcProvider implements AsyncSendable {
public readonly isMetaMask: false = false
public readonly chainId: number
public readonly url: string
public readonly host: string
public readonly path: string
public readonly batchWaitTimeMs: number
private nextId = 1
private batchTimeoutId: ReturnType<typeof setTimeout> | null = null
private batch: BatchItem[] = []
constructor(chainId: number, url: string, batchWaitTimeMs?: number) {
this.chainId = chainId
this.url = url
const parsed = new URL(url)
this.host = parsed.host
this.path = parsed.pathname
// how long to wait to batch calls
this.batchWaitTimeMs = batchWaitTimeMs ?? 50
}
public readonly clearBatch = async () => {
// console.info('Clearing batch', this.batch)
const { batch } = this
this.batch = []
this.batchTimeoutId = null
let response: Response
try {
response = await fetch(this.url, {
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json' },
body: JSON.stringify(batch.map((item) => item.request)),
})
} catch (error) {
batch.forEach(({ reject }) => reject(new Error('Failed to send batch call')))
return
}
if (!response.ok) {
batch.forEach(({ reject }) => reject(new RequestError(`${response.status}: ${response.statusText}`, -32000)))
return
}
let json
try {
json = await response.json()
} catch (error) {
batch.forEach(({ reject }) => reject(new Error('Failed to parse JSON response')))
return
}
const byKey = batch.reduce<{ [id: number]: BatchItem }>((memo, current) => {
memo[current.request.id] = current
return memo
}, {})
// eslint-disable-next-line no-restricted-syntax
for (const result of json) {
const {
resolve,
reject,
request: { method },
} = byKey[result.id]
if (resolve) {
if ('error' in result) {
reject(new RequestError(result?.error?.message, result?.error?.code, result?.error?.data))
} else if ('result' in result) {
resolve(result.result)
} else {
reject(new RequestError(`Received unexpected JSON-RPC response to ${method} request.`, -32000, result))
}
}
}
}
public readonly sendAsync = (
request: { jsonrpc: '2.0'; id: number | string | null; method: string; params?: any },
callback: (error: any, response: any) => void
): void => {
this.request(request.method, request.params)
.then((result) => callback(null, { jsonrpc: '2.0', id: request.id, result }))
.catch((error) => callback(error, null))
}
public readonly request = async (
method: string | { method: string; params: unknown[] },
params?: any
): Promise<unknown> => {
if (typeof method !== 'string') {
return this.request(method.method, method.params)
}
if (method === 'eth_chainId') {
return `0x${this.chainId.toString(16)}`
}
const promise = new Promise((resolve, reject) => {
this.batch.push({
request: {
jsonrpc: '2.0',
id: this.nextId++,
method,
params,
},
resolve,
reject,
})
})
this.batchTimeoutId = this.batchTimeoutId ?? setTimeout(this.clearBatch, this.batchWaitTimeMs)
return promise
}
}
export class NetworkConnector extends AbstractConnector {
private readonly providers: { [chainId: number]: MiniRpcProvider }
private currentChainId: number
constructor({ urls, defaultChainId }: NetworkConnectorArguments) {
invariant(defaultChainId || Object.keys(urls).length === 1, 'defaultChainId is a required argument with >1 url')
super({ supportedChainIds: Object.keys(urls).map((k): number => Number(k)) })
this.currentChainId = defaultChainId || Number(Object.keys(urls)[0])
this.providers = Object.keys(urls).reduce<{ [chainId: number]: MiniRpcProvider }>((accumulator, chainId) => {
accumulator[Number(chainId)] = new MiniRpcProvider(Number(chainId), urls[Number(chainId)])
return accumulator
}, {})
}
public get provider (): MiniRpcProvider {
return this.providers[this.currentChainId]
}
public async activate (): Promise<ConnectorUpdate> {
return { provider: this.providers[this.currentChainId], chainId: this.currentChainId, account: null }
}
public async getProvider (): Promise<MiniRpcProvider> {
return this.providers[this.currentChainId]
}
public async getChainId (): Promise<number> {
return this.currentChainId
}
public async getAccount (): Promise<null> {
return null
}
public deactivate () {
return null
}
}
export default NetworkConnector
最后记得在连接钱包成功后设置
window.localStorage.setItem(connectorLocalStorageKey, walletConfig.connectorId);
use
const { account } = useWeb3React()
这样就可以了。 上面代码基本写玩后后面不需要改动,只需要配置constants的代码即可,之后我会考虑再研究下web3-react然后出一些更高级易用的组件发布。
--完成--