美文网首页
DAPP-- web3 使用web3-react保持metaMa

DAPP-- web3 使用web3-react保持metaMa

作者: 反者道之动001 | 来源:发表于2021-06-16 17:50 被阅读0次

让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然后出一些更高级易用的组件发布。

--完成--

相关文章

网友评论

      本文标题:DAPP-- web3 使用web3-react保持metaMa

      本文链接:https://www.haomeiwen.com/subject/nfceeltx.html