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/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/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..d506017b 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,9 @@ 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',
+ );
const {
loadingText,
needsConfirmation,
diff --git a/packages/react-app/src/components/ConfirmTransferModal.jsx b/packages/react-app/src/components/ConfirmTransferModal.jsx
index e3bbe373..2de1613e 100644
--- a/packages/react-app/src/components/ConfirmTransferModal.jsx
+++ b/packages/react-app/src/components/ConfirmTransferModal.jsx
@@ -65,15 +65,22 @@ export const ConfirmTransferModal = ({ isOpen, onClose }) => {
};
const onClick = () => {
transfer().catch(error => {
- if (
- error &&
- error.message &&
- !error.message.includes('User denied transaction signature')
- ) {
+ if (error && error.message) {
+ showError(error.message);
+ } else {
showError(
'Impossible to perform the operation. Reload the application and try again.',
);
}
+ // if (
+ // error &&
+ // error.message &&
+ // !error.message.includes('User denied transaction signature')
+ // ) {
+ // showError(
+ // 'Impossible to perform the operation. Reload the application and try again.',
+ // );
+ // }
});
onClose();
};
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..d404479e 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 (
{
mb={2}
direction={{ base: 'column', sm: 'row' }}
>
-
+
{
const { account, providerChainId } = useContext(Web3Context);
@@ -58,6 +59,7 @@ export const Layout = ({ children }) => {
{valid ? children : }
+
);
};
diff --git a/packages/react-app/src/components/ToToken.jsx b/packages/react-app/src/components/ToToken.jsx
index 58cc9af5..314b08a6 100644
--- a/packages/react-app/src/components/ToToken.jsx
+++ b/packages/react-app/src/components/ToToken.jsx
@@ -1,15 +1,16 @@
import { Flex, Spinner, Text, useBreakpointValue } from '@chakra-ui/react';
import { BigNumber, utils } from 'ethers';
import React, { useContext, useEffect, useState } from 'react';
+import { defer } from 'rxjs';
import { BridgeContext } from '../contexts/BridgeContext';
import { Web3Context } from '../contexts/Web3Context';
-import { formatValue, logError } from '../lib/helpers';
+import { formatValue, getBridgeNetwork, logError } from '../lib/helpers';
import { fetchTokenBalance } from '../lib/token';
import { Logo } from './Logo';
export const ToToken = () => {
- const { account } = useContext(Web3Context);
+ const { account, providerChainId } = useContext(Web3Context);
const {
updateBalance,
toToken: token,
@@ -18,28 +19,34 @@ export const ToToken = () => {
toBalance: balance,
setToBalance: setBalance,
} = useContext(BridgeContext);
+ const chainId = getBridgeNetwork(providerChainId);
const smallScreen = useBreakpointValue({ base: true, lg: false });
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(toBalanceError => {
+ logError({ toBalanceError });
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 { 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..f3761088 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;
@@ -83,6 +86,16 @@ export const uniqueTokens = list => {
export const formatValue = (num, dec) => {
const str = utils.formatUnits(num, dec);
+ if (str.length > 50) {
+ const expStr = Number(str)
+ .toExponential()
+ .replace(/e\+?/, ' x 10^');
+ const split = expStr.split(' x 10^');
+ const first = Number(split[0]).toLocaleString('en', {
+ maximumFractionDigits: 4,
+ });
+ return `${first} x 10^${split[1]}`;
+ }
return Number(str).toLocaleString('en', { maximumFractionDigits: 4 });
};
@@ -126,8 +139,13 @@ export const getAccountString = account => {
};
export const logError = error => {
+ // eslint-disable-next-line no-console
+ console.error(error);
+};
+
+export const logDebug = error => {
if (process.env.REACT_APP_DEBUG_LOGS === 'true') {
// eslint-disable-next-line no-console
- console.error(error);
+ console.debug(error);
}
};
diff --git a/yarn.lock b/yarn.lock
index c875d3dd..e6fbd271 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -13688,6 +13688,13 @@ rxjs@^6.5.3, rxjs@^6.6.0, rxjs@^6.6.2:
dependencies:
tslib "^1.9.0"
+rxjs@^6.6.3:
+ version "6.6.3"
+ resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552"
+ integrity sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==
+ dependencies:
+ tslib "^1.9.0"
+
safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"