DAPP-- web3 使用web3-react保持metaMask等钱包连接

6,910 阅读4分钟

让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 alreadyconst triedEager = useEagerConnect()

 ​// after eagerly trying injected, if the network connect ever isn't active or in an error state, activate itduseEffect(() => {
   ​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 existsuseInactiveListener(!triedEager)

 ​// handle delayed loader stateconst [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 connectorif (!triedEager) {
   ​return null
 ​}

 ​// if the account context isn't active, and there's an error on the network context, it's an irrecoverable errorif (!active && networkError) {
   ​return (
     ​<div>unknownError</div>
   ​)
 ​}

 ​// if neither context is active, spinif (!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 doesconst [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 flaguseEffect(() => {
   ​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 doesuseEffect(() => {
   ​const { ethereum } = windowif (ethereum && ethereum.on && !active && !error && !suppress) {
     ​const handleChainChanged = () => {
       ​// eat errorsactivate(injected, undefined, true).catch((e) => {
         ​console.error('Failed to activate after chain changed', e)
       ​})
     ​}

     ​const handleAccountsChanged = (accounts: string[]) => {
       ​if (accounts.length > 0) {
         ​// eat errorsactivate(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) => voidreject: (error: Error) => void
}

class MiniRpcProvider implements AsyncSendable {
 ​public readonly isMetaMask: false = falsepublic readonly chainId: numberpublic readonly url: stringpublic readonly host: stringpublic readonly path: stringpublic readonly batchWaitTimeMs: numberprivate nextId = 1private batchTimeoutId: ReturnType<typeof setTimeout> | null = nullprivate batch: BatchItem[] = []

 ​constructor(chainId: number, url: string, batchWaitTimeMs?: number) {
   ​this.chainId = chainId
   ​this.url = url
   ​const parsed = new URL(url)
   ​this.host = parsed.hostthis.path = parsed.pathname// how long to wait to batch callsthis.batchWaitTimeMs = batchWaitTimeMs ?? 50
 ​}

 ​public readonly clearBatch = async () => {
   ​// console.info('Clearing batch', this.batch)const { batch } = thisthis.batch = []
   ​this.batchTimeoutId = nulllet response: Responsetry {
     ​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-syntaxfor (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: numberconstructor({ 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然后出一些更高级易用的组件发布。

--完成--