DAPP-- web3 使用web3-react保持metaMask等錢包連接

讓DAPP實(shí)現(xiàn)錢包保持鏈接

錢包在有效期內(nèi)自動(dòng)連接橘蜜,然后使用hooks函數(shù)獲取對(duì)應(yīng)的信息和之后跟智能合約交互做準(zhǔn)備。錢包連接其實(shí)也可以直接用web3和meta mask提供的方法寫(xiě)雷激,但是這樣有一個(gè)問(wèn)題是需要考慮很多場(chǎng)景和多錢包波附,這樣導(dǎo)致代碼量很大并且問(wèn)題可能很多姜骡。于是我們應(yīng)該尋找一些現(xiàn)成的方案來(lái)實(shí)現(xiàn)弃理。

在react里面有一個(gè)很優(yōu)秀的庫(kù)叫web3-react

還有一個(gè)很酷的連接錢包的react連接UI的庫(kù)叫web3modal,連接的過(guò)程不需要我們操作滔金。這個(gè)兩個(gè)庫(kù)都已經(jīng)在最大的交易網(wǎng)站上面使用了色解,不怕出問(wèn)題。

而因?yàn)檫@些優(yōu)秀的庫(kù)餐茵,所以導(dǎo)致所有知名的區(qū)塊鏈行業(yè)代碼都是使用react

下面多代碼預(yù)警冒签,但是核心就一個(gè),設(shè)置Providers,一個(gè)是web3-react需要的钟病,一個(gè)是自定義管理web3連接的Providers萧恕「账螅基于這個(gè)目的,我們需要編寫(xiě)一些hooks等代碼票唆。

下面直接附上代碼朴读。(解析uniswap出來(lái)的,可放心食用)

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>)
}

再設(shè)置管理的Provider走趋, 下次自動(dòng)連接而不需要重新重新簽名

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

最后記得在連接錢包成功后設(shè)置

window.localStorage.setItem(connectorLocalStorageKey, walletConfig.connectorId);

use

const { account } = useWeb3React()

這樣就可以了衅金。 上面代碼基本寫(xiě)玩后后面不需要改動(dòng),只需要配置constants的代碼即可簿煌,之后我會(huì)考慮再研究下web3-react然后出一些更高級(jí)易用的組件發(fā)布氮唯。

--完成--

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市姨伟,隨后出現(xiàn)的幾起案子惩琉,更是在濱河造成了極大的恐慌,老刑警劉巖夺荒,帶你破解...
    沈念sama閱讀 222,681評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瞒渠,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡技扼,警方通過(guò)查閱死者的電腦和手機(jī)伍玖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)剿吻,“玉大人窍箍,你說(shuō)我怎么就攤上這事±雎茫” “怎么了仔燕?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,421評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)魔招。 經(jīng)常有香客問(wèn)我,道長(zhǎng)五辽,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,114評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮滴某,結(jié)果婚禮上剪个,老公的妹妹穿的比我還像新娘。我一直安慰自己罪郊,他們只是感情好蠕蚜,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,116評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著悔橄,像睡著了一般靶累。 火紅的嫁衣襯著肌膚如雪腺毫。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,713評(píng)論 1 312
  • 那天挣柬,我揣著相機(jī)與錄音潮酒,去河邊找鬼。 笑死邪蛔,一個(gè)胖子當(dāng)著我的面吹牛急黎,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播侧到,決...
    沈念sama閱讀 41,170評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼勃教,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了匠抗?” 一聲冷哼從身側(cè)響起故源,我...
    開(kāi)封第一講書(shū)人閱讀 40,116評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎戈咳,沒(méi)想到半個(gè)月后心软,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,651評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡著蛙,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,714評(píng)論 3 342
  • 正文 我和宋清朗相戀三年删铃,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片踏堡。...
    茶點(diǎn)故事閱讀 40,865評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡猎唁,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出顷蟆,到底是詐尸還是另有隱情诫隅,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布帐偎,位于F島的核電站逐纬,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏削樊。R本人自食惡果不足惜豁生,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,211評(píng)論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望漫贞。 院中可真熱鬧甸箱,春花似錦、人聲如沸迅脐。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,699評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)谴蔑。三九已至豌骏,卻和暖如春龟梦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背肯适。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,814評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工变秦, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人框舔。 一個(gè)月前我還...
    沈念sama閱讀 49,299評(píng)論 3 379
  • 正文 我出身青樓蹦玫,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親刘绣。 傳聞我的和親對(duì)象是個(gè)殘疾皇子樱溉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,870評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容