chore(trading): display latency in block height progressing as warn or error (#3169)
This commit is contained in:
parent
67e602ebb1
commit
da7c0b84f7
@ -1,20 +1,30 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { NodeHealth, NodeUrl, HealthIndicator } from './footer';
|
import { NodeHealth, NodeUrl, HealthIndicator } from './footer';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import { Intent } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
|
jest.mock('@vegaprotocol/environment', () => ({
|
||||||
|
...jest.requireActual('@vegaprotocol/environment'),
|
||||||
|
useEnvironment: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(() => ({ VEGA_URL: 'https://vega-url.wtf' })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockSetNodeSwitcher = jest.fn();
|
||||||
|
jest.mock('../../stores', () => ({
|
||||||
|
...jest.requireActual('../../stores'),
|
||||||
|
useGlobalStore: () => mockSetNodeSwitcher,
|
||||||
|
}));
|
||||||
|
|
||||||
describe('NodeHealth', () => {
|
describe('NodeHealth', () => {
|
||||||
it('controls the node switcher dialog', async () => {
|
it('controls the node switcher dialog', async () => {
|
||||||
const mockOnClick = jest.fn();
|
render(<NodeHealth />, { wrapper: MockedProvider });
|
||||||
render(
|
await waitFor(() => {
|
||||||
<NodeHealth
|
expect(screen.getByRole('button')).toBeInTheDocument();
|
||||||
onClick={mockOnClick}
|
});
|
||||||
url={'https://api.n99.somenetwork.vega.xyz'}
|
|
||||||
blockHeight={100}
|
|
||||||
blockDiff={0}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
await userEvent.click(screen.getByRole('button'));
|
await userEvent.click(screen.getByRole('button'));
|
||||||
expect(mockOnClick).toHaveBeenCalled();
|
expect(mockSetNodeSwitcher).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -31,14 +41,22 @@ describe('NodeUrl', () => {
|
|||||||
|
|
||||||
describe('HealthIndicator', () => {
|
describe('HealthIndicator', () => {
|
||||||
const cases = [
|
const cases = [
|
||||||
{ diff: 0, classname: 'bg-vega-green-550', text: 'Operational' },
|
{
|
||||||
{ diff: 5, classname: 'bg-warning', text: '5 Blocks behind' },
|
intent: Intent.Success,
|
||||||
{ diff: null, classname: 'bg-danger', text: 'Non operational' },
|
text: 'Operational',
|
||||||
|
classname: 'bg-vega-green-550',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
intent: Intent.Warning,
|
||||||
|
text: '5 Blocks behind',
|
||||||
|
classname: 'bg-warning',
|
||||||
|
},
|
||||||
|
{ intent: Intent.Danger, text: 'Non operational', classname: 'bg-danger' },
|
||||||
];
|
];
|
||||||
it.each(cases)(
|
it.each(cases)(
|
||||||
'renders correct text and indicator color for $diff block difference',
|
'renders correct text and indicator color for $diff block difference',
|
||||||
(elem) => {
|
(elem) => {
|
||||||
render(<HealthIndicator blockDiff={elem.diff} />);
|
render(<HealthIndicator text={elem.text} intent={elem.intent} />);
|
||||||
expect(screen.getByTestId('indicator')).toHaveClass(elem.classname);
|
expect(screen.getByTestId('indicator')).toHaveClass(elem.classname);
|
||||||
expect(screen.getByText(elem.text)).toBeInTheDocument();
|
expect(screen.getByText(elem.text)).toBeInTheDocument();
|
||||||
}
|
}
|
||||||
|
@ -1,60 +1,45 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
import { useEnvironment, useNodeHealth } from '@vegaprotocol/environment';
|
import { useEnvironment, useNodeHealth } from '@vegaprotocol/environment';
|
||||||
import { useNavigatorOnline } from '@vegaprotocol/react-helpers';
|
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { Indicator, Intent } from '@vegaprotocol/ui-toolkit';
|
import type { Intent } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { Indicator } from '@vegaprotocol/ui-toolkit';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
||||||
import { useGlobalStore } from '../../stores';
|
import { useGlobalStore } from '../../stores';
|
||||||
|
|
||||||
export const Footer = () => {
|
export const Footer = () => {
|
||||||
const { VEGA_URL } = useEnvironment();
|
|
||||||
const setNodeSwitcher = useGlobalStore(
|
|
||||||
(store) => (open: boolean) => store.update({ nodeSwitcherDialog: open })
|
|
||||||
);
|
|
||||||
const { blockDiff, datanodeBlockHeight } = useNodeHealth();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="px-4 py-1 text-xs border-t border-default text-vega-light-300 dark:text-vega-dark-300 lg:fixed bottom-0 left-0 border-r bg-white dark:bg-black">
|
<footer className="px-4 py-1 text-xs border-t border-default text-vega-light-300 dark:text-vega-dark-300 lg:fixed bottom-0 left-0 border-r bg-white dark:bg-black">
|
||||||
{/* Pull left to align with top nav, due to button padding */}
|
{/* Pull left to align with top nav, due to button padding */}
|
||||||
<div className="-ml-2">
|
<div className="-ml-2">
|
||||||
{VEGA_URL && (
|
<NodeHealth />
|
||||||
<NodeHealth
|
|
||||||
url={VEGA_URL}
|
|
||||||
blockHeight={datanodeBlockHeight}
|
|
||||||
blockDiff={blockDiff}
|
|
||||||
onClick={() => setNodeSwitcher(true)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
interface NodeHealthProps {
|
|
||||||
url: string;
|
|
||||||
blockHeight: number | undefined;
|
|
||||||
blockDiff: number | null;
|
|
||||||
onClick: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NodeHealth = ({
|
export const NodeHealth = () => {
|
||||||
url,
|
const { VEGA_URL } = useEnvironment();
|
||||||
blockHeight,
|
const setNodeSwitcher = useGlobalStore(
|
||||||
blockDiff,
|
(store) => (open: boolean) => store.update({ nodeSwitcherDialog: open })
|
||||||
onClick,
|
);
|
||||||
}: NodeHealthProps) => {
|
const { datanodeBlockHeight, text, intent } = useNodeHealth();
|
||||||
return (
|
const onClick = useCallback(() => {
|
||||||
|
setNodeSwitcher(true);
|
||||||
|
}, [setNodeSwitcher]);
|
||||||
|
return VEGA_URL ? (
|
||||||
<FooterButton onClick={onClick} data-testid="node-health">
|
<FooterButton onClick={onClick} data-testid="node-health">
|
||||||
<FooterButtonPart>
|
<FooterButtonPart>
|
||||||
<HealthIndicator blockDiff={blockDiff} />
|
<HealthIndicator text={text} intent={intent} />
|
||||||
</FooterButtonPart>
|
</FooterButtonPart>
|
||||||
<FooterButtonPart>
|
<FooterButtonPart>
|
||||||
<NodeUrl url={url} />
|
<NodeUrl url={VEGA_URL} />
|
||||||
</FooterButtonPart>
|
</FooterButtonPart>
|
||||||
<FooterButtonPart>
|
<FooterButtonPart>
|
||||||
<span title={t('Block height')}>{blockHeight}</span>
|
<span title={t('Block height')}>{datanodeBlockHeight}</span>
|
||||||
</FooterButtonPart>
|
</FooterButtonPart>
|
||||||
</FooterButton>
|
</FooterButton>
|
||||||
);
|
) : null;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface NodeUrlProps {
|
interface NodeUrlProps {
|
||||||
@ -69,31 +54,11 @@ export const NodeUrl = ({ url }: NodeUrlProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface HealthIndicatorProps {
|
interface HealthIndicatorProps {
|
||||||
blockDiff: number | null;
|
text: string;
|
||||||
|
intent: Intent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// How many blocks behind the most advanced block that is
|
export const HealthIndicator = ({ text, intent }: HealthIndicatorProps) => {
|
||||||
// deemed acceptable for "Good" status
|
|
||||||
const BLOCK_THRESHOLD = 3;
|
|
||||||
|
|
||||||
export const HealthIndicator = ({ blockDiff }: HealthIndicatorProps) => {
|
|
||||||
const online = useNavigatorOnline();
|
|
||||||
|
|
||||||
let intent = Intent.Success;
|
|
||||||
let text = 'Operational';
|
|
||||||
|
|
||||||
if (!online) {
|
|
||||||
text = t('Offline');
|
|
||||||
intent = Intent.Danger;
|
|
||||||
} else if (blockDiff === null) {
|
|
||||||
// Block height query failed and null was returned
|
|
||||||
text = t('Non operational');
|
|
||||||
intent = Intent.Danger;
|
|
||||||
} else if (blockDiff >= BLOCK_THRESHOLD) {
|
|
||||||
text = t(`${blockDiff} Blocks behind`);
|
|
||||||
intent = Intent.Warning;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span title={t('Node health')}>
|
<span title={t('Node health')}>
|
||||||
<Indicator variant={intent} />
|
<Indicator variant={intent} />
|
||||||
|
@ -59,6 +59,11 @@ export function createClient({
|
|||||||
const timestamp = r?.headers.get('x-block-timestamp');
|
const timestamp = r?.headers.get('x-block-timestamp');
|
||||||
if (blockHeight && timestamp) {
|
if (blockHeight && timestamp) {
|
||||||
const state = useHeaderStore.getState();
|
const state = useHeaderStore.getState();
|
||||||
|
const urlState = state[r.url];
|
||||||
|
if (
|
||||||
|
!urlState?.blockHeight ||
|
||||||
|
urlState.blockHeight !== blockHeight
|
||||||
|
) {
|
||||||
useHeaderStore.setState({
|
useHeaderStore.setState({
|
||||||
...state,
|
...state,
|
||||||
[r.url]: {
|
[r.url]: {
|
||||||
@ -67,6 +72,7 @@ export function createClient({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return response;
|
return response;
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
@ -5,6 +5,7 @@ import { MockedProvider } from '@apollo/react-testing';
|
|||||||
import type { StatisticsQuery } from '../utils/__generated__/Node';
|
import type { StatisticsQuery } from '../utils/__generated__/Node';
|
||||||
import { StatisticsDocument } from '../utils/__generated__/Node';
|
import { StatisticsDocument } from '../utils/__generated__/Node';
|
||||||
import { useHeaderStore } from '@vegaprotocol/apollo-client';
|
import { useHeaderStore } from '@vegaprotocol/apollo-client';
|
||||||
|
import { Intent } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
const vegaUrl = 'https://foo.bar.com';
|
const vegaUrl = 'https://foo.bar.com';
|
||||||
|
|
||||||
@ -55,9 +56,24 @@ function setup(
|
|||||||
|
|
||||||
describe('useNodeHealth', () => {
|
describe('useNodeHealth', () => {
|
||||||
it.each([
|
it.each([
|
||||||
{ core: 1, node: 1, expected: 0 },
|
{
|
||||||
{ core: 1, node: 5, expected: -4 },
|
core: 1,
|
||||||
{ core: 10, node: 5, expected: 5 },
|
node: 1,
|
||||||
|
expectedText: 'Operational',
|
||||||
|
expectedIntent: Intent.Success,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
core: 1,
|
||||||
|
node: 5,
|
||||||
|
expectedText: 'Operational',
|
||||||
|
expectedIntent: Intent.Success,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
core: 10,
|
||||||
|
node: 5,
|
||||||
|
expectedText: '5 Blocks behind',
|
||||||
|
expectedIntent: Intent.Warning,
|
||||||
|
},
|
||||||
])(
|
])(
|
||||||
'provides difference core block $core and node block $node',
|
'provides difference core block $core and node block $node',
|
||||||
async (cases) => {
|
async (cases) => {
|
||||||
@ -65,12 +81,12 @@ describe('useNodeHealth', () => {
|
|||||||
blockHeight: cases.node,
|
blockHeight: cases.node,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
});
|
});
|
||||||
expect(result.current.blockDiff).toEqual(null);
|
expect(result.current.text).toEqual('Non operational');
|
||||||
expect(result.current.coreBlockHeight).toEqual(undefined);
|
expect(result.current.intent).toEqual(Intent.Danger);
|
||||||
expect(result.current.datanodeBlockHeight).toEqual(cases.node);
|
expect(result.current.datanodeBlockHeight).toEqual(cases.node);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(result.current.blockDiff).toEqual(cases.expected);
|
expect(result.current.text).toEqual(cases.expectedText);
|
||||||
expect(result.current.coreBlockHeight).toEqual(cases.core);
|
expect(result.current.intent).toEqual(cases.expectedIntent);
|
||||||
expect(result.current.datanodeBlockHeight).toEqual(cases.node);
|
expect(result.current.datanodeBlockHeight).toEqual(cases.node);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -90,25 +106,64 @@ describe('useNodeHealth', () => {
|
|||||||
blockHeight: 1,
|
blockHeight: 1,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
});
|
});
|
||||||
expect(result.current.blockDiff).toEqual(null);
|
expect(result.current.text).toEqual('Non operational');
|
||||||
expect(result.current.coreBlockHeight).toEqual(undefined);
|
expect(result.current.intent).toEqual(Intent.Danger);
|
||||||
expect(result.current.datanodeBlockHeight).toEqual(1);
|
expect(result.current.datanodeBlockHeight).toEqual(1);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(result.current.blockDiff).toEqual(null);
|
expect(result.current.text).toEqual('Non operational');
|
||||||
expect(result.current.coreBlockHeight).toEqual(undefined);
|
expect(result.current.intent).toEqual(Intent.Danger);
|
||||||
expect(result.current.datanodeBlockHeight).toEqual(1);
|
expect(result.current.datanodeBlockHeight).toEqual(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns 0 if no headers are found (waits until stats query resolves)', async () => {
|
it('returns 0 if no headers are found (waits until stats query resolves)', async () => {
|
||||||
const { result } = setup(createStatsMock(1), undefined);
|
const { result } = setup(createStatsMock(1), undefined);
|
||||||
expect(result.current.blockDiff).toEqual(null);
|
expect(result.current.text).toEqual('Non operational');
|
||||||
expect(result.current.coreBlockHeight).toEqual(undefined);
|
expect(result.current.intent).toEqual(Intent.Danger);
|
||||||
expect(result.current.datanodeBlockHeight).toEqual(undefined);
|
expect(result.current.datanodeBlockHeight).toEqual(undefined);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(result.current.blockDiff).toEqual(0);
|
expect(result.current.text).toEqual('Operational');
|
||||||
expect(result.current.coreBlockHeight).toEqual(1);
|
expect(result.current.intent).toEqual(Intent.Success);
|
||||||
expect(result.current.datanodeBlockHeight).toEqual(undefined);
|
expect(result.current.datanodeBlockHeight).toEqual(undefined);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Warning latency', async () => {
|
||||||
|
const now = 1678800900087;
|
||||||
|
const headerTimestamp = now - 4000;
|
||||||
|
const dateNow = new Date(now);
|
||||||
|
const dateHeaderTimestamp = new Date(headerTimestamp);
|
||||||
|
jest.useFakeTimers().setSystemTime(dateNow);
|
||||||
|
|
||||||
|
const { result } = setup(createStatsMock(2), {
|
||||||
|
blockHeight: 2,
|
||||||
|
timestamp: dateHeaderTimestamp,
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.text).toEqual('Warning delay ( >3 sec): 4.05 sec');
|
||||||
|
expect(result.current.intent).toEqual(Intent.Warning);
|
||||||
|
expect(result.current.datanodeBlockHeight).toEqual(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Erroneous latency', async () => {
|
||||||
|
const now = 1678800900087;
|
||||||
|
const headerTimestamp = now - 11000;
|
||||||
|
const dateNow = new Date(now);
|
||||||
|
const dateHeaderTimestamp = new Date(headerTimestamp);
|
||||||
|
jest.useFakeTimers().setSystemTime(dateNow);
|
||||||
|
|
||||||
|
const { result } = setup(createStatsMock(2), {
|
||||||
|
blockHeight: 2,
|
||||||
|
timestamp: dateHeaderTimestamp,
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.text).toEqual(
|
||||||
|
'Erroneous latency ( >10 sec): 11.05 sec'
|
||||||
|
);
|
||||||
|
expect(result.current.intent).toEqual(Intent.Danger);
|
||||||
|
expect(result.current.datanodeBlockHeight).toEqual(2);
|
||||||
|
});
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -2,16 +2,21 @@ import { useEffect, useMemo } from 'react';
|
|||||||
import { useStatisticsQuery } from '../utils/__generated__/Node';
|
import { useStatisticsQuery } from '../utils/__generated__/Node';
|
||||||
import { useHeaderStore } from '@vegaprotocol/apollo-client';
|
import { useHeaderStore } from '@vegaprotocol/apollo-client';
|
||||||
import { useEnvironment } from './use-environment';
|
import { useEnvironment } from './use-environment';
|
||||||
import { fromNanoSeconds } from '@vegaprotocol/utils';
|
import { useNavigatorOnline } from '@vegaprotocol/react-helpers';
|
||||||
|
import { Intent } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
|
||||||
const POLL_INTERVAL = 1000;
|
const POLL_INTERVAL = 1000;
|
||||||
|
const BLOCK_THRESHOLD = 3;
|
||||||
|
const ERROR_LATENCY = 10000;
|
||||||
|
const WARNING_LATENCY = 3000;
|
||||||
|
|
||||||
export const useNodeHealth = () => {
|
export const useNodeHealth = () => {
|
||||||
|
const online = useNavigatorOnline();
|
||||||
const url = useEnvironment((store) => store.VEGA_URL);
|
const url = useEnvironment((store) => store.VEGA_URL);
|
||||||
const headerStore = useHeaderStore();
|
const headerStore = useHeaderStore();
|
||||||
const headers = url ? headerStore[url] : undefined;
|
const headers = url ? headerStore[url] : undefined;
|
||||||
const { data, error, loading, startPolling, stopPolling } =
|
const { data, error, startPolling, stopPolling } = useStatisticsQuery({
|
||||||
useStatisticsQuery({
|
|
||||||
fetchPolicy: 'no-cache',
|
fetchPolicy: 'no-cache',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -20,12 +25,12 @@ export const useNodeHealth = () => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!headers) {
|
if (!headers?.blockHeight) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Number(data.statistics.blockHeight) - headers.blockHeight;
|
return Number(data.statistics.blockHeight) - headers.blockHeight;
|
||||||
}, [data, headers]);
|
}, [data?.statistics.blockHeight, headers?.blockHeight]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (error) {
|
if (error) {
|
||||||
@ -38,17 +43,43 @@ export const useNodeHealth = () => {
|
|||||||
}
|
}
|
||||||
}, [error, startPolling, stopPolling]);
|
}, [error, startPolling, stopPolling]);
|
||||||
|
|
||||||
|
const blockUpdateMsLatency = headers?.timestamp
|
||||||
|
? Date.now() - headers.timestamp.getTime()
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
const [text, intent] = useMemo(() => {
|
||||||
|
let intent = Intent.Success;
|
||||||
|
let text = 'Operational';
|
||||||
|
|
||||||
|
if (!online) {
|
||||||
|
text = t('Offline');
|
||||||
|
intent = Intent.Danger;
|
||||||
|
} else if (blockDiff === null) {
|
||||||
|
// Block height query failed and null was returned
|
||||||
|
text = t('Non operational');
|
||||||
|
intent = Intent.Danger;
|
||||||
|
} else if (blockUpdateMsLatency > ERROR_LATENCY) {
|
||||||
|
text = t('Erroneous latency ( >%s sec): %s sec', [
|
||||||
|
(ERROR_LATENCY / 1000).toString(),
|
||||||
|
(blockUpdateMsLatency / 1000).toFixed(2),
|
||||||
|
]);
|
||||||
|
intent = Intent.Danger;
|
||||||
|
} else if (blockDiff >= BLOCK_THRESHOLD) {
|
||||||
|
text = t(`%s Blocks behind`, String(blockDiff));
|
||||||
|
intent = Intent.Warning;
|
||||||
|
} else if (blockUpdateMsLatency > WARNING_LATENCY) {
|
||||||
|
text = t('Warning delay ( >%s sec): %s sec', [
|
||||||
|
(WARNING_LATENCY / 1000).toString(),
|
||||||
|
(blockUpdateMsLatency / 1000).toFixed(2),
|
||||||
|
]);
|
||||||
|
intent = Intent.Warning;
|
||||||
|
}
|
||||||
|
return [text, intent];
|
||||||
|
}, [online, blockDiff, blockUpdateMsLatency]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
error,
|
|
||||||
loading,
|
|
||||||
coreBlockHeight: data?.statistics
|
|
||||||
? Number(data.statistics.blockHeight)
|
|
||||||
: undefined,
|
|
||||||
coreVegaTime: data?.statistics
|
|
||||||
? fromNanoSeconds(data?.statistics.vegaTime)
|
|
||||||
: undefined,
|
|
||||||
datanodeBlockHeight: headers?.blockHeight,
|
datanodeBlockHeight: headers?.blockHeight,
|
||||||
datanodeVegaTime: headers?.timestamp,
|
text,
|
||||||
blockDiff,
|
intent,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user