From 768b3b29f0731149c421bf28e2c7e4bb98ad953d Mon Sep 17 00:00:00 2001 From: Art Date: Thu, 4 May 2023 09:51:24 +0200 Subject: [PATCH] feat(trading): protocol upgrade notification (#3517) --- .../src/support/proposal.functions.ts | 2 +- apps/governance/src/routes/home/index.tsx | 6 +- .../proposals-list/proposals-list.tsx | 2 +- .../protocol-upgrade-proposal-detail-info.tsx | 2 +- ...tocol-upgrade-proposals-list-item.spec.tsx | 2 +- .../protocol-upgrade-proposals-list-item.tsx | 2 +- .../proposals/proposals-container.tsx | 7 +- .../__generated__/ProtocolUpgradeProposals.ts | 59 ----------- .../protocol-upgrade-proposal-container.tsx | 4 +- .../protocol-upgrade-proposal.spec.tsx | 2 +- .../protocol-upgrade-proposal.tsx | 2 +- apps/trading/components/navbar/navbar.tsx | 9 +- .../components/viewing-banner/index.tsx | 4 +- apps/trading/pages/_app.page.tsx | 11 ++- libs/environment/src/hooks/use-links.ts | 19 +++- libs/proposals/src/components/index.ts | 2 + .../components/protocol-upgrade-countdown.tsx | 99 +++++++++++++++++++ ...protocol-upgrade-proposal-notification.tsx | 79 +++++++++++++++ libs/proposals/src/lib/index.ts | 1 + .../BlockStatistics.graphql | 6 ++ .../ProtocolUpgradeProposals.graphql | 4 +- .../__generated__/BlockStatistics.ts | 46 +++++++++ .../__generated__/ProtocolUpgradeProposals.ts | 62 ++++++++++++ .../lib/protocol-upgrade-proposals/index.ts | 3 + .../use-next-protocol-upgrade-proposals.ts | 53 ++++++++++ .../use-time-to-upgrade.spec.ts | 32 ++++++ .../use-time-to-upgrade.ts | 64 ++++++++++++ libs/utils/src/lib/time.spec.ts | 47 ++++++++- libs/utils/src/lib/time.ts | 44 +++++++++ 29 files changed, 591 insertions(+), 84 deletions(-) delete mode 100644 apps/governance/src/routes/proposals/protocol-upgrade/__generated__/ProtocolUpgradeProposals.ts create mode 100644 libs/proposals/src/components/protocol-upgrade-countdown.tsx create mode 100644 libs/proposals/src/components/protocol-upgrade-proposal-notification.tsx create mode 100644 libs/proposals/src/lib/protocol-upgrade-proposals/BlockStatistics.graphql rename {apps/governance/src/routes/proposals/protocol-upgrade => libs/proposals/src/lib/protocol-upgrade-proposals}/ProtocolUpgradeProposals.graphql (65%) create mode 100644 libs/proposals/src/lib/protocol-upgrade-proposals/__generated__/BlockStatistics.ts create mode 100644 libs/proposals/src/lib/protocol-upgrade-proposals/__generated__/ProtocolUpgradeProposals.ts create mode 100644 libs/proposals/src/lib/protocol-upgrade-proposals/index.ts create mode 100644 libs/proposals/src/lib/protocol-upgrade-proposals/use-next-protocol-upgrade-proposals.ts create mode 100644 libs/proposals/src/lib/protocol-upgrade-proposals/use-time-to-upgrade.spec.ts create mode 100644 libs/proposals/src/lib/protocol-upgrade-proposals/use-time-to-upgrade.ts diff --git a/apps/governance-e2e/src/support/proposal.functions.ts b/apps/governance-e2e/src/support/proposal.functions.ts index 42253c670..00bd0ece4 100644 --- a/apps/governance-e2e/src/support/proposal.functions.ts +++ b/apps/governance-e2e/src/support/proposal.functions.ts @@ -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); }); } diff --git a/apps/governance/src/routes/home/index.tsx b/apps/governance/src/routes/home/index.tsx index 3f5751ccf..00ffd171c 100644 --- a/apps/governance/src/routes/home/index.tsx +++ b/apps/governance/src/routes/home/index.tsx @@ -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', diff --git a/apps/governance/src/routes/proposals/components/proposals-list/proposals-list.tsx b/apps/governance/src/routes/proposals/components/proposals-list/proposals-list.tsx index f31921db5..c6b8e761f 100644 --- a/apps/governance/src/routes/proposals/components/proposals-list/proposals-list.tsx +++ b/apps/governance/src/routes/proposals/components/proposals-list/proposals-list.tsx @@ -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; diff --git a/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-info/protocol-upgrade-proposal-detail-info.tsx b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-info/protocol-upgrade-proposal-detail-info.tsx index 3aace79fd..95b3612f7 100644 --- a/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-info/protocol-upgrade-proposal-detail-info.tsx +++ b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposal-detail-info/protocol-upgrade-proposal-detail-info.tsx @@ -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; diff --git a/apps/governance/src/routes/proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item.spec.tsx b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item.spec.tsx index 65b78c073..4d0bdce7f 100644 --- a/apps/governance/src/routes/proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item.spec.tsx +++ b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item.spec.tsx @@ -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: diff --git a/apps/governance/src/routes/proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item.tsx b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item.tsx index c4be5acda..e96fb5941 100644 --- a/apps/governance/src/routes/proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item.tsx +++ b/apps/governance/src/routes/proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item.tsx @@ -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; diff --git a/apps/governance/src/routes/proposals/proposals/proposals-container.tsx b/apps/governance/src/routes/proposals/proposals/proposals-container.tsx index 4a92bd5d9..7d63a91f2 100644 --- a/apps/governance/src/routes/proposals/proposals/proposals-container.tsx +++ b/apps/governance/src/routes/proposals/proposals/proposals-container.tsx @@ -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', diff --git a/apps/governance/src/routes/proposals/protocol-upgrade/__generated__/ProtocolUpgradeProposals.ts b/apps/governance/src/routes/proposals/protocol-upgrade/__generated__/ProtocolUpgradeProposals.ts deleted file mode 100644 index ef5f9928f..000000000 --- a/apps/governance/src/routes/proposals/protocol-upgrade/__generated__/ProtocolUpgradeProposals.ts +++ /dev/null @@ -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, 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, 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) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useQuery(ProtocolUpgradesDocument, options); - } -export function useProtocolUpgradesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useLazyQuery(ProtocolUpgradesDocument, options); - } -export type ProtocolUpgradesQueryHookResult = ReturnType; -export type ProtocolUpgradesLazyQueryHookResult = ReturnType; -export type ProtocolUpgradesQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/apps/governance/src/routes/proposals/protocol-upgrade/protocol-upgrade-proposal-container.tsx b/apps/governance/src/routes/proposals/protocol-upgrade/protocol-upgrade-proposal-container.tsx index 72b54e039..8cbe6d03b 100644 --- a/apps/governance/src/routes/proposals/protocol-upgrade/protocol-upgrade-proposal-container.tsx +++ b/apps/governance/src/routes/proposals/protocol-upgrade/protocol-upgrade-proposal-container.tsx @@ -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, diff --git a/apps/governance/src/routes/proposals/protocol-upgrade/protocol-upgrade-proposal.spec.tsx b/apps/governance/src/routes/proposals/protocol-upgrade/protocol-upgrade-proposal.spec.tsx index 8e59e8b0d..229fbf001 100644 --- a/apps/governance/src/routes/proposals/protocol-upgrade/protocol-upgrade-proposal.spec.tsx +++ b/apps/governance/src/routes/proposals/protocol-upgrade/protocol-upgrade-proposal.spec.tsx @@ -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', diff --git a/apps/governance/src/routes/proposals/protocol-upgrade/protocol-upgrade-proposal.tsx b/apps/governance/src/routes/proposals/protocol-upgrade/protocol-upgrade-proposal.tsx index 6610a17ab..30747e594 100644 --- a/apps/governance/src/routes/proposals/protocol-upgrade/protocol-upgrade-proposal.tsx +++ b/apps/governance/src/routes/proposals/protocol-upgrade/protocol-upgrade-proposal.tsx @@ -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; diff --git a/apps/trading/components/navbar/navbar.tsx b/apps/trading/components/navbar/navbar.tsx index ccbd32b91..c2e5e7464 100644 --- a/apps/trading/components/navbar/navbar.tsx +++ b/apps/trading/components/navbar/navbar.tsx @@ -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={ <> + } - breakpoints={[521, 1067]} + breakpoints={[521, 1122]} > { const { isReadOnly, pubKey, disconnect } = useVegaWallet(); return isReadOnly ? ( - ) : ( -
- ); + ) : null; }; diff --git a/apps/trading/pages/_app.page.tsx b/apps/trading/pages/_app.page.tsx index 3996631d3..4f7d6ccab 100644 --- a/apps/trading/pages/_app.page.tsx +++ b/apps/trading/pages/_app.page.tsx @@ -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) {
- +
+ + +
diff --git a/libs/environment/src/hooks/use-links.ts b/libs/environment/src/hooks/use-links.ts index 674f29c46..69197976c 100644 --- a/libs/environment/src/hooks/use-links.ts +++ b/libs/environment/src/hooks/use-links.ts @@ -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; 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'; diff --git a/libs/proposals/src/components/index.ts b/libs/proposals/src/components/index.ts index 8bf06c268..f6dabd48d 100644 --- a/libs/proposals/src/components/index.ts +++ b/libs/proposals/src/components/index.ts @@ -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'; diff --git a/libs/proposals/src/components/protocol-upgrade-countdown.tsx b/libs/proposals/src/components/protocol-upgrade-countdown.tsx new file mode 100644 index 000000000..2d8ab4071 --- /dev/null +++ b/libs/proposals/src/components/protocol-upgrade-countdown.tsx @@ -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 = ( + <> + + {Number(data.upgradeBlockHeight) - Number(lastBlockHeight)} + {' '} + {t('blocks')} + + ); + break; + case ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING: + countdown = + time !== undefined ? ( + + {convertToCountdownString(time, '0:00:00:00')} + + ) : ( + + {t('estimating...')} + + ); + break; + } + + return ( + +
+ {' '} + + {t('Network upgrade in')} + {countdown} + +
+
+ ); +}; diff --git a/libs/proposals/src/components/protocol-upgrade-proposal-notification.tsx b/libs/proposals/src/components/protocol-upgrade-proposal-notification.tsx new file mode 100644 index 000000000..0fdc63813 --- /dev/null +++ b/libs/proposals/src/components/protocol-upgrade-proposal-notification.tsx @@ -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 = ( + <> + + {Number(upgradeBlockHeight) - Number(lastBlockHeight)} + {' '} + {t('blocks')} + + ); + break; + case ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING: + countdown = + time !== undefined ? ( + + {convertToCountdownString(time, '0:00:00:00')} + + ) : ( + + {t('estimating...')} + + ); + break; + } + + return ( + { + setVisible(false); + }} + > +
+ {t('The network will upgrade to %s in ', [data.vegaReleaseTag])} + {countdown} +
+
+ {t( + 'Trading activity will be interrupted, manage your risk appropriately.' + )}{' '} + + {t('View details')} + +
+
+ ); +}; diff --git a/libs/proposals/src/lib/index.ts b/libs/proposals/src/lib/index.ts index a9e213996..f8ae02537 100644 --- a/libs/proposals/src/lib/index.ts +++ b/libs/proposals/src/lib/index.ts @@ -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'; diff --git a/libs/proposals/src/lib/protocol-upgrade-proposals/BlockStatistics.graphql b/libs/proposals/src/lib/protocol-upgrade-proposals/BlockStatistics.graphql new file mode 100644 index 000000000..a1bcfc112 --- /dev/null +++ b/libs/proposals/src/lib/protocol-upgrade-proposals/BlockStatistics.graphql @@ -0,0 +1,6 @@ +query BlockStatistics { + statistics { + blockHeight + blockDuration + } +} diff --git a/apps/governance/src/routes/proposals/protocol-upgrade/ProtocolUpgradeProposals.graphql b/libs/proposals/src/lib/protocol-upgrade-proposals/ProtocolUpgradeProposals.graphql similarity index 65% rename from apps/governance/src/routes/proposals/protocol-upgrade/ProtocolUpgradeProposals.graphql rename to libs/proposals/src/lib/protocol-upgrade-proposals/ProtocolUpgradeProposals.graphql index 35959a69f..f895a3f53 100644 --- a/apps/governance/src/routes/proposals/protocol-upgrade/ProtocolUpgradeProposals.graphql +++ b/libs/proposals/src/lib/protocol-upgrade-proposals/ProtocolUpgradeProposals.graphql @@ -5,9 +5,9 @@ fragment ProtocolUpgradeProposalFields on ProtocolUpgradeProposal { status } -query ProtocolUpgrades { +query ProtocolUpgradeProposals($inState: ProtocolUpgradeProposalStatus) { lastBlockHeight - protocolUpgradeProposals { + protocolUpgradeProposals(inState: $inState) { edges { node { ...ProtocolUpgradeProposalFields diff --git a/libs/proposals/src/lib/protocol-upgrade-proposals/__generated__/BlockStatistics.ts b/libs/proposals/src/lib/protocol-upgrade-proposals/__generated__/BlockStatistics.ts new file mode 100644 index 000000000..c4c4ff579 --- /dev/null +++ b/libs/proposals/src/lib/protocol-upgrade-proposals/__generated__/BlockStatistics.ts @@ -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) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(BlockStatisticsDocument, options); + } +export function useBlockStatisticsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(BlockStatisticsDocument, options); + } +export type BlockStatisticsQueryHookResult = ReturnType; +export type BlockStatisticsLazyQueryHookResult = ReturnType; +export type BlockStatisticsQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/libs/proposals/src/lib/protocol-upgrade-proposals/__generated__/ProtocolUpgradeProposals.ts b/libs/proposals/src/lib/protocol-upgrade-proposals/__generated__/ProtocolUpgradeProposals.ts new file mode 100644 index 000000000..f9a49f138 --- /dev/null +++ b/libs/proposals/src/lib/protocol-upgrade-proposals/__generated__/ProtocolUpgradeProposals.ts @@ -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, status: Types.ProtocolUpgradeProposalStatus }; + +export type ProtocolUpgradeProposalsQueryVariables = Types.Exact<{ + inState?: Types.InputMaybe; +}>; + + +export type ProtocolUpgradeProposalsQuery = { __typename?: 'Query', lastBlockHeight: string, protocolUpgradeProposals?: { __typename?: 'ProtocolUpgradeProposalConnection', edges?: Array<{ __typename?: 'ProtocolUpgradeProposalEdge', node: { __typename?: 'ProtocolUpgradeProposal', upgradeBlockHeight: string, vegaReleaseTag: string, approvers: Array, 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) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(ProtocolUpgradeProposalsDocument, options); + } +export function useProtocolUpgradeProposalsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(ProtocolUpgradeProposalsDocument, options); + } +export type ProtocolUpgradeProposalsQueryHookResult = ReturnType; +export type ProtocolUpgradeProposalsLazyQueryHookResult = ReturnType; +export type ProtocolUpgradeProposalsQueryResult = Apollo.QueryResult; \ No newline at end of file diff --git a/libs/proposals/src/lib/protocol-upgrade-proposals/index.ts b/libs/proposals/src/lib/protocol-upgrade-proposals/index.ts new file mode 100644 index 000000000..9e18c4bf3 --- /dev/null +++ b/libs/proposals/src/lib/protocol-upgrade-proposals/index.ts @@ -0,0 +1,3 @@ +export * from './__generated__/ProtocolUpgradeProposals'; +export * from './use-next-protocol-upgrade-proposals'; +export * from './use-time-to-upgrade'; diff --git a/libs/proposals/src/lib/protocol-upgrade-proposals/use-next-protocol-upgrade-proposals.ts b/libs/proposals/src/lib/protocol-upgrade-proposals/use-next-protocol-upgrade-proposals.ts new file mode 100644 index 000000000..70bc3bff7 --- /dev/null +++ b/libs/proposals/src/lib/protocol-upgrade-proposals/use-next-protocol-upgrade-proposals.ts @@ -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, + }; +}; diff --git a/libs/proposals/src/lib/protocol-upgrade-proposals/use-time-to-upgrade.spec.ts b/libs/proposals/src/lib/protocol-upgrade-proposals/use-time-to-upgrade.spec.ts new file mode 100644 index 000000000..34343a748 --- /dev/null +++ b/libs/proposals/src/lib/protocol-upgrade-proposals/use-time-to-upgrade.spec.ts @@ -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); + }); + }); +}); diff --git a/libs/proposals/src/lib/protocol-upgrade-proposals/use-time-to-upgrade.ts b/libs/proposals/src/lib/protocol-upgrade-proposals/use-time-to-upgrade.ts new file mode 100644 index 000000000..10b5a22f5 --- /dev/null +++ b/libs/proposals/src/lib/protocol-upgrade-proposals/use-time-to-upgrade.ts @@ -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(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(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; +}; diff --git a/libs/utils/src/lib/time.spec.ts b/libs/utils/src/lib/time.spec.ts index 1c2518d4c..0cd024661 100644 --- a/libs/utils/src/lib/time.spec.ts +++ b/libs/utils/src/lib/time.spec.ts @@ -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); + }); +}); diff --git a/libs/utils/src/lib/time.ts b/libs/utils/src/lib/time.ts index 1ac28755f..0b41bcc53 100644 --- a/libs/utils/src/lib/time.ts +++ b/libs/utils/src/lib/time.ts @@ -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; +};