fix(trading): pick the best node to connect, slow notification notification, other node picking improvements (#5909)
This commit is contained in:
parent
bcb5351dfc
commit
3a56678403
@ -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 (
|
||||
<Splash>
|
||||
<div className="border border-vega-red m-10 mx-auto w-4/5 max-w-3xl rounded-lg overflow-hidden animate-shake">
|
||||
<div className="bg-vega-red text-white px-2 py-2 flex gap-1 items-center font-alpha calt uppercase">
|
||||
<VLogo className="h-4" />
|
||||
<span className="text-lg">{t('Failed to initialize the app')}</span>
|
||||
</div>
|
||||
<div className="p-4 text-left text-sm">
|
||||
<p className="mb-4">{reason}</p>
|
||||
<div className="text-center">
|
||||
<Button className="border-2" onClick={() => setNodeSwitcher(true)}>
|
||||
{t('Change node')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Splash>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<Splash>
|
||||
<div className="border border-transparent m-10 mx-auto w-4/5 max-w-3xl rounded-lg overflow-hidden">
|
||||
<div className="mt-11 p-4 text-left text-sm">
|
||||
<Loader />
|
||||
{showSlowNotification && (
|
||||
<>
|
||||
<p className="mt-4 text-center">
|
||||
{t(
|
||||
"It looks like you're connection is slow, try switching to another node."
|
||||
)}
|
||||
</p>
|
||||
<div className="mt-4 text-center">
|
||||
<Button
|
||||
className="border-2"
|
||||
onClick={() => setNodeSwitcher(true)}
|
||||
>
|
||||
{t('Change node')}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Splash>
|
||||
);
|
||||
};
|
||||
|
||||
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 <AppLoader />;
|
||||
}
|
||||
|
||||
const ERR_DATA_LOADER = (
|
||||
<Trans
|
||||
i18nKey="It appears that the connection to the node <0>{{VEGA_URL}}</0> does not return necessary data, try switching to another node."
|
||||
components={[
|
||||
<span key="vega" className="text-muted">
|
||||
{VEGA_URL}
|
||||
</span>,
|
||||
]}
|
||||
values={{
|
||||
VEGA_URL,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<NetworkLoader
|
||||
cache={cacheConfig}
|
||||
skeleton={<AppLoader />}
|
||||
failure={
|
||||
<AppFailure title={t('Could not initialize app')} error={error} />
|
||||
}
|
||||
skeleton={<Loading />}
|
||||
failure={<Failure reason={error} />}
|
||||
>
|
||||
<NodeGuard
|
||||
skeleton={<AppLoader />}
|
||||
failure={
|
||||
<NodeFailure
|
||||
title={t('Node: {{VEGA_URL}} is unsuitable', { VEGA_URL })}
|
||||
/>
|
||||
}
|
||||
<DataLoader
|
||||
skeleton={<Loading />}
|
||||
failure={<Failure reason={ERR_DATA_LOADER} />}
|
||||
>
|
||||
<DataLoader
|
||||
skeleton={<AppLoader />}
|
||||
failure={
|
||||
<AppFailure
|
||||
title={t('Could not load market data or asset data')}
|
||||
error={error}
|
||||
/>
|
||||
}
|
||||
<Web3Provider
|
||||
skeleton={<Loading />}
|
||||
failure={<Failure reason={t('Could not configure web3 provider')} />}
|
||||
>
|
||||
<Web3Provider
|
||||
skeleton={<AppLoader />}
|
||||
failure={
|
||||
<AppFailure title={t('Could not configure web3 provider')} />
|
||||
}
|
||||
>
|
||||
<WalletProvider config={config}>{children}</WalletProvider>
|
||||
</Web3Provider>
|
||||
</DataLoader>
|
||||
</NodeGuard>
|
||||
<WalletProvider config={config}>{children}</WalletProvider>
|
||||
</Web3Provider>
|
||||
</DataLoader>
|
||||
</NetworkLoader>
|
||||
);
|
||||
};
|
||||
|
@ -1,16 +0,0 @@
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const AppFailure = ({
|
||||
title,
|
||||
error,
|
||||
}: {
|
||||
title: string;
|
||||
error?: string | null;
|
||||
}) => {
|
||||
return (
|
||||
<Splash>
|
||||
<p className="mb-4 text-xl">{title}</p>
|
||||
{error && <p className="mb-8 text-sm">{error}</p>}
|
||||
</Splash>
|
||||
);
|
||||
};
|
@ -1 +0,0 @@
|
||||
export * from './app-failure';
|
@ -1,4 +1,3 @@
|
||||
export * from './app-failure';
|
||||
export * from './app-loader';
|
||||
export * from './network-loader';
|
||||
export * from './network-switcher';
|
||||
|
@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -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 && (
|
||||
<div className="break-all" data-testid="node">
|
||||
<div className="break-all flex gap-2" data-testid="node">
|
||||
<TradingRadio id={`node-url-${id}`} value={url} label={url} />
|
||||
{url.length > 0 && url !== 'custom' && (
|
||||
<span className="text-muted">
|
||||
<CopyWithTooltip text={url}>
|
||||
<button>
|
||||
<VegaIcon name={VegaIconNames.COPY} />
|
||||
</button>
|
||||
</CopyWithTooltip>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<LayoutCell
|
||||
|
@ -39,6 +39,19 @@ export const getMockStatisticsResult = (
|
||||
blockHeight: '11',
|
||||
vegaTime: new Date().toISOString(),
|
||||
},
|
||||
networkParametersConnection: {
|
||||
__typename: 'NetworkParametersConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'NetworkParameterEdge',
|
||||
node: {
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'a',
|
||||
value: '1',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
export const getMockQueryResult = (env: Networks): NodeCheckQuery => ({
|
||||
@ -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<T>(
|
||||
|
@ -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: () => ({
|
||||
|
@ -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<typeof createClient>;
|
||||
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<string | null> => {
|
||||
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<string | null> => {
|
||||
['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<EnvStore>()((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<EnvStore>()((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');
|
||||
},
|
||||
}));
|
||||
|
||||
|
@ -28,6 +28,16 @@ const createStatsMock = (
|
||||
blockHeight: blockHeight.toString(),
|
||||
vegaTime: '12345',
|
||||
},
|
||||
networkParametersConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
key: 'a',
|
||||
value: '1',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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</0>.": "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</0>.",
|
||||
"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}}</0> does not return necessary data, try switching to another node.": "It appears that the connection to the node <0>{{VEGA_URL}}</0> does not return necessary data, try switching to another node."
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
export const isValidUrl = (url?: string) => {
|
||||
export const isValidUrl = (url?: string | null) => {
|
||||
if (!url) return false;
|
||||
try {
|
||||
new URL(url);
|
||||
|
Loading…
Reference in New Issue
Block a user