From 938ad08a609f5d2d5997f7e6f12d32560c45a998 Mon Sep 17 00:00:00 2001 From: asiaznik Date: Mon, 4 Mar 2024 18:37:26 +0100 Subject: [PATCH] fix(trading): pick the best node to connect, slow notificatio, init error --- .../components/bootstrapper/bootstrapper.tsx | 128 +++++++++++++----- .../src/hooks/use-environment.spec.ts | 30 ++++ libs/environment/src/hooks/use-environment.ts | 109 ++++++++------- libs/environment/src/utils/NodeCheck.graphql | 10 ++ .../src/utils/__generated__/NodeCheck.ts | 10 +- .../trading-radio-group/radio-group.tsx | 53 +++++--- libs/utils/src/lib/is-valid-url.ts | 2 +- 7 files changed, 239 insertions(+), 103 deletions(-) diff --git a/apps/trading/components/bootstrapper/bootstrapper.tsx b/apps/trading/components/bootstrapper/bootstrapper.tsx index 422dbbbbd..23fa62899 100644 --- a/apps/trading/components/bootstrapper/bootstrapper.tsx +++ b/apps/trading/components/bootstrapper/bootstrapper.tsx @@ -1,63 +1,123 @@ 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(() => { + setTimeout(() => { + if (!showSlowNotification) setShowSlowNotification(true); + }, 5000); + }, [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/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..0a72e491f 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,35 +618,31 @@ 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' }); - return; - } - // No url found in env vars or localStorage, AND no nodes were found in // the config fetched from VEGA_CONFIG_URL, app initialization has failed if (!nodes || !nodes.length) { @@ -639,36 +653,37 @@ 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); - // 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); + // 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; - if (url !== null) { + 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 requsted node (previously + // connected or taken form env variable) or the currently best available + // node. + const url = requestedNode?.url || bestNode?.url; + + 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 { + } else { set({ status: 'failed', - error: 'No node found', + error: 'No suitable node found', }); - console.warn('No suitable vega node was found'); + console.warn('No suitable node was found'); } }, })); 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/ui-toolkit/src/components/trading-radio-group/radio-group.tsx b/libs/ui-toolkit/src/components/trading-radio-group/radio-group.tsx index 824626cbc..8e40f2f2f 100644 --- a/libs/ui-toolkit/src/components/trading-radio-group/radio-group.tsx +++ b/libs/ui-toolkit/src/components/trading-radio-group/radio-group.tsx @@ -3,6 +3,8 @@ import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'; import classNames from 'classnames'; import type { ReactNode } from 'react'; import { labelClasses } from '../checkbox'; +import { CopyWithTooltip } from '../copy-with-tooltip'; +import { VegaIcon, VegaIconNames } from '../icon'; export interface TradingRadioGroupProps { name?: string; @@ -86,25 +88,36 @@ export const TradingRadio = ({ 'border-vega-clight-700 dark:border-vega-cdark-700' ); return ( - + + + {value && value !== 'custom' && ( + + + + + + )} + ); }; 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);