import type { ReactNode } from 'react' ; import * as PropTypes from 'prop-types' ; import * as React from 'react' ; import { useCallback , useContext , useEffect , useMemo , useRef , useState , } from 'react' ; import { UnsupportedChainIdError , Web3ReactProvider , useWeb3React , } from '@web3-react/core' ; import JSBI from 'jsbi' ; import { Account , AccountType , Balance , Connector , ConnectorConfig , EthereumProvider , Status , Wallet , } from './types' ; import { getConnectors } from './connectors' ; import { ConnectionRejectedError , ChainUnsupportedError , ConnectorUnsupportedError , } from './errors' ; import { getAccountBalance , getAccountIsContract , getBlockNumber , getNetworkName , pollEvery , MAINNET_ID , TESTNET_ID , } from './utils' ; const NO_BALANCE = '-1' ; const UseWalletContext = React . createContext < WalletContext > ( null ) ; type WalletContext = { addBlockNumberListener : ( callback : ( blockNumber : number ) => void ) => void ; pollBalanceInterval : number ; pollBlockNumberInterval : number ; removeBlockNumberListener : ( callback : ( blockNumber : number ) => void ) => void ; wallet : Wallet ; } | null ; type UseWalletProviderProps = { testnet : boolean ; children : ReactNode ; connectors : { [ key ] : Connector | ConnectorConfig ; } ; pollBalanceInterval : number ; pollBlockNumberInterval : number ; } ; function useWallet ( ) { const walletContext = useContext ( UseWalletContext ) ; if ( walletContext === null ) { throw new Error ( 'useWallet() can only be used inside of <UseWalletProvider />, ' + 'please declare it at a higher level.' ) ; } const getBlockNumber = useGetBlockNumber ( ) ; const { wallet } = walletContext ; return useMemo ( ( ) => { return { ... wallet , getBlockNumber } ; } , [ getBlockNumber , wallet ] ) ; } function useGetBlockNumber ( ) { const walletContext = useContext ( UseWalletContext ) ; const [ blockNumber , setBlockNumber ] = useState < number | null > ( null ) ; const requestedBlockNumber = useRef < boolean > ( false ) ; const getBlockNumber = useCallback < ( ) => number | null > ( ( ) => { if ( walletContext === null ) { return null ; } requestedBlockNumber . current = true ; walletContext . addBlockNumberListener ( setBlockNumber ) ; return blockNumber ; } , [ walletContext , blockNumber ] ) ; useEffect ( ( ) => { if ( ! requestedBlockNumber . current || walletContext === null ) { return ; } walletContext . addBlockNumberListener ( setBlockNumber ) ; return ( ) => { walletContext . removeBlockNumberListener ( setBlockNumber ) ; } ; } , [ requestedBlockNumber , walletContext ] ) ; return getBlockNumber ; } function useWalletBalance ( { account , ethereum , pollBalanceInterval , } ) { const [ balance , setBalance ] = useState < Balance > ( NO_BALANCE ) ; useEffect ( ( ) => { if ( ! account || ! ethereum ) { return ; } let cancel = false ; const pollBalance = pollEvery < Balance , any > ( ( account , ethereum , onUpdate ) => { let lastBalance = NO_BALANCE ; return { async request ( ) { return getAccountBalance ( ethereum , account ) . then ( ( value ) => { return value ? JSBI . BigInt ( value ) . toString ( ) : NO_BALANCE ; } ) . catch ( ( ) => NO_BALANCE ) ; } , onResult ( balance ) { if ( ! cancel && balance !== lastBalance ) { lastBalance = balance ; onUpdate ( balance ) ; } } } ; } , pollBalanceInterval ) ; const stopPollingBalance = pollBalance ( account , ethereum , setBalance ) ; return ( ) => { cancel = true ; stopPollingBalance ( ) ; setBalance ( NO_BALANCE ) ; } ; } , [ account , ethereum , pollBalanceInterval ] ) ; return balance ; } function useWatchBlockNumber ( { ethereum , pollBlockNumberInterval , } ) { const lastBlockNumber = useRef < number | null > ( null ) ; const blockNumberListeners = useRef < Set < ( blockNumber : number ) => void > > ( new Set ( ) ) ; const addBlockNumberListener = useCallback ( ( cb ) => { if ( blockNumberListeners . current . has ( cb ) ) { return ; } cb ( lastBlockNumber . current ) ; blockNumberListeners . current . add ( cb ) ; } , [ ] ) ; const removeBlockNumberListener = useCallback ( ( cb ) => { blockNumberListeners . current . delete ( cb ) ; } , [ ] ) ; const updateBlockNumber = useCallback ( ( blockNumber ) => { if ( lastBlockNumber . current === blockNumber ) { return ; } lastBlockNumber . current = blockNumber ; blockNumberListeners . current . forEach ( ( cb ) => cb ( blockNumber ) ) ; } , [ ] ) ; useEffect ( ( ) => { if ( ! ethereum ) { updateBlockNumber ( null ) ; return ; } let cancel = false ; const pollBlockNumber = pollEvery ( ( ) => { return { request : ( ) => getBlockNumber ( ethereum ) , onResult : ( latestBlockNumber ) => { if ( ! cancel ) { updateBlockNumber ( latestBlockNumber === null ? null : JSBI . BigInt ( latestBlockNumber ) . toString ( ) ) ; } } } ; } , pollBlockNumberInterval ) ; const stopPollingBlockNumber = pollBlockNumber ( ) ; return ( ) => { cancel = true ; stopPollingBlockNumber ( ) ; } ; } , [ ethereum , pollBlockNumberInterval , updateBlockNumber ] ) ; return { addBlockNumberListener , removeBlockNumberListener } ; } function UseWalletProvider ( { testnet , children , connectors : connectorsInitsOrConfigs , pollBalanceInterval , pollBlockNumberInterval , } ) { const walletContext = useContext ( UseWalletContext ) ; if ( walletContext !== null ) { throw new Error ( '<UseWalletProvider /> has already been declared.' ) ; } const chainId = testnet ? TESTNET_ID : MAINNET_ID ; const [ connector , setConnector ] = useState < string | null > ( null ) ; const [ error , setError ] = useState < Error | null > ( null ) ; const [ type , setType ] = useState < AccountType | null > ( null ) ; const [ status , setStatus ] = useState < Status > ( 'disconnected' ) ; const web3ReactContext = useWeb3React ( ) ; const activationId = useRef < number > ( 0 ) ; const { account , library : ethereum } = web3ReactContext ; const balance = useWalletBalance ( { account , ethereum , pollBalanceInterval } ) ; const { addBlockNumberListener , removeBlockNumberListener , } = useWatchBlockNumber ( { ethereum , pollBlockNumberInterval } ) ; const connectors = useMemo ( ( ) => getConnectors ( connectorsInitsOrConfigs ) , [ connectorsInitsOrConfigs , ] ) ; const reset = useCallback ( ( ) => { if ( web3ReactContext . active ) { web3ReactContext . deactivate ( ) ; } setConnector ( null ) ; setError ( null ) ; setStatus ( 'disconnected' ) ; } , [ web3ReactContext ] ) ; const connect = useCallback ( async ( connectorId = 'injected' ) => { const id = ++ activationId . current ; reset ( ) ; if ( id !== activationId . current ) { return ; } if ( ! connectors [ connectorId ] ) { setStatus ( 'error' ) ; setError ( new ConnectorUnsupportedError ( connectorId ) ) ; return ; } setStatus ( 'connecting' ) ; const [ connectorInit , connectorConfig ] = connectors [ connectorId ] || [ ] ; const connector = await connectorInit ?. ( ) ; const web3ReactConnector = connector ?. web3ReactConnector ?. ( { chainId , ... ( connectorConfig || { } ) } ) ; if ( ! web3ReactConnector ) { setStatus ( 'error' ) ; setError ( new ConnectorUnsupportedError ( connectorId ) ) ; return ; } try { setConnector ( connectorId ) ; await web3ReactContext . activate ( web3ReactConnector , undefined , true ) ; setStatus ( 'connected' ) ; } catch ( err ) { if ( id !== activationId . current ) { return ; } setConnector ( null ) ; setStatus ( 'error' ) ; if ( err instanceof UnsupportedChainIdError ) { setError ( new ChainUnsupportedError ( - 1 , chainId ) ) ; return ; } if ( connector . handleActivationError ) { const handledError = connector . handleActivationError ( err ) ; if ( handledError ) { setError ( handledError ) ; return ; } } setError ( err ) ; } } , [ chainId , connectors , reset , web3ReactContext ] ) ; useEffect ( ( ) => { if ( ! account || ! ethereum ) { return ; } let cancel = false ; setType ( null ) ; getAccountIsContract ( ethereum , account ) . then ( ( isContract ) => { if ( ! cancel ) { setStatus ( 'connected' ) ; setType ( isContract ? 'contract' : 'normal' ) ; } } ) ; return ( ) => { cancel = true ; setStatus ( 'disconnected' ) ; setType ( null ) ; } ; } , [ account , ethereum ] ) ; const wallet = useMemo ( ( ) => ( { _web3ReactContext : web3ReactContext , account : account || null , balance , chainId , connect , connector , connectors , error , ethereum , networkName : getNetworkName ( chainId ) , reset , status , type } ) , [ account , balance , chainId , connect , connector , connectors , error , ethereum , type , reset , status , web3ReactContext , ] ) ; return ( < UseWalletContext . Provider value = { { addBlockNumberListener , pollBalanceInterval , pollBlockNumberInterval , removeBlockNumberListener , wallet } } > { children } < /UseWalletContext.Provider>); } UseWalletProvider . propTypes = { chainId : PropTypes . number , children : PropTypes . node , connectors : PropTypes . objectOf ( PropTypes . object ) , pollBalanceInterval : PropTypes . number , pollBlockNumberInterval : PropTypes . number } ; UseWalletProvider . defaultProps = { chainId : 1 , connectors : { } , pollBalanceInterval : 2000 , pollBlockNumberInterval : 5000 } ; function UseWalletProviderWrapper ( props ) { return ( < Web3ReactProvider getLibrary = { ( ethereum ) => ethereum } > < UseWalletProvider { ... props } / > < /Web3ReactProvider>); } UseWalletProviderWrapper . propTypes = UseWalletProvider . propTypes ; UseWalletProviderWrapper . defaultProps = UseWalletProvider . defaultProps ; export { ConnectionRejectedError , ChainUnsupportedError , ConnectorUnsupportedError , UseWalletProviderWrapper as UseWalletProvider , useWallet , } ; export default useWallet ;