From 3a3c412790cbd388ba026d0447312d59b99bdec1 Mon Sep 17 00:00:00 2001 From: dan13ram Date: Tue, 9 Feb 2021 13:49:43 +0530 Subject: [PATCH 1/6] using rpc urls for walletconnect & graphHealth toasts --- packages/react-app/.sample-env | 8 ++ .../src/components/BridgeHistory.jsx | 4 + .../src/components/BridgeLoadingModal.jsx | 5 + .../react-app/src/contexts/Web3Context.jsx | 10 +- .../react-app/src/hooks/useGraphHealth.js | 131 ++++++++++++++++++ packages/react-app/src/lib/constants.js | 18 ++- packages/react-app/src/lib/ethPrice.js | 26 ++-- packages/react-app/src/lib/graphHealth.js | 89 ++++++++++++ packages/react-app/src/lib/helpers.js | 10 ++ 9 files changed, 276 insertions(+), 25 deletions(-) create mode 100644 packages/react-app/src/hooks/useGraphHealth.js create mode 100644 packages/react-app/src/lib/graphHealth.js diff --git a/packages/react-app/.sample-env b/packages/react-app/.sample-env index b73a3612..a7cd3284 100644 --- a/packages/react-app/.sample-env +++ b/packages/react-app/.sample-env @@ -73,3 +73,11 @@ REACT_APP_UI_STATUS_UPDATE_INTERVAL=1000 # if unset default => false ###################################################### REACT_APP_DEBUG_LOGS=false + +###################################################### +# if unset default => +# UPDATE_INTERVAL => 15000 +# THRESHOLD_BLOCKS => 10 +###################################################### +REACT_APP_GRAPH_HEALTH_UPDATE_INTERVAL=15000 +REACT_APP_GRAPH_HEALTH_THRESHOLD_BLOCKS=10 diff --git a/packages/react-app/src/components/BridgeHistory.jsx b/packages/react-app/src/components/BridgeHistory.jsx index a9d498fc..1a8ae836 100644 --- a/packages/react-app/src/components/BridgeHistory.jsx +++ b/packages/react-app/src/components/BridgeHistory.jsx @@ -2,6 +2,7 @@ import { Checkbox, Flex, Grid, Text } from '@chakra-ui/react'; import React, { useState } from 'react'; import { Redirect } from 'react-router-dom'; +import { useGraphHealth } from '../hooks/useGraphHealth'; import { useUserHistory } from '../lib/history'; import { HistoryItem } from './HistoryItem'; import { HistoryPagination } from './HistoryPagination'; @@ -14,6 +15,9 @@ export const BridgeHistory = ({ page }) => { const [onlyUnReceived, setOnlyUnReceived] = useState(false); const { transfers, loading } = useUserHistory(); + useGraphHealth( + 'Cannot access history data. Wait for a few minutes and reload the application', + ); if (loading) { return ( diff --git a/packages/react-app/src/components/BridgeLoadingModal.jsx b/packages/react-app/src/components/BridgeLoadingModal.jsx index 493e609a..9083581c 100644 --- a/packages/react-app/src/components/BridgeLoadingModal.jsx +++ b/packages/react-app/src/components/BridgeLoadingModal.jsx @@ -13,6 +13,7 @@ import React, { useContext } from 'react'; import BlueTickImage from '../assets/blue-tick.svg'; import LoadingImage from '../assets/loading.svg'; import { BridgeContext } from '../contexts/BridgeContext'; +import { useGraphHealth } from '../hooks/useGraphHealth'; import { useTransactionStatus } from '../hooks/useTransactionStatus'; import { getMonitorUrl } from '../lib/helpers'; import { NeedsConfirmationModal } from './NeedsConfirmationModal'; @@ -28,6 +29,10 @@ export const BridgeLoadingModal = () => { const { loading, fromToken, txHash, totalConfirms } = useContext( BridgeContext, ); + useGraphHealth( + 'Cannot collect data to finalize the transfer. Wait for a few minutes, reload the application and look for your unclaimed transactions in the History tab', + true, + ); const { loadingText, needsConfirmation, diff --git a/packages/react-app/src/contexts/Web3Context.jsx b/packages/react-app/src/contexts/Web3Context.jsx index d886f640..573ef109 100644 --- a/packages/react-app/src/contexts/Web3Context.jsx +++ b/packages/react-app/src/contexts/Web3Context.jsx @@ -4,8 +4,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import Web3 from 'web3'; import Web3Modal from 'web3modal'; -import { INFURA_ID } from '../lib/constants'; -import { getNetworkName, logError } from '../lib/helpers'; +import { getNetworkName, getRPCUrl, logError } from '../lib/helpers'; export const Web3Context = React.createContext({}); @@ -30,7 +29,12 @@ const providerOptions = { walletconnect: { package: WalletConnectProvider, options: { - infuraId: INFURA_ID, + rpc: { + 1: getRPCUrl(1), + 42: getRPCUrl(42), + 100: getRPCUrl(100), + 77: getRPCUrl(77), + }, }, }, }; diff --git a/packages/react-app/src/hooks/useGraphHealth.js b/packages/react-app/src/hooks/useGraphHealth.js new file mode 100644 index 00000000..d5ac1892 --- /dev/null +++ b/packages/react-app/src/hooks/useGraphHealth.js @@ -0,0 +1,131 @@ +import { useToast } from '@chakra-ui/react'; +import { useContext, useEffect, useRef, useState } from 'react'; + +import { Web3Context } from '../contexts/Web3Context'; +import { HOME_NETWORK } from '../lib/constants'; +import { getHealthStatus } from '../lib/graphHealth'; +import { getBridgeNetwork, logDebug, logError } from '../lib/helpers'; +import { getEthersProvider } from '../lib/providers'; + +const FOREIGN_NETWORK = getBridgeNetwork(HOME_NETWORK); + +const { + REACT_APP_GRAPH_HEALTH_UPDATE_INTERVAL, + REACT_APP_GRAPH_HEALTH_THRESHOLD_BLOCKS, +} = process.env; + +const DEFAULT_GRAPH_HEALTH_UPDATE_INTERVAL = 15000; + +const DEFAULT_GRAPH_HEALTH_THRESHOLD_BLOCKS = 10; + +const UPDATE_INTERVAL = + REACT_APP_GRAPH_HEALTH_UPDATE_INTERVAL || + DEFAULT_GRAPH_HEALTH_UPDATE_INTERVAL; + +const THRESHOLD_BLOCKS = + REACT_APP_GRAPH_HEALTH_THRESHOLD_BLOCKS || + DEFAULT_GRAPH_HEALTH_THRESHOLD_BLOCKS; + +export const useGraphHealth = (description, onlyHome = false) => { + const { providerChainId } = useContext(Web3Context); + + const isHome = providerChainId === HOME_NETWORK; + + const [homeHealthy, setHomeHealthy] = useState(true); + + const [foreignHealthy, setForeignHealthy] = useState(true); + + const [loading, setLoading] = useState(false); + + useEffect(() => { + const subscriptions = []; + const unsubscribe = () => { + subscriptions.forEach(s => { + clearTimeout(s); + }); + }; + + const load = async () => { + try { + setLoading(true); + const [ + { homeHealth, foreignHealth }, + homeBlockNumber, + foreignBlockNumber, + ] = await Promise.all([ + getHealthStatus(), + getEthersProvider(HOME_NETWORK).getBlockNumber(), + getEthersProvider(FOREIGN_NETWORK).getBlockNumber(), + ]); + logDebug({ + homeHealth, + foreignHealth, + homeBlockNumber, + foreignBlockNumber, + message: 'updated graph health data', + }); + + setHomeHealthy( + homeHealth && + homeHealth.isReachable && + !homeHealth.isFailed && + homeHealth.isSynced && + Math.abs(homeHealth.latestBlockNumber - homeBlockNumber) < + THRESHOLD_BLOCKS, + ); + + setForeignHealthy( + foreignHealth && + foreignHealth.isReachable && + !foreignHealth.isFailed && + foreignHealth.isSynced && + Math.abs(foreignHealth.latestBlockNumber - foreignBlockNumber) < + THRESHOLD_BLOCKS, + ); + + const timeoutId = setTimeout(() => load(), UPDATE_INTERVAL); + subscriptions.push(timeoutId); + } catch (graphHealthError) { + logError({ graphHealthError }); + } finally { + setLoading(false); + } + }; + + // unsubscribe from previous polls + unsubscribe(); + + load(); + // unsubscribe when unmount component + return unsubscribe; + }, []); + + const toast = useToast(); + const toastIdRef = useRef(); + + useEffect(() => { + if (!loading) { + if (toastIdRef.current) { + toast.close(toastIdRef.current); + } + if (!(homeHealthy && foreignHealthy)) { + if (onlyHome && !isHome) return; + toastIdRef.current = toast({ + title: 'Subgraph Error', + description, + status: 'error', + duration: null, + isClosable: false, + }); + } + } + }, [ + homeHealthy, + foreignHealthy, + loading, + toast, + onlyHome, + isHome, + description, + ]); +}; diff --git a/packages/react-app/src/lib/constants.js b/packages/react-app/src/lib/constants.js index c5316876..abbbcf52 100644 --- a/packages/react-app/src/lib/constants.js +++ b/packages/react-app/src/lib/constants.js @@ -102,11 +102,18 @@ export const defaultTokens = { }, }; +export const subgraphNames = { + 100: 'raid-guild/xdai-omnibridge', + 1: 'raid-guild/mainnet-omnibridge', + 77: 'dan13ram/sokol-omnibridge', + 42: 'dan13ram/kovan-omnibridge', +}; + export const graphEndpoints = { - 100: 'https://api.thegraph.com/subgraphs/name/raid-guild/xdai-omnibridge', - 1: 'https://api.thegraph.com/subgraphs/name/raid-guild/mainnet-omnibridge', - 77: 'https://api.thegraph.com/subgraphs/name/dan13ram/sokol-omnibridge', - 42: 'https://api.thegraph.com/subgraphs/name/dan13ram/kovan-omnibridge', + 100: `https://api.thegraph.com/subgraphs/name/${subgraphNames[100]}`, + 1: `https://api.thegraph.com/subgraphs/name/${subgraphNames[1]}`, + 77: `https://api.thegraph.com/subgraphs/name/${subgraphNames[77]}`, + 42: `https://api.thegraph.com/subgraphs/name/${subgraphNames[42]}`, }; export const mediators = { @@ -148,3 +155,6 @@ export const defaultTokensUrl = { 42: '', 77: '', }; + +export const GRAPH_HEALTH_ENDPOINT = + 'https://api.thegraph.com/index-node/graphql'; diff --git a/packages/react-app/src/lib/ethPrice.js b/packages/react-app/src/lib/ethPrice.js index 4f3d243a..873660d5 100644 --- a/packages/react-app/src/lib/ethPrice.js +++ b/packages/react-app/src/lib/ethPrice.js @@ -1,28 +1,21 @@ -const ethPriceFromApi = async (fetchFn, options = {}) => { +import { logDebug, logError } from './helpers'; + +const ethPriceFromApi = async fetchFn => { try { const response = await fetchFn(); const json = await response.json(); const oracleEthPrice = json.ethereum.usd; if (!oracleEthPrice) { - options.logger && - options.logger.error && - options.logger.error(`Response from Oracle didn't include eth price`); + logError(`Response from Oracle didn't include eth price`); return null; } - options.logger && - options.logger.debug && - options.logger.debug( - { oracleEthPrice }, - 'Gas price updated using the API', - ); + logDebug({ oracleEthPrice, message: 'Gas price updated using the API' }); return oracleEthPrice; } catch (e) { - options.logger && - options.logger.error && - options.logger.error(`ETH Price API is not available. ${e.message}`); + logError(`ETH Price API is not available. ${e.message}`); } return null; }; @@ -34,7 +27,7 @@ const { const DEFAULT_ETH_PRICE_API_URL = 'https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=USD'; -const DEFAULT_ETH_PRICE_UPDATE_INTERVAL = 900000; +const DEFAULT_ETH_PRICE_UPDATE_INTERVAL = 15000; class EthPriceStore { ethPrice = null; @@ -52,11 +45,8 @@ class EthPriceStore { } async updateGasPrice() { - const oracleOptions = { - logger: console, - }; const fetchFn = () => fetch(this.ethPriceApiUrl); - this.ethPrice = await ethPriceFromApi(fetchFn, oracleOptions); + this.ethPrice = await ethPriceFromApi(fetchFn); setTimeout(() => this.updateGasPrice(), this.updateInterval); } diff --git a/packages/react-app/src/lib/graphHealth.js b/packages/react-app/src/lib/graphHealth.js new file mode 100644 index 00000000..0859284f --- /dev/null +++ b/packages/react-app/src/lib/graphHealth.js @@ -0,0 +1,89 @@ +import { gql, request } from 'graphql-request'; + +import { GRAPH_HEALTH_ENDPOINT, HOME_NETWORK } from './constants'; +import { getBridgeNetwork, getSubgraphName, logError } from './helpers'; + +const FOREIGN_NETWORK = getBridgeNetwork(HOME_NETWORK); + +const HOME_SUBGRAPH = getSubgraphName(HOME_NETWORK); +const FOREIGN_SUBGRAPH = getSubgraphName(FOREIGN_NETWORK); + +const healthQuery = gql` + query getHealthStatus($subgraphHome: String!, $subgraphForeign: String!) { + homeHealth: indexingStatusForCurrentVersion(subgraphName: $subgraphHome) { + synced + health + fatalError { + message + block { + number + hash + } + handler + } + chains { + chainHeadBlock { + number + } + latestBlock { + number + } + } + } + foreignHealth: indexingStatusForCurrentVersion( + subgraphName: $subgraphForeign + ) { + synced + health + fatalError { + message + block { + number + hash + } + handler + } + chains { + chainHeadBlock { + number + } + latestBlock { + number + } + } + } + } +`; + +const extractStatus = ({ fatalError, synced, chains }) => ({ + isReachable: true, + isFailed: !!fatalError, + isSynced: synced, + latestBlockNumber: Number(chains[0].latestBlock.number), +}); + +const failedStatus = { + isReachable: false, + isFailed: true, + isSynced: false, + latestBlockNumber: 0, +}; + +export const getHealthStatus = async () => { + try { + const data = await request(GRAPH_HEALTH_ENDPOINT, healthQuery, { + subgraphHome: HOME_SUBGRAPH, + subgraphForeign: FOREIGN_SUBGRAPH, + }); + return { + homeHealth: extractStatus(data.homeHealth), + foreignHealth: extractStatus(data.foreignHealth), + }; + } catch (graphHealthError) { + logError({ graphHealthError }); + } + return { + homeHealth: failedStatus, + foreignHealth: failedStatus, + }; +}; diff --git a/packages/react-app/src/lib/helpers.js b/packages/react-app/src/lib/helpers.js index afe09323..2b8330ff 100644 --- a/packages/react-app/src/lib/helpers.js +++ b/packages/react-app/src/lib/helpers.js @@ -9,6 +9,7 @@ import { mediators, networkLabels, networkNames, + subgraphNames, } from './constants'; import { getOverriddenMediator, isOverridden } from './overrides'; @@ -58,6 +59,8 @@ export const getNetworkLabel = chainId => networkLabels[chainId] || 'Unknown'; export const getAMBAddress = chainId => ambs[chainId] || ambs[100]; export const getGraphEndpoint = chainId => graphEndpoints[chainId] || graphEndpoints[100]; +export const getSubgraphName = chainId => + subgraphNames[chainId] || subgraphNames[100]; export const getRPCUrl = chainId => (chainUrls[chainId] || chainUrls[100]).rpc; export const getExplorerUrl = chainId => (chainUrls[chainId] || chainUrls[100]).explorer; @@ -131,3 +134,10 @@ export const logError = error => { console.error(error); } }; + +export const logDebug = error => { + if (process.env.REACT_APP_DEBUG_LOGS === 'true') { + // eslint-disable-next-line no-console + console.debug(error); + } +}; From f6a97de175c398be5cd4db7679329aa63220d39a Mon Sep 17 00:00:00 2001 From: dan13ram Date: Tue, 9 Feb 2021 17:25:35 +0530 Subject: [PATCH 2/6] better fetching of balance --- packages/react-app/package.json | 1 + .../src/components/BridgeLoadingModal.jsx | 1 - .../react-app/src/components/ConnectWeb3.jsx | 2 -- .../react-app/src/components/FromToken.jsx | 30 ++++++++++------- packages/react-app/src/components/Layout.jsx | 2 ++ packages/react-app/src/components/ToToken.jsx | 33 +++++++++++-------- yarn.lock | 7 ++++ 7 files changed, 48 insertions(+), 28 deletions(-) diff --git a/packages/react-app/package.json b/packages/react-app/package.json index bb9fc6f0..629a2fde 100644 --- a/packages/react-app/package.json +++ b/packages/react-app/package.json @@ -43,6 +43,7 @@ "react-dom": "16.12.0", "react-router-dom": "^5.2.0", "react-scripts": "3.4.1", + "rxjs": "^6.6.3", "web3": "^1.3.1", "web3modal": "^1.9.2" }, diff --git a/packages/react-app/src/components/BridgeLoadingModal.jsx b/packages/react-app/src/components/BridgeLoadingModal.jsx index 9083581c..d506017b 100644 --- a/packages/react-app/src/components/BridgeLoadingModal.jsx +++ b/packages/react-app/src/components/BridgeLoadingModal.jsx @@ -31,7 +31,6 @@ export const BridgeLoadingModal = () => { ); useGraphHealth( 'Cannot collect data to finalize the transfer. Wait for a few minutes, reload the application and look for your unclaimed transactions in the History tab', - true, ); const { loadingText, diff --git a/packages/react-app/src/components/ConnectWeb3.jsx b/packages/react-app/src/components/ConnectWeb3.jsx index 094ccafa..73e390fd 100644 --- a/packages/react-app/src/components/ConnectWeb3.jsx +++ b/packages/react-app/src/components/ConnectWeb3.jsx @@ -5,7 +5,6 @@ import { Web3Context } from '../contexts/Web3Context'; import { WalletFilledIcon } from '../icons/WalletFilledIcon'; import { HOME_NETWORK } from '../lib/constants'; import { getBridgeNetwork, getNetworkName } from '../lib/helpers'; -import { TermsOfServiceModal } from './TermsOfServiceModal'; export const ConnectWeb3 = () => { const { connectWeb3, loading, account, disconnect } = useContext(Web3Context); @@ -65,7 +64,6 @@ export const ConnectWeb3 = () => { Connect )} - ); }; diff --git a/packages/react-app/src/components/FromToken.jsx b/packages/react-app/src/components/FromToken.jsx index f9f013a0..828422ad 100644 --- a/packages/react-app/src/components/FromToken.jsx +++ b/packages/react-app/src/components/FromToken.jsx @@ -10,6 +10,7 @@ import { } from '@chakra-ui/react'; import { BigNumber, utils } from 'ethers'; import React, { useContext, useEffect, useState } from 'react'; +import { defer } from 'rxjs'; import DropDown from '../assets/drop-down.svg'; import { BridgeContext } from '../contexts/BridgeContext'; @@ -20,7 +21,7 @@ import { Logo } from './Logo'; import { SelectTokenModal } from './SelectTokenModal'; export const FromToken = () => { - const { account } = useContext(Web3Context); + const { account, providerChainId: chainId } = useContext(Web3Context); const { updateBalance, fromToken: token, @@ -36,23 +37,28 @@ export const FromToken = () => { const [balanceLoading, setBalanceLoading] = useState(false); useEffect(() => { - if (token && account) { + let subscription; + if (token && account && chainId === token.chainId) { setBalanceLoading(true); - setBalance(BigNumber.from(0)); - fetchTokenBalance(token, account) - .then(b => { - setBalance(b); - setBalanceLoading(false); - }) - .catch(contractError => { - logError({ contractError }); + subscription = defer(() => + fetchTokenBalance(token, account).catch(fromBalanceError => { + logError({ fromBalanceError }); setBalance(BigNumber.from(0)); setBalanceLoading(false); - }); + }), + ).subscribe(b => { + setBalance(b); + setBalanceLoading(false); + }); } else { setBalance(BigNumber.from(0)); } - }, [updateBalance, token, account, setBalance, setBalanceLoading]); + return () => { + if (subscription) { + subscription.unsubscribe(); + } + }; + }, [updateBalance, token, account, setBalance, setBalanceLoading, chainId]); return ( { const { account, providerChainId } = useContext(Web3Context); @@ -58,6 +59,7 @@ export const Layout = ({ children }) => { {valid ? children : }