From 3a5667840355ac87bb050aacc081579c634da884 Mon Sep 17 00:00:00 2001 From: Art Date: Fri, 8 Mar 2024 17:12:11 +0100 Subject: [PATCH] fix(trading): pick the best node to connect, slow notification notification, other node picking improvements (#5909) --- .../components/bootstrapper/bootstrapper.tsx | 131 +++++++++++++----- .../components/app-failure/app-failure.tsx | 16 --- .../src/components/app-failure/index.ts | 1 - libs/environment/src/components/index.ts | 1 - .../node-switcher/row-data.spec.tsx | 20 +++ .../src/components/node-switcher/row-data.tsx | 18 ++- .../src/hooks/mocks/apollo-client.tsx | 26 ++++ .../src/hooks/use-environment.spec.ts | 30 ++++ libs/environment/src/hooks/use-environment.ts | 126 +++++++++-------- .../src/hooks/use-node-health.spec.tsx | 10 ++ libs/environment/src/utils/NodeCheck.graphql | 10 ++ .../src/utils/__generated__/NodeCheck.ts | 10 +- libs/environment/src/utils/node.mock.ts | 13 ++ libs/i18n/src/locales/en/trading.json | 4 +- libs/utils/src/lib/is-valid-url.ts | 2 +- 15 files changed, 307 insertions(+), 111 deletions(-) delete mode 100644 libs/environment/src/components/app-failure/app-failure.tsx delete mode 100644 libs/environment/src/components/app-failure/index.ts diff --git a/apps/trading/components/bootstrapper/bootstrapper.tsx b/apps/trading/components/bootstrapper/bootstrapper.tsx index 422dbbbbd..9d7136753 100644 --- a/apps/trading/components/bootstrapper/bootstrapper.tsx +++ b/apps/trading/components/bootstrapper/bootstrapper.tsx @@ -1,63 +1,126 @@ import type { InMemoryCacheConfig } from '@apollo/client'; import { - AppFailure, AppLoader, NetworkLoader, - NodeFailure, - NodeGuard, useEnvironment, + useNodeSwitcherStore, } from '@vegaprotocol/environment'; -import { type ReactNode } from 'react'; +import { useEffect, type ReactNode, useState } from 'react'; import { Web3Provider } from './web3-provider'; import { useT } from '../../lib/use-t'; import { DataLoader } from './data-loader'; import { WalletProvider } from '@vegaprotocol/wallet-react'; import { useVegaWalletConfig } from '../../lib/hooks/use-vega-wallet-config'; +import { Trans } from 'react-i18next'; +import { Button, Loader, Splash, VLogo } from '@vegaprotocol/ui-toolkit'; + +const Failure = ({ reason }: { reason?: ReactNode }) => { + const t = useT(); + const setNodeSwitcher = useNodeSwitcherStore((store) => store.setDialogOpen); + return ( + +
+
+ + {t('Failed to initialize the app')} +
+
+

{reason}

+
+ +
+
+
+
+ ); +}; + +const Loading = () => { + const [showSlowNotification, setShowSlowNotification] = useState(false); + const t = useT(); + const setNodeSwitcher = useNodeSwitcherStore((store) => store.setDialogOpen); + useEffect(() => { + const to = setTimeout(() => { + if (!showSlowNotification) setShowSlowNotification(true); + }, 5000); + return () => { + clearTimeout(to); + }; + }, [showSlowNotification]); + return ( + +
+
+ + {showSlowNotification && ( + <> +

+ {t( + "It looks like you're connection is slow, try switching to another node." + )} +

+
+ +
+ + )} +
+
+
+ ); +}; export const Bootstrapper = ({ children }: { children: ReactNode }) => { const t = useT(); - const { error, VEGA_URL } = useEnvironment(); + + const { error, VEGA_URL } = useEnvironment((state) => ({ + error: state.error, + VEGA_URL: state.VEGA_URL, + })); const config = useVegaWalletConfig(); if (!config) { return ; } + const ERR_DATA_LOADER = ( + + {VEGA_URL} + , + ]} + values={{ + VEGA_URL, + }} + /> + ); + return ( } - failure={ - - } + skeleton={} + failure={} > - } - failure={ - - } + } + failure={} > - } - failure={ - - } + } + failure={} > - } - failure={ - - } - > - {children} - - - + {children} + + ); }; diff --git a/libs/environment/src/components/app-failure/app-failure.tsx b/libs/environment/src/components/app-failure/app-failure.tsx deleted file mode 100644 index 7860b73b1..000000000 --- a/libs/environment/src/components/app-failure/app-failure.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Splash } from '@vegaprotocol/ui-toolkit'; - -export const AppFailure = ({ - title, - error, -}: { - title: string; - error?: string | null; -}) => { - return ( - -

{title}

- {error &&

{error}

} -
- ); -}; diff --git a/libs/environment/src/components/app-failure/index.ts b/libs/environment/src/components/app-failure/index.ts deleted file mode 100644 index e3fd1b4c8..000000000 --- a/libs/environment/src/components/app-failure/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './app-failure'; diff --git a/libs/environment/src/components/index.ts b/libs/environment/src/components/index.ts index 6e0109c44..64bacb14d 100644 --- a/libs/environment/src/components/index.ts +++ b/libs/environment/src/components/index.ts @@ -1,4 +1,3 @@ -export * from './app-failure'; export * from './app-loader'; export * from './network-loader'; export * from './network-switcher'; diff --git a/libs/environment/src/components/node-switcher/row-data.spec.tsx b/libs/environment/src/components/node-switcher/row-data.spec.tsx index fea946089..b8d0fdb87 100644 --- a/libs/environment/src/components/node-switcher/row-data.spec.tsx +++ b/libs/environment/src/components/node-switcher/row-data.spec.tsx @@ -40,6 +40,16 @@ const mockStatsQuery = ( vegaTime: new Date().toISOString(), chainId: 'test-chain-id', }, + networkParametersConnection: { + edges: [ + { + node: { + key: 'a', + value: '1', + }, + }, + ], + }, }, }, }); @@ -335,6 +345,16 @@ describe('RowData', () => { vegaTime: new Date().toISOString(), chainId: 'test-chain-id', }, + networkParametersConnection: { + edges: [ + { + node: { + key: 'a', + value: '1', + }, + }, + ], + }, }, }, }; diff --git a/libs/environment/src/components/node-switcher/row-data.tsx b/libs/environment/src/components/node-switcher/row-data.tsx index f4dca0d49..a20ed719a 100644 --- a/libs/environment/src/components/node-switcher/row-data.tsx +++ b/libs/environment/src/components/node-switcher/row-data.tsx @@ -1,4 +1,9 @@ -import { TradingRadio } from '@vegaprotocol/ui-toolkit'; +import { + CopyWithTooltip, + TradingRadio, + VegaIcon, + VegaIconNames, +} from '@vegaprotocol/ui-toolkit'; import { useEffect, useState } from 'react'; import { CUSTOM_NODE_KEY } from '../../types'; import { @@ -127,8 +132,17 @@ export const RowData = ({ return ( <> {id !== CUSTOM_NODE_KEY && ( -
+
+ {url.length > 0 && url !== 'custom' && ( + + + + + + )}
)} ({ @@ -48,6 +61,19 @@ export const getMockQueryResult = (env: Networks): NodeCheckQuery => ({ blockHeight: '11', vegaTime: new Date().toISOString(), }, + networkParametersConnection: { + __typename: 'NetworkParametersConnection', + edges: [ + { + __typename: 'NetworkParameterEdge', + node: { + __typename: 'NetworkParameter', + key: 'a', + value: '1', + }, + }, + ], + }, }); function getHandler( diff --git a/libs/environment/src/hooks/use-environment.spec.ts b/libs/environment/src/hooks/use-environment.spec.ts index 1e3ab9e03..8b2b8bedc 100644 --- a/libs/environment/src/hooks/use-environment.spec.ts +++ b/libs/environment/src/hooks/use-environment.spec.ts @@ -34,6 +34,16 @@ const createDefaultMockClient = () => { blockHeight: '100', vegaTime: new Date().toISOString(), }, + networkParametersConnection: { + edges: [ + { + node: { + key: 'something', + value: 123, + }, + }, + ], + }, }, }), subscribe: () => ({ @@ -183,6 +193,16 @@ describe('useEnvironment', () => { blockHeight: '100', vegaTime: new Date(1).toISOString(), }, + networkParametersConnection: { + edges: [ + { + node: { + key: 'something', + value: 123, + }, + }, + ], + }, }, }); }, wait); @@ -244,6 +264,16 @@ describe('useEnvironment', () => { blockHeight: '100', vegaTime: new Date().toISOString(), }, + networkParametersConnection: { + edges: [ + { + node: { + key: 'something', + value: 123, + }, + }, + ], + }, }, }), subscribe: () => ({ diff --git a/libs/environment/src/hooks/use-environment.ts b/libs/environment/src/hooks/use-environment.ts index accd6e380..f1652caec 100644 --- a/libs/environment/src/hooks/use-environment.ts +++ b/libs/environment/src/hooks/use-environment.ts @@ -1,5 +1,5 @@ import { parse as tomlParse } from 'toml'; -import { isValidUrl, LocalStorage } from '@vegaprotocol/utils'; +import { LocalStorage, isValidUrl } from '@vegaprotocol/utils'; import { useEffect } from 'react'; import { create } from 'zustand'; import { createClient } from '@vegaprotocol/apollo-client'; @@ -22,6 +22,7 @@ import uniq from 'lodash/uniq'; import orderBy from 'lodash/orderBy'; import first from 'lodash/first'; import { canMeasureResponseTime, measureResponseTime } from '../utils/time'; +import compact from 'lodash/compact'; type Client = ReturnType; type ClientCollection = { @@ -70,10 +71,19 @@ const fetchConfig = async (url?: string) => { }; /** - * Find a suitable node by running a test query and test + * Find a suitable nodes by running a test query and test * subscription, against a list of clients, first to resolve wins */ -const findNode = async (clients: ClientCollection): Promise => { +const findHealthyNodes = async (nodes: string[]) => { + const clients: ClientCollection = {}; + nodes.forEach((url) => { + clients[url] = createClient({ + url, + cacheConfig: undefined, + retry: false, + connectToDevTools: false, + }); + }); const tests = Object.entries(clients).map((args) => testNode(...args)); try { const nodes = await Promise.all(tests); @@ -93,11 +103,10 @@ const findNode = async (clients: ClientCollection): Promise => { ['desc', 'desc', 'asc'] ); - const best = first(ordered); - return best ? best.url : null; + return ordered; } catch (err) { // All tests rejected, no suitable node found - return null; + return []; } }; @@ -142,6 +151,15 @@ const testQuery = ( }) .then((result) => { if (result && !result.error) { + const netParams = compact( + result.data.networkParametersConnection.edges?.map((n) => n?.node) + ); + if (netParams.length === 0) { + // any node that doesn't return the network parameters is considered + // failed + resolve(false); + return; + } const res = { blockHeight: Number(result.data.statistics.blockHeight), vegaTime: new Date(result.data.statistics.vegaTime), @@ -600,32 +618,34 @@ export const useEnvironment = create()((set, get) => ({ } const state = get(); - const storedUrl = LocalStorage.getItem(STORAGE_KEY); + + let storedUrl = LocalStorage.getItem(STORAGE_KEY); + if (!isValidUrl(storedUrl)) { + // remove invalid data from local storage + LocalStorage.removeItem(STORAGE_KEY); + storedUrl = null; + } let nodes: string[] | undefined; try { - nodes = await fetchConfig(state.VEGA_CONFIG_URL); - const enrichedNodes = uniq( - [...nodes, state.VEGA_URL, storedUrl].filter(Boolean) as string[] + nodes = uniq( + compact([ + // url from local storage + storedUrl, + // url from state (if set via env var) + state.VEGA_URL, + // urls from network configuration + ...(await fetchConfig(state.VEGA_CONFIG_URL)), + ]) ); - set({ nodes: enrichedNodes }); + set({ nodes }); } catch (err) { console.warn(`Could not fetch node config from ${state.VEGA_CONFIG_URL}`); } - // Node url found in localStorage, if its valid attempt to connect - if (storedUrl) { - if (isValidUrl(storedUrl)) { - set({ VEGA_URL: storedUrl, status: 'success' }); - return; - } else { - LocalStorage.removeItem(STORAGE_KEY); - } - } - - // VEGA_URL env var is set and is a valid url no need to proceed - if (state.VEGA_URL) { - set({ status: 'success' }); + // skip picking up the best node if VEGA_URL env variable is set + if (state.VEGA_URL && isValidUrl(state.VEGA_URL)) { + state.setUrl(state.VEGA_URL); return; } @@ -639,37 +659,35 @@ export const useEnvironment = create()((set, get) => ({ return; } - // Create a map of node urls to client instances - const clients: ClientCollection = {}; - nodes.forEach((url) => { - clients[url] = createClient({ - url, - cacheConfig: undefined, - retry: false, - connectToDevTools: false, - }); + const healthyNodes = await findHealthyNodes(nodes); + + // A requested node is a node to which the app was previously connected + // or the one set via env variable. + const requestedNodeUrl = storedUrl || state.VEGA_URL; + + const bestNode = first(healthyNodes); + const requestedNode = healthyNodes.find( + (n) => requestedNodeUrl && n.url === requestedNodeUrl + ); + if (!requestedNode) { + // remove unhealthy node url from local storage + LocalStorage.removeItem(STORAGE_KEY); + } + // A node's url (VEGA_URL) is either the requested node (previously + // connected or taken form env variable) or the currently best available + // node. + const url = requestedNode?.url || bestNode?.url; + + if (url != null) { + state.setUrl(url); + return; + } + + set({ + status: 'failed', + error: 'No suitable node found', }); - - // Find a suitable node to connect to by attempting a query and a - // subscription, first to fulfill both will be the resulting url. - const url = await findNode(clients); - - if (url !== null) { - set({ - status: 'success', - VEGA_URL: url, - }); - LocalStorage.setItem(STORAGE_KEY, url); - } - // Every node failed either to make a query or retrieve data from - // a subscription - else { - set({ - status: 'failed', - error: 'No node found', - }); - console.warn('No suitable vega node was found'); - } + console.warn('No suitable node was found'); }, })); diff --git a/libs/environment/src/hooks/use-node-health.spec.tsx b/libs/environment/src/hooks/use-node-health.spec.tsx index 92a67f1b4..e159000ed 100644 --- a/libs/environment/src/hooks/use-node-health.spec.tsx +++ b/libs/environment/src/hooks/use-node-health.spec.tsx @@ -28,6 +28,16 @@ const createStatsMock = ( blockHeight: blockHeight.toString(), vegaTime: '12345', }, + networkParametersConnection: { + edges: [ + { + node: { + key: 'a', + value: '1', + }, + }, + ], + }, }, }, }; diff --git a/libs/environment/src/utils/NodeCheck.graphql b/libs/environment/src/utils/NodeCheck.graphql index f5a2b4ad3..8580bed20 100644 --- a/libs/environment/src/utils/NodeCheck.graphql +++ b/libs/environment/src/utils/NodeCheck.graphql @@ -1,9 +1,19 @@ query NodeCheck { + # statistics needed to get the most recent node in terms of block height statistics { chainId blockHeight vegaTime } + # net params needed to filter out the nodes that are not suitable + networkParametersConnection { + edges { + node { + key + value + } + } + } } subscription NodeCheckTimeUpdate { diff --git a/libs/environment/src/utils/__generated__/NodeCheck.ts b/libs/environment/src/utils/__generated__/NodeCheck.ts index 21910e5be..c8d9239dc 100644 --- a/libs/environment/src/utils/__generated__/NodeCheck.ts +++ b/libs/environment/src/utils/__generated__/NodeCheck.ts @@ -6,7 +6,7 @@ const defaultOptions = {} as const; export type NodeCheckQueryVariables = Types.Exact<{ [key: string]: never; }>; -export type NodeCheckQuery = { __typename?: 'Query', statistics: { __typename?: 'Statistics', chainId: string, blockHeight: string, vegaTime: any } }; +export type NodeCheckQuery = { __typename?: 'Query', statistics: { __typename?: 'Statistics', chainId: string, blockHeight: string, vegaTime: any }, networkParametersConnection: { __typename?: 'NetworkParametersConnection', edges?: Array<{ __typename?: 'NetworkParameterEdge', node: { __typename?: 'NetworkParameter', key: string, value: string } } | null> | null } }; export type NodeCheckTimeUpdateSubscriptionVariables = Types.Exact<{ [key: string]: never; }>; @@ -21,6 +21,14 @@ export const NodeCheckDocument = gql` blockHeight vegaTime } + networkParametersConnection { + edges { + node { + key + value + } + } + } } `; diff --git a/libs/environment/src/utils/node.mock.ts b/libs/environment/src/utils/node.mock.ts index c1477eb4f..32b3e5b53 100644 --- a/libs/environment/src/utils/node.mock.ts +++ b/libs/environment/src/utils/node.mock.ts @@ -12,6 +12,19 @@ export const statisticsQuery = ( blockHeight: '11', vegaTime: new Date().toISOString(), }, + networkParametersConnection: { + __typename: 'NetworkParametersConnection', + edges: [ + { + __typename: 'NetworkParameterEdge', + node: { + __typename: 'NetworkParameter', + key: 'a', + value: '1', + }, + }, + ], + }, }; return merge(defaultResult, override); diff --git a/libs/i18n/src/locales/en/trading.json b/libs/i18n/src/locales/en/trading.json index 4bedc5a0d..86e063ce9 100644 --- a/libs/i18n/src/locales/en/trading.json +++ b/libs/i18n/src/locales/en/trading.json @@ -485,5 +485,7 @@ "See all the live games on the cards below. Every on-chain game is community funded and designed. <0>Find out how to create one.": "See all the live games on the cards below. Every on-chain game is community funded and designed. <0>Find out how to create one.", "Teams can earn rewards if they meet the goals set in the on-chain trading competitions. Track your earned rewards here, and see which teams are top of the leaderboard this month.": "Teams can earn rewards if they meet the goals set in the on-chain trading competitions. Track your earned rewards here, and see which teams are top of the leaderboard this month.", "Currently no active games on the network.": "Currently no active games on the network.", - "Currently no active teams on the network.": "Currently no active teams on the network." + "Currently no active teams on the network.": "Currently no active teams on the network.", + "It looks like you're connection is slow, try switching to another node.": "It looks like you're connection is slow, try switching to another node.", + "It appears that the connection to the node <0>{{VEGA_URL}} does not return necessary data, try switching to another node.": "It appears that the connection to the node <0>{{VEGA_URL}} does not return necessary data, try switching to another node." } diff --git a/libs/utils/src/lib/is-valid-url.ts b/libs/utils/src/lib/is-valid-url.ts index 6f68f96db..6532b8c31 100644 --- a/libs/utils/src/lib/is-valid-url.ts +++ b/libs/utils/src/lib/is-valid-url.ts @@ -1,4 +1,4 @@ -export const isValidUrl = (url?: string) => { +export const isValidUrl = (url?: string | null) => { if (!url) return false; try { new URL(url);