Compare commits

..
4 Commits
345 changed files with 8676 additions and 13267 deletions
+2 -2
View File
@@ -10,7 +10,7 @@ on:
inputs: inputs:
console-test-branch: console-test-branch:
type: choice type: choice
description: 'main: v0.73.13, develop: v0.74.0' description: 'main: v0.73.5, develop: v0.73.5'
options: options:
- main - main
- develop - develop
@@ -205,7 +205,7 @@ jobs:
# run tests # run tests
#---------------------------------------------- #----------------------------------------------
- name: Run tests - name: Run tests
run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v --numprocesses 4 --dist loadfile --durations=45 run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v --numprocesses 1 --dist loadfile --durations=45
working-directory: apps/trading/e2e working-directory: apps/trading/e2e
#---------------------------------------------- #----------------------------------------------
# upload traces # upload traces
+2 -2
View File
@@ -25,7 +25,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
rm package.json rm package.json
npm install --no-save @commitlint/cli@16.3.0 @commitlint/config-conventional@18.6.1 @commitlint/config-nx-scopes@18.6.1 nx@17.1.2 npm install --no-save @commitlint/cli @commitlint/config-conventional @commitlint/config-nx-scopes nx
- name: Check PR title - name: Check PR title
run: echo "${{ github.event.pull_request.title }}" | npx @commitlint/cli@16.3.0 --config ./commitlint.config-ci.js run: echo "${{ github.event.pull_request.title }}" | npx commitlint --config ./commitlint.config-ci.js
+5 -5
View File
@@ -4,7 +4,7 @@ The front-end monorepo provides a toolkit for building apps that interact with V
This repository is managed using [Nx](https://nx.dev). This repository is managed using [Nx](https://nx.dev).
## 🔎 Applications in this repo # 🔎 Applications in this repo
### [Block explorer](./apps/explorer) ### [Block explorer](./apps/explorer)
@@ -30,7 +30,7 @@ Hosting for static content being shared across apps, for example fonts.
The utility dApp for validators wishing to add or remove themselves as a signer of the multisig contract. The utility dApp for validators wishing to add or remove themselves as a signer of the multisig contract.
## 🧱 Libraries in this repo # 🧱 Libraries in this repo
### [UI toolkit](./libs/ui-toolkit) ### [UI toolkit](./libs/ui-toolkit)
@@ -53,7 +53,7 @@ A utility library for connecting to the Ethereum network and interacting with Ve
Generic react helpers that can be used across multiple applications, along with other utilities. Generic react helpers that can be used across multiple applications, along with other utilities.
## 💻 Develop # 💻 Develop
### Set up ### Set up
@@ -103,7 +103,7 @@ In CI linting, formatting and also run. These checks can be seen in the [CI work
Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more. Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more.
## 🐋 Hosting a console # 🐋 Hosting a console
To host a console there are two possible build scenarios for running the frontends: nx performed **outside** or **inside** docker build. For specific build instructions follow [build instructions](#build-instructions). To host a console there are two possible build scenarios for running the frontends: nx performed **outside** or **inside** docker build. For specific build instructions follow [build instructions](#build-instructions).
@@ -226,6 +226,6 @@ Note: The script is only needed if capsule was built for first time or fresh. To
vega wallet service run -n DV --load-tokens --tokens-passphrase-file passphrase --no-version-check --automatic-consent --home ~/.vegacapsule/testnet/wallet vega wallet service run -n DV --load-tokens --tokens-passphrase-file passphrase --no-version-check --automatic-consent --home ~/.vegacapsule/testnet/wallet
``` ```
## 📑 License # 📑 License
[MIT](./LICENSE) [MIT](./LICENSE)
@@ -1,7 +1,7 @@
import { getNewAssetTxBody } from '../support/governance.functions'; import { getNewAssetTxBody } from '../support/governance.functions';
context('Proposal page', { tags: '@smoke' }, function () { context('Proposal page', { tags: '@smoke' }, function () {
describe.skip('Verify elements on page', function () { describe('Verify elements on page', function () {
const proposalHeading = 'proposals-heading'; const proposalHeading = 'proposals-heading';
const dateTimeRegex = const dateTimeRegex =
/(\d{1,2})\/(\d{1,2})\/(\d{4}), (\d{1,2}):(\d{1,2}):(\d{1,2})/gm; /(\d{1,2})\/(\d{1,2})\/(\d{4}), (\d{1,2}):(\d{1,2}):(\d{1,2})/gm;
@@ -24,6 +24,10 @@ context('Proposal page', { tags: '@smoke' }, function () {
cy.get_element_by_col_id('title').should('have.text', proposalTitle); cy.get_element_by_col_id('title').should('have.text', proposalTitle);
cy.get_element_by_col_id('type').should('have.text', 'NewMarket'); cy.get_element_by_col_id('type').should('have.text', 'NewMarket');
cy.get_element_by_col_id('state').should('have.text', 'Enacted'); cy.get_element_by_col_id('state').should('have.text', 'Enacted');
cy.getByTestId('vote-progress').should('be.visible');
cy.getByTestId('vote-progress-bar-for')
.invoke('attr', 'style')
.should('eq', 'width: 100%;');
cy.get('[col-id="cDate"]') cy.get('[col-id="cDate"]')
.invoke('text') .invoke('text')
.should('match', dateTimeRegex); .should('match', dateTimeRegex);
@@ -69,6 +73,10 @@ context('Proposal page', { tags: '@smoke' }, function () {
'have.text', 'have.text',
'Waiting for Node Vote' 'Waiting for Node Vote'
); );
cy.getByTestId('vote-progress').should('be.visible');
cy.getByTestId('vote-progress-bar-against')
.invoke('attr', 'style')
.should('eq', 'width: 100%;');
cy.get('[col-id="cDate"]') cy.get('[col-id="cDate"]')
.invoke('text') .invoke('text')
.should('match', dateTimeRegex); .should('match', dateTimeRegex);
@@ -7,7 +7,6 @@ export type AssetBalanceProps = {
price: string; price: string;
showAssetLink?: boolean; showAssetLink?: boolean;
showAssetSymbol?: boolean; showAssetSymbol?: boolean;
rounded?: boolean;
}; };
/** /**
@@ -19,17 +18,12 @@ const AssetBalance = ({
price, price,
showAssetLink = true, showAssetLink = true,
showAssetSymbol = false, showAssetSymbol = false,
rounded = false,
}: AssetBalanceProps) => { }: AssetBalanceProps) => {
const { data: asset, loading } = useAssetDataProvider(assetId); const { data: asset, loading } = useAssetDataProvider(assetId);
const label = const label =
!loading && asset && asset.decimals !loading && asset && asset.decimals
? addDecimalsFixedFormatNumber( ? addDecimalsFixedFormatNumber(price, asset.decimals)
price,
asset.decimals,
rounded ? 0 : undefined
)
: price; : price;
return ( return (
@@ -41,7 +41,6 @@ export const Header = () => {
Routes.ASSETS, Routes.ASSETS,
Routes.MARKETS, Routes.MARKETS,
Routes.GOVERNANCE, Routes.GOVERNANCE,
Routes.TREASURY,
Routes.NETWORK_PARAMETERS, Routes.NETWORK_PARAMETERS,
Routes.GENESIS, Routes.GENESIS,
].map((n) => pages.find((r) => r.path === n)) ].map((n) => pages.find((r) => r.path === n))
@@ -32,17 +32,9 @@ export function getNameForParty(id: string, data?: ExplorerNodeNamesQuery) {
export type PartyLinkProps = Partial<ComponentProps<typeof Link>> & { export type PartyLinkProps = Partial<ComponentProps<typeof Link>> & {
id: string; id: string;
truncate?: boolean; truncate?: boolean;
networkLabel?: string;
truncateLength?: number;
}; };
const PartyLink = ({ const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
id,
truncate = false,
truncateLength = 4,
networkLabel = t('Network'),
...props
}: PartyLinkProps) => {
const { data } = useExplorerNodeNamesQuery(); const { data } = useExplorerNodeNamesQuery();
const name = useMemo(() => getNameForParty(id, data), [data, id]); const name = useMemo(() => getNameForParty(id, data), [data, id]);
const useName = name !== id; const useName = name !== id;
@@ -52,7 +44,7 @@ const PartyLink = ({
if (id === SPECIAL_CASE_NETWORK || id === SPECIAL_CASE_NETWORK_ID) { if (id === SPECIAL_CASE_NETWORK || id === SPECIAL_CASE_NETWORK_ID) {
return ( return (
<span className="font-mono" data-testid="network"> <span className="font-mono" data-testid="network">
{networkLabel} {t('Network')}
</span> </span>
); );
} }
@@ -78,11 +70,7 @@ const PartyLink = ({
{useName ? ( {useName ? (
name name
) : ( ) : (
<Hash <Hash text={truncate ? truncateMiddle(id, 4, 4) : id} />
text={
truncate ? truncateMiddle(id, truncateLength, truncateLength) : id
}
/>
)} )}
</Link> </Link>
</span> </span>
@@ -175,7 +175,6 @@ describe('Amend order details', () => {
const res = renderExistingAmend('123', 1, amend); const res = renderExistingAmend('123', 1, amend);
expect(await res.findByText('New size')).toBeInTheDocument(); expect(await res.findByText('New size')).toBeInTheDocument();
expect(await res.findByText('Size ±')).toBeInTheDocument();
}); });
it('Renders Reference if provided', async () => { it('Renders Reference if provided', async () => {
@@ -82,7 +82,7 @@ const AmendOrderDetails = ({ id, version, amend }: AmendOrderDetailsProps) => {
{amend.sizeDelta && amend.sizeDelta !== '0' ? ( {amend.sizeDelta && amend.sizeDelta !== '0' ? (
<div className="mb-12 md:mb-0"> <div className="mb-12 md:mb-0">
<h2 className="text-dark mb-4 text-2xl font-bold"> <h2 className="text-dark mb-4 text-2xl font-bold">
{t('Size ±')} {t('New size')}
</h2> </h2>
<h5 <h5
className={`mb-0 text-lg font-medium capitalize text-gray-500 ${getSideDeltaColour( className={`mb-0 text-lg font-medium capitalize text-gray-500 ${getSideDeltaColour(
@@ -93,16 +93,6 @@ const AmendOrderDetails = ({ id, version, amend }: AmendOrderDetailsProps) => {
</h5> </h5>
</div> </div>
) : null} ) : null}
{o && (
<div className="">
<h2 className="text-dark mb-4 text-2xl font-bold">
{t('New size')}
</h2>
<h5 className="mb-0 text-lg font-medium text-gray-500">
{o ? o.size : null}
</h5>
</div>
)}
{amend.price && amend.price !== '0' ? ( {amend.price && amend.price !== '0' ? (
<div className=""> <div className="">
@@ -1,4 +1,5 @@
import { type ProposalListFieldsFragment } from '@vegaprotocol/proposals'; import { type ProposalListFieldsFragment } from '@vegaprotocol/proposals';
import { VoteProgress } from '@vegaprotocol/proposals';
import { type AgGridReact } from 'ag-grid-react'; import { type AgGridReact } from 'ag-grid-react';
import { ExternalLink } from '@vegaprotocol/ui-toolkit'; import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { AgGrid } from '@vegaprotocol/datagrid'; import { AgGrid } from '@vegaprotocol/datagrid';
@@ -11,7 +12,12 @@ import { type ColDef } from 'ag-grid-community';
import type { RowClickedEvent } from 'ag-grid-community'; import type { RowClickedEvent } from 'ag-grid-community';
import { getDateTimeFormat } from '@vegaprotocol/utils'; import { getDateTimeFormat } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { ProposalStateMapping } from '@vegaprotocol/types'; import { ProposalStateMapping } from '@vegaprotocol/types';
import BigNumber from 'bignumber.js';
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment'; import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
import { BREAKPOINT_MD } from '../../config/breakpoints'; import { BREAKPOINT_MD } from '../../config/breakpoints';
import { JsonViewerDialog } from '../dialogs/json-viewer-dialog'; import { JsonViewerDialog } from '../dialogs/json-viewer-dialog';
@@ -25,7 +31,15 @@ type ProposalsTableProps = {
data: ProposalListFieldsFragment[] | null; data: ProposalListFieldsFragment[] | null;
}; };
export const ProposalsTable = ({ data }: ProposalsTableProps) => { export const ProposalsTable = ({ data }: ProposalsTableProps) => {
const { params } = useNetworkParams([
NetworkParams.governance_proposal_market_requiredMajority,
]);
const tokenLink = useLinks(DApp.Governance); const tokenLink = useLinks(DApp.Governance);
const requiredMajorityPercentage = useMemo(() => {
const requiredMajority =
params?.governance_proposal_market_requiredMajority ?? 1;
return new BigNumber(requiredMajority).times(100);
}, [params?.governance_proposal_market_requiredMajority]);
const gridRef = useRef<AgGridReact>(null); const gridRef = useRef<AgGridReact>(null);
useLayoutEffect(() => { useLayoutEffect(() => {
@@ -76,6 +90,33 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
return value ? ProposalStateMapping[value] : '-'; return value ? ProposalStateMapping[value] : '-';
}, },
}, },
{
colId: 'voting',
maxWidth: 100,
hide: window.innerWidth <= BREAKPOINT_MD,
headerName: t('Voting'),
cellRenderer: ({
data,
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
if (data) {
const yesTokens = new BigNumber(data.votes.yes.totalTokens);
const noTokens = new BigNumber(data.votes.no.totalTokens);
const totalTokensVoted = yesTokens.plus(noTokens);
const yesPercentage = totalTokensVoted.isZero()
? new BigNumber(0)
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
return (
<div className="flex h-full items-center justify-center pt-2 uppercase">
<VoteProgress
threshold={requiredMajorityPercentage}
progress={yesPercentage}
/>
</div>
);
}
return '-';
},
},
{ {
colId: 'cDate', colId: 'cDate',
maxWidth: 150, maxWidth: 150,
@@ -143,7 +184,7 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
}, },
}, },
], ],
[tokenLink] [requiredMajorityPercentage, tokenLink]
); );
return ( return (
<> <>
@@ -64,9 +64,7 @@ export const ProposalSummary = ({
return ( return (
<div className="w-auto max-w-lg border-2 border-solid border-vega-light-100 dark:border-vega-dark-200 p-5"> <div className="w-auto max-w-lg border-2 border-solid border-vega-light-100 dark:border-vega-dark-200 p-5">
{id && <ProposalStatusIcon id={id} />} {id && <ProposalStatusIcon id={id} />}
{rationale?.title && ( {rationale?.title && <h1 className="text-xl pb-1">{rationale.title}</h1>}
<h1 className="text-xl pb-1 break-all">{rationale.title}</h1>
)}
{rationale?.description && ( {rationale?.description && (
<div className="pt-2 text-sm leading-tight"> <div className="pt-2 text-sm leading-tight">
<ReactMarkdown <ReactMarkdown
@@ -1,46 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import { TxDetailsShared } from '../shared/tx-details-shared';
import { TableWithTbody } from '../../../table';
import type { components } from '../../../../../types/explorer';
import type { BlockExplorerTransactionResult } from '../../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../../routes/blocks/tendermint-blocks-response';
import { TableCell, TableRow } from '../../../table';
type Update = components['schemas']['v1UpdatePartyProfile'];
interface TxDetailsUpdatePartyProfileProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* Party profiles can be an alias and arbitrary key/values pairs.
* This component displays the alias, if any, but not the metadata. When there is
* some wider usage, we can decide how to render it. For now, it's available in the
* full TX details.
*/
export const TxDetailsUpdatePartyProfile = ({
txData,
pubKey,
blockData,
}: TxDetailsUpdatePartyProfileProps) => {
if (!txData?.command.updatePartyProfile) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const update: Update = txData.command.updatePartyProfile;
return (
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
{update.alias && (
<TableRow modifier="bordered">
<TableCell>{t('New alias')}</TableCell>
<TableCell>{update.alias}</TableCell>
</TableRow>
)}
</TableWithTbody>
);
};
@@ -1,18 +0,0 @@
query ExplorerTransferStatus($id: ID!) {
transfer(id: $id) {
transfer {
reference
timestamp
status
reason
fromAccountType
from
to
toAccountType
asset {
id
}
amount
}
}
}
@@ -1,61 +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 ExplorerTransferStatusQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
export type ExplorerTransferStatusQuery = { __typename?: 'Query', transfer?: { __typename?: 'TransferNode', transfer: { __typename?: 'Transfer', reference?: string | null, timestamp: any, status: Types.TransferStatus, reason?: string | null, fromAccountType: Types.AccountType, from: string, to: string, toAccountType: Types.AccountType, amount: string, asset?: { __typename?: 'Asset', id: string } | null } } | null };
export const ExplorerTransferStatusDocument = gql`
query ExplorerTransferStatus($id: ID!) {
transfer(id: $id) {
transfer {
reference
timestamp
status
reason
fromAccountType
from
to
toAccountType
asset {
id
}
amount
}
}
}
`;
/**
* __useExplorerTransferStatusQuery__
*
* To run a query within a React component, call `useExplorerTransferStatusQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerTransferStatusQuery` 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 } = useExplorerTransferStatusQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useExplorerTransferStatusQuery(baseOptions: Apollo.QueryHookOptions<ExplorerTransferStatusQuery, ExplorerTransferStatusQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerTransferStatusQuery, ExplorerTransferStatusQueryVariables>(ExplorerTransferStatusDocument, options);
}
export function useExplorerTransferStatusLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerTransferStatusQuery, ExplorerTransferStatusQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerTransferStatusQuery, ExplorerTransferStatusQueryVariables>(ExplorerTransferStatusDocument, options);
}
export type ExplorerTransferStatusQueryHookResult = ReturnType<typeof useExplorerTransferStatusQuery>;
export type ExplorerTransferStatusLazyQueryHookResult = ReturnType<typeof useExplorerTransferStatusLazyQuery>;
export type ExplorerTransferStatusQueryResult = Apollo.QueryResult<ExplorerTransferStatusQuery, ExplorerTransferStatusQueryVariables>;
@@ -111,7 +111,7 @@ export function TransferParticipants({
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 9" viewBox="0 0 16 9"
className="fill-white dark:fill-black" className="fill-vega-light-100 dark:fill-black"
> >
<path d="M0,0L8,9l8,-9Z" /> <path d="M0,0L8,9l8,-9Z" />
</svg> </svg>
@@ -120,7 +120,7 @@ export function TransferParticipants({
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 9" viewBox="0 0 16 9"
className="fill-vega-light-200 dark:fill-vega-dark-200" className="fill-vega-light-100 dark:fill-vega-dark-200"
> >
<path d="M0,0L8,9l8,-9Z" /> <path d="M0,0L8,9l8,-9Z" />
</svg> </svg>
@@ -1,223 +1,97 @@
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { AssetLink, MarketLink } from '../../../../links'; import { AssetLink, MarketLink } from '../../../../links';
import { headerClasses, wrapperClasses } from '../transfer-details';
import type { components } from '../../../../../../types/explorer'; import type { components } from '../../../../../../types/explorer';
import type { Recurring } from '../transfer-details'; import type { Recurring } from '../transfer-details';
import { import { DispatchMetricLabels } from '@vegaprotocol/types';
DispatchMetricLabels,
DistributionStrategy,
} from '@vegaprotocol/types';
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '@vegaprotocol/utils';
export type Metric = components['schemas']['vegaDispatchMetric']; export type Metric = components['schemas']['vegaDispatchMetric'];
export type Strategy = components['schemas']['vegaDispatchStrategy']; export type Strategy = components['schemas']['vegaDispatchStrategy'];
export const wrapperClasses = 'border pv-2 w-full flex-auto basis-full';
export const headerClasses =
'bg-solid bg-vega-light-150 dark:bg-vega-dark-150 text-center text-xl py-2 font-alpha calt';
const metricLabels: Record<Metric, string> = { const metricLabels: Record<Metric, string> = {
DISPATCH_METRIC_UNSPECIFIED: 'Unknown metric', DISPATCH_METRIC_UNSPECIFIED: 'Unknown metric',
...DispatchMetricLabels, ...DispatchMetricLabels,
}; };
// Maps the two (non-null) values of entityScope to the icon that represents it
const entityScopeIcons: Record<
string,
typeof VegaIconNames[keyof typeof VegaIconNames]
> = {
ENTITY_SCOPE_INDIVIDUALS: VegaIconNames.MAN,
ENTITY_SCOPE_TEAMS: VegaIconNames.TEAM,
};
const distributionStrategyLabel: Record<DistributionStrategy, string> = {
[DistributionStrategy.DISTRIBUTION_STRATEGY_PRO_RATA]: 'Pro Rata',
[DistributionStrategy.DISTRIBUTION_STRATEGY_RANK]: 'Ranked',
};
interface TransferRewardsProps { interface TransferRewardsProps {
recurring: Recurring; recurring: Recurring;
} }
/** /**
* Renders recurring transfers/game details in a way that is, perhaps, easy to understand * Renderer for a transfer. These can vary quite
* widely, essentially every field can be null.
* *
* @param transfer A recurring transfer object * @param transfer A recurring transfer object
*/ */
export function TransferRewards({ recurring }: TransferRewardsProps) { export function TransferRewards({ recurring }: TransferRewardsProps) {
const metric =
recurring?.dispatchStrategy?.metric || 'DISPATCH_METRIC_UNSPECIFIED';
if (!recurring || !recurring.dispatchStrategy) { if (!recurring || !recurring.dispatchStrategy) {
return null; return null;
} }
// Destructure to make things a bit more readable
const {
entityScope,
individualScope,
teamScope,
distributionStrategy,
lockPeriod,
markets,
stakingRequirement,
windowLength,
notionalTimeWeightedAveragePositionRequirement,
rankTable,
nTopPerformers,
} = recurring.dispatchStrategy;
return ( return (
<div className={wrapperClasses}> <div className={wrapperClasses}>
<h2 className={headerClasses}>{getRewardTitle(entityScope)}</h2> <h2 className={headerClasses}>{t('Reward metrics')}</h2>
<ul className="relative block rounded-lg py-6 text-left p-6"> <ul className="relative block rounded-lg py-6 text-center p-6">
{entityScope && entityScopeIcons[entityScope] ? ( {recurring.dispatchStrategy.assetForMetric ? (
<li> <li>
<strong>{t('Scope')}</strong>:{' '} <strong>{t('Asset')}</strong>:{' '}
<VegaIcon name={entityScopeIcons[entityScope]} /> <AssetLink assetId={recurring.dispatchStrategy.assetForMetric} />
&nbsp;
{individualScope ? individualScopeLabels[individualScope] : null}
{getScopeLabel(entityScope, teamScope)}
</li> </li>
) : null} ) : null}
{recurring.dispatchStrategy && <li>
recurring.dispatchStrategy.assetForMetric && ( <strong>{t('Metric')}</strong>: {metricLabels[metric]}
<li> </li>
<strong>{t('Asset for metric')}</strong>:{' '} {recurring.dispatchStrategy.markets &&
<AssetLink assetId={recurring.dispatchStrategy.assetForMetric} /> recurring.dispatchStrategy.markets.length > 0 ? (
</li>
)}
{recurring.dispatchStrategy.metric &&
metricLabels[recurring.dispatchStrategy.metric] && (
<li>
<strong>{t('Metric')}</strong>:{' '}
{metricLabels[recurring.dispatchStrategy.metric]}
</li>
)}
{lockPeriod && (
<li>
<strong>{t('Reward lock')}</strong>:&nbsp;
{recurring.dispatchStrategy.lockPeriod}{' '}
{recurring.dispatchStrategy.lockPeriod === '1'
? t('epoch')
: t('epochs')}
</li>
)}
{markets && markets.length > 0 ? (
<li> <li>
<strong>{t('Markets in scope')}</strong>: <strong>{t('Markets in scope')}</strong>:
<ul className="inline-block ml-1"> <ul>
{markets.map((m) => ( {recurring.dispatchStrategy.markets.map((m) => (
<li key={m} className="inline-block mr-2"> <li key={m}>
<MarketLink id={m} /> <MarketLink id={m} />
</li> </li>
))} ))}
</ul> </ul>
</li> </li>
) : null} ) : null}
<li>
{stakingRequirement && stakingRequirement !== '0' ? ( <strong>{t('Factor')}</strong>: {recurring.factor}
<li> </li>
<strong>{t('Staking requirement')}</strong>: {stakingRequirement}
</li>
) : null}
{windowLength && windowLength !== '0' ? (
<li>
<strong>{t('Window length')}</strong>:{' '}
{recurring.dispatchStrategy.windowLength}{' '}
{recurring.dispatchStrategy.windowLength === '1'
? t('epoch')
: t('epochs')}
</li>
) : null}
{notionalTimeWeightedAveragePositionRequirement &&
notionalTimeWeightedAveragePositionRequirement !== '' ? (
<li>
<strong>{t('Notional TWAP')}</strong>:{' '}
{notionalTimeWeightedAveragePositionRequirement}
</li>
) : null}
{nTopPerformers && (
<li>
<strong>{t('Elligible team members:')}</strong> top{' '}
{`${formatNumber(Number(nTopPerformers) * 100, 0)}%`}
</li>
)}
{distributionStrategy &&
distributionStrategy !== 'DISTRIBUTION_STRATEGY_UNSPECIFIED' && (
<li>
<strong>{t('Distribution strategy')}</strong>:{' '}
{distributionStrategyLabel[distributionStrategy]}
</li>
)}
</ul> </ul>
<div className="px-6 pt-1 pb-5">
{rankTable && rankTable.length > 0 ? (
<table className="border-collapse border border-gray-400 ">
<thead>
<tr>
<th className="border border-gray-300 bg-gray-300 px-3">
<strong>{t('Start rank')}</strong>
</th>
<th className="border border-gray-300 bg-gray-300 px-3">
<strong>{t('Share of reward pool')}</strong>
</th>
</tr>
</thead>
<tbody>
{rankTable.map((row, i) => {
return (
<tr key={`rank-${i}`}>
<td className="border border-slate-300 text-center">
{row.startRank}
</td>
<td className="border border-slate-300 text-center">
{row.shareRatio}
</td>
</tr>
);
})}
</tbody>
</table>
) : null}
</div>
</div> </div>
); );
} }
export function getScopeLabel( interface TransferRecurringStrategyProps {
scope: components['schemas']['vegaEntityScope'] | undefined, strategy: Strategy;
teamScope: readonly string[] | undefined
): string {
if (scope === 'ENTITY_SCOPE_TEAMS') {
if (teamScope && teamScope.length !== 0) {
return ` ${teamScope.length} teams`;
} else {
return t('All teams');
}
} else if (scope === 'ENTITY_SCOPE_INDIVIDUALS') {
return t('Individuals');
} else {
return '';
}
}
export function getRewardTitle(
scope?: components['schemas']['vegaEntityScope']
) {
if (scope === 'ENTITY_SCOPE_TEAMS') {
return t('Game');
}
return t('Reward metrics');
} }
const individualScopeLabels: Record< /**
components['schemas']['vegaIndividualScope'], * Simple renderer for a dispatch strategy in a recurring transfer
string *
> = { * @param strategy Dispatch strategy object
// Unspecified and All are not rendered */
INDIVIDUAL_SCOPE_UNSPECIFIED: '', export function TransferRecurringStrategy({
INDIVIDUAL_SCOPE_ALL: '', strategy,
INDIVIDUAL_SCOPE_IN_TEAM: '(in team)', }: TransferRecurringStrategyProps) {
INDIVIDUAL_SCOPE_NOT_IN_TEAM: '(not in team)', if (!strategy) {
}; return null;
}
return (
<>
{strategy.assetForMetric ? (
<li>
<strong>{t('Asset for metric')}</strong>:{' '}
<AssetLink assetId={strategy.assetForMetric} />
</li>
) : null}
<li>
<strong>{t('Metric')}</strong>: {strategy.metric}
</li>
</>
);
}
@@ -1,100 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import { headerClasses, wrapperClasses } from '../transfer-details';
import { Icon, Loader } from '@vegaprotocol/ui-toolkit';
import type { IconName } from '@vegaprotocol/ui-toolkit';
import type { ApolloError } from '@apollo/client';
import { TransferStatus, TransferStatusMapping } from '@vegaprotocol/types';
import { IconNames } from '@blueprintjs/icons';
interface TransferStatusProps {
status: TransferStatus | undefined;
error: ApolloError | undefined;
loading: boolean;
}
/**
* Renderer for a transfer. These can vary quite
* widely, essentially every field can be null.
*
* @param transfer A recurring transfer object
*/
export function TransferStatusView({ status, loading }: TransferStatusProps) {
if (!status) {
status = TransferStatus.STATUS_PENDING;
}
return (
<div className={wrapperClasses}>
<h2 className={headerClasses}>{t('Status')}</h2>
<div className="relative block rounded-lg py-6 text-center p-6">
{loading ? (
<div className="leading-10 mt-12">
<Loader size={'small'} />
</div>
) : (
<>
<p className="leading-10 my-2">
<TransferStatusIcon status={status} />
</p>
<p className="leading-10 my-2">{TransferStatusMapping[status]}</p>
</>
)}
</div>
</div>
);
}
interface TransferStatusIconProps {
status: TransferStatus;
}
export function TransferStatusIcon({ status }: TransferStatusIconProps) {
return (
<span title={TransferStatusMapping[status]}>
<Icon
name={getIconForStatus(status)}
className={getColourForStatus(status)}
/>
</span>
);
}
/**
* Simple mapping from status to icon name
* @param status TransferStatus
* @returns IconName
*/
export function getIconForStatus(status: TransferStatus): IconName {
switch (status) {
case TransferStatus.STATUS_PENDING:
return IconNames.TIME;
case TransferStatus.STATUS_DONE:
return IconNames.TICK;
case TransferStatus.STATUS_REJECTED:
return IconNames.CROSS;
case TransferStatus.STATUS_CANCELLED:
return IconNames.CROSS;
default:
return IconNames.TIME;
}
}
/**
* Simple mapping from status to colour
* @param status TransferStatus
* @returns string Tailwind classname
*/
export function getColourForStatus(status: TransferStatus): string {
switch (status) {
case TransferStatus.STATUS_PENDING:
return 'text-yellow-500';
case TransferStatus.STATUS_DONE:
return 'text-green-500';
case TransferStatus.STATUS_REJECTED:
return 'text-red-500';
case TransferStatus.STATUS_CANCELLED:
return 'text-red-600';
default:
return 'text-yellow-500';
}
}
@@ -2,15 +2,12 @@ import type { components } from '../../../../../types/explorer';
import { TransferRepeat } from './blocks/transfer-repeat'; import { TransferRepeat } from './blocks/transfer-repeat';
import { TransferRewards } from './blocks/transfer-rewards'; import { TransferRewards } from './blocks/transfer-rewards';
import { TransferParticipants } from './blocks/transfer-participants'; import { TransferParticipants } from './blocks/transfer-participants';
import { useExplorerTransferStatusQuery } from './__generated__/Transfer';
import { TransferStatusView } from './blocks/transfer-status';
import { TransferStatus } from '@vegaprotocol/types';
export type Recurring = components['schemas']['commandsv1RecurringTransfer']; export type Recurring = components['schemas']['commandsv1RecurringTransfer'];
export type Metric = components['schemas']['vegaDispatchMetric']; export type Metric = components['schemas']['vegaDispatchMetric'];
export const wrapperClasses = export const wrapperClasses =
'border border-vega-light-150 dark:border-vega-dark-200 pv-2 w-full sm:w-1/3 basis-1/3'; 'border border-vega-light-150 dark:border-vega-dark-200 rounded-md pv-2 mb-5 w-full sm:w-1/4 min-w-[200px] ';
export const headerClasses = export const headerClasses =
'bg-solid bg-vega-light-150 dark:bg-vega-dark-150 border-vega-light-150 text-center text-xl py-2 font-alpha calt'; 'bg-solid bg-vega-light-150 dark:bg-vega-dark-150 border-vega-light-150 text-center text-xl py-2 font-alpha calt';
@@ -19,7 +16,6 @@ export type Transfer = components['schemas']['commandsv1Transfer'];
interface TransferDetailsProps { interface TransferDetailsProps {
transfer: Transfer; transfer: Transfer;
from: string; from: string;
id: string;
} }
/** /**
@@ -28,24 +24,13 @@ interface TransferDetailsProps {
* *
* @param transfer A recurring transfer object * @param transfer A recurring transfer object
*/ */
export function TransferDetails({ transfer, from, id }: TransferDetailsProps) { export function TransferDetails({ transfer, from }: TransferDetailsProps) {
const recurring = transfer.recurring; const recurring = transfer.recurring;
// Currently all this is passed in to TransferStatus, but the extra details
// may be useful in the future.
const { data, error, loading } = useExplorerTransferStatusQuery({
variables: { id },
});
const status = error
? TransferStatus.STATUS_REJECTED
: data?.transfer?.transfer.status;
return ( return (
<div className="flex flex-wrap"> <div className="flex gap-5 flex-wrap">
<TransferParticipants from={from} transfer={transfer} /> <TransferParticipants from={from} transfer={transfer} />
{recurring ? <TransferRepeat recurring={transfer.recurring} /> : null} {recurring ? <TransferRepeat recurring={transfer.recurring} /> : null}
<TransferStatusView status={status} error={error} loading={loading} />
{recurring && recurring.dispatchStrategy ? ( {recurring && recurring.dispatchStrategy ? (
<TransferRewards recurring={transfer.recurring} /> <TransferRewards recurring={transfer.recurring} />
) : null} ) : null}
@@ -1,172 +0,0 @@
import {
getScopeLabel,
getRewardTitle,
TransferRewards,
} from './blocks/transfer-rewards';
import { render } from '@testing-library/react';
import type { components } from '../../../../../types/explorer';
import type { Recurring } from './transfer-details';
import {
DispatchMetric,
DistributionStrategy,
EntityScope,
IndividualScope,
} from '@vegaprotocol/types';
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
describe('getScopeLabel', () => {
it('should return the correct label for ENTITY_SCOPE_TEAMS with teamScope', () => {
const scope = 'ENTITY_SCOPE_TEAMS';
const teamScope = ['team1', 'team2', 'team3'];
const expectedLabel = ' 3 teams';
const result = getScopeLabel(scope, teamScope);
expect(result).toEqual(expectedLabel);
});
it('should return the correct label for ENTITY_SCOPE_TEAMS without teamScope', () => {
const scope = 'ENTITY_SCOPE_TEAMS';
const teamScope = undefined;
const expectedLabel = 'All teams';
const result = getScopeLabel(scope, teamScope);
expect(result).toEqual(expectedLabel);
});
it('should return the correct label for ENTITY_SCOPE_INDIVIDUALS', () => {
const scope = 'ENTITY_SCOPE_INDIVIDUALS';
const teamScope = undefined;
const expectedLabel = 'Individuals';
const result = getScopeLabel(scope, teamScope);
expect(result).toEqual(expectedLabel);
});
it('should return an empty string for unknown scope', () => {
const scope = 'UNKNOWN_SCOPE';
const teamScope = undefined;
const expectedLabel = '';
const result = getScopeLabel(
scope as unknown as components['schemas']['vegaEntityScope'],
teamScope
);
expect(result).toEqual(expectedLabel);
});
});
describe('getRewardTitle', () => {
it('should return the correct title for ENTITY_SCOPE_TEAMS', () => {
const scope = 'ENTITY_SCOPE_TEAMS';
const expectedTitle = 'Game';
const result = getRewardTitle(scope);
expect(result).toEqual(expectedTitle);
});
it('should return the correct title for other scopes', () => {
const scope = 'ENTITY_SCOPE_INDIVIDUALS';
const expectedTitle = 'Reward metrics';
const result = getRewardTitle(scope);
expect(result).toEqual(expectedTitle);
});
});
describe('TransferRewards', () => {
it('should render nothing if recurring dispatchStrategy is not provided', () => {
const { container } = render(
<TransferRewards recurring={null as unknown as Recurring} />
);
expect(container.firstChild).toBeNull();
});
it('should render nothing if recurring.dispatchStrategy is not provided', () => {
const { container } = render(
<TransferRewards recurring={{} as unknown as Recurring} />
);
expect(container.firstChild).toBeNull();
});
it('should render the reward details correctly', () => {
const recurring = {
dispatchStrategy: {
metric: DispatchMetric.DISPATCH_METRIC_AVERAGE_POSITION,
assetForMetric: '123',
entityScope: EntityScope.ENTITY_SCOPE_TEAMS,
individualScope: IndividualScope.INDIVIDUAL_SCOPE_IN_TEAM,
teamScope: [],
distributionStrategy:
DistributionStrategy.DISTRIBUTION_STRATEGY_PRO_RATA,
lockPeriod: 'lockPeriod',
markets: ['market1', 'market2'],
stakingRequirement: '1',
windowLength: 'windowLength',
notionalTimeWeightedAveragePositionRequirement:
'notionalTimeWeightedAveragePositionRequirement',
rankTable: [
{ startRank: 1, shareRatio: 0.2 },
{ startRank: 2, shareRatio: 0.3 },
],
nTopPerformers: 'nTopPerformers',
},
};
const { getByText } = render(
<MemoryRouter>
<MockedProvider>
<TransferRewards recurring={recurring} />
</MockedProvider>
</MemoryRouter>
);
expect(getByText('Game')).toBeInTheDocument();
expect(getByText('Scope')).toBeInTheDocument();
expect(getByText('Asset for metric')).toBeInTheDocument();
expect(getByText('Metric')).toBeInTheDocument();
expect(getByText('Reward lock')).toBeInTheDocument();
expect(getByText('Markets in scope')).toBeInTheDocument();
expect(getByText('Staking requirement')).toBeInTheDocument();
expect(getByText('Window length')).toBeInTheDocument();
expect(getByText('Notional TWAP')).toBeInTheDocument();
expect(getByText('Elligible team members:')).toBeInTheDocument();
expect(getByText('Distribution strategy')).toBeInTheDocument();
expect(getByText('Start rank')).toBeInTheDocument();
expect(getByText('Share of reward pool')).toBeInTheDocument();
});
it('should not render a rank table if recurring.dispatchStrategy.rankTable is not provided', () => {
const recurring = {
dispatchStrategy: {
entityScope: EntityScope.ENTITY_SCOPE_INDIVIDUALS,
individualScope: IndividualScope.INDIVIDUAL_SCOPE_ALL,
teamScope: ['team1', 'team2', 'team3'],
distributionStrategy:
DistributionStrategy.DISTRIBUTION_STRATEGY_PRO_RATA,
lockPeriod: 'lockPeriod',
markets: ['market1', 'market2'],
stakingRequirement: 'stakingRequirement',
windowLength: 'windowLength',
notionalTimeWeightedAveragePositionRequirement:
'notionalTimeWeightedAveragePositionRequirement',
nTopPerformers: 'nTopPerformers',
},
};
const { container } = render(
<MemoryRouter>
<MockedProvider>
<TransferRewards recurring={recurring} />
</MockedProvider>
</MemoryRouter>
);
expect(container.querySelector('table')).toBeNull();
});
});
@@ -34,7 +34,6 @@ import { TxDetailsUpdateReferralSet } from './tx-update-referral-set';
import { TxDetailsJoinTeam } from './tx-join-team'; import { TxDetailsJoinTeam } from './tx-join-team';
import { TxDetailsUpdateMarginMode } from './tx-update-margin-mode'; import { TxDetailsUpdateMarginMode } from './tx-update-margin-mode';
import { TxBatchProposal } from './tx-batch-proposal'; import { TxBatchProposal } from './tx-batch-proposal';
import { TxDetailsUpdatePartyProfile } from './proposal/tx-update-party-profile';
interface TxDetailsWrapperProps { interface TxDetailsWrapperProps {
txData: BlockExplorerTransactionResult | undefined; txData: BlockExplorerTransactionResult | undefined;
@@ -140,8 +139,6 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
return TxDetailsUpdateMarginMode; return TxDetailsUpdateMarginMode;
case 'Batch Proposal': case 'Batch Proposal':
return TxBatchProposal; return TxBatchProposal;
case 'Update Party Profile':
return TxDetailsUpdatePartyProfile;
default: default:
return TxDetailsGeneric; return TxDetailsGeneric;
} }
@@ -12,8 +12,6 @@ import { ProposalSignatureBundleNewAsset } from './proposal/signature-bundle-new
import { ProposalSignatureBundleUpdateAsset } from './proposal/signature-bundle-update'; import { ProposalSignatureBundleUpdateAsset } from './proposal/signature-bundle-update';
import { MarketLink } from '../../links'; import { MarketLink } from '../../links';
import { formatNumber } from '@vegaprotocol/utils'; import { formatNumber } from '@vegaprotocol/utils';
import { TransferDetails } from './transfer/transfer-details';
import { proposalToTransfer } from '../lib/proposal-to-transfer';
export type Proposal = components['schemas']['v1ProposalSubmission']; export type Proposal = components['schemas']['v1ProposalSubmission'];
export type ProposalTerms = components['schemas']['vegaProposalTerms']; export type ProposalTerms = components['schemas']['vegaProposalTerms'];
@@ -106,12 +104,6 @@ export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => {
? ProposalSignatureBundleNewAsset ? ProposalSignatureBundleNewAsset
: ProposalSignatureBundleUpdateAsset; : ProposalSignatureBundleUpdateAsset;
let transfer, from;
if (proposal.terms?.newTransfer?.changes) {
transfer = proposalToTransfer(proposal.terms?.newTransfer.changes);
from = proposal.terms.newTransfer.changes.source;
}
return ( return (
<> <>
<TableWithTbody className="mb-8" allowWrap={true}> <TableWithTbody className="mb-8" allowWrap={true}>
@@ -157,26 +149,14 @@ export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => {
</> </>
) : null} ) : null}
</TableWithTbody> </TableWithTbody>
<ProposalSummary <ProposalSummary
id={deterministicId} id={deterministicId}
rationale={proposal.rationale} rationale={proposal.rationale}
terms={proposal?.terms} terms={proposal?.terms}
/> />
{proposalRequiresSignatureBundle(proposal) && ( {proposalRequiresSignatureBundle(proposal) && (
<SignatureBundleComponent id={deterministicId} tx={tx} /> <SignatureBundleComponent id={deterministicId} tx={tx} />
)} )}
{transfer && (
<div className="mt-8">
<TransferDetails
transfer={transfer}
from={from || ''}
id={deterministicId}
/>
</div>
)}
</> </>
); );
}; };
@@ -13,7 +13,6 @@ import {
SPECIAL_CASE_NETWORK_ID, SPECIAL_CASE_NETWORK_ID,
} from '../../links/party-link/party-link'; } from '../../links/party-link/party-link';
import { txSignatureToDeterministicId } from '../lib/deterministic-ids'; import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
import Hash from '../../links/hash';
type Transfer = components['schemas']['commandsv1Transfer']; type Transfer = components['schemas']['commandsv1Transfer'];
@@ -61,7 +60,7 @@ export const TxDetailsTransfer = ({
} }
const from = txData.submitter; const from = txData.submitter;
const id = txSignatureToDeterministicId(txData.signature.value);
return ( return (
<> <>
<TableWithTbody className="mb-8" allowWrap={true}> <TableWithTbody className="mb-8" allowWrap={true}>
@@ -72,7 +71,7 @@ export const TxDetailsTransfer = ({
<TableRow modifier="bordered" data-testid="id"> <TableRow modifier="bordered" data-testid="id">
<TableCell {...sharedHeaderProps}>{t('Transfer ID')}</TableCell> <TableCell {...sharedHeaderProps}>{t('Transfer ID')}</TableCell>
<TableCell> <TableCell>
<Hash text={id} /> {txSignatureToDeterministicId(txData.signature.value)}
</TableCell> </TableCell>
</TableRow> </TableRow>
<TxDetailsShared <TxDetailsShared
@@ -106,7 +105,7 @@ export const TxDetailsTransfer = ({
</TableRow> </TableRow>
) : null} ) : null}
</TableWithTbody> </TableWithTbody>
<TransferDetails from={from} transfer={transfer} id={id} /> <TransferDetails from={from} transfer={transfer} />
</> </>
); );
}; };
@@ -1,29 +0,0 @@
import type { components } from '../../../../types/explorer';
type TransferProposal = components['schemas']['vegaNewTransferConfiguration'];
type ActualTransfer = components['schemas']['commandsv1Transfer'];
/**
* Converts a governance proposal for a transfer in to a transfer command that the
* TransferDetails component can then render. The types are very similar, but do not
* map precisely to each other due to some missing fields and some different field
* names.
*
* @param proposal Governance proposal for a transfer
* @returns transfer a Transfer object as if it had been submitted
*/
export function proposalToTransfer(proposal: TransferProposal): ActualTransfer {
return {
amount: proposal.amount,
asset: proposal.asset,
// On a transfer, 'from' is determined by the submitter, so there is no 'from' field
// fromAccountType does exist and is just named differently on the proposal
fromAccountType: proposal.sourceType,
oneOff: proposal.oneOff,
recurring: proposal.recurring,
// There is no reference applied on governance initiated transfers
reference: '',
to: proposal.destination,
toAccountType: proposal.destinationType,
};
}
@@ -44,7 +44,6 @@ export type FilterOption =
| 'Submit Order' | 'Submit Order'
| 'Transfer Funds' | 'Transfer Funds'
| 'Undelegate' | 'Undelegate'
| 'Update Party Profile'
| 'Update Referral Set' | 'Update Referral Set'
| 'Update Margin Mode' | 'Update Margin Mode'
| 'Validator Heartbeat' | 'Validator Heartbeat'
@@ -80,7 +79,6 @@ export const filterOptions: Record<string, FilterOption[]> = {
'Apply Referral Code', 'Apply Referral Code',
'Create Referral Set', 'Create Referral Set',
'Join Team', 'Join Team',
'Update Party Profile',
'Update Referral Set', 'Update Referral Set',
], ],
'External Data': ['Chain Event', 'Submit Oracle Data'], 'External Data': ['Chain Event', 'Submit Oracle Data'],
@@ -12,5 +12,4 @@ export const Routes = {
ORACLES: 'oracles', ORACLES: 'oracles',
NETWORK_PARAMETERS: 'network-parameters', NETWORK_PARAMETERS: 'network-parameters',
DISCLAIMER: 'disclaimer', DISCLAIMER: 'disclaimer',
TREASURY: 'treasury',
}; };
@@ -30,7 +30,6 @@ import { PartyAccountsByAsset } from './parties/id/accounts';
import { Disclaimer } from './pages/disclaimer'; import { Disclaimer } from './pages/disclaimer';
import { useFeatureFlags } from '@vegaprotocol/environment'; import { useFeatureFlags } from '@vegaprotocol/environment';
import RestrictedPage from './restricted'; import RestrictedPage from './restricted';
import { NetworkTreasury } from './treasury';
export type Navigable = { export type Navigable = {
path: string; path: string;
@@ -230,17 +229,6 @@ export const useRouterConfig = () => {
] ]
: []; : [];
const treasuryRoutes: Route[] = [
{
path: Routes.TREASURY,
handle: {
name: t('Treasury'),
text: t('Treasury'),
breadcrumb: () => <Link to={Routes.TREASURY}>{t('Treasury')}</Link>,
},
element: <NetworkTreasury />,
},
];
const validators: Route[] = featureFlags.EXPLORER_VALIDATORS const validators: Route[] = featureFlags.EXPLORER_VALIDATORS
? [ ? [
{ {
@@ -370,7 +358,6 @@ export const useRouterConfig = () => {
...marketsRoutes, ...marketsRoutes,
...networkParametersRoutes, ...networkParametersRoutes,
...validators, ...validators,
...treasuryRoutes,
], ],
}, },
{ {
@@ -1,12 +0,0 @@
query ExplorerTreasury {
assetsConnection(pagination: { last: 1000 }) {
edges {
node {
id
networkTreasuryAccount {
balance
}
}
}
}
}
@@ -1,44 +0,0 @@
query ExplorerTreasuryTransfers {
transfersConnection(
partyId: "network"
direction: ToOrFrom
pagination: { last: 200 }
) {
pageInfo {
hasNextPage
}
edges {
node {
transfer {
timestamp
from
amount
to
status
reason
toAccountType
fromAccountType
asset {
id
}
id
status
kind {
... on OneOffTransfer {
deliverOn
}
... on RecurringTransfer {
startEpoch
}
... on OneOffGovernanceTransfer {
deliverOn
}
... on RecurringGovernanceTransfer {
endEpoch
}
}
}
}
}
}
}
@@ -1,52 +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 ExplorerTreasuryQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type ExplorerTreasuryQuery = { __typename?: 'Query', assetsConnection?: { __typename?: 'AssetsConnection', edges?: Array<{ __typename?: 'AssetEdge', node: { __typename?: 'Asset', id: string, networkTreasuryAccount?: { __typename?: 'AccountBalance', balance: string } | null } } | null> | null } | null };
export const ExplorerTreasuryDocument = gql`
query ExplorerTreasury {
assetsConnection(pagination: {last: 1000}) {
edges {
node {
id
networkTreasuryAccount {
balance
}
}
}
}
}
`;
/**
* __useExplorerTreasuryQuery__
*
* To run a query within a React component, call `useExplorerTreasuryQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerTreasuryQuery` 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 } = useExplorerTreasuryQuery({
* variables: {
* },
* });
*/
export function useExplorerTreasuryQuery(baseOptions?: Apollo.QueryHookOptions<ExplorerTreasuryQuery, ExplorerTreasuryQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerTreasuryQuery, ExplorerTreasuryQueryVariables>(ExplorerTreasuryDocument, options);
}
export function useExplorerTreasuryLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerTreasuryQuery, ExplorerTreasuryQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerTreasuryQuery, ExplorerTreasuryQueryVariables>(ExplorerTreasuryDocument, options);
}
export type ExplorerTreasuryQueryHookResult = ReturnType<typeof useExplorerTreasuryQuery>;
export type ExplorerTreasuryLazyQueryHookResult = ReturnType<typeof useExplorerTreasuryLazyQuery>;
export type ExplorerTreasuryQueryResult = Apollo.QueryResult<ExplorerTreasuryQuery, ExplorerTreasuryQueryVariables>;
@@ -1,84 +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 ExplorerTreasuryTransfersQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type ExplorerTreasuryTransfersQuery = { __typename?: 'Query', transfersConnection?: { __typename?: 'TransferConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean }, edges?: Array<{ __typename?: 'TransferEdge', node: { __typename?: 'TransferNode', transfer: { __typename?: 'Transfer', timestamp: any, from: string, amount: string, to: string, status: Types.TransferStatus, reason?: string | null, toAccountType: Types.AccountType, fromAccountType: Types.AccountType, id: string, asset?: { __typename?: 'Asset', id: string } | null, kind: { __typename?: 'OneOffGovernanceTransfer', deliverOn?: any | null } | { __typename?: 'OneOffTransfer', deliverOn?: any | null } | { __typename?: 'RecurringGovernanceTransfer', endEpoch?: number | null } | { __typename?: 'RecurringTransfer', startEpoch: number } } } } | null> | null } | null };
export const ExplorerTreasuryTransfersDocument = gql`
query ExplorerTreasuryTransfers {
transfersConnection(
partyId: "network"
direction: ToOrFrom
pagination: {last: 200}
) {
pageInfo {
hasNextPage
}
edges {
node {
transfer {
timestamp
from
amount
to
status
reason
toAccountType
fromAccountType
asset {
id
}
id
status
kind {
... on OneOffTransfer {
deliverOn
}
... on RecurringTransfer {
startEpoch
}
... on OneOffGovernanceTransfer {
deliverOn
}
... on RecurringGovernanceTransfer {
endEpoch
}
}
}
}
}
}
}
`;
/**
* __useExplorerTreasuryTransfersQuery__
*
* To run a query within a React component, call `useExplorerTreasuryTransfersQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerTreasuryTransfersQuery` 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 } = useExplorerTreasuryTransfersQuery({
* variables: {
* },
* });
*/
export function useExplorerTreasuryTransfersQuery(baseOptions?: Apollo.QueryHookOptions<ExplorerTreasuryTransfersQuery, ExplorerTreasuryTransfersQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerTreasuryTransfersQuery, ExplorerTreasuryTransfersQueryVariables>(ExplorerTreasuryTransfersDocument, options);
}
export function useExplorerTreasuryTransfersLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerTreasuryTransfersQuery, ExplorerTreasuryTransfersQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerTreasuryTransfersQuery, ExplorerTreasuryTransfersQueryVariables>(ExplorerTreasuryTransfersDocument, options);
}
export type ExplorerTreasuryTransfersQueryHookResult = ReturnType<typeof useExplorerTreasuryTransfersQuery>;
export type ExplorerTreasuryTransfersLazyQueryHookResult = ReturnType<typeof useExplorerTreasuryTransfersLazyQuery>;
export type ExplorerTreasuryTransfersQueryResult = Apollo.QueryResult<ExplorerTreasuryTransfersQuery, ExplorerTreasuryTransfersQueryVariables>;
@@ -1,37 +0,0 @@
// NOTE: These are a temporary measure, pulled from an old branch on console.
import { IconNames } from '@blueprintjs/icons';
import { Icon } from '@vegaprotocol/ui-toolkit';
import { USDc } from './usdc';
import { Vega } from './vega';
import { USDt } from './usdt';
export interface AssetIconProps {
symbol: string;
}
/**
* A poorly implemented, limited support for asset icons.
*
* These are committed as 'deprecated' to discourage use outside the Treasury page. Rather
* than use this, a better approach would be to use source contract addresses to match assets.
* This will be done separately.
*
* @deprecated
*/
export function AssetIcon({ symbol }: AssetIconProps) {
const s = symbol.toLowerCase();
switch (s) {
case 'a4a16e250a09a86061ec83c2f9466fc9dc33d332f86876ee74b6f128a5cd6710': // mainnet
case 'c9fe6fc24fce121b2cc72680543a886055abb560043fda394ba5376203b7527d': // mainnet
return <USDc size={32} />;
case 'd1984e3d365faa05bcafbe41f50f90e3663ee7c0da22bb1e24b164e9532691b2': // mainnet
case 'fc7fd956078fb1fc9db5c19b88f0874c4299b2a7639ad05a47a28c0aef291b55': // testnet
return <Vega size={32} />;
case 'bf1e88d19db4b3ca0d1d5bdb73718a01686b18cf731ca26adedf3c8b83802bba': // mainnet
case 'ede4076aef07fd79502d14326c54ab3911558371baaf697a19d077f4f89de399': // testnet
return <USDt size={32} />;
default:
return <Icon name={IconNames.BANK_ACCOUNT} size={8} />;
}
}
@@ -1,24 +0,0 @@
/**
* See note in index.tsx. This component is intended as a placeholder for a
* better, more generic solution.
*
* @deprecated
*/
export const USDc = ({ size = 16 }: { size?: number }) => {
return (
<svg width={size} height={size} viewBox="0 0 2000 2000">
<path
d="M1000 2000c554.17 0 1000-445.83 1000-1000S1554.17 0 1000 0 0 445.83 0 1000s445.83 1000 1000 1000z"
fill="#2775ca"
/>
<path
d="M1275 1158.33c0-145.83-87.5-195.83-262.5-216.66-125-16.67-150-50-150-108.34s41.67-95.83 125-95.83c75 0 116.67 25 137.5 87.5 4.17 12.5 16.67 20.83 29.17 20.83h66.66c16.67 0 29.17-12.5 29.17-29.16v-4.17c-16.67-91.67-91.67-162.5-187.5-170.83v-100c0-16.67-12.5-29.17-33.33-33.34h-62.5c-16.67 0-29.17 12.5-33.34 33.34v95.83c-125 16.67-204.16 100-204.16 204.17 0 137.5 83.33 191.66 258.33 212.5 116.67 20.83 154.17 45.83 154.17 112.5s-58.34 112.5-137.5 112.5c-108.34 0-145.84-45.84-158.34-108.34-4.16-16.66-16.66-25-29.16-25h-70.84c-16.66 0-29.16 12.5-29.16 29.17v4.17c16.66 104.16 83.33 179.16 220.83 200v100c0 16.66 12.5 29.16 33.33 33.33h62.5c16.67 0 29.17-12.5 33.34-33.33v-100c125-20.84 208.33-108.34 208.33-220.84z"
fill="#fff"
/>
<path
d="M787.5 1595.83c-325-116.66-491.67-479.16-370.83-800 62.5-175 200-308.33 370.83-370.83 16.67-8.33 25-20.83 25-41.67V325c0-16.67-8.33-29.17-25-33.33-4.17 0-12.5 0-16.67 4.16-395.83 125-612.5 545.84-487.5 941.67 75 233.33 254.17 412.5 487.5 487.5 16.67 8.33 33.34 0 37.5-16.67 4.17-4.16 4.17-8.33 4.17-16.66v-58.34c0-12.5-12.5-29.16-25-37.5zM1229.17 295.83c-16.67-8.33-33.34 0-37.5 16.67-4.17 4.17-4.17 8.33-4.17 16.67v58.33c0 16.67 12.5 33.33 25 41.67 325 116.66 491.67 479.16 370.83 800-62.5 175-200 308.33-370.83 370.83-16.67 8.33-25 20.83-25 41.67V1700c0 16.67 8.33 29.17 25 33.33 4.17 0 12.5 0 16.67-4.16 395.83-125 612.5-545.84 487.5-941.67-75-237.5-258.34-416.67-487.5-491.67z"
fill="#fff"
/>
</svg>
);
};
@@ -1,20 +0,0 @@
/**
* See note in index.tsx. This component is intended as a placeholder for a
* better, more generic solution.
*
* @deprecated
*/
export const USDt = ({ size = 16 }: { size?: number }) => {
return (
<svg width={size} height={size} viewBox="0 0 339.43 295.27">
<path
fill="#50af95"
d="M62.15,1.45l-61.89,130a2.52,2.52,0,0,0,.54,2.94L167.95,294.56a2.55,2.55,0,0,0,3.53,0L338.63,134.4a2.52,2.52,0,0,0,.54-2.94l-61.89-130A2.5,2.5,0,0,0,275,0H64.45a2.5,2.5,0,0,0-2.3,1.45h0Z"
/>
<path
fill="#fff"
d="M191.19,144.8v0c-1.2.09-7.4,0.46-21.23,0.46-11,0-18.81-.33-21.55-0.46v0c-42.51-1.87-74.24-9.27-74.24-18.13s31.73-16.25,74.24-18.15v28.91c2.78,0.2,10.74.67,21.74,0.67,13.2,0,19.81-.55,21-0.66v-28.9c42.42,1.89,74.08,9.29,74.08,18.13s-31.65,16.24-74.08,18.12h0Zm0-39.25V79.68h59.2V40.23H89.21V79.68H148.4v25.86c-48.11,2.21-84.29,11.74-84.29,23.16s36.18,20.94,84.29,23.16v82.9h42.78V151.83c48-2.21,84.12-11.73,84.12-23.14s-36.09-20.93-84.12-23.15h0Zm0,0h0Z"
/>
</svg>
);
};
@@ -1,28 +0,0 @@
/**
* See note in index.tsx. This component is intended as a placeholder for a
* better, more generic solution.
*
* @deprecated
*/
export const Vega = ({ size = 16 }: { size?: number }) => {
return (
<svg width={size} height={size} viewBox="0 0 42 42">
<rect width="42" height="42" rx="21" fill="black" />
<path d="M13 27.2726H16.4545V10H13V27.2726Z" fill="white" />
<path d="M25.667 23.8181H29.1215V10H25.667V23.8181Z" fill="white" />
<path d="M19.333 33.6059H22.7875V30.1514H19.333V33.6059Z" fill="white" />
<path
d="M22.7871 30.7271H26.2416V27.2726H22.7871V30.7271Z"
fill="white"
/>
<path
d="M29.1211 27.2726H31.9999V23.8181H29.1211V27.2726Z"
fill="white"
/>
<path
d="M16.4551 30.7271H19.3339V27.2726H16.4551V30.7271Z"
fill="white"
/>
</svg>
);
};
@@ -1,171 +0,0 @@
import type { DeepPartial } from '@apollo/client/utilities';
import { parseResultsToAccounts } from './network-accounts-table';
import {
ExplorerTreasuryDocument,
type ExplorerTreasuryQuery,
} from '../__generated__/Treasury';
import { render, screen } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import { MemoryRouter } from 'react-router-dom';
import { NetworkAccountsTable } from './network-accounts-table';
describe('parseResultsToAccounts', () => {
it('should return an array of non-zero treasury accounts', () => {
const data: DeepPartial<ExplorerTreasuryQuery> = {
assetsConnection: {
edges: [
{
node: {
id: 'asset1',
networkTreasuryAccount: {
balance: '100',
},
},
},
{
node: {
id: 'has0assets',
networkTreasuryAccount: {
balance: '0',
},
},
},
{
node: {
id: 'asset3',
networkTreasuryAccount: {
balance: '50',
},
},
},
{
node: {
id: 'hasnonetworktreasuryaccount',
},
},
],
},
};
const result = parseResultsToAccounts(data as ExplorerTreasuryQuery);
expect(result).toHaveLength(2);
expect(result).toEqual([
{
assetId: 'asset1',
balance: '100',
type: 'ACCOUNT_TYPE_NETWORK_TREASURY',
},
{
assetId: 'asset3',
balance: '50',
type: 'ACCOUNT_TYPE_NETWORK_TREASURY',
},
]);
});
it('should return an empty array if no non-zero accounts are found', () => {
const data: DeepPartial<ExplorerTreasuryQuery> = {
assetsConnection: {
edges: [
{
node: {
id: 'asset1',
networkTreasuryAccount: {
balance: '0',
},
},
},
{
node: {
id: 'asset2',
networkTreasuryAccount: {
balance: '0',
},
},
},
],
},
};
const result = parseResultsToAccounts(data as ExplorerTreasuryQuery);
expect(result).toHaveLength(0);
expect(result).toEqual([]);
});
it('should handle missing data', () => {
const result = parseResultsToAccounts(
undefined as unknown as ExplorerTreasuryQuery
);
expect(result).toHaveLength(0);
expect(result).toEqual([]);
});
});
describe('NetworkAccountsTable', () => {
const mockData: ExplorerTreasuryQuery = {
assetsConnection: {
edges: [
{
node: {
id: 'asset1',
networkTreasuryAccount: {
balance: '100',
},
},
},
{
node: {
id: 'asset2',
networkTreasuryAccount: {
balance: '50',
},
},
},
],
},
};
const mocks = [
{
request: {
query: ExplorerTreasuryDocument,
},
result: {
data: mockData,
},
},
];
it('should render network accounts (as many as match - often just 1)', async () => {
render(
<MockedProvider mocks={mocks} addTypename={false}>
<MemoryRouter>
<NetworkAccountsTable />
</MemoryRouter>
</MockedProvider>
);
// Wait for the data to load
await screen.findByText('Loading...');
// Assert that the network accounts are rendered
expect(screen.getByText('asset1')).toBeInTheDocument();
expect(screen.getByText('asset2')).toBeInTheDocument();
});
it('should handle loading state', async () => {
render(
<MockedProvider mocks={mocks} addTypename={false}>
<MemoryRouter>
<NetworkAccountsTable />
</MemoryRouter>
</MockedProvider>
);
// Assert that the loading state is rendered
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
});
@@ -1,87 +0,0 @@
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import {
type ExplorerTreasuryQuery,
useExplorerTreasuryQuery,
} from '../__generated__/Treasury';
import AssetBalance from '../../../components/asset-balance/asset-balance';
import { AssetLink } from '../../../components/links';
import { useMemo } from 'react';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import { AssetIcon } from './asset-icon';
import { type NonZeroAccount } from '../network-treasury';
import { AccountType } from '@vegaprotocol/types';
import { removePaginationWrapper } from '@vegaprotocol/utils';
export const NetworkAccountsTable = () => {
const { data, loading, error } = useExplorerTreasuryQuery({
// This needs to ignore error as old assets may no longer properly resolve
errorPolicy: 'ignore',
});
const { screenSize } = useScreenDimensions();
const shouldRound = useMemo(
() => ['xs', 'sm', 'md', 'lg'].includes(screenSize),
[screenSize]
);
return (
<AsyncRenderer
data={data}
loading={loading}
error={error}
render={(data) => {
const c = parseResultsToAccounts(data);
return (
<section className="md:flex md:flex-row flex-wrap">
{c.map((a) => (
<div className="basis-1/2 md:basis-1/4">
<div className="bg-white rounded overflow-hidden shadow-lg dark:bg-black dark:border-slate-500 dark:border">
<div className="text-center p-6 bg-gray-100 dark:bg-slate-900 border-b dark:border-slate-500">
<p className="flex justify-center">
<AssetIcon symbol={a.assetId} />
</p>
<p className="mt-3" data-testid="name">
<AssetLink assetId={a.assetId} />
</p>
</div>
<div className="text-center py-5" data-testid="balance">
<AssetBalance
assetId={a.assetId}
price={a.balance}
showAssetSymbol={true}
rounded={shouldRound}
/>
</div>
</div>
</div>
))}
</section>
);
}}
/>
);
};
export function parseResultsToAccounts(
data: ExplorerTreasuryQuery
): NonZeroAccount[] {
const nonZeroAccounts: NonZeroAccount[] = [];
if (data?.assetsConnection?.edges) {
const edges = removePaginationWrapper(data?.assetsConnection?.edges);
if (edges) {
edges.forEach((edge) => {
if (
edge.networkTreasuryAccount &&
edge.networkTreasuryAccount?.balance !== '0'
) {
nonZeroAccounts.push({
assetId: edge.id,
balance: edge.networkTreasuryAccount?.balance,
type: AccountType.ACCOUNT_TYPE_NETWORK_TREASURY,
});
}
});
}
}
return nonZeroAccounts;
}
@@ -1,262 +0,0 @@
import { AccountType } from '@vegaprotocol/types';
import {
typeLabel,
getToAccountTypeLabel,
filterAccountTransfers,
} from './network-transfers-table';
import { render, screen } from '@testing-library/react';
import { NetworkTransfersTable } from './network-transfers-table';
import { MockedProvider } from '@apollo/client/testing';
import { MemoryRouter } from 'react-router-dom';
import {
ExplorerTreasuryTransfersDocument,
type ExplorerTreasuryTransfersQuery,
} from '../__generated__/TreasuryTransfers';
import type { DeepPartial } from '@apollo/client/utilities';
describe('typeLabel', () => {
it('should return "Transfer" for "OneOffTransfer" kind', () => {
expect(typeLabel('OneOffTransfer')).toBe('Transfer');
});
it('should return "Transfer" for "RecurringTransfer" kind', () => {
expect(typeLabel('RecurringTransfer')).toBe('Transfer');
});
it('should return "Governance" for "OneOffGovernanceTransfer" kind', () => {
expect(typeLabel('OneOffGovernanceTransfer')).toBe('Governance');
});
it('should return "Governance" for "RecurringGovernanceTransfer" kind', () => {
expect(typeLabel('RecurringGovernanceTransfer')).toBe('Governance');
});
it('should return "Unknown" for unknown kind', () => {
expect(typeLabel()).toBe('Unknown');
expect(typeLabel('')).toBe('Unknown');
expect(typeLabel('InvalidKind')).toBe('Unknown');
});
});
describe('getToAccountTypeLabel', () => {
it('should return "Treasury" when type is ACCOUNT_TYPE_NETWORK_TREASURY', () => {
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_NETWORK_TREASURY)
).toBe('Treasury');
});
it('should return "Fees" when type is any of the fee account types', () => {
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE)
).toBe('Fees');
expect(getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_FEES_MAKER)).toBe(
'Fees'
);
expect(getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY)).toBe(
'Fees'
);
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_LP_LIQUIDITY_FEES)
).toBe('Fees');
expect(
getToAccountTypeLabel(
AccountType.ACCOUNT_TYPE_PENDING_FEE_REFERRAL_REWARD
)
).toBe('Fees');
});
it('should return "Insurance" when type is ACCOUNT_TYPE_GLOBAL_INSURANCE', () => {
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_GLOBAL_INSURANCE)
).toBe('Insurance');
});
it('should return "Rewards" when type is any of the reward account types', () => {
expect(getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_GLOBAL_REWARD)).toBe(
'Rewards'
);
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION)
).toBe('Rewards');
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES)
).toBe('Rewards');
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES)
).toBe('Rewards');
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES)
).toBe('Rewards');
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS)
).toBe('Rewards');
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_REWARD_RELATIVE_RETURN)
).toBe('Rewards');
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY)
).toBe('Rewards');
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING)
).toBe('Rewards');
expect(getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_VESTED_REWARDS)).toBe(
'Rewards'
);
expect(
getToAccountTypeLabel(AccountType.ACCOUNT_TYPE_VESTING_REWARDS)
).toBe('Rewards');
});
it('should return "Other" for any other type', () => {
expect(getToAccountTypeLabel(undefined)).toBe('Other');
expect(getToAccountTypeLabel('unknown' as AccountType)).toBe('Other');
});
});
describe('filterAccountTransfers', () => {
it('filters out transactions that are not to or from a treasury account', () => {
const data: DeepPartial<ExplorerTreasuryTransfersQuery> = {
transfersConnection: {
edges: [
{
node: {
transfer: {
toAccountType: AccountType.ACCOUNT_TYPE_NETWORK_TREASURY,
fromAccountType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
},
},
},
{
node: {
transfer: {
toAccountType: AccountType.ACCOUNT_TYPE_NETWORK_TREASURY,
fromAccountType:
AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION,
},
},
},
{
node: {
transfer: {
toAccountType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
fromAccountType: AccountType.ACCOUNT_TYPE_NETWORK_TREASURY,
},
},
},
{
node: {
transfer: {
toAccountType: AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION,
fromAccountType:
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
},
},
},
],
},
};
const result = filterAccountTransfers(
data as ExplorerTreasuryTransfersQuery
);
expect(result).toHaveLength(3);
});
it('should return an empty array if no transfers match the filter', () => {
const data: DeepPartial<ExplorerTreasuryTransfersQuery> = {
transfersConnection: {
edges: [
{
node: {
transfer: {
toAccountType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
fromAccountType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
},
},
},
{
node: {
transfer: {
toAccountType: AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION,
fromAccountType:
AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION,
},
},
},
],
},
};
const result = filterAccountTransfers(
data as ExplorerTreasuryTransfersQuery
);
expect(result).toHaveLength(0);
});
});
describe('NetworkTransfersTable', () => {
it('renders table headers correctly', async () => {
const mocks = [
{
request: {
query: ExplorerTreasuryTransfersDocument,
},
result: {
data: {
transfersConnection: {
edges: [
{
node: {
transfer: {
id: '123',
toAccountType: AccountType.ACCOUNT_TYPE_NETWORK_TREASURY,
fromAccountType:
AccountType.ACCOUNT_TYPE_NETWORK_TREASURY,
amount: '100',
asset: {
id: '1',
},
timestamp: '2022-01-01T00:00:00Z',
from: 'network',
to: '7100a8a82ef45adb9efa070cc821c6c5c48172d6dc5f842431549490fe5897a0',
reason: '',
status: 'COMPLETED',
kind: {
__typename: 'OneOffGovernanceTransfer',
deliverOn: '123',
},
},
},
},
],
},
},
},
},
];
render(
<MockedProvider mocks={mocks} addTypename={true}>
<MemoryRouter>
<NetworkTransfersTable />
</MemoryRouter>
</MockedProvider>
);
expect(await screen.findByText('Amount')).toBeInTheDocument();
expect(screen.getByText('Asset')).toBeInTheDocument();
expect(screen.getByText('Age')).toBeInTheDocument();
expect(screen.getByText('From')).toBeInTheDocument();
expect(screen.getByText('To')).toBeInTheDocument();
expect(screen.getByText('Status')).toBeInTheDocument();
expect(screen.getByText('Type')).toBeInTheDocument();
expect(screen.getByTestId('from-account').textContent).toEqual('Treasury');
expect(screen.getByTestId('to-account').textContent).toEqual('7100…97a0');
expect(screen.getByTestId('transfer-kind').textContent).toEqual(
'Governance'
);
});
});
@@ -1,253 +0,0 @@
import { AsyncRenderer, Icon } from '@vegaprotocol/ui-toolkit';
import AssetBalance from '../../../components/asset-balance/asset-balance';
import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
import { AssetLink, PartyLink } from '../../../components/links';
import {
type ExplorerTreasuryTransfersQuery,
useExplorerTreasuryTransfersQuery,
} from '../__generated__/TreasuryTransfers';
import { TimeAgo } from '../../../components/time-ago';
import { TransferStatusIcon } from '../../../components/txs/details/transfer/blocks/transfer-status';
import { t } from '@vegaprotocol/i18n';
import { IconNames } from '@blueprintjs/icons';
import { useMemo } from 'react';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
export const colours = {
INCOMING: '!fill-vega-green-600 text-vega-green-600 mr-2',
OUTGOING: '!fill-vega-pink-600 text-vega-pink-600 mr-2',
};
export const theadClasses =
'py-2 border text-center bg-vega-light-150 dark:bg-vega-dark-150';
export function getToAccountTypeLabel(type?: AccountType): string {
switch (type) {
case AccountType.ACCOUNT_TYPE_NETWORK_TREASURY:
return t('Treasury');
case AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE:
case AccountType.ACCOUNT_TYPE_FEES_MAKER:
case AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY:
case AccountType.ACCOUNT_TYPE_LP_LIQUIDITY_FEES:
case AccountType.ACCOUNT_TYPE_PENDING_FEE_REFERRAL_REWARD:
return t('Fees');
case AccountType.ACCOUNT_TYPE_GLOBAL_INSURANCE:
return t('Insurance');
case AccountType.ACCOUNT_TYPE_GLOBAL_REWARD:
case AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION:
case AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES:
case AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES:
case AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES:
case AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS:
case AccountType.ACCOUNT_TYPE_REWARD_RELATIVE_RETURN:
case AccountType.ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY:
case AccountType.ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING:
case AccountType.ACCOUNT_TYPE_VESTED_REWARDS:
case AccountType.ACCOUNT_TYPE_VESTING_REWARDS:
return t('Rewards');
default:
return t('Other');
}
}
export function typeLabel(kind?: string): string {
switch (kind) {
case 'OneOffTransfer':
case 'RecurringTransfer':
return t('Transfer');
case 'OneOffGovernanceTransfer':
case 'RecurringGovernanceTransfer':
return t('Governance');
default:
return t('Unknown');
}
}
export function filterAccountTransfers(data: ExplorerTreasuryTransfersQuery) {
return data.transfersConnection?.edges
?.filter((edge) => {
if (
edge?.node.transfer.toAccountType ===
AccountType.ACCOUNT_TYPE_NETWORK_TREASURY
) {
return true;
} else if (
edge?.node.transfer.fromAccountType ===
AccountType.ACCOUNT_TYPE_NETWORK_TREASURY
) {
return true;
}
return false;
})
.map((edge) => {
return edge?.node.transfer;
});
}
export const NetworkTransfersTable = () => {
const { data, loading, error } = useExplorerTreasuryTransfersQuery({
// This needs to ignore error as old assets may no longer properly resolve
errorPolicy: 'ignore',
});
const { screenSize } = useScreenDimensions();
const shouldRound = useMemo(
() => ['xs', 'sm', 'md', 'lg'].includes(screenSize),
[screenSize]
);
const shouldTruncate = useMemo(
() => ['xs', 'sm', 'md', 'lg', 'xl'].includes(screenSize),
[screenSize]
);
const shouldHideColumns = useMemo(
() => ['xs', 'sm'].includes(screenSize),
[screenSize]
);
return (
<section>
<AsyncRenderer
data={data}
loading={loading}
error={error}
render={(data) => {
const c = filterAccountTransfers(data);
if (!c) {
return null;
}
return (
<table className="table-fixed border-spacing-3">
<thead>
<tr>
<th className={theadClasses}>{t('Amount')}</th>
<th className={theadClasses}>{t('Asset')}</th>
<th className={theadClasses}>{t('Age')}</th>
<th className={theadClasses}>{t('From')}</th>
<th className={theadClasses}>{t('To')}</th>
<th
className={`${theadClasses} ${
shouldHideColumns ? 'hidden' : ''
}`}
>
{t('Status')}
</th>
<th
className={`${theadClasses} ${
shouldHideColumns ? 'hidden' : ''
}`}
>
{t('Type')}
</th>
</tr>
</thead>
<tbody>
{c.map((a) => {
const isIncoming =
a?.toAccountType ===
AccountType.ACCOUNT_TYPE_NETWORK_TREASURY;
return (
<tr>
{a && a.amount && a.asset && (
<td
className={`px-2 py-1 border whitespace-nowrap text-right ${
isIncoming ? colours.INCOMING : colours.OUTGOING
}`}
title={a.amount}
>
{a &&
a.toAccountType ===
AccountType.ACCOUNT_TYPE_NETWORK_TREASURY ? (
<Icon
name={IconNames.PLUS}
className={colours.INCOMING}
/>
) : (
<Icon
name={IconNames.MINUS}
className={colours.OUTGOING}
/>
)}
<AssetBalance
assetId={a.asset.id}
price={a.amount}
showAssetLink={false}
rounded={shouldRound}
/>
</td>
)}
<td className="px-2 py-1 border whitespace-nowrap">
{a && a.amount && a.asset && (
<AssetLink
assetId={a.asset.id}
showAssetSymbol={true}
/>
)}
</td>
<td className="px-2 py-1 border">
{a && a.timestamp && <TimeAgo date={a.timestamp} />}
</td>
<td
className="px-2 py-1 border"
data-testid="from-account"
>
{a && a.from && (
<PartyLink
id={a.from}
truncate={true}
truncateLength={shouldTruncate ? 4 : 15}
networkLabel={t('Treasury')}
/>
)}
</td>
<td className="px-2 py-1 border" data-testid="to-account">
{a && a.to && (
<PartyLink
id={a.to}
networkLabel={t('Treasury')}
truncate={true}
truncateLength={shouldTruncate ? 4 : 15}
/>
)}
{a && !a.to && (
<span
className="underline decoration-dotted"
title={AccountTypeMapping[a.toAccountType]}
>
{getToAccountTypeLabel(a.toAccountType)}
</span>
)}
</td>
<td
className={`px-2 py-1 border text-center ${
shouldHideColumns ? 'hidden' : ''
}`}
>
{a && a.status && (
<TransferStatusIcon status={a.status} />
)}
</td>
<td
className={`px-2 py-1 border ${
shouldHideColumns ? 'hidden' : ''
}`}
>
<span
className="underline decoration-dotted"
title={a?.kind.__typename}
data-testid="transfer-kind"
>
{a && typeLabel(a.kind.__typename)}
</span>
</td>
</tr>
);
})}
</tbody>
</table>
);
}}
/>
</section>
);
};
@@ -1 +0,0 @@
export * from './network-treasury';
@@ -1,28 +0,0 @@
import { useDocumentTitle } from '../../hooks/use-document-title';
import type { AccountType } from '@vegaprotocol/types';
import { t } from '@vegaprotocol/i18n';
import { RouteTitle } from '../../components/route-title';
import { NetworkAccountsTable } from './components/network-accounts-table';
import { NetworkTransfersTable } from './components/network-transfers-table';
export type NonZeroAccount = {
assetId: string;
balance: string;
type: AccountType;
};
export const NetworkTreasury = () => {
useDocumentTitle(['Network Treasury']);
return (
<section>
<RouteTitle data-testid="block-header">{t(`Treasury`)}</RouteTitle>
<div>
<NetworkAccountsTable />
</div>
<div className="mt-5">
<h2 className="text-3xl mb-2">{t('Transfers')}</h2>
<NetworkTransfersTable />
</div>
</section>
);
};
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"short_name": "Explorer VEGA", "short_name": "Mainnet Stats",
"name": "Vega Protocol - Explorer", "name": "Vega Mainnet statistics",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",
@@ -2,7 +2,7 @@ export const proposalsData = {
proposalsConnection: { proposalsConnection: {
edges: [ edges: [
{ {
proposalNode: { node: {
id: 'e8ba9d268e12514644fd1fc7ff289292f4ce6489cc32cc73133aea52c04aef89', id: 'e8ba9d268e12514644fd1fc7ff289292f4ce6489cc32cc73133aea52c04aef89',
rationale: { rationale: {
title: 'Add asset Wrapped Ether', title: 'Add asset Wrapped Ether',
@@ -56,7 +56,7 @@ export const proposalsData = {
__typename: 'ProposalEdge', __typename: 'ProposalEdge',
}, },
{ {
proposalNode: { node: {
id: 'd848fc7881f13d366df5f61ab139d5fcfa72bf838151bb51b54381870e357931', id: 'd848fc7881f13d366df5f61ab139d5fcfa72bf838151bb51b54381870e357931',
rationale: { rationale: {
title: 'Add asset Dai Stablecoin', title: 'Add asset Dai Stablecoin',
@@ -110,7 +110,60 @@ export const proposalsData = {
__typename: 'ProposalEdge', __typename: 'ProposalEdge',
}, },
{ {
proposalNode: { node: {
id: 'ccbd651b4a1167fd73c4a0340ac759fa0a31ca487ad46a13254b741ad71947ed',
rationale: {
title: 'New DAI market',
description: 'New DAI market',
__typename: 'ProposalRationale',
},
reference: '0VFQusmmESdrP5GuL8naB6lxfoE3RPGaEeo7abdN',
state: 'STATE_ENACTED',
datetime: '2022-11-26T19:36:19.26034Z',
rejectionReason: null,
party: {
id: '69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f',
__typename: 'Party',
},
errorDetails: null,
terms: {
closingDatetime: '2022-11-26T19:36:42Z',
enactmentDatetime: '2023-03-22T13:57:37Z',
change: {
instrument: {
name: 'UNIDAI Monthly (Dec 2022)',
code: 'UNIDAI.MF21',
product: {
settlementAsset: { symbol: 'tDAI', __typename: 'Asset' },
__typename: 'FutureProduct',
},
__typename: 'InstrumentConfiguration',
},
__typename: 'NewMarket',
},
__typename: 'ProposalTerms',
},
votes: {
yes: {
totalTokens: '0',
totalNumber: '0',
totalEquityLikeShareWeight: '0',
__typename: 'ProposalVoteSide',
},
no: {
totalTokens: '0',
totalNumber: '0',
totalEquityLikeShareWeight: '0',
__typename: 'ProposalVoteSide',
},
__typename: 'ProposalVotes',
},
__typename: 'Proposal',
},
__typename: 'ProposalEdge',
},
{
node: {
id: 'bc70383f0e9515b15542cf4c63590cd2ca46b3363ba7c4a72af0e62112b3951b', id: 'bc70383f0e9515b15542cf4c63590cd2ca46b3363ba7c4a72af0e62112b3951b',
rationale: { rationale: {
title: 'USDC-III', title: 'USDC-III',
@@ -164,7 +217,60 @@ export const proposalsData = {
__typename: 'ProposalEdge', __typename: 'ProposalEdge',
}, },
{ {
proposalNode: { node: {
id: '9d9b2a9d0179d0e4ccb317f6c4a5db0b905d893190bfb5e5499985ef313281c8',
rationale: {
title: 'New BTC market',
description: 'New BTC market',
__typename: 'ProposalRationale',
},
reference: 'AXeRWS3TvLBFDgWOSHQpKFJf3NTbnWK6310q02fZ',
state: 'STATE_ENACTED',
datetime: '2022-11-26T19:36:19.26034Z',
rejectionReason: null,
party: {
id: '69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f',
__typename: 'Party',
},
errorDetails: null,
terms: {
closingDatetime: '2022-11-26T19:36:42Z',
enactmentDatetime: '2023-03-22T13:57:37Z',
change: {
instrument: {
name: 'ETHBTC Quarterly (Feb 2023)',
code: 'ETHBTC.QM21',
product: {
settlementAsset: { symbol: 'tBTC', __typename: 'Asset' },
__typename: 'FutureProduct',
},
__typename: 'InstrumentConfiguration',
},
__typename: 'NewMarket',
},
__typename: 'ProposalTerms',
},
votes: {
yes: {
totalTokens: '0',
totalNumber: '0',
totalEquityLikeShareWeight: '0',
__typename: 'ProposalVoteSide',
},
no: {
totalTokens: '0',
totalNumber: '0',
totalEquityLikeShareWeight: '0',
__typename: 'ProposalVoteSide',
},
__typename: 'ProposalVotes',
},
__typename: 'Proposal',
},
__typename: 'ProposalEdge',
},
{
node: {
id: '9c48796e7988769ededc2b2b02220b00e93f65f23e8141bf1fd23a6983d95943', id: '9c48796e7988769ededc2b2b02220b00e93f65f23e8141bf1fd23a6983d95943',
rationale: { rationale: {
title: 'Update governance.proposal.asset.requiredMajority', title: 'Update governance.proposal.asset.requiredMajority',
@@ -236,7 +236,7 @@ context(
}); });
// 1002-STKE-041 1002-STKE-053 // 1002-STKE-041 1002-STKE-053
it.skip( it(
'Able to remove part of a stake against a validator', 'Able to remove part of a stake against a validator',
// @ts-ignore clash between jest and cypress // @ts-ignore clash between jest and cypress
{ tags: '@smoke' }, { tags: '@smoke' },
@@ -27,7 +27,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
cy.getByTestId('app-announcement').should('not.exist'); cy.getByTestId('app-announcement').should('not.exist');
}); });
it.skip('should show open or enacted proposals without proposal summary', function () { it('should show open or enacted proposals without proposal summary', function () {
cy.get('body').then(($body) => { cy.get('body').then(($body) => {
if (!$body.find('[data-testid="proposals-list-item"]').length) { if (!$body.find('[data-testid="proposals-list-item"]').length) {
cy.createMarket(); cy.createMarket();
@@ -82,20 +82,20 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
it('should have information on active nodes', function () { it('should have information on active nodes', function () {
cy.getByTestId('node-information') cy.getByTestId('node-information')
.first() .first()
.should('contain.text', '1') .should('contain.text', '2')
.and('contain.text', 'active nodes'); .and('contain.text', 'active nodes');
}); });
it('should have information on consensus nodes', function () { it('should have information on consensus nodes', function () {
cy.getByTestId('node-information') cy.getByTestId('node-information')
.last() .last()
.should('contain.text', '1') .should('contain.text', '2')
.and('contain.text', 'consensus nodes'); .and('contain.text', 'consensus nodes');
}); });
it('should contain link to specific validators', function () { it('should contain link to specific validators', function () {
cy.getByTestId('validators') cy.getByTestId('validators')
.should('have.length', '1') .should('have.length', '2')
.each(($validator) => { .each(($validator) => {
cy.wrap($validator).find('a').should('have.attr', 'href'); cy.wrap($validator).find('a').should('have.attr', 'href');
}); });
@@ -159,7 +159,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
cy.getByTestId('node-url-custom').click({ force: true }); cy.getByTestId('node-url-custom').click({ force: true });
cy.get('input').should('exist'); cy.get('input').should('exist');
cy.getByTestId('connect').should('be.disabled'); cy.getByTestId('connect').should('be.disabled');
cy.getByTestId('dialog-close').click(); cy.getByTestId('icon-cross').click();
}); });
it('should display eth data', function () { it('should display eth data', function () {
@@ -189,7 +189,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
cy.viewport('iphone-xr'); cy.viewport('iphone-xr');
}); });
it.skip('should have burger button', () => { it('should have burger button', () => {
cy.getByTestId('button-menu-drawer').should('be.visible').click(); cy.getByTestId('button-menu-drawer').should('be.visible').click();
cy.getByTestId('menu-drawer').should('be.visible'); cy.getByTestId('menu-drawer').should('be.visible');
}); });
@@ -40,10 +40,10 @@ context('View functionality with public key', { tags: '@smoke' }, function () {
verifyConnectedToPubKey(); verifyConnectedToPubKey();
}); });
it.skip('Able to connect public key via wallet and view assets in wallet', function () { it('Able to connect public key via wallet and view assets in wallet', function () {
verifyConnectedToPubKey(); verifyConnectedToPubKey();
cy.getByTestId('currency-title', { timeout: 10000 }) cy.getByTestId('currency-title', { timeout: 10000 })
.should('have.length.at.least', 2) .should('have.length.at.least', 4)
.and('contain.text', 'USDC (fake)'); .and('contain.text', 'USDC (fake)');
}); });
+5 -4
View File
@@ -14,6 +14,7 @@ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/m
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/main/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/main/announcements.json
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72 NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
@@ -23,7 +24,7 @@ NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/ NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
#Test configuration variables #Test configuration variables
CYPRESS_FAIRGROUND=false CYPRESS_FAIRGROUND=false
@@ -32,7 +33,7 @@ LC_ALL="en_US.UTF-8"
# Cosmic elevator flags # Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true NX_METAMASK_SNAPS=true
NX_PRODUCT_PERPETUALS=true NX_PRODUCT_PERPETUALS=false
NX_UPDATE_MARKET_STATE=true NX_UPDATE_MARKET_STATE=false
NX_REFERRALS=true NX_REFERRALS=true
NX_GOVERNANCE_TRANSFERS=true NX_GOVERNANCE_TRANSFERS=false
+2 -1
View File
@@ -15,11 +15,12 @@ NX_ETH_WALLET_MNEMONIC=ozone access unlock valid olympic save include omit suppl
NX_LOCAL_PROVIDER_URL=http://localhost:8545/ NX_LOCAL_PROVIDER_URL=http://localhost:8545/
NX_VEGA_WALLET_URL=http://localhost:1789 NX_VEGA_WALLET_URL=http://localhost:1789
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
NX_VEGA_REST_URL=http://localhost:3008/api/v2/ NX_VEGA_REST_URL=http://localhost:3008/api/v2/
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/ NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
+2 -1
View File
@@ -8,13 +8,14 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_VEGA_EXPLORER_URL=# NX_VEGA_EXPLORER_URL=#
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-devnet1-k8s.ops.vega.xyz NX_TRANCHES_SERVICE_URL=https://tranches-devnet1-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_REST_URL=https://api.n00.devnet1.vega.xyz/api/v2/ NX_VEGA_REST_URL=https://api.n00.devnet1.vega.xyz/api/v2/
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996 NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/ NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/ NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
+1
View File
@@ -10,6 +10,7 @@ NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/
NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz NX_VEGA_EXPLORER_URL=https://explorer.vega.xyz
NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40:87edc2605e544f888305d7fc4a9141bd@o286262.ingest.sentry.io/5882996 NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40:87edc2605e544f888305d7fc4a9141bd@o286262.ingest.sentry.io/5882996
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-k8s.ops.vega.xyz NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_REST_URL=https://api.vega.community/api/v2/ NX_VEGA_REST_URL=https://api.vega.community/api/v2/
+1
View File
@@ -9,6 +9,7 @@ NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996 NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
NX_VEGA_EXPLORER_URL=https://explorer.mainnet-mirror.vega.rocks NX_VEGA_EXPLORER_URL=https://explorer.mainnet-mirror.vega.rocks
NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet NX_VEGA_DOCS_URL=https://docs.vega.xyz/mainnet
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-mirror-k8s.ops.vega.xyz NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-mirror-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_REST_URL=https://api.mainnet-mirror.vega.rocks/api/v2/ NX_VEGA_REST_URL=https://api.mainnet-mirror.vega.rocks/api/v2/
+2 -1
View File
@@ -5,6 +5,7 @@ NX_VEGA_NETWORKS='{"DEVNET":"https://dev.governance.vega.xyz","STAGNET1":"https:
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml
NX_VEGA_EXPLORER_URL=https://explorer.stagnet1.vega.rocks NX_VEGA_EXPLORER_URL=https://explorer.stagnet1.vega.rocks
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_REST_URL=https://api.n00.stagnet1.vega.xyz/api/v2/ NX_VEGA_REST_URL=https://api.n00.stagnet1.vega.xyz/api/v2/
@@ -12,7 +13,7 @@ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/m
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72 NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/ NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
+2 -1
View File
@@ -9,6 +9,7 @@ NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
NX_DELEGATIONS_PAGINATION=50
NX_TRANCHES_SERVICE_URL=https://tranches-testnet-k8s.ops.vega.xyz NX_TRANCHES_SERVICE_URL=https://tranches-testnet-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_REST_URL=https://api.n07.testnet.vega.xyz/api/v2/ NX_VEGA_REST_URL=https://api.n07.testnet.vega.xyz/api/v2/
@@ -17,7 +18,7 @@ NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/m
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72 NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/ NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
+1 -1
View File
@@ -14,7 +14,7 @@ NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/en-GB/firefox/addon/vega-wallet-beta/ NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega. NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.
-3
View File
@@ -62,9 +62,6 @@ const cache: InMemoryCacheConfig = {
Account: { Account: {
keyFields: false, keyFields: false,
}, },
Instrument: {
keyFields: ['code'],
},
Delegation: { Delegation: {
keyFields: false, keyFields: false,
// Only get full updates // Only get full updates
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"short_name": "Governance VEGA", "short_name": "Mainnet Stats",
"name": "Vega Protocol - Governance", "name": "Vega Mainnet statistics",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",
@@ -1,5 +1,5 @@
import classnames from 'classnames'; import classnames from 'classnames';
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit'; import { Icon } from '@vegaprotocol/ui-toolkit';
import type { Dispatch, SetStateAction, ReactNode } from 'react'; import type { Dispatch, SetStateAction, ReactNode } from 'react';
interface CollapsibleToggleProps { interface CollapsibleToggleProps {
@@ -15,19 +15,22 @@ export const CollapsibleToggle = ({
dataTestId, dataTestId,
children, children,
}: CollapsibleToggleProps) => { }: CollapsibleToggleProps) => {
const classes = classnames('transition-transform ease-in-out duration-300', { const classes = classnames(
'rotate-180': toggleState, 'mb-4 transition-transform ease-in-out duration-300',
}); {
'rotate-180': toggleState,
}
);
return ( return (
<button <button
onClick={() => setToggleState(!toggleState)} onClick={() => setToggleState(!toggleState)}
data-testid={dataTestId} data-testid={dataTestId}
> >
<div className="flex items-baseline gap-3"> <div className="flex items-center gap-3">
{children} {children}
<div className={classes} data-testid="toggle-icon-wrapper"> <div className={classes} data-testid="toggle-icon-wrapper">
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={20} /> <Icon name="chevron-down" size={8} />
</div> </div>
</div> </div>
</button> </button>
@@ -1,8 +1,7 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { type ReactNode } from 'react';
interface HeadingProps { interface HeadingProps {
title?: ReactNode; title?: string;
centerContent?: boolean; centerContent?: boolean;
marginTop?: boolean; marginTop?: boolean;
marginBottom?: boolean; marginBottom?: boolean;
@@ -1,44 +0,0 @@
import {
useLinks,
DApp,
CONSOLE_REWARDS_PAGE,
} from '@vegaprotocol/environment';
import {
ExternalLink,
Intent,
NotificationBanner,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { Trans } from 'react-i18next';
import { useMatch } from 'react-router-dom';
import Routes from '../../routes/routes';
import { type ReactNode } from 'react';
const ConsoleRewardsLink = ({ children }: { children: ReactNode }) => {
const consoleLink = useLinks(DApp.Console);
return (
<ExternalLink
href={consoleLink(CONSOLE_REWARDS_PAGE)}
className="underline inline-flex gap-1 items-center"
title="Rewards in Console"
>
<span>{children}</span>
<VegaIcon size={12} name={VegaIconNames.OPEN_EXTERNAL} />
</ExternalLink>
);
};
export const RewardsMovedNotification = () => {
const onRewardsPage = useMatch(Routes.REWARDS);
if (!onRewardsPage) return null;
return (
<NotificationBanner intent={Intent.Warning}>
<Trans
i18nKey="rewardsMovedNotification"
components={[<ConsoleRewardsLink>Console</ConsoleRewardsLink>]}
/>
</NotificationBanner>
);
};
@@ -10,7 +10,6 @@ import {
ProtocolUpgradeProposalNotification, ProtocolUpgradeProposalNotification,
} from '@vegaprotocol/proposals'; } from '@vegaprotocol/proposals';
import { ViewingAsBanner } from '@vegaprotocol/ui-toolkit'; import { ViewingAsBanner } from '@vegaprotocol/ui-toolkit';
import { RewardsMovedNotification } from '../notifications/rewards-moved-notification';
interface AppLayoutProps { interface AppLayoutProps {
children: ReactNode; children: ReactNode;
@@ -46,10 +45,8 @@ export const AppLayout = ({ children }: AppLayoutProps) => {
const NotificationsContainer = () => { const NotificationsContainer = () => {
const { isReadOnly, pubKey, disconnect } = useVegaWallet(); const { isReadOnly, pubKey, disconnect } = useVegaWallet();
return ( return (
<div data-testid="banners"> <div data-testid="banners">
<RewardsMovedNotification />
<ProtocolUpgradeProposalNotification <ProtocolUpgradeProposalNotification
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING} mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
/> />
@@ -4,6 +4,7 @@ import keyBy from 'lodash/keyBy';
import uniq from 'lodash/uniq'; import uniq from 'lodash/uniq';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ENV } from '../../config';
import noIcon from '../../images/token-no-icon.png'; import noIcon from '../../images/token-no-icon.png';
import vegaBlack from '../../images/vega_black.png'; import vegaBlack from '../../images/vega_black.png';
@@ -36,6 +37,7 @@ export const usePollForDelegations = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { pubKey } = useVegaWallet(); const { pubKey } = useVegaWallet();
const client = useApolloClient(); const client = useApolloClient();
const { delegationsPagination } = ENV;
const [delegations, setDelegations] = React.useState< const [delegations, setDelegations] = React.useState<
WalletDelegationFieldsFragment[] WalletDelegationFieldsFragment[]
>([]); >([]);
@@ -66,9 +68,11 @@ export const usePollForDelegations = () => {
query: DelegationsDocument, query: DelegationsDocument,
variables: { variables: {
partyId: pubKey, partyId: pubKey,
delegationsPagination: { delegationsPagination: delegationsPagination
first: 50, ? {
}, first: Number(delegationsPagination),
}
: undefined,
}, },
fetchPolicy: 'network-only', fetchPolicy: 'network-only',
}) })
@@ -232,14 +236,14 @@ export const usePollForDelegations = () => {
// will just continue to fail // will just continue to fail
clearInterval(interval); clearInterval(interval);
}); });
}, 20000); }, 1000);
} }
return () => { return () => {
clearInterval(interval); clearInterval(interval);
mounted = false; mounted = false;
}; };
}, [client, decimals, pubKey, t, vegaToken.address]); }, [delegationsPagination, client, decimals, pubKey, t, vegaToken.address]);
return { delegations, currentStakeAvailable, delegatedNodes, accounts }; return { delegations, currentStakeAvailable, delegatedNodes, accounts };
}; };
+1
View File
@@ -65,6 +65,7 @@ export const ENV = {
docsUrl: windowOrDefault('NX_VEGA_DOCS_URL'), docsUrl: windowOrDefault('NX_VEGA_DOCS_URL'),
ethWalletMnemonic: windowOrDefault('NX_ETH_WALLET_MNEMONIC'), ethWalletMnemonic: windowOrDefault('NX_ETH_WALLET_MNEMONIC'),
localProviderUrl: windowOrDefault('NX_LOCAL_PROVIDER_URL'), localProviderUrl: windowOrDefault('NX_LOCAL_PROVIDER_URL'),
delegationsPagination: windowOrDefault('NX_DELEGATIONS_PAGINATION'),
rest: windowOrDefault('NX_VEGA_REST_URL'), rest: windowOrDefault('NX_VEGA_REST_URL'),
addresses: addresses:
ContractAddresses[(envName === 'local' ? 'CUSTOM' : envName) as Networks], ContractAddresses[(envName === 'local' ? 'CUSTOM' : envName) as Networks],
@@ -20,7 +20,6 @@ describe('getMultisigStatus', () => {
expect(result).toEqual({ expect(result).toEqual({
multisigStatus: MultisigStatus.noNodes, multisigStatus: MultisigStatus.noNodes,
showMultisigStatusError: true, showMultisigStatusError: true,
zeroScoreNodes: [],
}); });
}); });
@@ -36,7 +35,6 @@ describe('getMultisigStatus', () => {
expect(result).toEqual({ expect(result).toEqual({
multisigStatus: MultisigStatus.correct, multisigStatus: MultisigStatus.correct,
showMultisigStatusError: false, showMultisigStatusError: false,
zeroScoreNodes: [],
}); });
}); });
@@ -52,22 +50,6 @@ describe('getMultisigStatus', () => {
expect(result).toEqual({ expect(result).toEqual({
multisigStatus: MultisigStatus.nodeNeedsRemoving, multisigStatus: MultisigStatus.nodeNeedsRemoving,
showMultisigStatusError: true, showMultisigStatusError: true,
zeroScoreNodes: [
{
id: '1',
rewardScore: {
multisigScore: '0',
},
stakedTotal: '1000',
},
{
id: '2',
rewardScore: {
multisigScore: '0',
},
stakedTotal: '1000',
},
],
}); });
}); });
@@ -83,15 +65,6 @@ describe('getMultisigStatus', () => {
expect(result).toEqual({ expect(result).toEqual({
multisigStatus: MultisigStatus.nodeNeedsAdding, multisigStatus: MultisigStatus.nodeNeedsAdding,
showMultisigStatusError: true, showMultisigStatusError: true,
zeroScoreNodes: [
{
id: '1',
rewardScore: {
multisigScore: '0',
},
stakedTotal: '1000',
},
],
}); });
}); });
}); });
@@ -1,8 +1,5 @@
import { removePaginationWrapper } from '@vegaprotocol/utils'; import { removePaginationWrapper } from '@vegaprotocol/utils';
import type { import type { PreviousEpochQuery } from '../routes/staking/__generated__/PreviousEpoch';
PreviousEpochQuery,
ValidatorNodeFragment,
} from '../routes/staking/__generated__/PreviousEpoch';
export enum MultisigStatus { export enum MultisigStatus {
'correct' = 'correct', 'correct' = 'correct',
@@ -20,15 +17,12 @@ export const getMultisigStatusInfo = (
previousEpochData?.epoch.validatorsConnection?.edges previousEpochData?.epoch.validatorsConnection?.edges
); );
const zeroScore = (node: ValidatorNodeFragment) => const hasZero = allNodesInPreviousEpoch.some(
Number(node.rewardScore?.multisigScore) === 0; (node) => Number(node?.rewardScore?.multisigScore) === 0
const oneScore = (node: ValidatorNodeFragment) => );
Number(node.rewardScore?.multisigScore) === 1; const hasOne = allNodesInPreviousEpoch.some(
(node) => Number(node?.rewardScore?.multisigScore) === 1
const hasZero = allNodesInPreviousEpoch.some(zeroScore); );
const hasOne = allNodesInPreviousEpoch.some(oneScore);
const zeroScoreNodes = allNodesInPreviousEpoch.filter(zeroScore);
if (hasZero && hasOne) { if (hasZero && hasOne) {
// If any individual node has 0 it means that node is missing from the multisig and needs to be added // If any individual node has 0 it means that node is missing from the multisig and needs to be added
@@ -44,6 +38,5 @@ export const getMultisigStatusInfo = (
return { return {
showMultisigStatusError: status !== MultisigStatus.correct, showMultisigStatusError: status !== MultisigStatus.correct,
multisigStatus: status, multisigStatus: status,
zeroScoreNodes,
}; };
}; };
+24 -19
View File
@@ -1,4 +1,3 @@
import compact from 'lodash/compact';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@@ -13,10 +12,10 @@ import { useRefreshAfterEpoch } from '../../hooks/use-refresh-after-epoch';
import { ProposalsListItem } from '../proposals/components/proposals-list-item'; import { ProposalsListItem } from '../proposals/components/proposals-list-item';
import { ProtocolUpgradeProposalsListItem } from '../proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item'; import { ProtocolUpgradeProposalsListItem } from '../proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item';
import Routes from '../routes'; import Routes from '../routes';
import { ExternalLinks } from '@vegaprotocol/environment'; import { ExternalLinks, useFeatureFlags } from '@vegaprotocol/environment';
import { removePaginationWrapper } from '@vegaprotocol/utils'; import { removePaginationWrapper } from '@vegaprotocol/utils';
import { useNodesQuery } from '../staking/home/__generated__/Nodes'; import { useNodesQuery } from '../staking/home/__generated__/Nodes';
import { useProposalsQuery } from '../proposals/__generated__/Proposals'; import { useProposalsQuery } from '../proposals/proposals/__generated__/Proposals';
import { import {
getNotRejectedProposals, getNotRejectedProposals,
getNotRejectedProtocolUpgradeProposals, getNotRejectedProtocolUpgradeProposals,
@@ -32,7 +31,7 @@ import {
orderByUpgradeBlockHeight, orderByUpgradeBlockHeight,
} from '../proposals/components/proposals-list/proposals-list'; } from '../proposals/components/proposals-list/proposals-list';
import { BigNumber } from '../../lib/bignumber'; import { BigNumber } from '../../lib/bignumber';
import { type Proposal, type BatchProposal } from '../proposals/types'; import { type Proposal } from '../proposals/types';
const nodesToShow = 6; const nodesToShow = 6;
@@ -40,7 +39,7 @@ const HomeProposals = ({
proposals, proposals,
protocolUpgradeProposals, protocolUpgradeProposals,
}: { }: {
proposals: Array<Proposal | BatchProposal>; proposals: Proposal[];
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[]; protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -61,9 +60,12 @@ const HomeProposals = ({
<ProtocolUpgradeProposalsListItem key={index} proposal={proposal} /> <ProtocolUpgradeProposalsListItem key={index} proposal={proposal} />
))} ))}
{compact(proposals).map((proposal) => { {proposals.map(
return <ProposalsListItem key={proposal.id} proposal={proposal} />; (proposal) =>
})} proposal?.id && (
<ProposalsListItem key={proposal.id} proposal={proposal} />
)
)}
</ul> </ul>
<div className="mt-6"> <div className="mt-6">
@@ -173,6 +175,7 @@ export const ValidatorDetailsLink = ({
}; };
const GovernanceHome = ({ name }: RouteChildProps) => { const GovernanceHome = ({ name }: RouteChildProps) => {
const featureFlags = useFeatureFlags((state) => state.flags);
useDocumentTitle(name); useDocumentTitle(name);
const { t } = useTranslation(); const { t } = useTranslation();
const { const {
@@ -183,6 +186,11 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
pollInterval: 5000, pollInterval: 5000,
fetchPolicy: 'network-only', fetchPolicy: 'network-only',
errorPolicy: 'ignore', errorPolicy: 'ignore',
variables: {
includeNewMarketProductFields: !!featureFlags.PRODUCT_PERPETUALS,
includeUpdateMarketStates: !!featureFlags.UPDATE_MARKET_STATE,
includeUpdateReferralPrograms: !!featureFlags.REFERRALS,
},
}); });
const { const {
@@ -204,18 +212,15 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
useRefreshAfterEpoch(validatorsData?.epoch.timestamps.expiry, refetch); useRefreshAfterEpoch(validatorsData?.epoch.timestamps.expiry, refetch);
const proposals = useMemo(() => { const proposals = useMemo(
if (!proposalsData?.proposalsConnection?.edges?.length) return []; () =>
return proposalsData proposalsData
? getNotRejectedProposals( ? getNotRejectedProposals(
compact( removePaginationWrapper(proposalsData.proposalsConnection?.edges)
proposalsData.proposalsConnection.edges.map(
(edge) => edge?.proposalNode
)
) )
) : [],
: []; [proposalsData]
}, [proposalsData]); );
const sortedProposals = useMemo( const sortedProposals = useMemo(
() => orderByDate(proposals).reverse(), () => orderByDate(proposals).reverse(),
@@ -1,493 +0,0 @@
fragment UpdateMarketStates on UpdateMarketState {
__typename
updateType
market {
decimalPlaces
id
tradableInstrument {
instrument {
product {
__typename
... on Future {
quoteName
}
... on Perpetual {
quoteName
}
}
name
code
}
}
}
updateType
price
}
fragment UpdateReferralPrograms on UpdateReferralProgram {
__typename
benefitTiers {
minimumEpochs
minimumRunningNotionalTakerVolume
referralDiscountFactor
referralRewardFactor
}
endOfProgram: endOfProgramTimestamp
windowLength
stakingTiers {
minimumStakedTokens
referralRewardMultiplier
}
}
fragment UpdateVolumeDiscountPrograms on UpdateVolumeDiscountProgram {
__typename
benefitTiers {
minimumRunningNotionalTakerVolume
volumeDiscountFactor
}
endOfProgramTimestamp
windowLength
}
# I prefix due to clash in libs/proposals
fragment IUpdateMarketFields on UpdateMarket {
__typename
marketId
updateMarketConfiguration {
instrument {
code
product {
... on UpdateFutureProduct {
quoteName
dataSourceSpecForSettlementData {
sourceType {
... on DataSourceDefinitionInternal {
sourceType {
... on DataSourceSpecConfigurationTime {
conditions {
operator
value
}
}
}
}
... on DataSourceDefinitionExternal {
sourceType {
... on DataSourceSpecConfiguration {
signers {
signer {
... on PubKey {
key
}
... on ETHAddress {
address
}
}
}
filters {
key {
name
type
}
conditions {
operator
value
}
}
}
}
}
}
}
# dataSourceSpecForTradingTermination {
# sourceType {
# ... on DataSourceDefinitionInternal {
# sourceType {
# ... on DataSourceSpecConfigurationTime {
# conditions {
# operator
# value
# }
# }
# }
# }
# ... on DataSourceDefinitionExternal {
# sourceType {
# ... on DataSourceSpecConfiguration {
# signers {
# signer {
# ... on PubKey {
# key
# }
# ... on ETHAddress {
# address
# }
# }
# }
# filters {
# key {
# name
# type
# }
# conditions {
# operator
# value
# }
# }
# }
# }
# }
# }
# }
dataSourceSpecBinding {
settlementDataProperty
tradingTerminationProperty
}
}
... on UpdatePerpetualProduct {
quoteName
dataSourceSpecForSettlementData {
sourceType {
... on DataSourceDefinitionInternal {
sourceType {
... on DataSourceSpecConfigurationTime {
conditions {
operator
value
}
}
}
}
... on DataSourceDefinitionExternal {
sourceType {
... on DataSourceSpecConfiguration {
signers {
signer {
... on PubKey {
key
}
... on ETHAddress {
address
}
}
}
filters {
key {
name
type
}
conditions {
operator
value
}
}
}
}
}
}
}
dataSourceSpecBinding {
settlementDataProperty
settlementScheduleProperty
}
}
}
}
metadata
priceMonitoringParameters {
triggers {
horizonSecs
probability
auctionExtensionSecs
}
}
liquidityMonitoringParameters {
targetStakeParameters {
timeWindow
scalingFactor
}
}
riskParameters {
... on UpdateMarketSimpleRiskModel {
simple {
factorLong
factorShort
}
}
... on UpdateMarketLogNormalRiskModel {
logNormal {
riskAversionParameter
tau
params {
r
sigma
mu
}
}
}
}
}
}
# I prefix due to clash in libs/proposals
fragment INewMarketFields on NewMarket {
__typename
decimalPlaces
metadata
riskParameters {
... on LogNormalRiskModel {
riskAversionParameter
tau
params {
mu
r
sigma
}
}
... on SimpleRiskModel {
params {
factorLong
factorShort
}
}
}
successorConfiguration {
parentMarketId
}
instrument {
name
code
product {
... on FutureProduct {
settlementAsset {
id
name
symbol
decimals
quantum
}
quoteName
dataSourceSpecBinding {
settlementDataProperty
tradingTerminationProperty
}
dataSourceSpecForSettlementData {
sourceType {
... on DataSourceDefinitionInternal {
sourceType {
... on DataSourceSpecConfigurationTime {
conditions {
operator
value
}
}
}
}
... on DataSourceDefinitionExternal {
sourceType {
... on DataSourceSpecConfiguration {
signers {
signer {
... on PubKey {
key
}
... on ETHAddress {
address
}
}
}
filters {
key {
name
type
}
conditions {
operator
value
}
}
}
}
}
}
}
}
... on PerpetualProduct {
settlementAsset {
id
name
symbol
decimals
quantum
}
quoteName
}
}
}
priceMonitoringParameters {
triggers {
horizonSecs
probability
auctionExtensionSecs
}
}
liquidityMonitoringParameters {
targetStakeParameters {
timeWindow
scalingFactor
}
}
positionDecimalPlaces
linearSlippageFactor
}
# I prefix due to clash in lib/proposals
fragment INewAssetFields on NewAsset {
__typename
name
symbol
decimals
quantum
source {
... on BuiltinAsset {
maxFaucetAmountMint
}
... on ERC20 {
contractAddress
withdrawThreshold
lifetimeLimit
}
}
}
# I prefix due to clash in libs/proposals
fragment IUpdateAssetFields on UpdateAsset {
__typename
assetId
quantum
source {
... on UpdateERC20 {
lifetimeLimit
withdrawThreshold
}
}
}
# I prefix due to clash in libs/proposals
fragment IUpdateNetworkParameterFields on UpdateNetworkParameter {
__typename
networkParameter {
key
value
}
}
fragment VoteFields on ProposalVotes {
yes {
totalTokens
totalNumber
totalEquityLikeShareWeight
}
no {
totalTokens
totalNumber
totalEquityLikeShareWeight
}
}
fragment ProposalTermsFields on ProposalTerms {
closingDatetime
enactmentDatetime
change {
__typename
...UpdateMarketStates
...UpdateReferralPrograms
...UpdateVolumeDiscountPrograms
...INewMarketFields
...IUpdateMarketFields
...INewAssetFields
...IUpdateNetworkParameterFields
...IUpdateAssetFields
}
}
fragment ProposalFields on Proposal {
id
rationale {
title
description
}
reference
state
datetime
rejectionReason
party {
id
}
errorDetails
terms {
...ProposalTermsFields
}
votes {
...VoteFields
}
}
fragment BatchProposalFields on BatchProposal {
id
rationale {
title
description
}
reference
state
datetime
rejectionReason
party {
id
}
errorDetails
batchTerms {
closingDatetime
changes {
enactmentDatetime
}
}
subProposals {
datetime
terms {
...ProposalTermsFields
}
}
votes {
...VoteFields
}
}
query Proposals {
proposalsConnection {
edges {
proposalNode {
__typename
... on Proposal {
...ProposalFields
}
... on BatchProposal {
...BatchProposalFields
}
}
}
}
}
query Proposal($proposalId: ID!) {
proposal(id: $proposalId) {
... on Proposal {
...ProposalFields
}
... on BatchProposal {
...BatchProposalFields
}
}
}
File diff suppressed because one or more lines are too long
@@ -3,13 +3,9 @@ import { ProposalState } from '@vegaprotocol/types';
import { ProposalInfoLabel } from '../proposal-info-label'; import { ProposalInfoLabel } from '../proposal-info-label';
import { type ReactNode } from 'react'; import { type ReactNode } from 'react';
import { type ProposalInfoLabelVariant } from '../proposal-info-label'; import { type ProposalInfoLabelVariant } from '../proposal-info-label';
import { type Proposal, type BatchProposal } from '../../types'; import { type Proposal } from '../../types';
export const CurrentProposalState = ({ export const CurrentProposalState = ({ proposal }: { proposal: Proposal }) => {
proposal,
}: {
proposal: Proposal | BatchProposal;
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
let proposalStatus: ReactNode; let proposalStatus: ReactNode;
let variant = 'tertiary' as ProposalInfoLabelVariant; let variant = 'tertiary' as ProposalInfoLabelVariant;
@@ -79,22 +79,13 @@ export const ListAsset = ({
) { ) {
return null; return null;
} }
if (data.asset.source.__typename !== 'ERC20') return null;
if (data.asset.source.__typename !== 'ERC20') {
return null;
}
if (data.asset.status !== Schema.AssetStatus.STATUS_PENDING_LISTING) { if (data.asset.status !== Schema.AssetStatus.STATUS_PENDING_LISTING) {
return null; return null;
} }
if (errorAsset || errorBundle) return null;
if (errorAsset || errorBundle) {
return null;
}
const { assetSource, signatures, vegaAssetId, nonce } = const { assetSource, signatures, vegaAssetId, nonce } =
assetData.erc20ListAssetBundle; assetData.erc20ListAssetBundle;
return ( return (
<div className="mb-8"> <div className="mb-8">
<h3 className="mb-2 text-xl">{t('ListAsset')}</h3> <h3 className="mb-2 text-xl">{t('ListAsset')}</h3>
@@ -2,53 +2,19 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SubHeading } from '../../../../components/heading'; import { SubHeading } from '../../../../components/heading';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle'; import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import { import { AssetDetail, AssetDetailsTable } from '@vegaprotocol/assets';
AssetDetail, import type { AssetFieldsFragment } from '@vegaprotocol/assets';
AssetDetailsTable,
useAssetQuery,
} from '@vegaprotocol/assets';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import {
type INewAssetFieldsFragment,
type IUpdateAssetFieldsFragment,
} from '../../__generated__/Proposals';
export const ProposalAssetDetails = ({ export const ProposalAssetDetails = ({
change, asset,
assetId, originalAsset,
}: { }: {
change: IUpdateAssetFieldsFragment | INewAssetFieldsFragment; asset: AssetFieldsFragment;
assetId: string; originalAsset?: AssetFieldsFragment;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [showAssetDetails, setShowAssetDetails] = useState(false); const [showAssetDetails, setShowAssetDetails] = useState(false);
const { data } = useAssetQuery({
fetchPolicy: 'network-only',
variables: {
assetId,
},
});
if (!data) return null;
let asset = removePaginationWrapper(data?.assetsConnection?.edges)[0];
const originalAsset = asset;
if (change.__typename === 'UpdateAsset') {
asset = {
...asset,
quantum: change.quantum,
source: { ...asset.source },
};
if (asset.source.__typename === 'ERC20') {
asset.source.lifetimeLimit = change.source.lifetimeLimit;
asset.source.withdrawThreshold = change.source.withdrawThreshold;
}
}
return ( return (
<section data-testid="proposal-asset-details"> <section data-testid="proposal-asset-details">
<CollapsibleToggle <CollapsibleToggle
@@ -6,46 +6,16 @@ import {
KeyValueTableRow, KeyValueTableRow,
RoundedWrapper, RoundedWrapper,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { type Proposal, type BatchProposal } from '../../types'; import { type Proposal } from '../../types';
interface ProposalChangeTableProps { interface ProposalChangeTableProps {
proposal: Proposal | BatchProposal; proposal: Proposal;
} }
export const ProposalChangeTable = ({ proposal }: ProposalChangeTableProps) => { export const ProposalChangeTable = ({ proposal }: ProposalChangeTableProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const closingTimeRow = const terms = proposal?.terms;
proposal.__typename === 'Proposal' ? (
<KeyValueTableRow>
{isFuture(new Date(proposal.terms?.closingDatetime))
? t('closesOn')
: t('closedOn')}
{formatDateWithLocalTimezone(new Date(proposal.terms?.closingDatetime))}
</KeyValueTableRow>
) : proposal.__typename === 'BatchProposal' ? (
<KeyValueTableRow>
{isFuture(new Date(proposal.batchTerms?.closingDatetime))
? t('closesOn')
: t('closedOn')}
{formatDateWithLocalTimezone(
new Date(proposal.batchTerms?.closingDatetime)
)}
</KeyValueTableRow>
) : null;
const enactmentRow =
proposal.__typename === 'Proposal' &&
proposal.terms.change.__typename !== 'NewFreeform' ? (
<KeyValueTableRow>
{isFuture(new Date(proposal.terms?.enactmentDatetime || 0))
? t('proposedEnactment')
: t('enactedOn')}
{formatDateWithLocalTimezone(
new Date(proposal.terms?.enactmentDatetime || 0)
)}
</KeyValueTableRow>
) : null;
return ( return (
<RoundedWrapper paddingBottom={true}> <RoundedWrapper paddingBottom={true}>
@@ -54,8 +24,22 @@ export const ProposalChangeTable = ({ proposal }: ProposalChangeTableProps) => {
{t('id')} {t('id')}
{proposal?.id} {proposal?.id}
</KeyValueTableRow> </KeyValueTableRow>
{closingTimeRow} <KeyValueTableRow>
{enactmentRow} {isFuture(new Date(terms?.closingDatetime))
? t('closesOn')
: t('closedOn')}
{formatDateWithLocalTimezone(new Date(terms?.closingDatetime))}
</KeyValueTableRow>
{terms?.change.__typename !== 'NewFreeform' ? (
<KeyValueTableRow>
{isFuture(new Date(terms?.enactmentDatetime || 0))
? t('proposedEnactment')
: t('enactedOn')}
{formatDateWithLocalTimezone(
new Date(terms?.enactmentDatetime || 0)
)}
</KeyValueTableRow>
) : null}
<KeyValueTableRow> <KeyValueTableRow>
{t('proposedBy')} {t('proposedBy')}
<span style={{ wordBreak: 'break-word' }}>{proposal?.party.id}</span> <span style={{ wordBreak: 'break-word' }}>{proposal?.party.id}</span>
@@ -18,21 +18,20 @@ import {
nextWeek, nextWeek,
mockWalletContext, mockWalletContext,
createUserVoteQueryMock, createUserVoteQueryMock,
networkParamsQueryMock,
} from '../../test-helpers/mocks'; } from '../../test-helpers/mocks';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import { VoteState } from '../vote-details/use-user-vote'; import { VoteState } from '../vote-details/use-user-vote';
import { import { useNewTransferProposalDetails } from '@vegaprotocol/proposals';
InstrumentDetailsDocument,
useNewTransferProposalDetails,
type InstrumentDetailsQuery,
type InstrumentDetailsQueryVariables,
} from '@vegaprotocol/proposals';
import { type MockedResponse } from '@apollo/client/testing'; import { type MockedResponse } from '@apollo/client/testing';
import { type Proposal } from '../../types'; import { type Proposal } from '../../types';
jest.mock('@vegaprotocol/proposals', () => ({ jest.mock('@vegaprotocol/proposals', () => ({
...jest.requireActual('@vegaprotocol/proposals'), ...jest.requireActual('@vegaprotocol/proposals'),
useSuccessorMarketProposalDetails: () => ({
code: 'PARENT_CODE',
parentMarketId: 'PARENT_ID',
}),
useNewTransferProposalDetails: jest.fn(), useNewTransferProposalDetails: jest.fn(),
})); }));
@@ -45,7 +44,7 @@ const renderComponent = (
render( render(
<AppStateProvider> <AppStateProvider>
<BrowserRouter> <BrowserRouter>
<MockedProvider mocks={[networkParamsQueryMock, ...mocks]}> <MockedProvider mocks={mocks}>
<VegaWalletContext.Provider value={mockWalletContext}> <VegaWalletContext.Provider value={mockWalletContext}>
<ProposalHeader <ProposalHeader
proposal={proposal} proposal={proposal}
@@ -62,39 +61,10 @@ describe('Proposal header', () => {
afterAll(() => { afterAll(() => {
jest.clearAllMocks(); jest.clearAllMocks();
}); });
it('Renders New market proposal', () => {
it('Renders New market proposal', async () => { useFeatureFlags.setState({ flags: { SUCCESSOR_MARKETS: true } });
const parentMarketId = 'parent-id';
const parentCode = 'parent-code';
const parentName = 'parent-name';
const mock: MockedResponse<
InstrumentDetailsQuery,
InstrumentDetailsQueryVariables
> = {
request: {
query: InstrumentDetailsDocument,
variables: {
marketId: parentMarketId,
},
},
result: {
data: {
market: {
__typename: 'Market',
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
code: parentCode,
name: parentName,
},
},
},
},
},
};
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
rationale: { rationale: {
title: 'New some market', title: 'New some market',
@@ -103,9 +73,6 @@ describe('Proposal header', () => {
terms: { terms: {
change: { change: {
__typename: 'NewMarket', __typename: 'NewMarket',
successorConfiguration: {
parentMarketId,
},
instrument: { instrument: {
__typename: 'InstrumentConfiguration', __typename: 'InstrumentConfiguration',
name: 'Some market', name: 'Some market',
@@ -120,9 +87,7 @@ describe('Proposal header', () => {
}, },
}, },
}, },
}), })
undefined,
[mock]
); );
expect(screen.getByTestId('proposal-title')).toHaveTextContent( expect(screen.getByTestId('proposal-title')).toHaveTextContent(
'New some market' 'New some market'
@@ -131,13 +96,14 @@ describe('Proposal header', () => {
expect(screen.getByTestId('proposal-details')).toHaveTextContent( expect(screen.getByTestId('proposal-details')).toHaveTextContent(
'tGBP settled future.' 'tGBP settled future.'
); );
expect( expect(screen.getByTestId('proposal-successor-info')).toHaveTextContent(
await screen.findByTestId('proposal-successor-info') 'PARENT_CODE'
).toHaveTextContent(parentCode); );
}); });
it('Renders Update market proposal', () => { it('Renders Update market proposal', () => {
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
rationale: { rationale: {
title: 'New market id', title: 'New market id',
@@ -160,12 +126,13 @@ describe('Proposal header', () => {
screen.queryByTestId('proposal-description') screen.queryByTestId('proposal-description')
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
expect(screen.getByTestId('proposal-details')).toHaveTextContent( expect(screen.getByTestId('proposal-details')).toHaveTextContent(
'Update to market: MarketId' 'Update to market ID: MarketId'
); );
}); });
it('Renders New asset proposal - ERC20', () => { it('Renders New asset proposal - ERC20', () => {
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
rationale: { rationale: {
title: 'New asset: Fake currency', title: 'New asset: Fake currency',
@@ -195,10 +162,8 @@ describe('Proposal header', () => {
it('Renders New asset proposal - BuiltInAsset', () => { it('Renders New asset proposal - BuiltInAsset', () => {
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
rationale: {
title: 'New asset',
},
terms: { terms: {
change: { change: {
__typename: 'NewAsset', __typename: 'NewAsset',
@@ -212,7 +177,9 @@ describe('Proposal header', () => {
}, },
}) })
); );
expect(screen.getByTestId('proposal-title')).toHaveTextContent('New asset'); expect(screen.getByTestId('proposal-title')).toHaveTextContent(
'New asset proposal'
);
expect(screen.getByTestId('proposal-type')).toHaveTextContent('New asset'); expect(screen.getByTestId('proposal-type')).toHaveTextContent('New asset');
expect(screen.getByTestId('proposal-details')).toHaveTextContent( expect(screen.getByTestId('proposal-details')).toHaveTextContent(
'Symbol: BIA. Max faucet amount mint: 300' 'Symbol: BIA. Max faucet amount mint: 300'
@@ -221,6 +188,7 @@ describe('Proposal header', () => {
it('Renders Update network', () => { it('Renders Update network', () => {
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
rationale: { rationale: {
title: 'Network parameter', title: 'Network parameter',
@@ -250,6 +218,7 @@ describe('Proposal header', () => {
it('Renders Freeform proposal - short rationale', () => { it('Renders Freeform proposal - short rationale', () => {
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
id: 'short', id: 'short',
rationale: { rationale: {
@@ -271,6 +240,7 @@ describe('Proposal header', () => {
it('Renders Freeform proposal - long rationale (105 chars) - listing', () => { it('Renders Freeform proposal - long rationale (105 chars) - listing', () => {
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
id: 'long', id: 'long',
rationale: { rationale: {
@@ -296,6 +266,7 @@ describe('Proposal header', () => {
// Remove once proposals have rationale and re-enable above tests // Remove once proposals have rationale and re-enable above tests
it('Renders Freeform proposal - id for title', () => { it('Renders Freeform proposal - id for title', () => {
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
id: 'freeform id', id: 'freeform id',
rationale: { rationale: {
@@ -352,6 +323,7 @@ describe('Proposal header', () => {
it('Renders proposal state: Enacted', () => { it('Renders proposal state: Enacted', () => {
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
state: ProposalState.STATE_ENACTED, state: ProposalState.STATE_ENACTED,
terms: { terms: {
@@ -364,6 +336,7 @@ describe('Proposal header', () => {
it('Renders proposal state: Passed', () => { it('Renders proposal state: Passed', () => {
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
state: ProposalState.STATE_PASSED, state: ProposalState.STATE_PASSED,
terms: { terms: {
@@ -377,6 +350,7 @@ describe('Proposal header', () => {
it('Renders proposal state: Waiting for node vote', () => { it('Renders proposal state: Waiting for node vote', () => {
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
state: ProposalState.STATE_WAITING_FOR_NODE_VOTE, state: ProposalState.STATE_WAITING_FOR_NODE_VOTE,
terms: { terms: {
@@ -391,6 +365,7 @@ describe('Proposal header', () => {
it('Renders proposal state: Open', () => { it('Renders proposal state: Open', () => {
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
state: ProposalState.STATE_OPEN, state: ProposalState.STATE_OPEN,
votes: { votes: {
@@ -459,6 +434,8 @@ describe('Proposal header', () => {
}); });
}); });
jest.mock('@vegaprotocol/proposals');
describe('<NewTransferSummary />', () => { describe('<NewTransferSummary />', () => {
it('renders null if no details are provided', () => { it('renders null if no details are provided', () => {
(useNewTransferProposalDetails as jest.Mock).mockReturnValue(null); (useNewTransferProposalDetails as jest.Mock).mockReturnValue(null);
@@ -1,4 +1,4 @@
import { Trans, useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
CopyWithTooltip, CopyWithTooltip,
Lozenge, Lozenge,
@@ -8,13 +8,14 @@ import {
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { shorten } from '@vegaprotocol/utils'; import { shorten } from '@vegaprotocol/utils';
import { Heading, SubHeading } from '../../../../components/heading'; import { Heading, SubHeading } from '../../../../components/heading';
import { type ReactNode } from 'react';
import { truncateMiddle } from '../../../../lib/truncate-middle'; import { truncateMiddle } from '../../../../lib/truncate-middle';
import { CurrentProposalState } from '../current-proposal-state'; import { CurrentProposalState } from '../current-proposal-state';
import { ProposalInfoLabel } from '../proposal-info-label'; import { ProposalInfoLabel } from '../proposal-info-label';
import { import {
useCancelTransferProposalDetails, useCancelTransferProposalDetails,
useInstrumentDetailsQuery,
useNewTransferProposalDetails, useNewTransferProposalDetails,
useSuccessorMarketProposalDetails,
} from '@vegaprotocol/proposals'; } from '@vegaprotocol/proposals';
import { import {
CONSOLE_MARKET_PAGE, CONSOLE_MARKET_PAGE,
@@ -26,542 +27,217 @@ import Routes from '../../../routes';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { type VoteState } from '../vote-details/use-user-vote'; import { type VoteState } from '../vote-details/use-user-vote';
import { VoteBreakdown } from '../vote-breakdown'; import { VoteBreakdown } from '../vote-breakdown';
import { import { GovernanceTransferKindMapping } from '@vegaprotocol/types';
GovernanceTransferKindMapping, import { type Proposal } from '../../types';
type ProposalRejectionReason,
ProposalRejectionReasonMapping,
ProposalState,
} from '@vegaprotocol/types';
import { type Proposal, type BatchProposal } from '../../types';
import { type ProposalTermsFieldsFragment } from '../../__generated__/Proposals';
import { differenceInHours, format, formatDistanceToNowStrict } from 'date-fns';
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
import { MarketName } from '../proposal/market-name';
import { Indicator } from '../proposal/indicator';
const ProposalTypeTags = ({
proposal,
}: {
proposal: Proposal | BatchProposal;
}) => {
if (proposal.__typename === 'Proposal') {
return (
<div data-testid="proposal-type">
<ProposalTypeTag terms={proposal.terms} />
</div>
);
}
if (proposal.__typename === 'BatchProposal') {
return (
<div data-testid="proposal-type">
<ProposalInfoLabel variant="secondary">BatchProposal</ProposalInfoLabel>
</div>
);
}
return null;
};
const ProposalTypeTag = ({ terms }: { terms: ProposalTermsFieldsFragment }) => {
const { t } = useTranslation();
switch (terms.change.__typename) {
// Speical case for markets where we want to show the product type in the tag
case 'NewMarket': {
return (
<ProposalInfoLabel variant="secondary">
{t(
terms.change?.instrument?.product?.__typename
? `NewMarket${terms.change.instrument.product.__typename}`
: 'NewMarket'
)}
</ProposalInfoLabel>
);
}
default: {
return (
<ProposalInfoLabel variant="secondary">
{t(terms.change.__typename)}
</ProposalInfoLabel>
);
}
}
};
const ProposalDetails = ({
proposal,
}: {
proposal: Proposal | BatchProposal;
}) => {
const { t } = useTranslation();
const featureFlags = useFeatureFlags((store) => store.flags);
const consoleLink = useLinks(DApp.Console);
const renderDetails = (terms: ProposalTermsFieldsFragment) => {
switch (terms.change?.__typename) {
case 'NewMarket': {
const getAsset = (terms: ProposalTermsFieldsFragment) => {
if (
terms?.change.__typename === 'NewMarket' &&
(terms.change.instrument.product?.__typename === 'FutureProduct' ||
terms.change.instrument.product?.__typename ===
'PerpetualProduct')
) {
return terms.change.instrument.product.settlementAsset;
}
return undefined;
};
return (
<>
{terms.change.successorConfiguration && (
<ParentMarketCode
parentMarketId={
terms.change.successorConfiguration.parentMarketId
}
/>
)}
<span>
{t('Code')}: {terms.change.instrument.code}.
</span>{' '}
{terms && getAsset(terms)?.symbol ? (
<>
<span className="font-semibold">{getAsset(terms)?.symbol}</span>{' '}
{t('settled future')}.
</>
) : (
''
)}
</>
);
}
case 'UpdateMarketState': {
const marketPageLink = consoleLink(
CONSOLE_MARKET_PAGE.replace(':marketId', terms.change.market.id)
);
return (
<span>
{featureFlags.UPDATE_MARKET_STATE &&
terms.change?.market?.id &&
terms.change.updateType ? (
<>
<span>{t(terms.change.updateType)}: </span>
<span className="inline-flex gap-2">
<span className="break-all">
<MarketName marketId={terms.change.market.id} />
</span>
<span className="inline-flex items-end gap-0">
<CopyWithTooltip
text={terms.change.market.id}
description={t('copyId')}
>
<button className="inline-block px-1">
<VegaIcon size={20} name={VegaIconNames.COPY} />
</button>
</CopyWithTooltip>
<Tooltip description={t('OpenInConsole')} align="center">
<Link
className="inline-block px-1"
to={marketPageLink}
target="_blank"
>
<VegaIcon
size={20}
name={VegaIconNames.OPEN_EXTERNAL}
/>
</Link>
</Tooltip>
</span>
</span>
</>
) : null}
</span>
);
}
case 'UpdateMarket': {
const marketPageLink = consoleLink(
CONSOLE_MARKET_PAGE.replace(':marketId', terms.change.marketId)
);
return (
<>
<span>{t('UpdateToMarket')}: </span>
<span className="inline-flex items-start gap-2">
<span className="break-all">
<MarketName marketId={terms.change.marketId} />
</span>
<span className="inline-flex items-end gap-0">
<CopyWithTooltip
text={terms.change.marketId}
description={t('copyId')}
>
<button className="inline-block px-1">
<VegaIcon size={20} name={VegaIconNames.COPY} />
</button>
</CopyWithTooltip>
<Tooltip description={t('OpenInConsole')} align="center">
<Link
className="inline-block px-1"
target="_blank"
to={marketPageLink}
>
<VegaIcon size={20} name={VegaIconNames.OPEN_EXTERNAL} />
</Link>
</Tooltip>
</span>
</span>
</>
);
}
case 'UpdateReferralProgram': {
return null;
}
case 'UpdateVolumeDiscountProgram': {
return null;
}
case 'NewAsset': {
return (
<>
<span>{t('Symbol')}:</span>{' '}
<Lozenge>{terms.change.symbol}.</Lozenge>{' '}
{terms.change.source.__typename === 'ERC20' && (
<>
<span>{t('ERC20ContractAddress')}:</span>{' '}
<Lozenge>{terms.change.source.contractAddress}</Lozenge>
</>
)}{' '}
{terms.change.source.__typename === 'BuiltinAsset' && (
<>
<span>{t('MaxFaucetAmountMint')}:</span>{' '}
<Lozenge>{terms.change.source.maxFaucetAmountMint}</Lozenge>
</>
)}
</>
);
}
case 'UpdateNetworkParameter': {
return (
<Trans
i18nKey="Change <lozenge>{{key}}</lozenge> to <lozenge>{{value}}</lozenge>"
values={{
key: terms.change.networkParameter.key,
value: terms.change.networkParameter.value,
}}
components={{
// @ts-ignore children passed by i18next
lozenge: <Lozenge className="text-xs" />,
}}
/>
);
}
case 'NewFreeform': {
return <span />;
}
case 'UpdateAsset': {
return (
<Trans
i18nKey="Asset ID: <lozenge>{{id}}</lozenge>"
values={{
id: truncateMiddle(terms.change.assetId),
}}
components={{
// @ts-ignore children passed by i18next
lozenge: <Lozenge />,
}}
/>
);
}
case 'NewTransfer':
return featureFlags.GOVERNANCE_TRANSFERS ? (
<NewTransferSummary proposalId={proposal?.id} />
) : null;
case 'CancelTransfer':
return featureFlags.GOVERNANCE_TRANSFERS ? (
<CancelTransferSummary proposalId={proposal?.id} />
) : null;
default: {
return null;
}
}
};
let details = null;
if (proposal.__typename === 'Proposal') {
details = (
<div>
<div>{renderDetails(proposal.terms)}</div>
<VoteStateText
state={proposal.state}
closingDatetime={proposal.terms.closingDatetime}
enactmentDatetime={proposal.terms.enactmentDatetime}
rejectionReason={proposal.rejectionReason}
/>
</div>
);
}
if (proposal.__typename === 'BatchProposal' && proposal.subProposals) {
details = (
<div>
<h3 className="text-xl border-b border-default pb-3 mb-3">
Proposals in batch
</h3>
<ul className="flex flex-col gap-2 border-b border-default pb-3 mb-3">
{proposal.subProposals.map((p, i) => {
if (!p?.terms) return null;
return (
<li
key={i}
className="grid grid-cols-[40px_minmax(0,1fr)] grid-rows-1 gap-3 items-center"
>
<Indicator indicator={i + 1} />
<span>
<div>{renderDetails(p.terms)}</div>
<SubProposalStateText
state={proposal.state}
enactmentDatetime={p.terms.enactmentDatetime}
/>
</span>
</li>
);
})}
</ul>
<BatchProposalStateText
state={proposal.state}
closingDatetime={proposal.batchTerms?.closingDatetime}
rejectionReason={proposal.rejectionReason}
/>
</div>
);
}
return (
<div
data-testid="proposal-details"
className="break-words mb-6 text-vega-light-200"
>
{details}
</div>
);
};
const VoteStateText = ({
state,
closingDatetime,
enactmentDatetime,
rejectionReason,
}: {
state: ProposalState;
closingDatetime: string;
enactmentDatetime: string;
rejectionReason: ProposalRejectionReason | null | undefined;
}) => {
const { t } = useTranslation();
const nowToCloseInHours = differenceInHours(
new Date(closingDatetime),
new Date()
);
const props = {
'data-testid': 'vote-details',
};
switch (state) {
case ProposalState.STATE_ENACTED: {
return (
<p {...props}>
{t('enactedOn{{date}}', {
enactmentDate:
enactmentDatetime &&
format(new Date(enactmentDatetime), DATE_FORMAT_DETAILED),
})}
</p>
);
}
case ProposalState.STATE_PASSED:
case ProposalState.STATE_WAITING_FOR_NODE_VOTE: {
return (
<p {...props}>
{t('enactsOn{{date}}', {
enactmentDate:
enactmentDatetime &&
format(new Date(enactmentDatetime), DATE_FORMAT_DETAILED),
})}
</p>
);
}
case ProposalState.STATE_OPEN: {
return (
<p {...props}>
<span className={nowToCloseInHours < 6 ? 'text-vega-orange' : ''}>
{t('{{time}} left to vote', {
time: formatDistanceToNowStrict(new Date(closingDatetime)),
})}
</span>
</p>
);
}
case ProposalState.STATE_DECLINED: {
return <p {...props}>{t(state)}</p>;
}
case ProposalState.STATE_REJECTED: {
const props = { 'data-testid': 'vote-status' };
if (rejectionReason) {
return (
<p {...props}>{t(ProposalRejectionReasonMapping[rejectionReason])}</p>
);
}
return <p {...props}>{t('Proposal rejected')}</p>;
}
default: {
return null;
}
}
};
/**
* Renders state details relevant to the sub proposal, namely the enactment
* date and time
*/
const SubProposalStateText = ({
state,
enactmentDatetime,
}: {
state: ProposalState;
enactmentDatetime: string;
}) => {
const { t } = useTranslation();
const props = {
'data-testid': 'vote-details',
className: 'm-0',
};
switch (state) {
case ProposalState.STATE_ENACTED: {
return (
<p {...props}>
{t('enactedOn{{date}}', {
enactmentDate:
enactmentDatetime &&
format(new Date(enactmentDatetime), DATE_FORMAT_DETAILED),
})}
</p>
);
}
case ProposalState.STATE_OPEN:
case ProposalState.STATE_PASSED:
case ProposalState.STATE_WAITING_FOR_NODE_VOTE: {
return (
<p {...props}>
{t('enactsOn{{date}}', {
enactmentDate:
enactmentDatetime &&
format(new Date(enactmentDatetime), DATE_FORMAT_DETAILED),
})}
</p>
);
}
case ProposalState.STATE_REJECTED:
case ProposalState.STATE_DECLINED: {
// If voting is still open we render a single clost time for all sub proposals
return null;
}
default: {
return null;
}
}
};
/**
* Renders state details relevant for the entire batch. IE. if the proposal was
* rejected or declined, or the vote close time. Does not render enactment times as
* those are relevant to the sub proposal
*/
const BatchProposalStateText = ({
state,
closingDatetime,
rejectionReason,
}: {
state: ProposalState;
closingDatetime: string;
rejectionReason: ProposalRejectionReason | null | undefined;
}) => {
const { t } = useTranslation();
const nowToCloseInHours = differenceInHours(
new Date(closingDatetime),
new Date()
);
const props = {
'data-testid': 'vote-details',
};
switch (state) {
case ProposalState.STATE_ENACTED:
case ProposalState.STATE_PASSED:
case ProposalState.STATE_WAITING_FOR_NODE_VOTE: {
return null;
}
case ProposalState.STATE_OPEN: {
return (
<p {...props}>
<span className={nowToCloseInHours < 6 ? 'text-vega-orange' : ''}>
{t('{{time}} left to vote', {
time: formatDistanceToNowStrict(new Date(closingDatetime)),
})}
</span>
</p>
);
}
case ProposalState.STATE_DECLINED: {
return <p {...props}>{t(state)}</p>;
}
case ProposalState.STATE_REJECTED: {
const props = { 'data-testid': 'vote-status' };
if (rejectionReason) {
return (
<p {...props}>{t(ProposalRejectionReasonMapping[rejectionReason])}</p>
);
}
return <p {...props}>{t('Proposal rejected')}</p>;
}
default: {
return null;
}
}
};
export const ProposalHeader = ({ export const ProposalHeader = ({
proposal, proposal,
isListItem = true, isListItem = true,
voteState, voteState,
}: { }: {
proposal: Proposal | BatchProposal; proposal: Proposal;
isListItem?: boolean; isListItem?: boolean;
voteState?: VoteState | null; voteState?: VoteState | null;
}) => { }) => {
const featureFlags = useFeatureFlags((state) => state.flags);
const { t } = useTranslation(); const { t } = useTranslation();
const change = proposal?.terms.change;
const consoleLink = useLinks(DApp.Console);
let details: ReactNode;
let proposalType = '';
let fallbackTitle = '';
const title = proposal?.rationale.title.trim(); const title = proposal?.rationale.title.trim();
const fallbackTitle = t(
proposal.__typename === 'Proposal'
? 'Unknown proposal'
: 'Unknown batch proposal'
);
const titleContent = shorten(title ?? '', 100); const titleContent = shorten(title ?? '', 100);
const getAsset = (proposal: Proposal) => {
const terms = proposal?.terms;
if (
terms?.change.__typename === 'NewMarket' &&
(terms.change.instrument.product?.__typename === 'FutureProduct' ||
terms.change.instrument.product?.__typename === 'PerpetualProduct')
) {
return terms.change.instrument.product.settlementAsset;
}
return undefined;
};
switch (change?.__typename) {
case 'NewMarket': {
proposalType =
featureFlags.PRODUCT_PERPETUALS &&
change?.instrument?.product?.__typename
? `NewMarket${change?.instrument?.product?.__typename}`
: 'NewMarket';
fallbackTitle = t('NewMarketProposal');
details = (
<>
{featureFlags.SUCCESSOR_MARKETS && (
<SuccessorCode proposalId={proposal?.id} />
)}
<span>
{t('Code')}: {change.instrument.code}.
</span>{' '}
{proposal?.terms && getAsset(proposal)?.symbol ? (
<>
<span className="font-semibold">
{getAsset(proposal)?.symbol}
</span>{' '}
{t('settled future')}.
</>
) : (
''
)}
</>
);
break;
}
case 'UpdateMarketState': {
proposalType =
featureFlags.UPDATE_MARKET_STATE && change?.updateType
? t(change.updateType)
: 'UpdateMarketState';
fallbackTitle = t('UpdateMarketStateProposal');
details = (
<span>
{featureFlags.UPDATE_MARKET_STATE &&
change?.market?.id &&
change.updateType ? (
<>
{t(change.updateType)}: {truncateMiddle(change.market.id)}
</>
) : null}
</span>
);
break;
}
case 'UpdateMarket': {
proposalType = 'UpdateMarket';
fallbackTitle = t('UpdateMarketProposal');
details = (
<>
<span>{t('UpdateToMarket')}:</span>{' '}
<span className="inline-flex items-start gap-2">
<span className="break-all">{change.marketId} </span>
<span className="inline-flex items-end gap-0">
<CopyWithTooltip
text={change.marketId}
description={t('copyToClipboard')}
>
<button className="inline-block px-1">
<VegaIcon size={20} name={VegaIconNames.COPY} />
</button>
</CopyWithTooltip>
<Tooltip description={t('OpenInConsole')} align="center">
<button
className="inline-block px-1"
onClick={() => {
const marketPageLink = consoleLink(
CONSOLE_MARKET_PAGE.replace(':marketId', change.marketId)
);
window.open(marketPageLink, '_blank');
}}
>
<VegaIcon size={20} name={VegaIconNames.OPEN_EXTERNAL} />
</button>
</Tooltip>
</span>
</span>
</>
);
break;
}
case 'UpdateReferralProgram': {
proposalType = 'UpdateReferralProgram';
fallbackTitle = t('UpdateReferralProgramProposal');
break;
}
case 'UpdateVolumeDiscountProgram': {
proposalType = 'UpdateVolumeDiscountProgram';
fallbackTitle = t('UpdateVolumeDiscountProgramProposal');
break;
}
case 'NewAsset': {
proposalType = 'NewAsset';
fallbackTitle = t('NewAssetProposal');
details = (
<>
<span>{t('Symbol')}:</span> <Lozenge>{change.symbol}.</Lozenge>{' '}
{change.source.__typename === 'ERC20' && (
<>
<span>{t('ERC20ContractAddress')}:</span>{' '}
<Lozenge>{change.source.contractAddress}</Lozenge>
</>
)}{' '}
{change.source.__typename === 'BuiltinAsset' && (
<>
<span>{t('MaxFaucetAmountMint')}:</span>{' '}
<Lozenge>{change.source.maxFaucetAmountMint}</Lozenge>
</>
)}
</>
);
break;
}
case 'UpdateNetworkParameter': {
proposalType = 'NetworkParameter';
fallbackTitle = t('NetworkParameterProposal');
details = (
<>
<span>{t('Change')}:</span>{' '}
<Lozenge>{change.networkParameter.key}</Lozenge>{' '}
<span>{t('to')}</span>{' '}
<span className="whitespace-nowrap">
<Lozenge>{change.networkParameter.value}</Lozenge>
</span>
</>
);
break;
}
case 'NewFreeform': {
proposalType = 'Freeform';
fallbackTitle = t('FreeformProposal');
details = <span />;
break;
}
case 'UpdateAsset': {
proposalType = 'UpdateAsset';
fallbackTitle = t('UpdateAssetProposal');
details = (
<>
<span>{t('AssetID')}:</span>{' '}
<Lozenge>{truncateMiddle(change.assetId)}</Lozenge>
</>
);
break;
}
case 'NewTransfer':
proposalType = 'NewTransfer';
fallbackTitle = t('NewTransferProposal');
details = featureFlags.GOVERNANCE_TRANSFERS ? (
<NewTransferSummary proposalId={proposal?.id} />
) : null;
break;
case 'CancelTransfer':
proposalType = 'CancelTransfer';
fallbackTitle = t('CancelTransferProposal');
details = featureFlags.GOVERNANCE_TRANSFERS ? (
<CancelTransferSummary proposalId={proposal?.id} />
) : null;
break;
}
return ( return (
<> <>
<div className="flex items-center justify-between gap-4 mb-6 text-sm"> <div className="flex items-center justify-between gap-4 mb-6 text-sm">
<ProposalTypeTags proposal={proposal} /> <div data-testid="proposal-type">
<ProposalInfoLabel variant="secondary">
{t(`${proposalType}`)}
</ProposalInfoLabel>
</div>
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
{(voteState === 'Yes' || voteState === 'No') && ( {(voteState === 'Yes' || voteState === 'No') && (
@@ -588,43 +264,50 @@ export const ProposalHeader = ({
<div data-testid="proposal-title" className="break-all"> <div data-testid="proposal-title" className="break-all">
{isListItem ? ( {isListItem ? (
<header> <header>
<SubHeading title={titleContent || fallbackTitle} /> <SubHeading
title={titleContent || fallbackTitle || t('Unknown proposal')}
/>
</header> </header>
) : ( ) : (
<Heading title={titleContent || fallbackTitle} /> <Heading
title={titleContent || fallbackTitle || t('Unknown proposal')}
/>
)} )}
</div> </div>
<ProposalDetails proposal={proposal} />
{details && (
<div
data-testid="proposal-details"
className="break-words mb-6 text-vega-light-200"
>
{details}
</div>
)}
<VoteBreakdown proposal={proposal} /> <VoteBreakdown proposal={proposal} />
</> </>
); );
}; };
export const ParentMarketCode = ({ export const SuccessorCode = ({
parentMarketId, proposalId,
}: { }: {
parentMarketId: string; proposalId?: string | null;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { data } = useInstrumentDetailsQuery({ const successor = useSuccessorMarketProposalDetails(proposalId);
variables: {
marketId: parentMarketId,
},
});
if (!data?.market?.tradableInstrument.instrument.code) return null; return successor.parentMarketId || successor.code ? (
return (
<span className="block" data-testid="proposal-successor-info"> <span className="block" data-testid="proposal-successor-info">
{t('Successor market to')}:{' '} {t('Successor market to')}:{' '}
<Link <Link
to={`${Routes.PROPOSALS}/${parentMarketId}`} to={`${Routes.PROPOSALS}/${successor.parentMarketId}`}
className="hover:underline" className="hover:underline"
> >
{data.market.tradableInstrument.instrument.code} {successor.code || successor.parentMarketId}
</Link> </Link>
</span> </span>
); ) : null;
}; };
export const NewTransferSummary = ({ export const NewTransferSummary = ({
@@ -3,8 +3,14 @@ import { useTranslation } from 'react-i18next';
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit'; import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading'; import { SubHeading } from '../../../../components/heading';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle'; import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
export const ProposalJson = ({ proposal }: { proposal?: unknown }) => { export const ProposalJson = ({
proposal,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [showDetails, setShowDetails] = useState(false); const [showDetails, setShowDetails] = useState(false);
@@ -18,7 +24,7 @@ export const ProposalJson = ({ proposal }: { proposal?: unknown }) => {
<SubHeading title={t('proposalJson')} /> <SubHeading title={t('proposalJson')} />
</CollapsibleToggle> </CollapsibleToggle>
{showDetails && <SyntaxHighlighter size="smaller" data={proposal} />} {showDetails && <SyntaxHighlighter data={proposal} />}
</section> </section>
); );
}; };
@@ -5,11 +5,6 @@ import {
} from './proposal-market-changes'; } from './proposal-market-changes';
import type { JsonValue } from '../../../../components/json-diff'; import type { JsonValue } from '../../../../components/json-diff';
jest.mock('../proposal/market-name.tsx', () => ({
...jest.requireActual('../proposal/market-name.tsx'),
MarketName: jest.fn(),
}));
describe('applyImmutableKeysFromEarlierVersion', () => { describe('applyImmutableKeysFromEarlierVersion', () => {
it('returns an empty object if any argument is not an object or null', () => { it('returns an empty object if any argument is not an object or null', () => {
const earlierVersion: JsonValue = null; const earlierVersion: JsonValue = null;
@@ -62,21 +57,33 @@ describe('applyImmutableKeysFromEarlierVersion', () => {
describe('ProposalMarketChanges', () => { describe('ProposalMarketChanges', () => {
it('renders correctly', () => { it('renders correctly', () => {
const { getByTestId } = render( const { getByTestId } = render(
<ProposalMarketChanges marketId="market-id" updateProposalNode={null} /> <ProposalMarketChanges
originalProposal={{}}
latestEnactedProposal={{}}
updatedProposal={{}}
/>
); );
expect(getByTestId('proposal-market-changes')).toBeInTheDocument(); expect(getByTestId('proposal-market-changes')).toBeInTheDocument();
}); });
it('JsonDiff is not visible when showChanges is false', () => { it('JsonDiff is not visible when showChanges is false', () => {
const { queryByTestId } = render( const { queryByTestId } = render(
<ProposalMarketChanges marketId="market-id" updateProposalNode={null} /> <ProposalMarketChanges
originalProposal={{}}
latestEnactedProposal={{}}
updatedProposal={{}}
/>
); );
expect(queryByTestId('json-diff')).not.toBeInTheDocument(); expect(queryByTestId('json-diff')).not.toBeInTheDocument();
}); });
it('JsonDiff is visible when showChanges is true', async () => { it('JsonDiff is visible when showChanges is true', async () => {
const { getByTestId } = render( const { getByTestId } = render(
<ProposalMarketChanges marketId="market-id" updateProposalNode={null} /> <ProposalMarketChanges
originalProposal={{}}
latestEnactedProposal={{}}
updatedProposal={{}}
/>
); );
fireEvent.click(getByTestId('proposal-market-changes-toggle')); fireEvent.click(getByTestId('proposal-market-changes-toggle'));
expect(getByTestId('json-diff')).toBeInTheDocument(); expect(getByTestId('json-diff')).toBeInTheDocument();
@@ -1,24 +1,12 @@
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import set from 'lodash/set'; import set from 'lodash/set';
import get from 'lodash/get'; import get from 'lodash/get';
import compact from 'lodash/compact'; import { JsonDiff } from '../../../../components/json-diff';
import orderBy from 'lodash/orderBy';
import { JsonDiff, type JsonValue } from '../../../../components/json-diff';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useState } from 'react'; import { useState } from 'react';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle'; import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import { SubHeading } from '../../../../components/heading'; import { SubHeading } from '../../../../components/heading';
import { import type { JsonValue } from '../../../../components/json-diff';
useFetchProposal,
useFetchProposals,
flatten,
isBatchProposalNode,
isSingleProposalNode,
type ProposalNode,
type SingleProposalData,
type SubProposalData,
} from '../proposal/proposal-utils';
import { MarketName } from '../proposal/market-name';
const immutableKeys = [ const immutableKeys = [
'decimalPlaces', 'decimalPlaces',
@@ -28,8 +16,8 @@ const immutableKeys = [
]; ];
export const applyImmutableKeysFromEarlierVersion = ( export const applyImmutableKeysFromEarlierVersion = (
earlierVersion: unknown, earlierVersion: JsonValue,
updatedVersion: unknown updatedVersion: JsonValue
) => { ) => {
if ( if (
typeof earlierVersion !== 'object' || typeof earlierVersion !== 'object' ||
@@ -45,94 +33,26 @@ export const applyImmutableKeysFromEarlierVersion = (
// Overwrite the immutable keys in the updatedVersionCopy with the earlier values // Overwrite the immutable keys in the updatedVersionCopy with the earlier values
immutableKeys.forEach((key) => { immutableKeys.forEach((key) => {
const earlier = get(earlierVersion, key); set(updatedVersionCopy, key, get(earlierVersion, key));
if (earlier) set(updatedVersionCopy, key, earlier);
}); });
return updatedVersionCopy; return updatedVersionCopy;
}; };
interface ProposalMarketChangesProps { interface ProposalMarketChangesProps {
marketId: string; originalProposal: JsonValue;
/** This are the changes from proposal */ latestEnactedProposal: JsonValue | undefined;
updateProposalNode: ProposalNode | null; updatedProposal: JsonValue;
indicator?: number;
} }
export const ProposalMarketChanges = ({ export const ProposalMarketChanges = ({
marketId, originalProposal,
updateProposalNode, latestEnactedProposal,
indicator, updatedProposal,
}: ProposalMarketChangesProps) => { }: ProposalMarketChangesProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [showChanges, setShowChanges] = useState(false); const [showChanges, setShowChanges] = useState(false);
const { data: originalProposalData } = useFetchProposal({
proposalId: marketId,
});
const { data: enactedProposalsData } = useFetchProposals({
proposalState: 'STATE_ENACTED',
proposalType: 'TYPE_UPDATE_MARKET',
});
let updateProposal: SingleProposalData | SubProposalData | undefined;
if (isBatchProposalNode(updateProposalNode)) {
updateProposal = updateProposalNode.proposals.find(
(p, i) =>
p.terms.updateMarket?.marketId === marketId &&
(indicator != null ? i === indicator - 1 : true)
);
}
if (isSingleProposalNode(updateProposalNode)) {
updateProposal = updateProposalNode.proposal;
}
// this should get the proposal before the current one
const enactedUpdateMarketProposals = orderBy(
compact(
flatten(enactedProposalsData).filter((enacted) => {
const related = enacted.terms.updateMarket?.marketId === marketId;
const notCurrent =
enacted.id !== updateProposal?.id ||
('batchId' in enacted && enacted.batchId !== updateProposal.id);
const beforeCurrent =
Number(enacted.terms.enactmentTimestamp) <
Number(updateProposal?.terms.enactmentTimestamp);
return related && notCurrent && beforeCurrent;
})
),
[(proposal) => Number(proposal.terms.enactmentTimestamp)],
'desc'
);
const latestEnactedProposal =
enactedUpdateMarketProposals.length > 0
? enactedUpdateMarketProposals[0]
: undefined;
let originalProposal;
if (isBatchProposalNode(originalProposalData)) {
originalProposal = originalProposalData.proposals.find(
(proposal) => proposal.id === marketId && proposal.terms.newMarket != null
);
}
if (isSingleProposalNode(originalProposalData)) {
originalProposal = originalProposalData.proposal;
}
// LEFT SIDE: update market proposal enacted just before this one
// or original new market proposal
const left =
latestEnactedProposal?.terms.updateMarket?.changes ||
originalProposal?.terms.newMarket?.changes;
// RIGHT SIDE: this update market proposal
const right = applyImmutableKeysFromEarlierVersion(
left,
updateProposal?.terms.updateMarket?.changes
);
return ( return (
<section data-testid="proposal-market-changes"> <section data-testid="proposal-market-changes">
<CollapsibleToggle <CollapsibleToggle
@@ -140,18 +60,25 @@ export const ProposalMarketChanges = ({
setToggleState={setShowChanges} setToggleState={setShowChanges}
dataTestId={'proposal-market-changes-toggle'} dataTestId={'proposal-market-changes-toggle'}
> >
<SubHeading <SubHeading title={t('updatesToMarket')} />
title={
<>
{t('UpdateToMarket')}: <MarketName marketId={marketId} truncate />
</>
}
/>
</CollapsibleToggle> </CollapsibleToggle>
{showChanges && ( {showChanges && (
<div className="mb-6 bg-vega-cdark-900 p-2 rounded-lg"> <div className="mb-6">
<JsonDiff left={left as JsonValue} right={right as JsonValue} /> <JsonDiff
left={latestEnactedProposal || originalProposal}
right={
latestEnactedProposal
? applyImmutableKeysFromEarlierVersion(
latestEnactedProposal,
updatedProposal
)
: applyImmutableKeysFromEarlierVersion(
originalProposal,
updatedProposal
)
}
/>
</div> </div>
)} )}
</section> </section>
@@ -18,7 +18,6 @@ import {
getDataSourceSpecForTradingTermination, getDataSourceSpecForTradingTermination,
getSigners, getSigners,
MarginScalingFactorsPanel, MarginScalingFactorsPanel,
marketInfoProvider,
} from '@vegaprotocol/markets'; } from '@vegaprotocol/markets';
import { import {
Button, Button,
@@ -29,8 +28,8 @@ import {
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading'; import { SubHeading } from '../../../../components/heading';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle'; import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import type { MarketInfo } from '@vegaprotocol/markets';
import { create } from 'zustand'; import { create } from 'zustand';
import { useDataProvider } from '@vegaprotocol/data-provider';
type MarketDataDialogState = { type MarketDataDialogState = {
isOpen: boolean; isOpen: boolean;
@@ -49,29 +48,18 @@ export const useMarketDataDialogStore = create<MarketDataDialogState>(
const marketDataHeaderStyles = const marketDataHeaderStyles =
'font-alpha calt text-base border-b border-vega-dark-200 mt-2 py-2'; 'font-alpha calt text-base border-b border-vega-dark-200 mt-2 py-2';
export const ProposalMarketData = ({ proposalId }: { proposalId: string }) => { export const ProposalMarketData = ({
marketData,
parentMarketData,
}: {
marketData: MarketInfo;
parentMarketData?: MarketInfo;
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { isOpen, open, close } = useMarketDataDialogStore(); const { isOpen, open, close } = useMarketDataDialogStore();
const [showDetails, setShowDetails] = useState(false); const [showDetails, setShowDetails] = useState(false);
const { data: marketData } = useDataProvider({ if (!marketData) {
dataProvider: marketInfoProvider,
skipUpdates: true,
variables: {
marketId: proposalId,
},
});
const { data: parentMarketData } = useDataProvider({
dataProvider: marketInfoProvider,
skipUpdates: true,
skip: !marketData?.parentMarketID,
variables: {
marketId: marketData?.parentMarketID || '',
},
});
if (!marketData || !parentMarketData) {
return null; return null;
} }
@@ -7,6 +7,7 @@ import {
formatReferralRewardMultiplier, formatReferralRewardMultiplier,
ProposalReferralProgramDetails, ProposalReferralProgramDetails,
} from './proposal-referral-program-details'; } from './proposal-referral-program-details';
import { generateProposal } from '../../test-helpers/generate-proposals';
jest.mock('../../../../contexts/app-state/app-state-context', () => ({ jest.mock('../../../../contexts/app-state/app-state-context', () => ({
useAppState: () => ({ useAppState: () => ({
@@ -58,65 +59,87 @@ describe('ProposalReferralProgramDetails helper functions', () => {
}); });
}); });
const mockChange = { const mockReferralProposal = generateProposal({
__typename: 'UpdateReferralProgram' as const, terms: {
benefitTiers: [ change: {
{ __typename: 'UpdateReferralProgram',
minimumEpochs: 6, benefitTiers: [
minimumRunningNotionalTakerVolume: '10000', {
referralDiscountFactor: '0.001', minimumEpochs: 6,
referralRewardFactor: '0.001', minimumRunningNotionalTakerVolume: '10000',
referralDiscountFactor: '0.001',
referralRewardFactor: '0.001',
},
{
minimumEpochs: 24,
minimumRunningNotionalTakerVolume: '500000',
referralDiscountFactor: '0.005',
referralRewardFactor: '0.005',
},
{
minimumEpochs: 48,
minimumRunningNotionalTakerVolume: '1000000',
referralDiscountFactor: '0.01',
referralRewardFactor: '0.01',
},
],
endOfProgram: '2026-10-03T10:34:34Z',
windowLength: 3,
stakingTiers: [
{
minimumStakedTokens: '1',
referralRewardMultiplier: '1',
},
{
minimumStakedTokens: '2',
referralRewardMultiplier: '2',
},
{
minimumStakedTokens: '5',
referralRewardMultiplier: '3',
},
],
}, },
{ },
minimumEpochs: 24, });
minimumRunningNotionalTakerVolume: '500000',
referralDiscountFactor: '0.005',
referralRewardFactor: '0.005',
},
{
minimumEpochs: 48,
minimumRunningNotionalTakerVolume: '1000000',
referralDiscountFactor: '0.01',
referralRewardFactor: '0.01',
},
],
endOfProgram: '2026-10-03T10:34:34Z',
windowLength: 3,
stakingTiers: [
{
minimumStakedTokens: '1',
referralRewardMultiplier: '1',
},
{
minimumStakedTokens: '2',
referralRewardMultiplier: '2',
},
{
minimumStakedTokens: '5',
referralRewardMultiplier: '3',
},
],
};
describe('<ProposalReferralProgramDetails />', () => { describe('<ProposalReferralProgramDetails />', () => {
it('should not render if proposal is null', () => { it('should not render if proposal is null', () => {
render(<ProposalReferralProgramDetails change={null} />); render(<ProposalReferralProgramDetails proposal={null} />);
expect(
screen.queryByTestId('proposal-referral-program-details')
).toBeNull();
});
it('should not render if __typename is not UpdateReferralProgram', () => {
const updateMarketProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateMarket',
},
},
});
render(<ProposalReferralProgramDetails proposal={updateMarketProposal} />);
expect( expect(
screen.queryByTestId('proposal-referral-program-details') screen.queryByTestId('proposal-referral-program-details')
).toBeNull(); ).toBeNull();
}); });
it('should not render if there are no relevant fields', () => { it('should not render if there are no relevant fields', () => {
const emptyChange = {}; const incompleteProposal = generateProposal({
// @ts-ignore change deliberately empty terms: {
render(<ProposalReferralProgramDetails change={emptyChange} />); change: {},
},
});
render(<ProposalReferralProgramDetails proposal={incompleteProposal} />);
expect( expect(
screen.queryByTestId('proposal-referral-program-details') screen.queryByTestId('proposal-referral-program-details')
).toBeNull(); ).toBeNull();
}); });
it('should render relevant fields if present', () => { it('should render relevant fields if present', () => {
render(<ProposalReferralProgramDetails change={mockChange} />); render(<ProposalReferralProgramDetails proposal={mockReferralProposal} />);
expect( expect(
screen.getByTestId('proposal-referral-program-window-length') screen.getByTestId('proposal-referral-program-window-length')
).toBeInTheDocument(); ).toBeInTheDocument();
@@ -13,10 +13,10 @@ import {
} from '@vegaprotocol/utils'; } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { useAppState } from '../../../../contexts/app-state/app-state-context'; import { useAppState } from '../../../../contexts/app-state/app-state-context';
import { type UpdateReferralProgramsFragment } from '../../__generated__/Proposals'; import { type Proposal } from '../../types';
interface ProposalReferralProgramDetailsProps { interface ProposalReferralProgramDetailsProps {
change: UpdateReferralProgramsFragment | null; proposal: Proposal | null;
} }
export const formatEndOfProgramTimestamp = (value: string) => { export const formatEndOfProgramTimestamp = (value: string) => {
@@ -44,19 +44,20 @@ export const formatReferralRewardMultiplier = (value: string) => {
}; };
export const ProposalReferralProgramDetails = ({ export const ProposalReferralProgramDetails = ({
change, proposal,
}: ProposalReferralProgramDetailsProps) => { }: ProposalReferralProgramDetailsProps) => {
const { const {
appState: { decimals }, appState: { decimals },
} = useAppState(); } = useAppState();
const { t } = useTranslation(); const { t } = useTranslation();
if (proposal?.terms?.change?.__typename !== 'UpdateReferralProgram') {
return null;
}
if (change?.__typename !== 'UpdateReferralProgram') return null; const benefitTiers = proposal?.terms?.change?.benefitTiers.slice();
const stakingTiers = proposal?.terms?.change?.stakingTiers.slice();
const benefitTiers = change?.benefitTiers.slice(); const windowLength = proposal?.terms?.change?.windowLength;
const stakingTiers = change?.stakingTiers.slice(); const endOfProgramTimestamp = proposal?.terms?.change?.endOfProgram;
const windowLength = change?.windowLength;
const endOfProgramTimestamp = change?.endOfProgram;
if ( if (
!benefitTiers && !benefitTiers &&
@@ -6,14 +6,15 @@ import {
RoundedWrapper, RoundedWrapper,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading'; import { SubHeading } from '../../../../components/heading';
import { type Proposal } from '../../types';
export const ProposalCancelTransferDetails = ({ export const ProposalCancelTransferDetails = ({
proposalId, proposal,
}: { }: {
proposalId: string; proposal: Proposal;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const details = useCancelTransferProposalDetails(proposalId); const details = useCancelTransferProposalDetails(proposal?.id);
if (!details) { if (!details) {
return null; return null;
@@ -19,16 +19,17 @@ import {
addDecimalsFormatNumberQuantum, addDecimalsFormatNumberQuantum,
formatDateWithLocalTimezone, formatDateWithLocalTimezone,
} from '@vegaprotocol/utils'; } from '@vegaprotocol/utils';
import { type Proposal } from '../../types';
export const ProposalTransferDetails = ({ export const ProposalTransferDetails = ({
proposalId, proposal,
}: { }: {
proposalId: string; proposal: Proposal;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const details = useNewTransferProposalDetails(proposalId); const details = useNewTransferProposalDetails(proposal?.id);
if (!details) { if (!details) {
return null; return null;
} }
@@ -1,5 +1,6 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { ProposalUpdateBenefitTiers } from './proposal-update-benefit-tiers-details'; import { ProposalUpdateBenefitTiers } from './proposal-update-benefit-tiers-details';
import { generateProposal } from '../../test-helpers/generate-proposals';
jest.mock('../../../../contexts/app-state/app-state-context', () => ({ jest.mock('../../../../contexts/app-state/app-state-context', () => ({
useAppState: () => ({ useAppState: () => ({
@@ -9,71 +10,109 @@ jest.mock('../../../../contexts/app-state/app-state-context', () => ({
}), }),
})); }));
const mockChange1 = { const mockVestingBenefitTierProposal = generateProposal({
__typename: 'UpdateNetworkParameter' as const, terms: {
networkParameter: { change: {
key: 'blah.blah.benefitTiers', __typename: 'UpdateNetworkParameter',
value: JSON.stringify({ networkParameter: {
tiers: [ key: 'blah.blah.benefitTiers',
{ value: JSON.stringify({
minimum_quantum_balance: '10000', tiers: [
reward_multiplier: '0.05', {
}, minimum_quantum_balance: '10000',
{ reward_multiplier: '0.05',
minimum_quantum_balance: '500000000000', },
reward_multiplier: '0.1', {
}, minimum_quantum_balance: '500000000000',
{ reward_multiplier: '0.1',
minimum_quantum_balance: '10000000000000', },
reward_multiplier: '10', {
}, minimum_quantum_balance: '10000000000000',
], reward_multiplier: '10',
}), },
],
}),
},
},
}, },
}; });
const mockChange2 = { const mockActivityStreakBenefitTierProposal = generateProposal({
__typename: 'UpdateNetworkParameter' as const, terms: {
networkParameter: { change: {
key: 'blah.blah.benefitTiers', __typename: 'UpdateNetworkParameter',
value: JSON.stringify({ networkParameter: {
tiers: [ key: 'blah.blah.benefitTiers',
{ value: JSON.stringify({
minimum_activity_streak: '10000', tiers: [
vesting_multiplier: '5', {
reward_multiplier: '0.1', minimum_activity_streak: '10000',
}, vesting_multiplier: '5',
{ reward_multiplier: '0.1',
minimum_activity_streak: '10000000000000', },
vesting_multiplier: '100', {
reward_multiplier: '10', minimum_activity_streak: '10000000000000',
}, vesting_multiplier: '100',
], reward_multiplier: '10',
}), },
],
}),
},
},
}, },
}; });
describe('ProposalUpdateBenefitTiers', () => { describe('ProposalUpdateBenefitTiers', () => {
it('should not render if proposal is null', () => { it('should not render if proposal is null', () => {
render(<ProposalUpdateBenefitTiers change={null} />); render(<ProposalUpdateBenefitTiers proposal={null} />);
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
});
it('should not render if __typename is not UpdateNetworkParameter', () => {
const updateMarketProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateMarket',
},
},
});
render(<ProposalUpdateBenefitTiers proposal={updateMarketProposal} />);
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
});
it('should not render if there are no relevant fields', () => {
const incompleteProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateNetworkParameter',
},
},
});
render(<ProposalUpdateBenefitTiers proposal={incompleteProposal} />);
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull(); expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
}); });
it('should not render if there are relevant fields that are empty', () => { it('should not render if there are relevant fields that are empty', () => {
const incompleteProposal = { const incompleteProposal = generateProposal({
__typename: 'UpdateNetworkParameter' as const, terms: {
networkParameter: { change: {
key: 'blah.blah.benefitTiers', __typename: 'UpdateNetworkParameter',
value: JSON.stringify({}), networkParameter: {
key: 'blah.blah.benefitTiers',
value: JSON.stringify({}),
},
},
}, },
}; });
render(<ProposalUpdateBenefitTiers change={incompleteProposal} />); render(<ProposalUpdateBenefitTiers proposal={incompleteProposal} />);
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull(); expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
}); });
it('should render a valid vesting benefit tier proposal', () => { it('should render a valid vesting benefit tier proposal', () => {
render(<ProposalUpdateBenefitTiers change={mockChange1} />); render(
<ProposalUpdateBenefitTiers proposal={mockVestingBenefitTierProposal} />
);
// 3 tiers in the sample data // 3 tiers in the sample data
expect(screen.getByText('Tier 1')).toBeInTheDocument(); expect(screen.getByText('Tier 1')).toBeInTheDocument();
@@ -94,7 +133,11 @@ describe('ProposalUpdateBenefitTiers', () => {
}); });
it('should render a valid activity streak benefit tier proposal', () => { it('should render a valid activity streak benefit tier proposal', () => {
render(<ProposalUpdateBenefitTiers change={mockChange2} />); render(
<ProposalUpdateBenefitTiers
proposal={mockActivityStreakBenefitTierProposal}
/>
);
// 3 tiers in the sample data // 3 tiers in the sample data
expect(screen.getByText('Tier 1')).toBeInTheDocument(); expect(screen.getByText('Tier 1')).toBeInTheDocument();
@@ -11,7 +11,7 @@ import {
} from '../proposal-referral-program-details'; } from '../proposal-referral-program-details';
import { formatNumberPercentage } from '@vegaprotocol/utils'; import { formatNumberPercentage } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { type IUpdateNetworkParameterFieldsFragment } from '../../__generated__/Proposals'; import { type Proposal } from '../../types';
// These types are not generated as it's not known how dynamic these are // These types are not generated as it's not known how dynamic these are
type VestingBenefitTier = { type VestingBenefitTier = {
@@ -43,7 +43,7 @@ export const formatVolumeDiscountFactor = (value: string) => {
}; };
interface ProposalReferralProgramDetailsProps { interface ProposalReferralProgramDetailsProps {
change: IUpdateNetworkParameterFieldsFragment | null; proposal: Proposal | null;
} }
/** /**
@@ -55,17 +55,17 @@ interface ProposalReferralProgramDetailsProps {
* It only renders known fields so that they can be formatted correctly. * It only renders known fields so that they can be formatted correctly.
*/ */
export const ProposalUpdateBenefitTiers = ({ export const ProposalUpdateBenefitTiers = ({
change, proposal,
}: ProposalReferralProgramDetailsProps) => { }: ProposalReferralProgramDetailsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
if ( if (
change?.__typename !== 'UpdateNetworkParameter' || proposal?.terms?.change?.__typename !== 'UpdateNetworkParameter' ||
change?.networkParameter.key.slice(-13) !== '.benefitTiers' proposal?.terms?.change?.networkParameter.key.slice(-13) !== '.benefitTiers'
) { ) {
return null; return null;
} }
const benefitTiersString = change?.networkParameter.value; const benefitTiersString = proposal?.terms?.change?.networkParameter.value;
const benefitTiers = getBenefitTiers(benefitTiersString); const benefitTiers = getBenefitTiers(benefitTiersString);
if (!benefitTiers) { if (!benefitTiers) {
@@ -1,78 +1,86 @@
import { fireEvent, render, screen } from '@testing-library/react'; import { fireEvent, render, screen } from '@testing-library/react';
import { ProposalUpdateMarketState } from './proposal-update-market-state'; import { ProposalUpdateMarketState } from './proposal-update-market-state';
import { generateProposal } from '../../test-helpers/generate-proposals';
import { MarketUpdateType } from '@vegaprotocol/types'; import { MarketUpdateType } from '@vegaprotocol/types';
jest.mock('../proposal/market-name.tsx', () => ({
...jest.requireActual('../proposal/market-name.tsx'),
MarketName: jest.fn(),
}));
describe('<ProposalUpdateMarketState />', () => { describe('<ProposalUpdateMarketState />', () => {
const suspendProposal = { const suspendProposal = generateProposal({
__typename: 'UpdateMarketState' as const, terms: {
market: { change: {
id: '1', __typename: 'UpdateMarketState',
decimalPlaces: 0, market: {
tradableInstrument: { id: '1',
instrument: { decimalPlaces: 0,
name: 'suspendProposal Name', tradableInstrument: {
code: 'suspendProposal Code', instrument: {
product: { name: 'suspendProposal Name',
__typename: 'Future' as const, code: 'suspendProposal Code',
quoteName: 'USD', product: {
__typename: 'Future',
quoteName: 'USD',
},
},
}, },
}, },
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_SUSPEND,
}, },
}, },
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_SUSPEND, });
};
const resumeProposal = { const resumeProposal = generateProposal({
__typename: 'UpdateMarketState' as const, terms: {
market: { change: {
id: '1', __typename: 'UpdateMarketState',
decimalPlaces: 0, market: {
tradableInstrument: { id: '1',
instrument: { decimalPlaces: 0,
name: 'resumeProposal Name', tradableInstrument: {
code: 'resumeProposal Code', instrument: {
product: { name: 'resumeProposal Name',
__typename: 'Future' as const, code: 'resumeProposal Code',
quoteName: 'USD', product: {
__typename: 'Future',
quoteName: 'USD',
},
},
}, },
}, },
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_RESUME,
}, },
}, },
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_RESUME, });
};
const terminateProposal = { const terminateProposal = generateProposal({
__typename: 'UpdateMarketState' as const, terms: {
market: { change: {
id: '1', __typename: 'UpdateMarketState',
decimalPlaces: 0, market: {
tradableInstrument: { id: '1',
instrument: { decimalPlaces: 0,
name: 'terminateProposal Name', tradableInstrument: {
code: 'terminateProposal Code', instrument: {
product: { name: 'terminateProposal Name',
__typename: 'Future' as const, code: 'terminateProposal Code',
quoteName: 'USD', product: {
__typename: 'Future',
quoteName: 'USD',
},
},
}, },
}, },
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
price: '123',
}, },
}, },
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE, });
price: '123',
};
it('should render nothing if proposal is null', () => { it('should render nothing if proposal is null', () => {
render(<ProposalUpdateMarketState change={null} />); render(<ProposalUpdateMarketState proposal={null} />);
expect(screen.queryByTestId('proposal-update-market-state')).toBeNull(); expect(screen.queryByTestId('proposal-update-market-state')).toBeNull();
}); });
it('should toggle details when CollapsibleToggle is clicked', () => { it('should toggle details when CollapsibleToggle is clicked', () => {
render(<ProposalUpdateMarketState change={suspendProposal} />); render(<ProposalUpdateMarketState proposal={suspendProposal} />);
expect( expect(
screen.queryByTestId('proposal-update-market-state-table') screen.queryByTestId('proposal-update-market-state-table')
@@ -86,7 +94,7 @@ describe('<ProposalUpdateMarketState />', () => {
}); });
it('should display suspend market information when showDetails is true', () => { it('should display suspend market information when showDetails is true', () => {
render(<ProposalUpdateMarketState change={suspendProposal} />); render(<ProposalUpdateMarketState proposal={suspendProposal} />);
fireEvent.click(screen.getByTestId('proposal-market-data-toggle')); fireEvent.click(screen.getByTestId('proposal-market-data-toggle'));
@@ -95,7 +103,7 @@ describe('<ProposalUpdateMarketState />', () => {
}); });
it('should display resume market information when showDetails is true', () => { it('should display resume market information when showDetails is true', () => {
render(<ProposalUpdateMarketState change={resumeProposal} />); render(<ProposalUpdateMarketState proposal={resumeProposal} />);
fireEvent.click(screen.getByTestId('proposal-market-data-toggle')); fireEvent.click(screen.getByTestId('proposal-market-data-toggle'));
@@ -104,7 +112,7 @@ describe('<ProposalUpdateMarketState />', () => {
}); });
it('should display terminate market information when showDetails is true', () => { it('should display terminate market information when showDetails is true', () => {
render(<ProposalUpdateMarketState change={terminateProposal} />); render(<ProposalUpdateMarketState proposal={terminateProposal} />);
fireEvent.click(screen.getByTestId('proposal-market-data-toggle')); fireEvent.click(screen.getByTestId('proposal-market-data-toggle'));
@@ -8,31 +8,29 @@ import { Row } from '@vegaprotocol/markets';
import { useState } from 'react'; import { useState } from 'react';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle'; import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import { SubHeading } from '../../../../components/heading'; import { SubHeading } from '../../../../components/heading';
import { type UpdateMarketStatesFragment } from '../../__generated__/Proposals'; import { type Proposal } from '../../types';
import { MarketUpdateTypeMapping } from '@vegaprotocol/types';
import { MarketName } from '../proposal/market-name';
interface ProposalUpdateMarketStateProps { interface ProposalUpdateMarketStateProps {
change: UpdateMarketStatesFragment | null; proposal: Proposal | null;
} }
export const ProposalUpdateMarketState = ({ export const ProposalUpdateMarketState = ({
change, proposal,
}: ProposalUpdateMarketStateProps) => { }: ProposalUpdateMarketStateProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [showDetails, setShowDetails] = useState(false); const [showDetails, setShowDetails] = useState(false);
let market;
let isTerminate = false;
if (!change || change.__typename !== 'UpdateMarketState') { if (!proposal) {
return null; return null;
} }
const market = change?.market; if (proposal?.terms.change.__typename === 'UpdateMarketState') {
const isTerminate = market = proposal?.terms?.change?.market;
change?.updateType === 'MARKET_STATE_UPDATE_TYPE_TERMINATE'; isTerminate =
proposal?.terms?.change?.updateType ===
let toggleTitle = t(change.updateType); 'MARKET_STATE_UPDATE_TYPE_TERMINATE';
if (toggleTitle.length === 0) {
toggleTitle = t('MarketDetails');
} }
return ( return (
@@ -42,29 +40,17 @@ export const ProposalUpdateMarketState = ({
setToggleState={setShowDetails} setToggleState={setShowDetails}
dataTestId="proposal-market-data-toggle" dataTestId="proposal-market-data-toggle"
> >
<SubHeading <SubHeading title={t('MarketDetails')} />
title={
<>
{toggleTitle}: <MarketName marketId={market?.id} />
</>
}
/>
</CollapsibleToggle> </CollapsibleToggle>
{showDetails && ( {showDetails && (
<RoundedWrapper paddingBottom={true} marginBottomLarge={true}> <RoundedWrapper paddingBottom={true} marginBottomLarge={true}>
{change.__typename === 'UpdateMarketState' && ( {proposal?.terms.change.__typename === 'UpdateMarketState' && (
<KeyValueTable data-testid="proposal-update-market-state-table"> <KeyValueTable data-testid="proposal-update-market-state-table">
<KeyValueTableRow> <KeyValueTableRow>
{t('marketId')} {t('marketId')}
{market?.id} {market?.id}
</KeyValueTableRow> </KeyValueTableRow>
<KeyValueTableRow>
{t('State')}
<span className="bg-vega-green-650 px-1">
{MarketUpdateTypeMapping[change.updateType]}
</span>
</KeyValueTableRow>
<KeyValueTableRow> <KeyValueTableRow>
{t('marketName')} {t('marketName')}
{market?.tradableInstrument?.instrument?.name} {market?.tradableInstrument?.instrument?.name}
@@ -73,10 +59,10 @@ export const ProposalUpdateMarketState = ({
{t('marketCode')} {t('marketCode')}
{market?.tradableInstrument?.instrument?.code} {market?.tradableInstrument?.instrument?.code}
</KeyValueTableRow> </KeyValueTableRow>
{isTerminate && market && ( {isTerminate && (
<Row <Row
field="termination-price" field="termination-price"
value={change?.price} value={proposal?.terms?.change?.price}
assetSymbol={ assetSymbol={
market?.tradableInstrument?.instrument?.product market?.tradableInstrument?.instrument?.product
?.__typename === 'Future' || ?.__typename === 'Future' ||
@@ -1,5 +1,6 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { ProposalVolumeDiscountProgramDetails } from './proposal-volume-discount-program-details'; import { ProposalVolumeDiscountProgramDetails } from './proposal-volume-discount-program-details';
import { generateProposal } from '../../test-helpers/generate-proposals';
jest.mock('../../../../contexts/app-state/app-state-context', () => ({ jest.mock('../../../../contexts/app-state/app-state-context', () => ({
useAppState: () => ({ useAppState: () => ({
@@ -9,56 +10,95 @@ jest.mock('../../../../contexts/app-state/app-state-context', () => ({
}), }),
})); }));
const mockChange = { const mockReferralProposal = generateProposal({
__typename: 'UpdateVolumeDiscountProgram' as const, terms: {
benefitTiers: [ change: {
{ __typename: 'UpdateVolumeDiscountProgram',
minimumRunningNotionalTakerVolume: '10000', benefitTiers: [
volumeDiscountFactor: '0.05', {
minimumRunningNotionalTakerVolume: '10000',
volumeDiscountFactor: '0.05',
},
{
minimumRunningNotionalTakerVolume: '50000',
volumeDiscountFactor: '0.1',
},
{
minimumRunningNotionalTakerVolume: '100000',
volumeDiscountFactor: '0.15',
},
{
minimumRunningNotionalTakerVolume: '250000',
volumeDiscountFactor: '0.2',
},
{
minimumRunningNotionalTakerVolume: '500000',
volumeDiscountFactor: '0.25',
},
{
minimumRunningNotionalTakerVolume: '1000000',
volumeDiscountFactor: '0.3',
},
{
minimumRunningNotionalTakerVolume: '1500000',
volumeDiscountFactor: '0.35',
},
{
minimumRunningNotionalTakerVolume: '2000000',
volumeDiscountFactor: '0.4',
},
],
endOfProgramTimestamp: '1970-01-01T00:00:01.791568493Z',
windowLength: 7,
}, },
{ },
minimumRunningNotionalTakerVolume: '50000', });
volumeDiscountFactor: '0.1',
},
{
minimumRunningNotionalTakerVolume: '100000',
volumeDiscountFactor: '0.15',
},
{
minimumRunningNotionalTakerVolume: '250000',
volumeDiscountFactor: '0.2',
},
{
minimumRunningNotionalTakerVolume: '500000',
volumeDiscountFactor: '0.25',
},
{
minimumRunningNotionalTakerVolume: '1000000',
volumeDiscountFactor: '0.3',
},
{
minimumRunningNotionalTakerVolume: '1500000',
volumeDiscountFactor: '0.35',
},
{
minimumRunningNotionalTakerVolume: '2000000',
volumeDiscountFactor: '0.4',
},
],
endOfProgramTimestamp: '1970-01-01T00:00:01.791568493Z',
windowLength: 7,
};
describe('ProposalVolumeDiscountProgramDetails', () => { describe('ProposalVolumeDiscountProgramDetails', () => {
it('should not render if proposal is null', () => { it('should not render if proposal is null', () => {
render(<ProposalVolumeDiscountProgramDetails change={null} />); render(<ProposalVolumeDiscountProgramDetails proposal={null} />);
expect(
screen.queryByTestId('proposal-volume-discount-program-details')
).toBeNull();
});
it('should not render if __typename is not UpdateVolumeDiscountProgram', () => {
const updateMarketProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateMarket',
},
},
});
render(
<ProposalVolumeDiscountProgramDetails proposal={updateMarketProposal} />
);
expect(
screen.queryByTestId('proposal-volume-discount-program-details')
).toBeNull();
});
it('should not render if there are no relevant fields', () => {
const incompleteProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateVolumeDiscountProgram',
},
},
});
render(
<ProposalVolumeDiscountProgramDetails proposal={incompleteProposal} />
);
expect( expect(
screen.queryByTestId('proposal-volume-discount-program-details') screen.queryByTestId('proposal-volume-discount-program-details')
).toBeNull(); ).toBeNull();
}); });
it('should render relevant fields if present', () => { it('should render relevant fields if present', () => {
render(<ProposalVolumeDiscountProgramDetails change={mockChange} />); render(
<ProposalVolumeDiscountProgramDetails proposal={mockReferralProposal} />
);
expect( expect(
screen.getByTestId('proposal-volume-discount-program-window-length') screen.getByTestId('proposal-volume-discount-program-window-length')
).toBeInTheDocument(); ).toBeInTheDocument();
@@ -11,10 +11,10 @@ import {
} from '../proposal-referral-program-details'; } from '../proposal-referral-program-details';
import { formatNumberPercentage } from '@vegaprotocol/utils'; import { formatNumberPercentage } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { type UpdateVolumeDiscountProgramsFragment } from '../../__generated__/Proposals'; import { type Proposal } from '../../types';
interface ProposalReferralProgramDetailsProps { interface ProposalReferralProgramDetailsProps {
change: UpdateVolumeDiscountProgramsFragment | null; proposal: Proposal | null;
} }
export const formatVolumeDiscountFactor = (value: string) => { export const formatVolumeDiscountFactor = (value: string) => {
@@ -22,16 +22,16 @@ export const formatVolumeDiscountFactor = (value: string) => {
}; };
export const ProposalVolumeDiscountProgramDetails = ({ export const ProposalVolumeDiscountProgramDetails = ({
change, proposal,
}: ProposalReferralProgramDetailsProps) => { }: ProposalReferralProgramDetailsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
if (change?.__typename !== 'UpdateVolumeDiscountProgram') { if (proposal?.terms?.change?.__typename !== 'UpdateVolumeDiscountProgram') {
return null; return null;
} }
const benefitTiers = change.benefitTiers; const benefitTiers = proposal?.terms?.change?.benefitTiers;
const windowLength = change?.windowLength; const windowLength = proposal?.terms?.change?.windowLength;
const endOfProgramTimestamp = change?.endOfProgramTimestamp; const endOfProgramTimestamp = proposal?.terms?.change?.endOfProgramTimestamp;
if (!benefitTiers && !windowLength && !endOfProgramTimestamp) { if (!benefitTiers && !windowLength && !endOfProgramTimestamp) {
return null; return null;
@@ -1,46 +0,0 @@
import classNames from 'classnames';
// rainbow-ish order
const COLOURS = ['red', 'pink', 'orange', 'yellow', 'green', 'blue', 'purple'];
const getColour = (indicator: number, max = COLOURS.length) => {
const available =
max < COLOURS.length ? COLOURS.slice(COLOURS.length - max) : COLOURS;
const tiers = Object.keys(available).length;
let index = Math.abs(indicator - 1);
if (indicator >= tiers) {
index = index % tiers;
}
return available[index];
};
export const getStyle = (indicator: number, max = COLOURS.length) =>
classNames({
'bg-vega-yellow-400 before:bg-vega-yellow-400':
'yellow' === getColour(indicator, max),
'bg-vega-green-400 before:bg-vega-green-400':
'green' === getColour(indicator, max),
'bg-vega-blue-400 before:bg-vega-blue-400':
'blue' === getColour(indicator, max),
'bg-vega-purple-400 before:bg-vega-purple-400':
'purple' === getColour(indicator, max),
'bg-vega-pink-400 before:bg-vega-pink-400':
'pink' === getColour(indicator, max),
'bg-vega-orange-400 before:bg-vega-orange-400':
'orange' === getColour(indicator, max),
'bg-vega-red-400 before:bg-vega-red-400':
'red' === getColour(indicator, max),
'bg-vega-clight-600 before:bg-vega-clight-600':
'none' === getColour(indicator, max),
});
export const getIndicatorStyle = (indicator: number) =>
classNames(
'rounded-sm text-black inline-block px-1 py-1 font-alpha calt h-8 w-7 text-center',
'text-border-1',
getStyle(indicator),
// Comment below if you want to remove the "chevron"
'relative mr-[11px] z-1',
'before:absolute before:z-0 before:top-1 before:right-[-11px] before:rounded-sm',
"before:w-[22.62px] before:h-[22.62px] before:rotate-45 before:content-['']"
);
@@ -1,9 +0,0 @@
import { getIndicatorStyle } from './colours';
export const Indicator = ({ indicator }: { indicator: number }) => (
<div className={getIndicatorStyle(indicator)}>
<span className="absolute top-0 left-0 p-1 w-full text-center z-1">
{indicator}
</span>
</div>
);
@@ -1,21 +0,0 @@
import { useMarketInfoQuery } from '@vegaprotocol/markets';
import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
export const MarketName = ({
marketId,
truncate,
}: {
marketId?: string;
truncate?: boolean;
}) => {
const { data } = useMarketInfoQuery({
variables: {
marketId: marketId || '',
},
skip: !marketId,
});
const id = truncate ? truncateMiddle(marketId || '') : marketId;
return <span>{data?.market?.tradableInstrument.instrument.code || id}</span>;
};
@@ -1,158 +0,0 @@
import { Trans, useTranslation } from 'react-i18next';
import { type ProposalTermsFieldsFragment } from '../../__generated__/Proposals';
import { type Proposal, type BatchProposal } from '../../types';
import { ListAsset } from '../list-asset';
import { ProposalAssetDetails } from '../proposal-asset-details';
import { ProposalMarketChanges } from '../proposal-market-changes';
import { ProposalMarketData } from '../proposal-market-data';
import { ProposalReferralProgramDetails } from '../proposal-referral-program-details';
import {
ProposalCancelTransferDetails,
ProposalTransferDetails,
} from '../proposal-transfer';
import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers';
import { ProposalUpdateMarketState } from '../proposal-update-market-state';
import { ProposalVolumeDiscountProgramDetails } from '../proposal-volume-discount-program-details';
import { type ProposalNode } from './proposal-utils';
import { Lozenge } from '@vegaprotocol/ui-toolkit';
import { Indicator } from './indicator';
import { SubHeading } from '../../../../components/heading';
export const ProposalChangeDetails = ({
proposal,
terms,
restData,
indicator,
}: {
proposal: Proposal | BatchProposal;
terms: ProposalTermsFieldsFragment;
restData: ProposalNode | null;
indicator?: number;
}) => {
const { t } = useTranslation();
let details = null;
switch (terms.change.__typename) {
case 'NewAsset': {
if (proposal.id && terms.change.source.__typename === 'ERC20') {
details = (
<div>
<ListAsset
assetId={proposal.id}
withdrawalThreshold={terms.change.source.withdrawThreshold}
lifetimeLimit={terms.change.source.lifetimeLimit}
/>
<ProposalAssetDetails change={terms.change} assetId={proposal.id} />
</div>
);
}
break;
}
case 'UpdateAsset': {
if (proposal.id) {
details = (
<ProposalAssetDetails
change={terms.change}
assetId={terms.change.assetId}
/>
);
}
break;
}
case 'NewMarket': {
if (proposal.id) {
details = <ProposalMarketData proposalId={proposal.id} />;
}
break;
}
case 'UpdateMarket': {
if (proposal.id) {
details = (
<div className="flex flex-col gap-4">
<ProposalMarketData proposalId={proposal.id} />
<ProposalMarketChanges
indicator={indicator}
marketId={terms.change.marketId}
updateProposalNode={restData}
/>
</div>
);
}
break;
}
case 'NewTransfer': {
if (proposal.id) {
details = <ProposalTransferDetails proposalId={proposal.id} />;
}
break;
}
case 'CancelTransfer': {
if (proposal.id) {
details = <ProposalCancelTransferDetails proposalId={proposal.id} />;
}
break;
}
case 'UpdateMarketState': {
details = <ProposalUpdateMarketState change={terms.change} />;
break;
}
case 'UpdateReferralProgram': {
details = <ProposalReferralProgramDetails change={terms.change} />;
break;
}
case 'UpdateVolumeDiscountProgram': {
details = <ProposalVolumeDiscountProgramDetails change={terms.change} />;
break;
}
case 'UpdateNetworkParameter': {
if (
terms.change.networkParameter.key === 'rewards.vesting.benefitTiers' ||
terms.change.networkParameter.key ===
'rewards.activityStreak.benefitTiers'
) {
details = <ProposalUpdateBenefitTiers change={terms.change} />;
} else {
details = (
<div className="mb-4">
<SubHeading title={t(terms.change.__typename as string)} />
<span>
<Trans
i18nKey="Change <lozenge>{{key}}</lozenge> to <lozenge>{{value}}</lozenge>"
values={{
key: terms.change.networkParameter.key,
value: terms.change.networkParameter.value,
}}
components={{
// @ts-ignore children passed by i18next
lozenge: <Lozenge />,
}}
/>
</span>
</div>
);
}
break;
}
case 'NewFreeform':
case 'NewSpotMarket':
case 'UpdateSpotMarket':
default: {
break;
}
}
if (indicator != null && details != null) {
details = (
<div className="grid grid-cols-[40px_minmax(0,1fr)] grid-rows-1 gap-3 mb-3">
<div className="w-10">
<Indicator indicator={indicator} />
</div>
<div>{details}</div>
</div>
);
}
return details;
};
@@ -1,261 +0,0 @@
import compact from 'lodash/compact';
import { ENV } from '../../../../config';
import { useEffect, useState } from 'react';
type Maybe<T> = T | null | undefined;
type ProposalState =
| 'STATE_UNSPECIFIED'
| 'STATE_FAILED'
| 'STATE_OPEN'
| 'STATE_PASSED'
| 'STATE_REJECTED'
| 'STATE_DECLINED'
| 'STATE_ENACTED'
| 'STATE_WAITING_FOR_NODE_VOTE';
type ProposalType =
| 'TYPE_UNSPECIFIED'
| 'TYPE_ALL'
| 'TYPE_NEW_MARKET'
| 'TYPE_UPDATE_MARKET'
| 'TYPE_NETWORK_PARAMETERS'
| 'TYPE_NEW_ASSET'
| 'TYPE_NEW_FREE_FORM'
| 'TYPE_UPDATE_ASSET'
| 'TYPE_NEW_SPOT_MARKET'
| 'TYPE_UPDATE_SPOT_MARKET'
| 'TYPE_NEW_TRANSFER'
| 'TYPE_CANCEL_TRANSFER'
| 'TYPE_UPDATE_MARKET_STATE'
| 'TYPE_UPDATE_REFERRAL_PROGRAM'
| 'TYPE_UPDATE_VOLUME_DISCOUNT_PROGRAM';
type ProposalNodeType = 'TYPE_SINGLE_OR_UNSPECIFIED' | 'TYPE_BATCH';
type ProposalData = {
id: string;
rationale: {
description: string;
title: string;
};
state: ProposalState;
timestamp: string;
};
type Terms = {
cancelTransfer?: { changes: unknown };
enactmentTimestamp: string;
newAsset?: { changes: unknown };
newFreeform: object;
newMarket?: { changes: unknown };
newSpotMarket?: { changes: unknown };
newTransfer?: { changes: unknown };
updateAsset?: { assetId: string; changes: unknown };
updateMarket?: { marketId: string; changes: unknown };
updateMarketState?: {
changes: {
marketId: string;
price: string;
updateType:
| 'MARKET_STATE_UPDATE_TYPE_UNSPECIFIED'
| 'MARKET_STATE_UPDATE_TYPE_TERMINATE'
| 'MARKET_STATE_UPDATE_TYPE_SUSPEND'
| 'MARKET_STATE_UPDATE_TYPE_RESUME';
};
};
updateNetworkParameter?: { changes: unknown };
updateReferralProgram?: { changes: unknown };
updateSpotMarket?: { marketId: string; changes: unknown };
updateVolumeDiscountProgram?: { changes: unknown };
};
export type SingleProposalData = ProposalData & {
terms: Terms & {
closingTimestamp: string;
validationTimestamp: string;
};
};
type BatchProposalData = ProposalData & {
batchTerms: {
changes: Terms[];
};
};
export type SubProposalData = SingleProposalData & {
batchId: string;
};
export type ProposalNode = {
proposal: ProposalData;
proposalType: ProposalNodeType;
proposals: SubProposalData[];
};
type SingleProposalNode = ProposalNode & {
proposal: SingleProposalData;
proposalType: 'TYPE_SINGLE_OR_UNSPECIFIED';
proposals: [];
};
type BatchProposalNode = ProposalNode & {
proposal: BatchProposalData;
proposalType: 'TYPE_BATCH';
};
export const isProposalNode = (node: unknown): node is ProposalNode =>
Boolean(
typeof node === 'object' &&
node &&
'proposal' in node &&
typeof node.proposal === 'object' &&
node?.proposal &&
'id' in node.proposal &&
node?.proposal?.id
);
export const isSingleProposalNode = (
node: Maybe<ProposalNode>
): node is SingleProposalNode =>
Boolean(
node &&
node?.proposalType === 'TYPE_SINGLE_OR_UNSPECIFIED' &&
node?.proposal
);
export const isBatchProposalNode = (
node: Maybe<ProposalNode>
): node is BatchProposalNode =>
Boolean(
node &&
node?.proposalType === 'TYPE_BATCH' &&
node?.proposal &&
'batchTerms' in node.proposal &&
node?.proposals?.length > 0
);
// this includes also batch proposals with `updateMarket`s 👍
const PROPOSALS_ENDPOINT = `${ENV.rest}governances?proposalState=:proposalState&proposalType=:proposalType`;
// this can be queried also by sub proposal id as `proposalId` and it will
// return full batch proposal data with all of its sub proposals including
// the requested one inside `proposals` array.
const PROPOSAL_ENDPOINT = `${ENV.rest}governance?proposalId=:proposalId`;
export const getProposals = async ({
proposalState,
proposalType,
}: {
proposalState: ProposalState;
proposalType: ProposalType;
}) => {
try {
const response = await fetch(
PROPOSALS_ENDPOINT.replace(':proposalState', proposalState).replace(
':proposalType',
proposalType
)
);
if (response.ok) {
const data = await response.json();
if (
data &&
'connection' in data &&
data.connection &&
'edges' in data.connection &&
data.connection.edges?.length > 0
) {
const nodes = compact(
data.connection.edges.map((e: { node?: object }) => e?.node)
).filter(isProposalNode);
return nodes;
}
}
} catch {
// NOOP - ignore errors
}
return [];
};
export const getProposal = async ({ proposalId }: { proposalId: string }) => {
try {
const response = await fetch(
PROPOSAL_ENDPOINT.replace(':proposalId', proposalId)
);
if (response.ok) {
const data = await response.json();
if (data && 'data' in data && isProposalNode(data.data)) {
return data.data as ProposalNode;
}
}
} catch (err) {
// NOOP - ignore errors
}
return null;
};
export const useFetchProposal = ({ proposalId }: { proposalId?: string }) => {
const [data, setData] = useState<ProposalNode | null>(null);
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
const cb = async () => {
if (!proposalId) return;
setLoading(true);
const data = await getProposal({ proposalId });
setLoading(false);
if (data) {
setData(data);
}
};
cb();
}, [proposalId]);
return { data, loading };
};
export const useFetchProposals = ({
proposalState,
proposalType,
}: {
proposalState: ProposalState;
proposalType: ProposalType;
}) => {
const [data, setData] = useState<ProposalNode[]>([]);
const [loading, setLoading] = useState<boolean>(false);
useEffect(() => {
const cb = async () => {
setLoading(true);
const data = await getProposals({ proposalState, proposalType });
setLoading(false);
if (data) {
setData(data);
}
};
cb();
}, [proposalState, proposalType]);
return { data, loading };
};
export const flatten = (
nodes: ProposalNode[]
): (SingleProposalData | SubProposalData)[] => {
const flattenNodes = [];
for (const node of nodes) {
if (isSingleProposalNode(node)) {
flattenNodes.push(node.proposal);
}
if (isBatchProposalNode(node)) {
for (const sub of node.proposals) {
flattenNodes.push(sub);
}
}
}
return flattenNodes;
};
@@ -6,6 +6,7 @@ import { render, screen } from '@testing-library/react';
import { generateProposal } from '../../test-helpers/generate-proposals'; import { generateProposal } from '../../test-helpers/generate-proposals';
import { Proposal } from './proposal'; import { Proposal } from './proposal';
import { ProposalState } from '@vegaprotocol/types'; import { ProposalState } from '@vegaprotocol/types';
import { mockNetworkParams } from '../../test-helpers/mocks';
import { type Proposal as IProposal } from '../../types'; import { type Proposal as IProposal } from '../../types';
jest.mock('@vegaprotocol/network-parameters', () => ({ jest.mock('@vegaprotocol/network-parameters', () => ({
@@ -37,12 +38,6 @@ jest.mock('../list-asset', () => ({
ListAsset: () => <div data-testid="proposal-list-asset"></div>, ListAsset: () => <div data-testid="proposal-list-asset"></div>,
})); }));
jest.mock('./proposal-change-details', () => ({
ProposalChangeDetails: () => (
<div data-testid="proposal-change-details"></div>
),
}));
const vegaWalletConfig: VegaWalletConfig = { const vegaWalletConfig: VegaWalletConfig = {
network: 'TESTNET', network: 'TESTNET',
vegaUrl: 'https://vega.xyz', vegaUrl: 'https://vega.xyz',
@@ -61,7 +56,11 @@ const renderComponent = (proposal: IProposal) => {
<MemoryRouter> <MemoryRouter>
<MockedProvider> <MockedProvider>
<VegaWalletProvider config={vegaWalletConfig}> <VegaWalletProvider config={vegaWalletConfig}>
<Proposal restData={null} proposal={proposal} /> <Proposal
restData={{}}
proposal={proposal}
networkParams={mockNetworkParams}
/>
</VegaWalletProvider> </VegaWalletProvider>
</MockedProvider> </MockedProvider>
</MemoryRouter> </MemoryRouter>
@@ -100,6 +99,27 @@ it('renders each section', async () => {
expect(await screen.findByTestId('proposal-header')).toBeInTheDocument(); expect(await screen.findByTestId('proposal-header')).toBeInTheDocument();
expect(screen.getByTestId('proposal-change-table')).toBeInTheDocument(); expect(screen.getByTestId('proposal-change-table')).toBeInTheDocument();
expect(screen.getByTestId('proposal-json')).toBeInTheDocument(); expect(screen.getByTestId('proposal-json')).toBeInTheDocument();
expect(screen.getByTestId('proposal-change-details')).toBeInTheDocument();
expect(screen.queryByTestId('proposal-list-asset')).not.toBeInTheDocument(); expect(screen.queryByTestId('proposal-list-asset')).not.toBeInTheDocument();
}); });
it('renders whitelist section if proposal is new asset and source is erc20', async () => {
const proposal = generateProposal({
terms: {
change: {
__typename: 'NewAsset',
name: 'foo',
symbol: 'FOO',
decimals: 18,
quantum: '1',
source: {
__typename: 'ERC20',
lifetimeLimit: '1',
withdrawThreshold: '100',
},
},
},
});
renderComponent(proposal);
expect(screen.getByTestId('proposal-list-asset')).toBeInTheDocument();
});
@@ -5,27 +5,152 @@ import { ProposalHeader } from '../proposal-detail-header/proposal-header';
import { ProposalDescription } from '../proposal-description'; import { ProposalDescription } from '../proposal-description';
import { ProposalChangeTable } from '../proposal-change-table'; import { ProposalChangeTable } from '../proposal-change-table';
import { ProposalJson } from '../proposal-json'; import { ProposalJson } from '../proposal-json';
import { ProposalAssetDetails } from '../proposal-asset-details';
import { ProposalReferralProgramDetails } from '../proposal-referral-program-details';
import { ProposalVolumeDiscountProgramDetails } from '../proposal-volume-discount-program-details';
import { UserVote } from '../vote-details'; import { UserVote } from '../vote-details';
import { ListAsset } from '../list-asset';
import Routes from '../../../routes'; import Routes from '../../../routes';
import { ProposalMarketData } from '../proposal-market-data';
import { type MarketInfo } from '@vegaprotocol/markets';
import { type AssetQuery } from '@vegaprotocol/assets';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import { ProposalState } from '@vegaprotocol/types'; import { ProposalState } from '@vegaprotocol/types';
import { type ProposalNode } from './proposal-utils'; import { ProposalMarketChanges } from '../proposal-market-changes';
import { ProposalUpdateMarketState } from '../proposal-update-market-state';
import { type NetworkParamsResult } from '@vegaprotocol/network-parameters';
import { useVoteSubmit } from '@vegaprotocol/proposals'; import { useVoteSubmit } from '@vegaprotocol/proposals';
import { useUserVote } from '../vote-details/use-user-vote'; import { useUserVote } from '../vote-details/use-user-vote';
import { type Proposal as IProposal, type BatchProposal } from '../../types'; import {
import { ProposalChangeDetails } from './proposal-change-details'; ProposalCancelTransferDetails,
import { type JsonValue } from 'type-fest'; ProposalTransferDetails,
} from '../proposal-transfer';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers';
import { type Proposal as IProposal } from '../../types';
export interface ProposalProps { export interface ProposalProps {
proposal: IProposal | BatchProposal; proposal: IProposal;
networkParams: Partial<NetworkParamsResult>;
marketData?: MarketInfo | null;
parentMarketData?: MarketInfo | null;
assetData?: AssetQuery | null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
restData: ProposalNode | null; restData: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
originalMarketProposalRestData?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mostRecentlyEnactedAssociatedMarketProposal?: any;
} }
export const Proposal = ({ proposal, restData }: ProposalProps) => { export const Proposal = ({
proposal,
networkParams,
restData,
marketData,
parentMarketData,
assetData,
originalMarketProposalRestData,
mostRecentlyEnactedAssociatedMarketProposal,
}: ProposalProps) => {
const featureFlags = useFeatureFlags((state) => state.flags);
const { t } = useTranslation(); const { t } = useTranslation();
const { submit, Dialog, finalizedVote, transaction } = useVoteSubmit(); const { submit, Dialog, finalizedVote, transaction } = useVoteSubmit();
const { voteState, voteDatetime } = useUserVote(proposal?.id, finalizedVote); const { voteState, voteDatetime } = useUserVote(proposal?.id, finalizedVote);
if (!proposal) {
return null;
}
let asset = assetData
? removePaginationWrapper(assetData.assetsConnection?.edges)[0]
: undefined;
const originalAsset = asset;
if (proposal.terms.change.__typename === 'UpdateAsset' && asset) {
asset = {
...asset,
quantum: proposal.terms.change.quantum,
source: { ...asset.source },
};
if (asset.source.__typename === 'ERC20') {
asset.source.lifetimeLimit = proposal.terms.change.source.lifetimeLimit;
asset.source.withdrawThreshold =
proposal.terms.change.source.withdrawThreshold;
}
}
let minVoterBalance = null;
if (networkParams) {
switch (proposal.terms.change.__typename) {
case 'UpdateMarket':
case 'UpdateMarketState':
minVoterBalance =
networkParams.governance_proposal_updateMarket_minVoterBalance;
break;
case 'NewMarket':
minVoterBalance =
networkParams.governance_proposal_market_minVoterBalance;
break;
case 'NewAsset':
minVoterBalance =
networkParams.governance_proposal_asset_minVoterBalance;
break;
case 'UpdateAsset':
minVoterBalance =
networkParams.governance_proposal_updateAsset_minVoterBalance;
break;
case 'UpdateNetworkParameter':
minVoterBalance =
networkParams.governance_proposal_updateNetParam_minVoterBalance;
break;
case 'NewFreeform':
minVoterBalance =
networkParams.governance_proposal_freeform_minVoterBalance;
break;
case 'NewTransfer':
// TODO: check minVoterBalance for 'NewTransfer'
minVoterBalance =
networkParams.governance_proposal_freeform_minVoterBalance;
break;
case 'CancelTransfer':
// TODO: check minVoterBalance for 'CancelTransfer'
minVoterBalance =
networkParams.governance_proposal_freeform_minVoterBalance;
break;
case 'UpdateReferralProgram':
minVoterBalance =
networkParams.governance_proposal_referralProgram_minVoterBalance;
break;
case 'UpdateVolumeDiscountProgram':
minVoterBalance =
networkParams.governance_proposal_VolumeDiscountProgram_minVoterBalance;
break;
}
}
// Show governance transfer details only if the GOVERNANCE_TRANSFERS flag is on.
const governanceTransferDetails = featureFlags.GOVERNANCE_TRANSFERS && (
<>
{proposal.terms.change.__typename === 'NewTransfer' && (
/** Governance New Transfer Details */
<div className="mb-4">
<ProposalTransferDetails proposal={proposal} />
</div>
)}
{proposal.terms.change.__typename === 'CancelTransfer' && (
/** Governance Cancel Transfer Details */
<div className="mb-4">
<ProposalCancelTransferDetails proposal={proposal} />
</div>
)}
</>
);
return ( return (
<section data-testid="proposal"> <section data-testid="proposal">
<div className="flex items-center gap-1 mb-6"> <div className="flex items-center gap-1 mb-6">
@@ -56,37 +181,91 @@ export const Proposal = ({ proposal, restData }: ProposalProps) => {
<ProposalChangeTable proposal={proposal} /> <ProposalChangeTable proposal={proposal} />
</div> </div>
{proposal.terms.change.__typename === 'NewAsset' &&
proposal.terms.change.source.__typename === 'ERC20' &&
proposal.id ? (
<ListAsset
assetId={proposal.id}
withdrawalThreshold={proposal.terms.change.source.withdrawThreshold}
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
/>
) : null}
<div className="mb-4"> <div className="mb-4">
<ProposalDescription description={proposal.rationale.description} /> <ProposalDescription description={proposal.rationale.description} />
</div> </div>
<div className="mb-4 flex flex-col gap-0"> {marketData && (
{proposal.__typename === 'Proposal' ? ( <div className="mb-4">
<ProposalChangeDetails <ProposalMarketData
proposal={proposal} marketData={marketData}
terms={proposal.terms} parentMarketData={parentMarketData ? parentMarketData : undefined}
restData={restData}
/> />
) : proposal.__typename === 'BatchProposal' ? ( </div>
proposal.subProposals?.map((p, i) => { )}
if (!p?.terms) return null;
return ( {proposal.terms.change.__typename === 'UpdateMarketState' && (
<ProposalChangeDetails <div className="mb-4">
indicator={i + 1} <ProposalUpdateMarketState proposal={proposal} />
key={i} </div>
proposal={proposal} )}
terms={p.terms}
restData={restData} {proposal.terms.change.__typename === 'UpdateMarket' && (
/> <div className="mb-4">
); <ProposalMarketChanges
}) originalProposal={
) : null} originalMarketProposalRestData?.data?.proposal?.terms?.newMarket
</div> ?.changes || {}
}
latestEnactedProposal={
mostRecentlyEnactedAssociatedMarketProposal?.node?.proposal?.terms
?.updateMarket?.changes || {}
}
updatedProposal={
restData?.data?.proposal?.terms?.updateMarket?.changes || {}
}
/>
</div>
)}
{(proposal.terms.change.__typename === 'NewAsset' ||
proposal.terms.change.__typename === 'UpdateAsset') &&
asset && (
<div className="mb-4">
<ProposalAssetDetails asset={asset} originalAsset={originalAsset} />
</div>
)}
{proposal.terms.change.__typename === 'UpdateReferralProgram' && (
<div className="mb-4">
<ProposalReferralProgramDetails proposal={proposal} />
</div>
)}
{proposal.terms.change.__typename === 'UpdateVolumeDiscountProgram' && (
<div className="mb-4">
<ProposalVolumeDiscountProgramDetails proposal={proposal} />
</div>
)}
{proposal.terms.change.__typename === 'UpdateNetworkParameter' &&
proposal.terms.change.networkParameter.key.slice(-13) ===
'.benefitTiers' && (
<div className="mb-4">
<ProposalUpdateBenefitTiers proposal={proposal} />
</div>
)}
{governanceTransferDetails}
<div className="mb-10"> <div className="mb-10">
<RoundedWrapper paddingBottom={true}> <RoundedWrapper paddingBottom={true}>
<UserVote <UserVote
proposal={proposal} proposal={proposal}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={
networkParams?.spam_protection_voting_min_tokens
}
submit={submit} submit={submit}
dialog={Dialog} dialog={Dialog}
transaction={transaction} transaction={transaction}
@@ -97,7 +276,7 @@ export const Proposal = ({ proposal, restData }: ProposalProps) => {
</div> </div>
<div className="mb-6"> <div className="mb-6">
<ProposalJson proposal={restData?.proposal as unknown as JsonValue} /> <ProposalJson proposal={restData?.data?.proposal} />
</div> </div>
</section> </section>
); );
@@ -1,25 +1,149 @@
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { VegaWalletContext } from '@vegaprotocol/wallet'; import { VegaWalletContext } from '@vegaprotocol/wallet';
import { type MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { format } from 'date-fns';
import { ProposalRejectionReason, ProposalState } from '@vegaprotocol/types';
import { generateProposal } from '../../test-helpers/generate-proposals';
import { ProposalsListItemDetails } from './proposals-list-item-details'; import { ProposalsListItemDetails } from './proposals-list-item-details';
import { mockWalletContext } from '../../test-helpers/mocks'; import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
import {
mockWalletContext,
networkParamsQueryMock,
fiveMinutes,
fiveHours,
fiveDays,
lastWeek,
nextWeek,
} from '../../test-helpers/mocks';
import { type Proposal } from '../../types';
const renderComponent = (id: string) => const renderComponent = (
proposal: Proposal,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mocks: MockedResponse<any>[] = [networkParamsQueryMock]
) =>
render( render(
<Router> <Router>
<VegaWalletContext.Provider value={mockWalletContext}> <MockedProvider mocks={mocks}>
<ProposalsListItemDetails id={id} /> <AppStateProvider>
</VegaWalletContext.Provider> <VegaWalletContext.Provider value={mockWalletContext}>
<ProposalsListItemDetails proposal={proposal} />
</VegaWalletContext.Provider>
</AppStateProvider>
</MockedProvider>
</Router> </Router>
); );
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(0);
});
afterAll(() => {
jest.useRealTimers();
});
describe('Proposals list item details', () => { describe('Proposals list item details', () => {
it('links to single proposal page', () => { it('Renders proposal state: Enacted', () => {
const proposalId = 'proposal-id'; renderComponent(
renderComponent(proposalId); generateProposal({
expect(screen.getByRole('link')).toHaveAttribute( state: ProposalState.STATE_ENACTED,
'href', terms: {
expect.stringContaining(proposalId) enactmentDatetime: lastWeek.toString(),
},
})
);
expect(screen.getByTestId('vote-details')).toHaveTextContent(
format(lastWeek, DATE_FORMAT_DETAILED)
);
});
it('Renders proposal state: Passed', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
})
);
expect(screen.getByTestId('vote-details')).toHaveTextContent(
`Enacts on ${format(nextWeek, DATE_FORMAT_DETAILED)}`
);
});
it('Renders proposal state: Waiting for node vote', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_WAITING_FOR_NODE_VOTE,
terms: {
enactmentDatetime: nextWeek.toString(),
},
})
);
expect(screen.getByTestId('vote-details')).toHaveTextContent(
`Enacts on ${format(nextWeek, DATE_FORMAT_DETAILED)}`
);
});
it('Renders proposal state: Open - 5 minutes left to vote', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
terms: {
closingDatetime: fiveMinutes.toString(),
},
})
);
expect(screen.getByTestId('vote-details')).toHaveTextContent(
'5 minutes left to vote'
);
});
it('Renders proposal state: Open - 5 hours left to vote', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
terms: {
closingDatetime: fiveHours.toString(),
},
})
);
expect(screen.getByTestId('vote-details')).toHaveTextContent(
'5 hours left to vote'
);
});
it('Renders proposal state: Open - 5 days left to vote', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
terms: {
closingDatetime: fiveDays.toString(),
},
})
);
expect(screen.getByTestId('vote-details')).toHaveTextContent(
'5 days left to vote'
);
});
it('Renders proposal state: Rejected', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_REJECTED,
terms: {
enactmentDatetime: lastWeek.toString(),
},
rejectionReason:
ProposalRejectionReason.PROPOSAL_ERROR_INVALID_FUTURE_PRODUCT,
})
);
expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Invalid future product'
); );
}); });
}); });
@@ -1,16 +1,101 @@
import { type ReactNode } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Button } from '@vegaprotocol/ui-toolkit'; import { Button } from '@vegaprotocol/ui-toolkit';
import { differenceInHours, format, formatDistanceToNowStrict } from 'date-fns';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
import {
ProposalRejectionReasonMapping,
ProposalState,
} from '@vegaprotocol/types';
import Routes from '../../../routes'; import Routes from '../../../routes';
import { type Proposal } from '../../types';
export const ProposalsListItemDetails = ({ id }: { id: string }) => { export const ProposalsListItemDetails = ({
proposal,
}: {
proposal: Proposal;
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
const state = proposal?.state;
const nowToEnactmentInHours = differenceInHours(
new Date(proposal?.terms.closingDatetime),
new Date()
);
let voteDetails: ReactNode;
let voteStatus: ReactNode;
switch (state) {
case ProposalState.STATE_ENACTED: {
voteDetails =
proposal?.terms.enactmentDatetime &&
t('enactedOn{{date}}', {
enactmentDate:
proposal?.terms.enactmentDatetime &&
format(
new Date(proposal?.terms.enactmentDatetime),
DATE_FORMAT_DETAILED
),
});
break;
}
case ProposalState.STATE_PASSED: {
voteDetails =
proposal?.terms.change.__typename !== 'NewFreeform' &&
t('enactsOn{{date}}', {
enactmentDate:
proposal?.terms.enactmentDatetime &&
format(
new Date(proposal.terms.enactmentDatetime),
DATE_FORMAT_DETAILED
),
});
break;
}
case ProposalState.STATE_WAITING_FOR_NODE_VOTE: {
voteDetails =
proposal?.terms.change.__typename !== 'NewFreeform' &&
t('enactsOn{{date}}', {
enactmentDate:
proposal?.terms.enactmentDatetime &&
format(
new Date(proposal.terms.enactmentDatetime),
DATE_FORMAT_DETAILED
),
});
break;
}
case ProposalState.STATE_OPEN: {
voteDetails = (
<span className={nowToEnactmentInHours < 6 ? 'text-vega-orange' : ''}>
{formatDistanceToNowStrict(new Date(proposal?.terms.closingDatetime))}{' '}
{t('left to vote')}
</span>
);
break;
}
case ProposalState.STATE_REJECTED: {
voteStatus = proposal?.rejectionReason && (
<>{t(ProposalRejectionReasonMapping[proposal.rejectionReason])}</>
);
break;
}
}
return ( return (
<div className="mt-4 items-start text-sm"> <div className="mt-4 items-start text-sm">
<Link to={`${Routes.PROPOSALS}/${id}`}> <div className="flex items-center gap-2 text-vega-light-300 mb-2">
<Button data-testid="view-proposal-btn">{t('viewDetails')}</Button> {voteDetails && <span data-testid="vote-details">{voteDetails}</span>}
</Link> {voteDetails && voteStatus && <span>&middot;</span>}
{voteStatus && <span data-testid="vote-status">{voteStatus}</span>}
</div>
{proposal?.id && (
<Link to={`${Routes.PROPOSALS}/${proposal.id}`}>
<Button data-testid="view-proposal-btn">{t('viewDetails')}</Button>
</Link>
)}
</div> </div>
); );
}; };
@@ -2,10 +2,10 @@ import { RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { ProposalHeader } from '../proposal-detail-header/proposal-header'; import { ProposalHeader } from '../proposal-detail-header/proposal-header';
import { ProposalsListItemDetails } from './proposals-list-item-details'; import { ProposalsListItemDetails } from './proposals-list-item-details';
import { useUserVote } from '../vote-details/use-user-vote'; import { useUserVote } from '../vote-details/use-user-vote';
import { type Proposal, type BatchProposal } from '../../types'; import { type Proposal } from '../../types';
interface ProposalsListItemProps { interface ProposalsListItemProps {
proposal?: Proposal | BatchProposal; proposal?: Proposal | null;
} }
export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => { export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => {
@@ -16,7 +16,7 @@ export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => {
<li id={proposal.id} data-testid="proposals-list-item"> <li id={proposal.id} data-testid="proposals-list-item">
<RoundedWrapper paddingBottom={true} heightFull={true}> <RoundedWrapper paddingBottom={true} heightFull={true}>
<ProposalHeader proposal={proposal} voteState={voteState} /> <ProposalHeader proposal={proposal} voteState={voteState} />
<ProposalsListItemDetails id={proposal.id} /> <ProposalsListItemDetails proposal={proposal} />
</RoundedWrapper> </RoundedWrapper>
</li> </li>
); );
@@ -3,12 +3,14 @@ import {
generateProtocolUpgradeProposal, generateProtocolUpgradeProposal,
} from '../../test-helpers/generate-proposals'; } from '../../test-helpers/generate-proposals';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider'; import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { ProposalsList } from './proposals-list'; import { ProposalsList } from './proposals-list';
import { ProposalState } from '@vegaprotocol/types'; import { ProposalState } from '@vegaprotocol/types';
import { fireEvent, render, screen, within } from '@testing-library/react'; import { fireEvent, render, screen, within } from '@testing-library/react';
import { import {
mockWalletContext,
networkParamsQueryMock, networkParamsQueryMock,
lastWeek, lastWeek,
nextWeek, nextWeek,
@@ -18,16 +20,6 @@ import {
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals'; import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { type Proposal } from '../../types'; import { type Proposal } from '../../types';
jest.mock('../vote-details/use-user-vote', () => ({
useUserVote: jest.fn().mockImplementation(() => ({ voteState: 'NotCast' })),
}));
jest.mock('../proposals-list-item', () => ({
ProposalsListItem: ({ proposal }: { proposal: { id: string } }) => (
<div data-testid="proposals-list-item" id={proposal.id} />
),
}));
const openProposalClosesNextMonth = generateProposal({ const openProposalClosesNextMonth = generateProposal({
id: 'proposal1', id: 'proposal1',
state: ProposalState.STATE_OPEN, state: ProposalState.STATE_OPEN,
@@ -77,10 +69,12 @@ const renderComponent = (
<Router> <Router>
<MockedProvider mocks={[networkParamsQueryMock]}> <MockedProvider mocks={[networkParamsQueryMock]}>
<AppStateProvider> <AppStateProvider>
<ProposalsList <VegaWalletContext.Provider value={mockWalletContext}>
proposals={proposals} <ProposalsList
protocolUpgradeProposals={protocolUpgradeProposals || []} proposals={proposals}
/> protocolUpgradeProposals={protocolUpgradeProposals || []}
/>
</VegaWalletContext.Provider>
</AppStateProvider> </AppStateProvider>
</MockedProvider> </MockedProvider>
</Router> </Router>
@@ -94,6 +88,10 @@ afterAll(() => {
jest.useRealTimers(); jest.useRealTimers();
}); });
jest.mock('../vote-details/use-user-vote', () => ({
useUserVote: jest.fn().mockImplementation(() => ({ voteState: 'NotCast' })),
}));
describe('Proposals list', () => { describe('Proposals list', () => {
it('Render a page title and link to the make proposal form', async () => { it('Render a page title and link to the make proposal form', async () => {
render(renderComponent([])); render(renderComponent([]));
@@ -11,20 +11,19 @@ import { Button, Toggle } from '@vegaprotocol/ui-toolkit';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { ExternalLink } from '@vegaprotocol/ui-toolkit'; import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { ExternalLinks } from '@vegaprotocol/environment'; import { ExternalLinks } from '@vegaprotocol/environment';
import { type ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals'; import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { type BatchProposal, type Proposal } from '../../types'; import { type Proposal } from '../../types';
type Proposals = Array<Proposal | BatchProposal>;
interface ProposalsListProps { interface ProposalsListProps {
proposals: Proposals; proposals: Proposal[];
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[]; protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
lastBlockHeight?: string; lastBlockHeight?: string;
} }
interface SortedProposalsProps { interface SortedProposalsProps {
open: Proposals; open: Proposal[];
closed: Proposals; closed: Proposal[];
} }
interface SortedProtocolUpgradeProposalsProps { interface SortedProtocolUpgradeProposalsProps {
@@ -32,26 +31,15 @@ interface SortedProtocolUpgradeProposalsProps {
closed: ProtocolUpgradeProposalFieldsFragment[]; closed: ProtocolUpgradeProposalFieldsFragment[];
} }
export const orderByDate = (arr: Proposals) => export const orderByDate = (arr: Proposal[]) =>
orderBy( orderBy(
arr, arr,
[ [
(p) => { (p) =>
if (p.__typename === 'BatchProposal') { p?.terms?.enactmentDatetime
// Batch proposals can have different enactment dates, this could be improved by ordering ? new Date(p?.terms?.enactmentDatetime).getTime()
// by soonest enactment date in the batch : // has to be defaulted to 0 because new Date(null).getTime() -> NaN which is first when ordered
return new Date(p.batchTerms?.closingDatetime || p.datetime); new Date(p?.terms?.closingDatetime || 0).getTime(),
}
if (p.__typename === 'Proposal') {
return p?.terms?.enactmentDatetime
? new Date(p?.terms?.enactmentDatetime).getTime()
: // has to be defaulted to 0 because new Date(null).getTime() -> NaN which is first when ordered
new Date(p?.terms?.closingDatetime || 0).getTime();
}
throw new Error('invalid proposal');
},
(p) => new Date(p?.datetime).getTime(), (p) => new Date(p?.datetime).getTime(),
], ],
['asc', 'asc'] ['asc', 'asc']
@@ -88,40 +76,18 @@ export const ProposalsList = ({
const sortedProposals: SortedProposalsProps = useMemo(() => { const sortedProposals: SortedProposalsProps = useMemo(() => {
const initialSorting = proposals.reduce( const initialSorting = proposals.reduce(
(acc, proposal) => { (acc: SortedProposalsProps, proposal) => {
if (proposal.__typename === 'Proposal') { if (isFuture(new Date(proposal?.terms.closingDatetime))) {
if (isFuture(new Date(proposal?.terms.closingDatetime))) { acc.open.push(proposal);
acc.open.push(proposal); } else {
} else { acc.closed.push(proposal);
acc.closed.push(proposal);
}
return acc;
} }
if (proposal.__typename === 'BatchProposal') {
if (
// this could be improved by sorting by soonest enactment date of all the
// sub proposals
isFuture(
new Date(
proposal.batchTerms?.closingDatetime || proposal.datetime
)
)
) {
acc.open.push(proposal);
} else {
acc.closed.push(proposal);
}
return acc;
}
return acc; return acc;
}, },
{ {
open: [], open: [],
closed: [], closed: [],
} as SortedProposalsProps }
); );
return { return {
open: open:
@@ -155,7 +121,7 @@ export const ProposalsList = ({
}; };
}, [protocolUpgradeProposals, lastBlockHeight]); }, [protocolUpgradeProposals, lastBlockHeight]);
const filterPredicate = (p: Proposal | BatchProposal) => const filterPredicate = (p: ProposalFieldsFragment | Proposal) =>
p?.id?.includes(filterString) || p?.id?.includes(filterString) ||
p?.party?.id?.toString().includes(filterString); p?.party?.id?.toString().includes(filterString);
@@ -206,7 +172,7 @@ export const ProposalsList = ({
/> />
)} )}
<section className="-mx-4 p-4 mb-8 bg-vega-cdark-900 rounded-l-sm"> <section className="-mx-4 p-4 mb-8 bg-vega-dark-100">
<SubHeading title={t('openProposals')} /> <SubHeading title={t('openProposals')} />
{sortedProposals.open.length > 0 || {sortedProposals.open.length > 0 ||
@@ -1,8 +1,17 @@
import { generateProposal } from '../../test-helpers/generate-proposals'; import { generateProposal } from '../../test-helpers/generate-proposals';
import { MockedProvider } from '@apollo/client/testing';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { BrowserRouter as Router } from 'react-router-dom';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { RejectedProposalsList } from './rejected-proposals-list'; import { RejectedProposalsList } from './rejected-proposals-list';
import { ProposalState } from '@vegaprotocol/types'; import { ProposalState } from '@vegaprotocol/types';
import { render, screen } from '@testing-library/react'; import { render, screen, waitFor, within } from '@testing-library/react';
import { nextWeek, lastMonth } from '../../test-helpers/mocks'; import {
mockWalletContext,
networkParamsQueryMock,
nextWeek,
lastMonth,
} from '../../test-helpers/mocks';
import { type Proposal } from '../../types'; import { type Proposal } from '../../types';
const rejectedProposalClosesNextWeek = generateProposal({ const rejectedProposalClosesNextWeek = generateProposal({
@@ -27,15 +36,31 @@ const rejectedProposalClosedLastMonth = generateProposal({
}); });
const renderComponent = (proposals: Proposal[]) => ( const renderComponent = (proposals: Proposal[]) => (
<RejectedProposalsList proposals={proposals} /> <Router>
<MockedProvider mocks={[networkParamsQueryMock]}>
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<RejectedProposalsList proposals={proposals} />
</VegaWalletContext.Provider>
</AppStateProvider>
</MockedProvider>
</Router>
); );
jest.mock('../proposals-list-item', () => ({ beforeAll(() => {
ProposalsListItem: () => <div data-testid="proposals-list-item" />, jest.useFakeTimers();
jest.setSystemTime(0);
});
afterAll(() => {
jest.useRealTimers();
});
jest.mock('../vote-details/use-user-vote', () => ({
useUserVote: jest.fn().mockImplementation(() => ({ voteState: 'NotCast' })),
})); }));
describe('Rejected proposals list', () => { describe('Rejected proposals list', () => {
it('Renders a list of proposals', () => { it('Renders a list of proposals', async () => {
render( render(
renderComponent([ renderComponent([
rejectedProposalClosedLastMonth, rejectedProposalClosedLastMonth,
@@ -43,13 +68,25 @@ describe('Rejected proposals list', () => {
]) ])
); );
expect(screen.getAllByTestId('proposals-list-item')).toHaveLength(2); await waitFor(() => {
const rejectedProposals = within(
screen.getByTestId('rejected-proposals')
);
const rejectedProposalsItems = rejectedProposals.getAllByTestId(
'proposals-list-item'
);
expect(rejectedProposalsItems).toHaveLength(2);
});
}); });
it('Displays text when there are no proposals', async () => { it('Displays text when there are no proposals', async () => {
render(renderComponent([])); render(renderComponent([]));
expect(screen.getByTestId('no-rejected-proposals')).toBeInTheDocument(); await waitFor(() => {
expect(screen.queryByTestId('rejected-proposals')).not.toBeInTheDocument(); expect(screen.getByTestId('no-rejected-proposals')).toBeInTheDocument();
expect(
screen.queryByTestId('rejected-proposals')
).not.toBeInTheDocument();
});
}); });
}); });
@@ -3,17 +3,17 @@ import { useTranslation } from 'react-i18next';
import { Heading } from '../../../../components/heading'; import { Heading } from '../../../../components/heading';
import { ProposalsListItem } from '../proposals-list-item'; import { ProposalsListItem } from '../proposals-list-item';
import { ProposalsListFilter } from '../proposals-list-filter'; import { ProposalsListFilter } from '../proposals-list-filter';
import { type BatchProposal, type Proposal } from '../../types'; import { type Proposal } from '../../types';
interface ProposalsListProps { interface ProposalsListProps {
proposals: Array<Proposal | BatchProposal>; proposals: Proposal[];
} }
export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => { export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [filterString, setFilterString] = useState(''); const [filterString, setFilterString] = useState('');
const filterPredicate = (p: Proposal | BatchProposal) => const filterPredicate = (p: Proposal) =>
p?.id?.includes(filterString) || p?.id?.includes(filterString) ||
p?.party?.id?.toString().includes(filterString); p?.party?.id?.toString().includes(filterString);

Some files were not shown because too many files have changed in this diff Show More