feat(trading): protocol upgrade notification (#3517)

This commit is contained in:
Art 2023-05-04 09:51:24 +02:00 committed by GitHub
parent 378946f22b
commit 768b3b29f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 591 additions and 84 deletions

View File

@ -89,6 +89,6 @@ export function mockNetworkUpgradeProposal() {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Nodes', nodeData);
aliasGQLQuery(req, 'Proposals', proposalsData);
aliasGQLQuery(req, 'ProtocolUpgrades', upgradeProposalsData);
aliasGQLQuery(req, 'ProtocolUpgradeProposals', upgradeProposalsData);
});
}

View File

@ -15,7 +15,6 @@ import Routes from '../routes';
import { ExternalLinks, removePaginationWrapper } from '@vegaprotocol/utils';
import { useNodesQuery } from '../staking/home/__generated__/Nodes';
import { useProposalsQuery } from '../proposals/proposals/__generated__/Proposals';
import { useProtocolUpgradesQuery } from '../proposals/protocol-upgrade/__generated__/ProtocolUpgradeProposals';
import {
getNotRejectedProposals,
getNotRejectedProtocolUpgradeProposals,
@ -25,7 +24,8 @@ import * as Schema from '@vegaprotocol/types';
import type { RouteChildProps } from '..';
import type { ProposalFieldsFragment } from '../proposals/proposals/__generated__/Proposals';
import type { NodesFragmentFragment } from '../staking/home/__generated__/Nodes';
import type { ProtocolUpgradeProposalFieldsFragment } from '../proposals/protocol-upgrade/__generated__/ProtocolUpgradeProposals';
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { useProtocolUpgradeProposalsQuery } from '@vegaprotocol/proposals';
const nodesToShow = 6;
@ -181,7 +181,7 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
data: protocolUpgradesData,
loading: protocolUpgradesLoading,
error: protocolUpgradesError,
} = useProtocolUpgradesQuery({
} = useProtocolUpgradeProposalsQuery({
pollInterval: 5000,
fetchPolicy: 'network-only',
errorPolicy: 'ignore',

View File

@ -12,7 +12,7 @@ import { ExternalLinks } from '@vegaprotocol/utils';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProtocolUpgradeProposalFieldsFragment } from '../../protocol-upgrade/__generated__/ProtocolUpgradeProposals';
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
interface ProposalsListProps {
proposals: Array<ProposalFieldsFragment | ProposalQuery['proposal']>;

View File

@ -5,7 +5,7 @@ import {
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import type { ProtocolUpgradeProposalFieldsFragment } from '../../protocol-upgrade/__generated__/ProtocolUpgradeProposals';
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
export interface ProtocolUpgradeProposalDetailInfoProps {
proposal: ProtocolUpgradeProposalFieldsFragment;

View File

@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react';
import { BrowserRouter } from 'react-router-dom';
import { ProtocolUpgradeProposalsListItem } from './protocol-upgrade-proposals-list-item';
import { ProtocolUpgradeProposalStatus } from '@vegaprotocol/types';
import type { ProtocolUpgradeProposalFieldsFragment } from '../../protocol-upgrade/__generated__/ProtocolUpgradeProposals';
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
const proposal = {
status:

View File

@ -11,8 +11,8 @@ import { stripFullStops } from '@vegaprotocol/utils';
import { ProtocolUpgradeProposalStatus } from '@vegaprotocol/types';
import { SubHeading } from '../../../../components/heading';
import type { ReactNode } from 'react';
import type { ProtocolUpgradeProposalFieldsFragment } from '../../protocol-upgrade/__generated__/ProtocolUpgradeProposals';
import Routes from '../../../routes';
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
interface ProtocolProposalsListItemProps {
proposal: ProtocolUpgradeProposalFieldsFragment;

View File

@ -13,10 +13,9 @@ import {
} from '@vegaprotocol/types';
import type { NodeConnection, NodeEdge } from '@vegaprotocol/utils';
import type { ProposalFieldsFragment } from './__generated__/Proposals';
import type { ProtocolUpgradeProposalFieldsFragment } from '../protocol-upgrade/__generated__/ProtocolUpgradeProposals';
import orderBy from 'lodash/orderBy';
import { useProtocolUpgradesQuery } from '../protocol-upgrade/__generated__/ProtocolUpgradeProposals';
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { useProtocolUpgradeProposalsQuery } from '@vegaprotocol/proposals';
const orderByDate = (arr: ProposalFieldsFragment[]) =>
orderBy(
@ -73,7 +72,7 @@ export const ProposalsContainer = () => {
data: protocolUpgradesData,
loading: protocolUpgradesLoading,
error: protocolUpgradesError,
} = useProtocolUpgradesQuery({
} = useProtocolUpgradeProposalsQuery({
pollInterval: 5000,
fetchPolicy: 'network-only',
errorPolicy: 'ignore',

View File

@ -1,59 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ProtocolUpgradeProposalFieldsFragment = { __typename?: 'ProtocolUpgradeProposal', upgradeBlockHeight: string, vegaReleaseTag: string, approvers: Array<string>, status: Types.ProtocolUpgradeProposalStatus };
export type ProtocolUpgradesQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type ProtocolUpgradesQuery = { __typename?: 'Query', lastBlockHeight: string, protocolUpgradeProposals?: { __typename?: 'ProtocolUpgradeProposalConnection', edges?: Array<{ __typename?: 'ProtocolUpgradeProposalEdge', node: { __typename?: 'ProtocolUpgradeProposal', upgradeBlockHeight: string, vegaReleaseTag: string, approvers: Array<string>, status: Types.ProtocolUpgradeProposalStatus } }> | null } | null };
export const ProtocolUpgradeProposalFieldsFragmentDoc = gql`
fragment ProtocolUpgradeProposalFields on ProtocolUpgradeProposal {
upgradeBlockHeight
vegaReleaseTag
approvers
status
}
`;
export const ProtocolUpgradesDocument = gql`
query ProtocolUpgrades {
lastBlockHeight
protocolUpgradeProposals {
edges {
node {
...ProtocolUpgradeProposalFields
}
}
}
}
${ProtocolUpgradeProposalFieldsFragmentDoc}`;
/**
* __useProtocolUpgradesQuery__
*
* To run a query within a React component, call `useProtocolUpgradesQuery` and pass it any options that fit your needs.
* When your component renders, `useProtocolUpgradesQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useProtocolUpgradesQuery({
* variables: {
* },
* });
*/
export function useProtocolUpgradesQuery(baseOptions?: Apollo.QueryHookOptions<ProtocolUpgradesQuery, ProtocolUpgradesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ProtocolUpgradesQuery, ProtocolUpgradesQueryVariables>(ProtocolUpgradesDocument, options);
}
export function useProtocolUpgradesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ProtocolUpgradesQuery, ProtocolUpgradesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ProtocolUpgradesQuery, ProtocolUpgradesQueryVariables>(ProtocolUpgradesDocument, options);
}
export type ProtocolUpgradesQueryHookResult = ReturnType<typeof useProtocolUpgradesQuery>;
export type ProtocolUpgradesLazyQueryHookResult = ReturnType<typeof useProtocolUpgradesLazyQuery>;
export type ProtocolUpgradesQueryResult = Apollo.QueryResult<ProtocolUpgradesQuery, ProtocolUpgradesQueryVariables>;

View File

@ -6,14 +6,14 @@ import * as Schema from '@vegaprotocol/types';
import { ProtocolUpgradeProposal } from './protocol-upgrade-proposal';
import { ProposalNotFound } from '../components/proposal-not-found';
import { useProtocolUpgradesQuery } from './__generated__/ProtocolUpgradeProposals';
import { useNodesQuery } from '../../staking/home/__generated__/Nodes';
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
import { useProtocolUpgradeProposalsQuery } from '@vegaprotocol/proposals';
export const ProtocolUpgradeProposalContainer = () => {
const params = useParams<{ proposalReleaseTag: string }>();
const { data, loading, error, refetch } = useProtocolUpgradesQuery({
const { data, loading, error, refetch } = useProtocolUpgradeProposalsQuery({
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
skip: !params.proposalReleaseTag,

View File

@ -9,7 +9,7 @@ import {
import { ProtocolUpgradeProposalStatus } from '@vegaprotocol/types';
import { getNormalisedVotingPower } from '../../staking/shared';
import type { NodesFragmentFragment } from '../../staking/home/__generated__/Nodes';
import type { ProtocolUpgradeProposalFieldsFragment } from './__generated__/ProtocolUpgradeProposals';
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
const mockProposal: ProtocolUpgradeProposalFieldsFragment = {
vegaReleaseTag: 'v0.1.234',

View File

@ -3,8 +3,8 @@ import { ProtocolUpgradeProposalDetailHeader } from '../components/protocol-upgr
import { ProtocolUpdateProposalDetailApprovals } from '../components/protocol-upgrade-proposal-detail-approvals';
import { ProtocolUpgradeProposalDetailInfo } from '../components/protocol-upgrade-proposal-detail-info';
import { getNormalisedVotingPower } from '../../staking/shared';
import type { ProtocolUpgradeProposalFieldsFragment } from './__generated__/ProtocolUpgradeProposals';
import type { NodesFragmentFragment } from '../../staking/home/__generated__/Nodes';
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
export interface ProtocolUpgradeProposalProps {
proposal: ProtocolUpgradeProposalFieldsFragment;

View File

@ -24,6 +24,10 @@ import {
import { Links, Routes } from '../../pages/client-router';
import { createDocsLinks } from '@vegaprotocol/utils';
import { SettingsButton } from '../../client-pages/settings';
import {
ProtocolUpgradeCountdown,
ProtocolUpgradeCountdownMode,
} from '@vegaprotocol/proposals';
export const Navbar = ({
theme = 'system',
@ -45,11 +49,14 @@ export const Navbar = ({
theme={theme}
actions={
<>
<ProtocolUpgradeCountdown
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
/>
<SettingsButton />
<VegaWalletConnectButton />
</>
}
breakpoints={[521, 1067]}
breakpoints={[521, 1122]}
>
<NavigationList
className="[.drawer-content_&]:border-b [.drawer-content_&]:border-b-vega-light-200 dark:[.drawer-content_&]:border-b-vega-dark-200 [.drawer-content_&]:pb-8 [.drawer-content_&]:mb-2"

View File

@ -5,7 +5,5 @@ export const ViewingBanner = () => {
const { isReadOnly, pubKey, disconnect } = useVegaWallet();
return isReadOnly ? (
<ViewingAsBanner pubKey={pubKey} disconnect={disconnect} />
) : (
<div />
);
) : null;
};

View File

@ -37,6 +37,10 @@ import { ENV } from '../lib/config';
import { useDataProvider } from '@vegaprotocol/react-helpers';
import { activeOrdersProvider } from '@vegaprotocol/orders';
import { useTelemetryApproval } from '../lib/hooks/use-telemetry-approval';
import {
ProtocolUpgradeCountdownMode,
ProtocolUpgradeProposalNotification,
} from '@vegaprotocol/proposals';
const DEFAULT_TITLE = t('Welcome to Vega trading!');
@ -89,7 +93,12 @@ function AppBody({ Component }: AppProps) {
<div className={gridClasses}>
<AnnouncementBanner />
<Navbar theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'system'} />
<ViewingBanner />
<div data-testid="banners">
<ProtocolUpgradeProposalNotification
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
/>
<ViewingBanner />
</div>
<main data-testid={location.pathname}>
<Component />
</main>

View File

@ -2,6 +2,7 @@ import trim from 'lodash/trim';
import { useCallback } from 'react';
import { Networks } from '../types';
import { useEnvironment } from './use-environment';
import { stripFullStops } from '@vegaprotocol/utils';
type Net = Exclude<Networks, 'CUSTOM'>;
export enum DApp {
@ -89,15 +90,31 @@ export const useEtherscanLink = () => {
// Vega blog
export const BLOG = 'https://blog.vega.xyz/';
// Token pages
// Governance pages
export const TOKEN_NEW_MARKET_PROPOSAL = '/proposals/propose/new-market';
export const TOKEN_NEW_NETWORK_PARAM_PROPOSAL =
'/proposals/propose/network-parameter';
export const TOKEN_GOVERNANCE = '/proposals';
export const TOKEN_PROPOSALS = '/proposals';
export const TOKEN_PROPOSAL = '/proposals/:id';
export const TOKEN_PROTOCOL_UPGRADE_PROPOSAL =
'/proposals/protocol-upgrade/:tag';
export const TOKEN_VALIDATOR = '/validators/:id';
/**
* Generates link to the protocol upgrade proposal details on Governance
*/
export const useProtocolUpgradeProposalLink = () => {
const governance = useLinks(DApp.Token);
return (releaseTag: string) =>
governance(
TOKEN_PROTOCOL_UPGRADE_PROPOSAL.replace(
':tag',
stripFullStops(releaseTag)
)
);
};
// Explorer pages
export const EXPLORER_TX = '/txs/:hash';
export const EXPLORER_ORACLE = '/oracles/:id';

View File

@ -1,2 +1,4 @@
export * from './asset-proposal-notification';
export * from './market-proposal-notification';
export * from './protocol-upgrade-countdown';
export * from './protocol-upgrade-proposal-notification';

View File

@ -0,0 +1,99 @@
import { t } from '@vegaprotocol/i18n';
import { useNextProtocolUpgradeProposal, useTimeToUpgrade } from '../lib';
import { convertToCountdownString } from '@vegaprotocol/utils';
import { IconNames } from '@blueprintjs/icons';
import classNames from 'classnames';
import { Icon, NavigationContext } from '@vegaprotocol/ui-toolkit';
import { useProtocolUpgradeProposalLink } from '@vegaprotocol/environment';
import { useContext } from 'react';
export enum ProtocolUpgradeCountdownMode {
IN_BLOCKS,
IN_ESTIMATED_TIME_REMAINING,
}
type ProtocolUpgradeCountdownProps = {
mode?: ProtocolUpgradeCountdownMode;
};
export const ProtocolUpgradeCountdown = ({
mode = ProtocolUpgradeCountdownMode.IN_BLOCKS,
}: ProtocolUpgradeCountdownProps) => {
const { theme } = useContext(NavigationContext);
const { data, lastBlockHeight } = useNextProtocolUpgradeProposal();
const time = useTimeToUpgrade(
data && data.upgradeBlockHeight
? Number(data.upgradeBlockHeight)
: undefined
);
const detailsLink = useProtocolUpgradeProposalLink();
if (!data) return null;
const emphasis = classNames(
'text-vega-orange-500 dark:text-vega-orange-500',
{
'!text-black': theme === 'yellow',
}
);
let countdown;
switch (mode) {
case ProtocolUpgradeCountdownMode.IN_BLOCKS:
countdown = (
<>
<span className={emphasis}>
{Number(data.upgradeBlockHeight) - Number(lastBlockHeight)}
</span>{' '}
{t('blocks')}
</>
);
break;
case ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING:
countdown =
time !== undefined ? (
<span className={emphasis}>
{convertToCountdownString(time, '0:00:00:00')}
</span>
) : (
<span
className={classNames('italic lowercase text-vega-orange-600', {
'!text-black': theme === 'yellow',
})}
>
{t('estimating...')}
</span>
);
break;
}
return (
<a
href={detailsLink(data.vegaReleaseTag)}
target="_blank"
rel="noreferrer nofollow noopener"
>
<div
data-testid="protocol-upgrade-counter"
className={classNames(
'flex flex-nowrap items-center text-xs py-2 px-4',
'border rounded',
'border-vega-orange-500 dark:border-vega-orange-500',
'bg-vega-orange-300 dark:bg-vega-orange-700',
{
'!bg-transparent !border-black': theme === 'yellow',
}
)}
>
<Icon
name={IconNames.WARNING_SIGN}
size={3}
className={classNames('mr-2', emphasis)}
/>{' '}
<span className="flex gap-1 flex-nowrap whitespace-nowrap">
<span>{t('Network upgrade in')} </span>
{countdown}
</span>
</div>
</a>
);
};

View File

@ -0,0 +1,79 @@
import {
ExternalLink,
Intent,
NotificationBanner,
} from '@vegaprotocol/ui-toolkit';
import { useNextProtocolUpgradeProposal, useTimeToUpgrade } from '../lib';
import { t } from '@vegaprotocol/i18n';
import { useProtocolUpgradeProposalLink } from '@vegaprotocol/environment';
import { ProtocolUpgradeCountdownMode } from './protocol-upgrade-countdown';
import { convertToCountdownString } from '@vegaprotocol/utils';
import { useState } from 'react';
type ProtocolUpgradeProposalNotificationProps = {
mode?: ProtocolUpgradeCountdownMode;
};
export const ProtocolUpgradeProposalNotification = ({
mode = ProtocolUpgradeCountdownMode.IN_BLOCKS,
}: ProtocolUpgradeProposalNotificationProps) => {
const [visible, setVisible] = useState(true);
const { data, lastBlockHeight } = useNextProtocolUpgradeProposal();
const detailsLink = useProtocolUpgradeProposalLink();
const time = useTimeToUpgrade(
data && data.upgradeBlockHeight
? Number(data.upgradeBlockHeight)
: undefined
);
if (!data || !lastBlockHeight || !visible) return null;
const { vegaReleaseTag, upgradeBlockHeight } = data;
let countdown;
switch (mode) {
case ProtocolUpgradeCountdownMode.IN_BLOCKS:
countdown = (
<>
<span className="text-vega-orange-500">
{Number(upgradeBlockHeight) - Number(lastBlockHeight)}
</span>{' '}
{t('blocks')}
</>
);
break;
case ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING:
countdown =
time !== undefined ? (
<span className="text-vega-orange-500">
{convertToCountdownString(time, '0:00:00:00')}
</span>
) : (
<span className="text-vega-orange-600 lowercase italic">
{t('estimating...')}
</span>
);
break;
}
return (
<NotificationBanner
intent={Intent.Warning}
onClose={() => {
setVisible(false);
}}
>
<div className="uppercase ">
{t('The network will upgrade to %s in ', [data.vegaReleaseTag])}
{countdown}
</div>
<div>
{t(
'Trading activity will be interrupted, manage your risk appropriately.'
)}{' '}
<ExternalLink href={detailsLink(vegaReleaseTag)}>
{t('View details')}
</ExternalLink>
</div>
</NotificationBanner>
);
};

View File

@ -3,3 +3,4 @@ export * from './voting-hooks';
export * from './proposals-data-provider';
export * from './proposals-list';
export * from './voting-progress';
export * from './protocol-upgrade-proposals';

View File

@ -0,0 +1,6 @@
query BlockStatistics {
statistics {
blockHeight
blockDuration
}
}

View File

@ -5,9 +5,9 @@ fragment ProtocolUpgradeProposalFields on ProtocolUpgradeProposal {
status
}
query ProtocolUpgrades {
query ProtocolUpgradeProposals($inState: ProtocolUpgradeProposalStatus) {
lastBlockHeight
protocolUpgradeProposals {
protocolUpgradeProposals(inState: $inState) {
edges {
node {
...ProtocolUpgradeProposalFields

View File

@ -0,0 +1,46 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type BlockStatisticsQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type BlockStatisticsQuery = { __typename?: 'Query', statistics: { __typename?: 'Statistics', blockHeight: string, blockDuration: string } };
export const BlockStatisticsDocument = gql`
query BlockStatistics {
statistics {
blockHeight
blockDuration
}
}
`;
/**
* __useBlockStatisticsQuery__
*
* To run a query within a React component, call `useBlockStatisticsQuery` and pass it any options that fit your needs.
* When your component renders, `useBlockStatisticsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useBlockStatisticsQuery({
* variables: {
* },
* });
*/
export function useBlockStatisticsQuery(baseOptions?: Apollo.QueryHookOptions<BlockStatisticsQuery, BlockStatisticsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<BlockStatisticsQuery, BlockStatisticsQueryVariables>(BlockStatisticsDocument, options);
}
export function useBlockStatisticsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<BlockStatisticsQuery, BlockStatisticsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<BlockStatisticsQuery, BlockStatisticsQueryVariables>(BlockStatisticsDocument, options);
}
export type BlockStatisticsQueryHookResult = ReturnType<typeof useBlockStatisticsQuery>;
export type BlockStatisticsLazyQueryHookResult = ReturnType<typeof useBlockStatisticsLazyQuery>;
export type BlockStatisticsQueryResult = Apollo.QueryResult<BlockStatisticsQuery, BlockStatisticsQueryVariables>;

View File

@ -0,0 +1,62 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ProtocolUpgradeProposalFieldsFragment = { __typename?: 'ProtocolUpgradeProposal', upgradeBlockHeight: string, vegaReleaseTag: string, approvers: Array<string>, status: Types.ProtocolUpgradeProposalStatus };
export type ProtocolUpgradeProposalsQueryVariables = Types.Exact<{
inState?: Types.InputMaybe<Types.ProtocolUpgradeProposalStatus>;
}>;
export type ProtocolUpgradeProposalsQuery = { __typename?: 'Query', lastBlockHeight: string, protocolUpgradeProposals?: { __typename?: 'ProtocolUpgradeProposalConnection', edges?: Array<{ __typename?: 'ProtocolUpgradeProposalEdge', node: { __typename?: 'ProtocolUpgradeProposal', upgradeBlockHeight: string, vegaReleaseTag: string, approvers: Array<string>, status: Types.ProtocolUpgradeProposalStatus } }> | null } | null };
export const ProtocolUpgradeProposalFieldsFragmentDoc = gql`
fragment ProtocolUpgradeProposalFields on ProtocolUpgradeProposal {
upgradeBlockHeight
vegaReleaseTag
approvers
status
}
`;
export const ProtocolUpgradeProposalsDocument = gql`
query ProtocolUpgradeProposals($inState: ProtocolUpgradeProposalStatus) {
lastBlockHeight
protocolUpgradeProposals(inState: $inState) {
edges {
node {
...ProtocolUpgradeProposalFields
}
}
}
}
${ProtocolUpgradeProposalFieldsFragmentDoc}`;
/**
* __useProtocolUpgradeProposalsQuery__
*
* To run a query within a React component, call `useProtocolUpgradeProposalsQuery` and pass it any options that fit your needs.
* When your component renders, `useProtocolUpgradeProposalsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useProtocolUpgradeProposalsQuery({
* variables: {
* inState: // value for 'inState'
* },
* });
*/
export function useProtocolUpgradeProposalsQuery(baseOptions?: Apollo.QueryHookOptions<ProtocolUpgradeProposalsQuery, ProtocolUpgradeProposalsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ProtocolUpgradeProposalsQuery, ProtocolUpgradeProposalsQueryVariables>(ProtocolUpgradeProposalsDocument, options);
}
export function useProtocolUpgradeProposalsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ProtocolUpgradeProposalsQuery, ProtocolUpgradeProposalsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ProtocolUpgradeProposalsQuery, ProtocolUpgradeProposalsQueryVariables>(ProtocolUpgradeProposalsDocument, options);
}
export type ProtocolUpgradeProposalsQueryHookResult = ReturnType<typeof useProtocolUpgradeProposalsQuery>;
export type ProtocolUpgradeProposalsLazyQueryHookResult = ReturnType<typeof useProtocolUpgradeProposalsLazyQuery>;
export type ProtocolUpgradeProposalsQueryResult = Apollo.QueryResult<ProtocolUpgradeProposalsQuery, ProtocolUpgradeProposalsQueryVariables>;

View File

@ -0,0 +1,3 @@
export * from './__generated__/ProtocolUpgradeProposals';
export * from './use-next-protocol-upgrade-proposals';
export * from './use-time-to-upgrade';

View File

@ -0,0 +1,53 @@
import { useMemo } from 'react';
import * as Schema from '@vegaprotocol/types';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import { useProtocolUpgradeProposalsQuery } from './__generated__/ProtocolUpgradeProposals';
export const useNextProtocolUpgradeProposals = (since?: number) => {
const { data, loading, error } = useProtocolUpgradeProposalsQuery({
pollInterval: 5000,
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
variables: {
inState:
Schema.ProtocolUpgradeProposalStatus
.PROTOCOL_UPGRADE_PROPOSAL_STATUS_APPROVED,
},
});
const nextUpgrades = useMemo(() => {
if (!data) return [];
const proposals = removePaginationWrapper(
data?.protocolUpgradeProposals?.edges
);
return proposals
.filter(
(p) =>
Number(p.upgradeBlockHeight) > (since || Number(data.lastBlockHeight))
)
.sort(
(a, b) => Number(a.upgradeBlockHeight) - Number(b.upgradeBlockHeight)
);
}, [data, since]);
return {
data: nextUpgrades,
lastBlockHeight: data?.lastBlockHeight,
loading,
error,
};
};
export const useNextProtocolUpgradeProposal = (since?: number) => {
const { data, lastBlockHeight, loading, error } =
useNextProtocolUpgradeProposals(since);
return {
data: !data ? undefined : data[0],
lastBlockHeight,
loading,
error,
};
};

View File

@ -0,0 +1,32 @@
import { renderHook, waitFor } from '@testing-library/react';
import { useTimeToUpgrade } from './use-time-to-upgrade';
jest.mock('./__generated__/BlockStatistics', () => ({
...jest.requireActual('./__generated__/BlockStatistics'),
useBlockStatisticsQuery: jest.fn(() => {
return {
data: {
statistics: {
blockHeight: 1,
blockDuration: 500,
},
},
};
}),
}));
describe('useTimeToUpgrade', () => {
it.each([
[-1, -1000],
[0, -500],
[1, 0],
[2, 500],
[3, 1000],
[10, 4500],
])('time in %d block(s) should be %d ms', async (block, avg) => {
const { result } = renderHook(() => useTimeToUpgrade(block, 1));
await waitFor(() => {
expect(result.current).toEqual(avg);
});
});
});

View File

@ -0,0 +1,64 @@
import { useEffect, useState } from 'react';
import { useBlockStatisticsQuery } from './__generated__/BlockStatistics';
import sum from 'lodash/sum';
const DEFAULT_POLLS = 10;
const INTERVAL = 1000;
const durations = [] as number[];
const useAverageBlockDuration = (polls = DEFAULT_POLLS) => {
const [avg, setAvg] = useState<number | undefined>(undefined);
const { data } = useBlockStatisticsQuery({
pollInterval: INTERVAL,
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
skip: durations.length === polls,
});
useEffect(() => {
if (durations.length < polls && data) {
durations.push(parseFloat(data.statistics.blockDuration));
}
if (durations.length === polls) {
const averageBlockDuration = sum(durations) / durations.length; // ms
console.log('setting avg', averageBlockDuration);
setAvg(averageBlockDuration);
}
}, [data, polls]);
return avg;
};
export const useTimeToUpgrade = (
upgradeBlockHeight?: number,
polls = DEFAULT_POLLS
) => {
const [time, setTime] = useState<number | undefined>(undefined);
const avg = useAverageBlockDuration(polls);
const { data } = useBlockStatisticsQuery({
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
});
useEffect(() => {
const t =
(Number(upgradeBlockHeight) - Number(data?.statistics.blockHeight)) *
Number(avg);
if (!isNaN(t)) {
setTime(t);
}
}, [avg, data?.statistics.blockHeight, upgradeBlockHeight]);
useEffect(() => {
const i = setInterval(() => {
if (time !== undefined) {
setTime(time - 1000);
}
}, 1000);
return () => {
clearInterval(i);
};
}, [time]);
return time;
};

View File

@ -1,4 +1,8 @@
import { getSecondsFromInterval } from './time';
import {
convertToCountdown,
convertToCountdownString,
getSecondsFromInterval,
} from './time';
describe('getSecondsFromInterval', () => {
it('returns 0 for bad data', () => {
@ -34,3 +38,44 @@ describe('getSecondsFromInterval', () => {
expect(getSecondsFromInterval('1D1h30m1s')).toEqual(91801);
});
});
describe('convertToCountdown', () => {
it.each([
[1 * 1000, [0, 0, 0, 1]],
[2 * 1000, [0, 0, 0, 2]],
[3999, [0, 0, 0, 3]],
[1 * 60 * 1000 + 3 * 1000, [0, 0, 1, 3]],
[12 * 60 * 1000 + 3 * 1000, [0, 0, 12, 3]],
[3 * 60 * 60 * 1000 + 12 * 60 * 1000 + 3 * 1000, [0, 3, 12, 3]],
[
30 * 24 * 60 * 60 * 1000 + 3 * 60 * 60 * 1000 + 12 * 60 * 1000 + 3 * 1000,
[30, 3, 12, 3],
],
[
-1 *
(30 * 24 * 60 * 60 * 1000 +
3 * 60 * 60 * 1000 +
12 * 60 * 1000 +
3 * 1000),
[30, 3, 12, 3],
],
])('converts %d ms to %s', (time, countdown) => {
expect(convertToCountdown(time)).toEqual(countdown);
});
});
describe('convertToCountdownString', () => {
it.each([
[1 * 1000, '00m01s'],
[2 * 1000, '00m02s'],
[1 * 60 * 1000 + 3 * 1000, '01m03s'],
[12 * 60 * 1000 + 3 * 1000, '12m03s'],
[3 * 60 * 60 * 1000 + 12 * 60 * 1000 + 3 * 1000, '03h12m03s'],
[
30 * 24 * 60 * 60 * 1000 + 3 * 60 * 60 * 1000 + 12 * 60 * 1000 + 3 * 1000,
'30d03h12m03s',
],
])('converts %d ms to %s', (time, countdown) => {
expect(convertToCountdownString(time)).toEqual(countdown);
});
});

View File

@ -46,3 +46,47 @@ export function getSecondsFromInterval(str: string) {
}
return seconds;
}
export const convertToCountdown = (time: number) => {
const s = 1000;
const m = 1000 * 60;
const h = 1000 * 60 * 60;
const d = 1000 * 60 * 60 * 24;
const t = Math.abs(time);
const days = Math.floor(t / d);
const hours = Math.floor((t - days * d) / h);
const minutes = Math.floor((t - days * d - hours * h) / m);
const seconds = Math.floor((t - days * d - hours * h - minutes * m) / s);
return [days, hours, minutes, seconds];
};
/**
* Converts given time in ms to countdown string, e.g. 1d20h34m10s
*/
export const convertToCountdownString = (
time: number,
pattern = '0d00h00m00s'
) => {
const values = convertToCountdown(time);
let i = 0;
const countdown = pattern
.replace(/00*/g, (match) => {
const value = String(values[i++]);
if (value.length < match.length) {
const filler = Array(match.length - value.length)
.fill('0')
.join('');
return `${filler}${value}`;
}
return value;
})
.replace(/^00*[^\d]*/g, '') // replace leading 00, e.g. 00d01h23m45s -> 01h23m45s
.replace(/^00[^\d]*/g, ''); // replace leading 00, e.g. 00d00h23m45s -> 23m45s
return countdown;
};