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

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

--完成--

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,444评论 4 365
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,867评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,157评论 0 248
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,312评论 0 214
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,673评论 3 289
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,802评论 1 223
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,010评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,743评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,470评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,696评论 2 250
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,187评论 1 262
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,538评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,188评论 3 240
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,127评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,902评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,889评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,741评论 2 274

推荐阅读更多精彩内容