diff --git a/packages/boba/gateway/src/actions/setupAction.js b/packages/boba/gateway/src/actions/setupAction.js index 9c4a8885f9..cc21373918 100644 --- a/packages/boba/gateway/src/actions/setupAction.js +++ b/packages/boba/gateway/src/actions/setupAction.js @@ -81,3 +81,15 @@ export function setWalletConnected( state ) { return dispatch({ type: 'SETUP/WALLET_CONNECTED', payload: state }) } } + +export function setChainIdChanged() { + return function (dispatch) { + return dispatch({ type: 'SETUP/CHAINIDCHANGED/SET' }) + } +} + +export function resetChainIdChanged() { + return function (dispatch) { + return dispatch({ type: 'SETUP/CHAINIDCHANGED/RESET' }) + } +} diff --git a/packages/boba/gateway/src/actions/tokenAction.js b/packages/boba/gateway/src/actions/tokenAction.js index e3627aec26..4154462e34 100644 --- a/packages/boba/gateway/src/actions/tokenAction.js +++ b/packages/boba/gateway/src/actions/tokenAction.js @@ -225,3 +225,9 @@ export async function addToken ( tokenContractAddressL1 ) { return {currency: _tokenContractAddressL1, L1address: _tokenContractAddressL1, L2address: '', symbol: 'Not found', error: 'Not found'}; } } + +export function restTokenList () { + return function (dispatch) { + return dispatch({ type: 'TOKEN/GET/RESET' }); + } +} diff --git a/packages/boba/gateway/src/components/disconnect/Disconnect.js b/packages/boba/gateway/src/components/disconnect/Disconnect.js new file mode 100644 index 0000000000..d98b11b429 --- /dev/null +++ b/packages/boba/gateway/src/components/disconnect/Disconnect.js @@ -0,0 +1,57 @@ +/* +Copyright 2021-present Boba Network. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. */ + +import React from 'react'; +import { useDispatch } from 'react-redux'; +import { LoginOutlined } from '@mui/icons-material'; +import { IconButton, Tooltip } from '@mui/material'; + +import { + setLayer, + setConnect, + setConnectBOBA, + setConnectETH, + setEnableAccount, + setWalletConnected + } from 'actions/setupAction'; + +import networkService from 'services/networkService'; + +function Disconnect () { + + const dispatch = useDispatch(); + + const disconnect = async () => { + await networkService.walletService.disconnectWallet() + dispatch(setLayer(null)) + dispatch(setConnect(false)) + dispatch(setConnectBOBA(false)) + dispatch(setConnectETH(false)) + dispatch(setWalletConnected(false)) + dispatch(setEnableAccount(false)) + } + + return ( + <> + + + + + + + ); +} + +export default React.memo(Disconnect); diff --git a/packages/boba/gateway/src/components/mainMenu/layerSwitcher/LayerSwitcher.js b/packages/boba/gateway/src/components/mainMenu/layerSwitcher/LayerSwitcher.js index a2535ec68e..3dd506578a 100644 --- a/packages/boba/gateway/src/components/mainMenu/layerSwitcher/LayerSwitcher.js +++ b/packages/boba/gateway/src/components/mainMenu/layerSwitcher/LayerSwitcher.js @@ -36,6 +36,7 @@ import { selectConnectBOBA, selectConnect, selectWalletConnected, + selectChainIdChanged, } from 'selectors/setupSelector' import { @@ -53,7 +54,7 @@ import { setEnableAccount, setWalletAddress } from 'actions/setupAction' import { fetchTransactions, fetchBalances } from 'actions/networkAction' -import { openModal } from 'actions/uiAction' +import { closeModal, openModal } from 'actions/uiAction' import Button from 'components/button/Button.js' import { L1_ICONS, L2_ICONS } from 'util/network/network.util.js' import { LAYER } from 'util/constant.js' @@ -76,6 +77,7 @@ function LayerSwitcher({ visisble = true, isButton = false }) { const connectBOBARequest = useSelector(selectConnectBOBA()) const connectRequest = useSelector(selectConnect()) const walletConnected = useSelector(selectWalletConnected()) + const chainIdChanged = useSelector(selectChainIdChanged()) const theme = useTheme() const isMobile = useMediaQuery(theme.breakpoints.down('md')) @@ -84,15 +86,8 @@ function LayerSwitcher({ visisble = true, isButton = false }) { ? truncate(networkService.account, 6, 4, '...') : '' - const chainChangedFromMM = JSON.parse( - localStorage.getItem('chainChangedFromMM') - ) - const wantChain = JSON.parse(localStorage.getItem('wantChain')) - const chainChangedInit = JSON.parse(localStorage.getItem('chainChangedInit')) - const dispatchBootAccount = useCallback(() => { - - if (!accountEnabled && baseEnabled) initializeAccount() + if ((!accountEnabled && baseEnabled) || chainIdChanged) initializeAccount() async function initializeAccount() { @@ -112,6 +107,7 @@ function LayerSwitcher({ visisble = true, isButton = false }) { return false } else if (initialized === LAYER.L1 || initialized === LAYER.L2) { + dispatch(closeModal('wrongNetworkModal')) dispatch(setLayer(initialized)) dispatch(setEnableAccount(true)) dispatch(setWalletAddress(networkService.account)) @@ -122,25 +118,35 @@ function LayerSwitcher({ visisble = true, isButton = false }) { return false } } - }, [dispatch, accountEnabled, network, networkType, baseEnabled]) + }, [dispatch, accountEnabled, network, networkType, baseEnabled, chainIdChanged]) const doConnectToLayer = useCallback((layer) => { async function doConnect() { try { - localStorage.setItem('wantChain', JSON.stringify(layer)) - if (networkService.provider) { - await networkService.switchChain(layer) + if (networkService.walletService.provider) { + const response = await networkService.switchChain(layer) + if (response) { + if (layer === 'L1') { + dispatch(setConnectBOBA(false)) + } else { + dispatch(setConnectETH(false)) + } + dispatchBootAccount() + } else { + dispatch(setConnectETH(false)) + dispatch(setConnectBOBA(false)) + } } else { dispatch(openModal('walletSelectorModal')) } } catch (err) { console.log('ERROR', err) - dispatch(setConnectETH(false)); - dispatch(setConnectBOBA(false)); + dispatch(setConnectETH(false)) + dispatch(setConnectBOBA(false)) } } doConnect(); - }, [dispatch]) + }, [dispatch, dispatchBootAccount]) useEffect(() => { if (walletConnected) { @@ -150,30 +156,10 @@ function LayerSwitcher({ visisble = true, isButton = false }) { useEffect(() => { // detect mismatch and correct the mismatch - if (wantChain === 'L1' && layer === 'L2') { - dispatchBootAccount() - } else if (wantChain === 'L2' && layer === 'L1') { - dispatchBootAccount() - } - }, [wantChain, layer, dispatchBootAccount]) - - useEffect(() => { - // auto reconnect to MM if we just switched chains from - // with the chain switcher, and then unset the flag. - if (chainChangedInit) { - dispatchBootAccount() - localStorage.setItem('chainChangedInit', false) - } - }, [chainChangedInit, dispatchBootAccount]) - - useEffect(() => { - // auto reconnect to MM if we just switched chains from - // inside MM, and then unset the flag. - if (chainChangedFromMM) { + if (layer === 'L1' || layer === 'L2') { dispatchBootAccount() - localStorage.setItem('chainChangedFromMM', false) } - }, [chainChangedFromMM, dispatchBootAccount]) + }, [layer, dispatchBootAccount]) // listening for l1 connection request useEffect(() => { @@ -190,7 +176,7 @@ function LayerSwitcher({ visisble = true, isButton = false }) { }, [ connectBOBARequest, doConnectToLayer ]) useEffect(() => { - if (connectRequest) { + if (connectRequest && !networkService.walletService.provider) { dispatch(openModal('walletSelectorModal')) } }, [dispatch, connectRequest]) diff --git a/packages/boba/gateway/src/components/modal/Modal.js b/packages/boba/gateway/src/components/modal/Modal.js index b842fcbea8..29be52615d 100644 --- a/packages/boba/gateway/src/components/modal/Modal.js +++ b/packages/boba/gateway/src/components/modal/Modal.js @@ -61,7 +61,7 @@ function _Modal({ { !!newStyle ? - + diff --git a/packages/boba/gateway/src/components/pageHeader/PageHeader.js b/packages/boba/gateway/src/components/pageHeader/PageHeader.js index c36227d05c..83ba1e7b0a 100644 --- a/packages/boba/gateway/src/components/pageHeader/PageHeader.js +++ b/packages/boba/gateway/src/components/pageHeader/PageHeader.js @@ -22,6 +22,7 @@ import CloseIcon from 'components/icons/CloseIcon' import networkService from 'services/networkService' import { makeStyles } from '@mui/styles' import Copy from 'components/copy/Copy' +import Disconnect from 'components/disconnect/Disconnect' import { useSelector } from 'react-redux' import { selectAccountEnabled, @@ -169,7 +170,10 @@ const PageHeader = ({ maintenance }) => { {!!accountEnabled ? ( - + <> + + + ) : null} diff --git a/packages/boba/gateway/src/containers/modals/walletSelector/WalletSelectorModal.js b/packages/boba/gateway/src/containers/modals/walletSelector/WalletSelectorModal.js index b158ee3264..deef7e55f8 100644 --- a/packages/boba/gateway/src/containers/modals/walletSelector/WalletSelectorModal.js +++ b/packages/boba/gateway/src/containers/modals/walletSelector/WalletSelectorModal.js @@ -21,12 +21,7 @@ function WalletSelectorModal ({ open }) { const connectToWallet = async (type) => { try { - if (type === 'metamask') { - await networkService.walletService.connectMetaMask() - } - if (type === 'walletconnect') { - await networkService.walletService.connectWalletConnect() - } + await networkService.walletService.connectWallet(type) dispatch(closeModal('walletSelectorModal')) dispatch(setWalletConnected(true)) } catch (error) { @@ -43,21 +38,25 @@ function WalletSelectorModal ({ open }) { } return ( - + - - Connect to Wallet - connectToWallet('metamask')}> metamask - + MetaMask connectToWallet('walletconnect')}> walletconnect - + WalletConnect diff --git a/packages/boba/gateway/src/containers/modals/wrongNetwork/WrongNetworkModal.js b/packages/boba/gateway/src/containers/modals/wrongNetwork/WrongNetworkModal.js index a124b69872..9738834722 100644 --- a/packages/boba/gateway/src/containers/modals/wrongNetwork/WrongNetworkModal.js +++ b/packages/boba/gateway/src/containers/modals/wrongNetwork/WrongNetworkModal.js @@ -4,15 +4,23 @@ import { closeModal } from 'actions/uiAction'; import Button from 'components/button/Button'; import Modal from 'components/modal/Modal'; -import React from 'react'; +import React, { useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { selectNetwork } from 'selectors/networkSelector'; +import { restTokenList } from 'actions/tokenAction'; function WrongNetworkModal({open}) { const dispatch = useDispatch(); const network = useSelector(selectNetwork()); + useEffect(() => { + if (open){ + dispatch(restTokenList()) + } + }, [dispatch, open]) + + function handleClose() { dispatch(setConnect(false)); dispatch(closeModal('wrongNetworkModal')); diff --git a/packages/boba/gateway/src/reducers/setupReducer.js b/packages/boba/gateway/src/reducers/setupReducer.js index 1ce820ea21..2eb75cb8ad 100644 --- a/packages/boba/gateway/src/reducers/setupReducer.js +++ b/packages/boba/gateway/src/reducers/setupReducer.js @@ -31,6 +31,7 @@ const initialState = { connectBOBA: false, connect: false, walletConnected: false, + chainIdChanged: false, } function setupReducer(state = initialState, action) { @@ -101,6 +102,16 @@ function setupReducer(state = initialState, action) { appChain: action.payload, network: action.payload } + case 'SETUP/CHAINIDCHANGED/SET': + return { + ...state, + chainIdChanged: true + } + case 'SETUP/CHAINIDCHANGED/RESET': + return { + ...state, + chainIdChanged: false + } default: return state } diff --git a/packages/boba/gateway/src/reducers/tokenReducer.js b/packages/boba/gateway/src/reducers/tokenReducer.js index a3b6b40ee7..04a473a370 100644 --- a/packages/boba/gateway/src/reducers/tokenReducer.js +++ b/packages/boba/gateway/src/reducers/tokenReducer.js @@ -31,6 +31,9 @@ const initialState = { function tokenReducer(state = initialState, action) { switch (action.type) { + case 'TOKEN/GET/RESET': + state = initialState + return state case 'TOKEN/GET/SUCCESS': return { ...state, diff --git a/packages/boba/gateway/src/selectors/setupSelector.js b/packages/boba/gateway/src/selectors/setupSelector.js index 36ee043031..47b36934f9 100644 --- a/packages/boba/gateway/src/selectors/setupSelector.js +++ b/packages/boba/gateway/src/selectors/setupSelector.js @@ -92,3 +92,9 @@ export function selectWalletConnected () { return state.setup['walletConnected'] } } + +export function selectChainIdChanged () { + return function (state) { + return state.setup['chainIdChanged'] + } +} diff --git a/packages/boba/gateway/src/services/networkService.js b/packages/boba/gateway/src/services/networkService.js index 6d84b89bd6..0dba550707 100644 --- a/packages/boba/gateway/src/services/networkService.js +++ b/packages/boba/gateway/src/services/networkService.js @@ -672,6 +672,8 @@ class NetworkService { const L1ChainId = networkDetail['L1']['chainId'] const L2ChainId = networkDetail['L2']['chainId'] + this.walletService.bindProviderListeners() + // there are numerous possible chains we could be on also, either L1 or L2 // at this point, we only know whether we want to be on which network etc @@ -680,11 +682,9 @@ class NetworkService { } else if(!!NETWORK[ network ] && networkMM.chainId === L1ChainId) { this.L1orL2 = 'L1'; } else { - this.walletService.bindProviderListeners() return 'wrongnetwork' } - this.walletService.bindProviderListeners() // this should not do anything unless we changed chains if (this.L1orL2 === 'L2') { await this.getBobaFeeChoice() @@ -700,6 +700,9 @@ class NetworkService { async switchChain(targetLayer) { + // ignore request if we are already on the target layer + if (!targetLayer) { return false } + const networkDetail = getNetworkDetail({ network: this.networkGateway, networkType: this.networkType @@ -719,7 +722,7 @@ class NetworkService { blockExplorerUrls: [networkDetail[targetLayer]?.blockExplorerUrl?.slice(0, -1)] } - await this.walletService.switchChain(targetIDHex, chainParam) + return await this.walletService.switchChain(targetIDHex, chainParam) } async getSevens() { @@ -4016,9 +4019,13 @@ class NetworkService { /***** L2 Fee *****/ /***********************************************/ async estimateL2Fee(payload=this.payloadForL1SecurityFee) { - const l2GasPrice = await this.L2Provider.getGasPrice() - const l2GasEstimate = await this.L2Provider.estimateGas(payload) - return l2GasPrice.mul(l2GasEstimate).toNumber() + try { + const l2GasPrice = await this.L2Provider.getGasPrice() + const l2GasEstimate = await this.L2Provider.estimateGas(payload) + return l2GasPrice.mul(l2GasEstimate).toNumber() + } catch { + return 0 + } } /***********************************************/ diff --git a/packages/boba/gateway/src/services/wallet.service.js b/packages/boba/gateway/src/services/wallet.service.js index e7f0b868f6..6c73e3b71f 100644 --- a/packages/boba/gateway/src/services/wallet.service.js +++ b/packages/boba/gateway/src/services/wallet.service.js @@ -17,6 +17,7 @@ limitations under the License. */ import { providers } from "ethers" import WalletConnectProvider from "@walletconnect/web3-provider" import { rpcUrls } from 'util/network/network.util' +import store from 'store' class WalletService { constructor() { @@ -41,11 +42,10 @@ class WalletService { async disconnectMetaMask() { try { await window.ethereum.request({ method: "eth_requestAccounts", params: [{ eth_accounts: {} }] }) - this.provider = null - this.account = null - this.walletType = null + return true } catch (e) { console.log(`Error disconnecting wallet: ${e}`) + return false } } @@ -53,95 +53,105 @@ class WalletService { window.ethereum.on('accountsChanged', () => { window.location.reload() }) - window.ethereum.on('chainChanged', () => { - const chainChangedInit = JSON.parse(localStorage.getItem('chainChangedInit')) - // do not reload window in the special case where the user - // changed chains AND conncted at the same time - // otherwise the user gets confused about why they are going through - // two window reloads - if(chainChangedInit) { - localStorage.setItem('chainChangedInit', false) - } else { - localStorage.setItem('chainChangedFromMM', true) - window.location.reload() - } + + window.ethereum.on('chainChanged', (chainId) => { + console.log(`MetaMask chain changed to ${chainId}`) + store.dispatch({ type: 'SETUP/CHAINIDCHANGED/SET' }) }) } async connectWalletConnect() { - console.log(rpcUrls) this.walletConnectProvider = new WalletConnectProvider({ rpc: rpcUrls }) - await this.walletConnectProvider.enable(); + await this.walletConnectProvider.enable() this.provider = new providers.Web3Provider(this.walletConnectProvider) this.account = await this.provider.getSigner().getAddress() this.walletType = 'walletconnect' } + async disconnectWalletConnect() { + try { + await this.walletConnectProvider.disconnect() + return true + } catch (e) { + console.log(`Error disconnecting wallet: ${e}`) + return false + } + } + async listenWalletConnect() { this.walletConnectProvider.on("accountsChanged", (accounts) => { - window.location.reload() - }); - - this.walletConnectProvider.on("chainChanged", (chainId) => { - const chainChangedInit = JSON.parse(localStorage.getItem('chainChangedInit')) - // do not reload window in the special case where the user - // changed chains AND conncted at the same time - // otherwise the user gets confused about why they are going through - // two window reloads - if(chainChangedInit) { - localStorage.setItem('chainChangedInit', false) - } else { - localStorage.setItem('chainChangedFromMM', true) + if (this.account !== accounts[0]) { window.location.reload() } }); - this.walletConnectProvider.on("disconnect", (code, reason) => { - console.log("WalletConnect disconnect: ", code, reason) - window.location.reload() + this.walletConnectProvider.on("chainChanged", (chainId) => { + console.log(`walletconnect chain changed to: ${chainId}`) + store.dispatch({ type: 'SETUP/CHAINIDCHANGED/SET' }) }); } async switchChain(chainId, chainInfo) { const provider = this.walletType === 'metamask' ? window.ethereum : this.walletConnectProvider try { - if (this.walletType === 'walletconnect') { + await provider.request({ + method: "wallet_switchEthereumChain", + params: [{ chainId }], + }) + const chainIdChanged = await provider.request({ method: 'eth_chainId' }) + // walletconnect does not return error code 4902 if the chain is not exist + // so we need to add the code of adding chain. + if (this.walletType === 'walletconnect' && chainIdChanged !== chainId) { await provider.request({ method: "wallet_addEthereumChain", params: [chainInfo, this.account], }) } - // walletconnect does not return error code 4902 if the chain is not exist - // so we need to add the code of adding chain. - await provider.request({ - method: "wallet_switchEthereumChain", - params: [{ chainId }], - }) + await this.connectWallet(this.walletType) + return true } catch (error) { - await provider.request({ - method: "wallet_addEthereumChain", - params: [chainInfo, this.account], - }) if (error.code === 4902) { try { await provider.request({ method: "wallet_addEthereumChain", params: [chainInfo, this.account], }) - provider.on('chainChanged', this.handleChangeChainOnce) + await this.connectWallet(this.walletType) + return true } catch (addError) { console.log(`Error adding chain: ${addError}`) - throw new Error(addError.code) + return false } } else { - console.log(`Error switching chain: ${JSON.stringify(error)}`) - throw new Error(error.code) + console.log(`Error switching chain: ${error?.message}`) + return false } } } - + + async connectWallet(type) { + if (type === 'metamask') { + await this.connectMetaMask() + } + if (type === 'walletconnect') { + await this.connectWalletConnect() + } + } + + async disconnectWallet() { + let result = false + if (this.walletType === 'metamask') { + result = await this.disconnectMetaMask() + } + if (this.walletType === 'walletconnect') { + result = await this.disconnectWalletConnect() + } + this.resetValues() + return result + } + bindProviderListeners() { if (this.walletType === 'metamask') { this.listenMetaMask() @@ -151,13 +161,12 @@ class WalletService { } } - handleChangeChainOnce(chainID_hex_string) { - - localStorage.setItem('chainChangedInit', true) - - localStorage.setItem('newChain', Number(chainID_hex_string)) - // and remove the listner - window.ethereum.removeListener('chainChanged', this.handleChangeChainOnce) + resetValues() { + this.walletConnectProvider = null + this.provider = null + this.account = null + this.walletType = null + store.dispatch({ type: 'SETUP/CHAINIDCHANGED/RESET' }) } }