Compare commits

..
289 changed files with 7723 additions and 12637 deletions
+2 -2
View File
@@ -10,7 +10,7 @@ on:
inputs:
console-test-branch:
type: choice
description: 'main: v0.73.13, develop: v0.74.0'
description: 'main: v0.73.5, develop: v0.73.5'
options:
- main
- develop
@@ -205,7 +205,7 @@ jobs:
# 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
#----------------------------------------------
# upload traces
+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).
## 🔎 Applications in this repo
# 🔎 Applications in this repo
### [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.
## 🧱 Libraries in this repo
# 🧱 Libraries in this repo
### [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.
## 💻 Develop
# 💻 Develop
### 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.
## 🐋 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).
@@ -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
```
## 📑 License
# 📑 License
[MIT](./LICENSE)
@@ -1,7 +1,7 @@
import { getNewAssetTxBody } from '../support/governance.functions';
context('Proposal page', { tags: '@smoke' }, function () {
describe.skip('Verify elements on page', function () {
describe('Verify elements on page', function () {
const proposalHeading = 'proposals-heading';
const dateTimeRegex =
/(\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('type').should('have.text', 'NewMarket');
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"]')
.invoke('text')
.should('match', dateTimeRegex);
@@ -69,6 +73,10 @@ context('Proposal page', { tags: '@smoke' }, function () {
'have.text',
'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"]')
.invoke('text')
.should('match', dateTimeRegex);
@@ -7,7 +7,6 @@ export type AssetBalanceProps = {
price: string;
showAssetLink?: boolean;
showAssetSymbol?: boolean;
rounded?: boolean;
};
/**
@@ -19,17 +18,12 @@ const AssetBalance = ({
price,
showAssetLink = true,
showAssetSymbol = false,
rounded = false,
}: AssetBalanceProps) => {
const { data: asset, loading } = useAssetDataProvider(assetId);
const label =
!loading && asset && asset.decimals
? addDecimalsFixedFormatNumber(
price,
asset.decimals,
rounded ? 0 : undefined
)
? addDecimalsFixedFormatNumber(price, asset.decimals)
: price;
return (
@@ -41,7 +41,6 @@ export const Header = () => {
Routes.ASSETS,
Routes.MARKETS,
Routes.GOVERNANCE,
Routes.TREASURY,
Routes.NETWORK_PARAMETERS,
Routes.GENESIS,
].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>> & {
id: string;
truncate?: boolean;
networkLabel?: string;
truncateLength?: number;
};
const PartyLink = ({
id,
truncate = false,
truncateLength = 4,
networkLabel = t('Network'),
...props
}: PartyLinkProps) => {
const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
const { data } = useExplorerNodeNamesQuery();
const name = useMemo(() => getNameForParty(id, data), [data, id]);
const useName = name !== id;
@@ -52,7 +44,7 @@ const PartyLink = ({
if (id === SPECIAL_CASE_NETWORK || id === SPECIAL_CASE_NETWORK_ID) {
return (
<span className="font-mono" data-testid="network">
{networkLabel}
{t('Network')}
</span>
);
}
@@ -78,11 +70,7 @@ const PartyLink = ({
{useName ? (
name
) : (
<Hash
text={
truncate ? truncateMiddle(id, truncateLength, truncateLength) : id
}
/>
<Hash text={truncate ? truncateMiddle(id, 4, 4) : id} />
)}
</Link>
</span>
@@ -175,7 +175,6 @@ describe('Amend order details', () => {
const res = renderExistingAmend('123', 1, amend);
expect(await res.findByText('New size')).toBeInTheDocument();
expect(await res.findByText('Size ±')).toBeInTheDocument();
});
it('Renders Reference if provided', async () => {
@@ -82,7 +82,7 @@ const AmendOrderDetails = ({ id, version, amend }: AmendOrderDetailsProps) => {
{amend.sizeDelta && amend.sizeDelta !== '0' ? (
<div className="mb-12 md:mb-0">
<h2 className="text-dark mb-4 text-2xl font-bold">
{t('Size ±')}
{t('New size')}
</h2>
<h5
className={`mb-0 text-lg font-medium capitalize text-gray-500 ${getSideDeltaColour(
@@ -93,16 +93,6 @@ const AmendOrderDetails = ({ id, version, amend }: AmendOrderDetailsProps) => {
</h5>
</div>
) : 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' ? (
<div className="">
@@ -1,4 +1,5 @@
import { type ProposalListFieldsFragment } from '@vegaprotocol/proposals';
import { VoteProgress } from '@vegaprotocol/proposals';
import { type AgGridReact } from 'ag-grid-react';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { AgGrid } from '@vegaprotocol/datagrid';
@@ -11,7 +12,12 @@ import { type ColDef } from 'ag-grid-community';
import type { RowClickedEvent } from 'ag-grid-community';
import { getDateTimeFormat } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { ProposalStateMapping } from '@vegaprotocol/types';
import BigNumber from 'bignumber.js';
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
import { BREAKPOINT_MD } from '../../config/breakpoints';
import { JsonViewerDialog } from '../dialogs/json-viewer-dialog';
@@ -25,7 +31,15 @@ type ProposalsTableProps = {
data: ProposalListFieldsFragment[] | null;
};
export const ProposalsTable = ({ data }: ProposalsTableProps) => {
const { params } = useNetworkParams([
NetworkParams.governance_proposal_market_requiredMajority,
]);
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);
useLayoutEffect(() => {
@@ -76,6 +90,33 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
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',
maxWidth: 150,
@@ -143,7 +184,7 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
},
},
],
[tokenLink]
[requiredMajorityPercentage, tokenLink]
);
return (
<>
@@ -64,9 +64,7 @@ export const ProposalSummary = ({
return (
<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} />}
{rationale?.title && (
<h1 className="text-xl pb-1 break-all">{rationale.title}</h1>
)}
{rationale?.title && <h1 className="text-xl pb-1">{rationale.title}</h1>}
{rationale?.description && (
<div className="pt-2 text-sm leading-tight">
<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
xmlns="http://www.w3.org/2000/svg"
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" />
</svg>
@@ -120,7 +120,7 @@ export function TransferParticipants({
<svg
xmlns="http://www.w3.org/2000/svg"
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" />
</svg>
@@ -1,223 +1,97 @@
import { t } from '@vegaprotocol/i18n';
import { AssetLink, MarketLink } from '../../../../links';
import { headerClasses, wrapperClasses } from '../transfer-details';
import type { components } from '../../../../../../types/explorer';
import type { Recurring } from '../transfer-details';
import {
DispatchMetricLabels,
DistributionStrategy,
} from '@vegaprotocol/types';
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '@vegaprotocol/utils';
import { DispatchMetricLabels } from '@vegaprotocol/types';
export type Metric = components['schemas']['vegaDispatchMetric'];
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> = {
DISPATCH_METRIC_UNSPECIFIED: 'Unknown metric',
...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 {
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
*/
export function TransferRewards({ recurring }: TransferRewardsProps) {
const metric =
recurring?.dispatchStrategy?.metric || 'DISPATCH_METRIC_UNSPECIFIED';
if (!recurring || !recurring.dispatchStrategy) {
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 (
<div className={wrapperClasses}>
<h2 className={headerClasses}>{getRewardTitle(entityScope)}</h2>
<ul className="relative block rounded-lg py-6 text-left p-6">
{entityScope && entityScopeIcons[entityScope] ? (
<h2 className={headerClasses}>{t('Reward metrics')}</h2>
<ul className="relative block rounded-lg py-6 text-center p-6">
{recurring.dispatchStrategy.assetForMetric ? (
<li>
<strong>{t('Scope')}</strong>:{' '}
<VegaIcon name={entityScopeIcons[entityScope]} />
&nbsp;
{individualScope ? individualScopeLabels[individualScope] : null}
{getScopeLabel(entityScope, teamScope)}
<strong>{t('Asset')}</strong>:{' '}
<AssetLink assetId={recurring.dispatchStrategy.assetForMetric} />
</li>
) : null}
{recurring.dispatchStrategy &&
recurring.dispatchStrategy.assetForMetric && (
<li>
<strong>{t('Asset for metric')}</strong>:{' '}
<AssetLink assetId={recurring.dispatchStrategy.assetForMetric} />
</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>
<strong>{t('Metric')}</strong>: {metricLabels[metric]}
</li>
{recurring.dispatchStrategy.markets &&
recurring.dispatchStrategy.markets.length > 0 ? (
<li>
<strong>{t('Markets in scope')}</strong>:
<ul className="inline-block ml-1">
{markets.map((m) => (
<li key={m} className="inline-block mr-2">
<ul>
{recurring.dispatchStrategy.markets.map((m) => (
<li key={m}>
<MarketLink id={m} />
</li>
))}
</ul>
</li>
) : null}
{stakingRequirement && stakingRequirement !== '0' ? (
<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>
)}
<li>
<strong>{t('Factor')}</strong>: {recurring.factor}
</li>
</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>
);
}
export function getScopeLabel(
scope: components['schemas']['vegaEntityScope'] | undefined,
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');
interface TransferRecurringStrategyProps {
strategy: Strategy;
}
const individualScopeLabels: Record<
components['schemas']['vegaIndividualScope'],
string
> = {
// Unspecified and All are not rendered
INDIVIDUAL_SCOPE_UNSPECIFIED: '',
INDIVIDUAL_SCOPE_ALL: '',
INDIVIDUAL_SCOPE_IN_TEAM: '(in team)',
INDIVIDUAL_SCOPE_NOT_IN_TEAM: '(not in team)',
};
/**
* Simple renderer for a dispatch strategy in a recurring transfer
*
* @param strategy Dispatch strategy object
*/
export function TransferRecurringStrategy({
strategy,
}: TransferRecurringStrategyProps) {
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 { TransferRewards } from './blocks/transfer-rewards';
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 Metric = components['schemas']['vegaDispatchMetric'];
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 =
'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 {
transfer: Transfer;
from: string;
id: string;
}
/**
@@ -28,24 +24,13 @@ interface TransferDetailsProps {
*
* @param transfer A recurring transfer object
*/
export function TransferDetails({ transfer, from, id }: TransferDetailsProps) {
export function TransferDetails({ transfer, from }: TransferDetailsProps) {
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 (
<div className="flex flex-wrap">
<div className="flex gap-5 flex-wrap">
<TransferParticipants from={from} transfer={transfer} />
{recurring ? <TransferRepeat recurring={transfer.recurring} /> : null}
<TransferStatusView status={status} error={error} loading={loading} />
{recurring && recurring.dispatchStrategy ? (
<TransferRewards recurring={transfer.recurring} />
) : 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 { TxDetailsUpdateMarginMode } from './tx-update-margin-mode';
import { TxBatchProposal } from './tx-batch-proposal';
import { TxDetailsUpdatePartyProfile } from './proposal/tx-update-party-profile';
interface TxDetailsWrapperProps {
txData: BlockExplorerTransactionResult | undefined;
@@ -140,8 +139,6 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
return TxDetailsUpdateMarginMode;
case 'Batch Proposal':
return TxBatchProposal;
case 'Update Party Profile':
return TxDetailsUpdatePartyProfile;
default:
return TxDetailsGeneric;
}
@@ -12,8 +12,6 @@ import { ProposalSignatureBundleNewAsset } from './proposal/signature-bundle-new
import { ProposalSignatureBundleUpdateAsset } from './proposal/signature-bundle-update';
import { MarketLink } from '../../links';
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 ProposalTerms = components['schemas']['vegaProposalTerms'];
@@ -106,12 +104,6 @@ export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => {
? ProposalSignatureBundleNewAsset
: ProposalSignatureBundleUpdateAsset;
let transfer, from;
if (proposal.terms?.newTransfer?.changes) {
transfer = proposalToTransfer(proposal.terms?.newTransfer.changes);
from = proposal.terms.newTransfer.changes.source;
}
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
@@ -157,26 +149,14 @@ export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => {
</>
) : null}
</TableWithTbody>
<ProposalSummary
id={deterministicId}
rationale={proposal.rationale}
terms={proposal?.terms}
/>
{proposalRequiresSignatureBundle(proposal) && (
<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,
} from '../../links/party-link/party-link';
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
import Hash from '../../links/hash';
type Transfer = components['schemas']['commandsv1Transfer'];
@@ -61,7 +60,7 @@ export const TxDetailsTransfer = ({
}
const from = txData.submitter;
const id = txSignatureToDeterministicId(txData.signature.value);
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
@@ -72,7 +71,7 @@ export const TxDetailsTransfer = ({
<TableRow modifier="bordered" data-testid="id">
<TableCell {...sharedHeaderProps}>{t('Transfer ID')}</TableCell>
<TableCell>
<Hash text={id} />
{txSignatureToDeterministicId(txData.signature.value)}
</TableCell>
</TableRow>
<TxDetailsShared
@@ -106,7 +105,7 @@ export const TxDetailsTransfer = ({
</TableRow>
) : null}
</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'
| 'Transfer Funds'
| 'Undelegate'
| 'Update Party Profile'
| 'Update Referral Set'
| 'Update Margin Mode'
| 'Validator Heartbeat'
@@ -80,7 +79,6 @@ export const filterOptions: Record<string, FilterOption[]> = {
'Apply Referral Code',
'Create Referral Set',
'Join Team',
'Update Party Profile',
'Update Referral Set',
],
'External Data': ['Chain Event', 'Submit Oracle Data'],
@@ -12,5 +12,4 @@ export const Routes = {
ORACLES: 'oracles',
NETWORK_PARAMETERS: 'network-parameters',
DISCLAIMER: 'disclaimer',
TREASURY: 'treasury',
};
@@ -30,7 +30,6 @@ import { PartyAccountsByAsset } from './parties/id/accounts';
import { Disclaimer } from './pages/disclaimer';
import { useFeatureFlags } from '@vegaprotocol/environment';
import RestrictedPage from './restricted';
import { NetworkTreasury } from './treasury';
export type Navigable = {
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
? [
{
@@ -370,7 +358,6 @@ export const useRouterConfig = () => {
...marketsRoutes,
...networkParametersRoutes,
...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",
"name": "Vega Protocol - Explorer",
"short_name": "Mainnet Stats",
"name": "Vega Mainnet statistics",
"icons": [
{
"src": "favicon.ico",
@@ -2,7 +2,7 @@ export const proposalsData = {
proposalsConnection: {
edges: [
{
proposalNode: {
node: {
id: 'e8ba9d268e12514644fd1fc7ff289292f4ce6489cc32cc73133aea52c04aef89',
rationale: {
title: 'Add asset Wrapped Ether',
@@ -56,7 +56,7 @@ export const proposalsData = {
__typename: 'ProposalEdge',
},
{
proposalNode: {
node: {
id: 'd848fc7881f13d366df5f61ab139d5fcfa72bf838151bb51b54381870e357931',
rationale: {
title: 'Add asset Dai Stablecoin',
@@ -110,7 +110,60 @@ export const proposalsData = {
__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',
rationale: {
title: 'USDC-III',
@@ -164,7 +217,60 @@ export const proposalsData = {
__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',
rationale: {
title: 'Update governance.proposal.asset.requiredMajority',
@@ -236,7 +236,7 @@ context(
});
// 1002-STKE-041 1002-STKE-053
it.skip(
it(
'Able to remove part of a stake against a validator',
// @ts-ignore clash between jest and cypress
{ tags: '@smoke' },
@@ -27,7 +27,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
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) => {
if (!$body.find('[data-testid="proposals-list-item"]').length) {
cy.createMarket();
@@ -82,20 +82,20 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
it('should have information on active nodes', function () {
cy.getByTestId('node-information')
.first()
.should('contain.text', '1')
.should('contain.text', '2')
.and('contain.text', 'active nodes');
});
it('should have information on consensus nodes', function () {
cy.getByTestId('node-information')
.last()
.should('contain.text', '1')
.should('contain.text', '2')
.and('contain.text', 'consensus nodes');
});
it('should contain link to specific validators', function () {
cy.getByTestId('validators')
.should('have.length', '1')
.should('have.length', '2')
.each(($validator) => {
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.get('input').should('exist');
cy.getByTestId('connect').should('be.disabled');
cy.getByTestId('dialog-close').click();
cy.getByTestId('icon-cross').click();
});
it('should display eth data', function () {
@@ -189,7 +189,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
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('menu-drawer').should('be.visible');
});
@@ -40,10 +40,10 @@ context('View functionality with public key', { tags: '@smoke' }, function () {
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();
cy.getByTestId('currency-title', { timeout: 10000 })
.should('have.length.at.least', 2)
.should('have.length.at.least', 4)
.and('contain.text', 'USDC (fake)');
});
+3 -3
View File
@@ -33,7 +33,7 @@ LC_ALL="en_US.UTF-8"
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_PRODUCT_PERPETUALS=false
NX_UPDATE_MARKET_STATE=false
NX_REFERRALS=true
NX_GOVERNANCE_TRANSFERS=true
NX_GOVERNANCE_TRANSFERS=false
-3
View File
@@ -62,9 +62,6 @@ const cache: InMemoryCacheConfig = {
Account: {
keyFields: false,
},
Instrument: {
keyFields: ['code'],
},
Delegation: {
keyFields: false,
// Only get full updates
+2 -2
View File
@@ -1,6 +1,6 @@
{
"short_name": "Governance VEGA",
"name": "Vega Protocol - Governance",
"short_name": "Mainnet Stats",
"name": "Vega Mainnet statistics",
"icons": [
{
"src": "favicon.ico",
@@ -1,5 +1,5 @@
import classnames from 'classnames';
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { Icon } from '@vegaprotocol/ui-toolkit';
import type { Dispatch, SetStateAction, ReactNode } from 'react';
interface CollapsibleToggleProps {
@@ -30,7 +30,7 @@ export const CollapsibleToggle = ({
<div className="flex items-center gap-3">
{children}
<div className={classes} data-testid="toggle-icon-wrapper">
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={20} />
<Icon name="chevron-down" size={8} />
</div>
</div>
</button>
@@ -1,8 +1,7 @@
import classNames from 'classnames';
import { type ReactNode } from 'react';
interface HeadingProps {
title?: ReactNode;
title?: string;
centerContent?: boolean;
marginTop?: boolean;
marginBottom?: boolean;
+24 -19
View File
@@ -1,4 +1,3 @@
import compact from 'lodash/compact';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
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 { ProtocolUpgradeProposalsListItem } from '../proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item';
import Routes from '../routes';
import { ExternalLinks } from '@vegaprotocol/environment';
import { ExternalLinks, useFeatureFlags } from '@vegaprotocol/environment';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import { useNodesQuery } from '../staking/home/__generated__/Nodes';
import { useProposalsQuery } from '../proposals/__generated__/Proposals';
import { useProposalsQuery } from '../proposals/proposals/__generated__/Proposals';
import {
getNotRejectedProposals,
getNotRejectedProtocolUpgradeProposals,
@@ -32,7 +31,7 @@ import {
orderByUpgradeBlockHeight,
} from '../proposals/components/proposals-list/proposals-list';
import { BigNumber } from '../../lib/bignumber';
import { type Proposal, type BatchProposal } from '../proposals/types';
import { type Proposal } from '../proposals/types';
const nodesToShow = 6;
@@ -40,7 +39,7 @@ const HomeProposals = ({
proposals,
protocolUpgradeProposals,
}: {
proposals: Array<Proposal | BatchProposal>;
proposals: Proposal[];
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
}) => {
const { t } = useTranslation();
@@ -61,9 +60,12 @@ const HomeProposals = ({
<ProtocolUpgradeProposalsListItem key={index} proposal={proposal} />
))}
{compact(proposals).map((proposal) => {
return <ProposalsListItem key={proposal.id} proposal={proposal} />;
})}
{proposals.map(
(proposal) =>
proposal?.id && (
<ProposalsListItem key={proposal.id} proposal={proposal} />
)
)}
</ul>
<div className="mt-6">
@@ -173,6 +175,7 @@ export const ValidatorDetailsLink = ({
};
const GovernanceHome = ({ name }: RouteChildProps) => {
const featureFlags = useFeatureFlags((state) => state.flags);
useDocumentTitle(name);
const { t } = useTranslation();
const {
@@ -183,6 +186,11 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
pollInterval: 5000,
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
variables: {
includeNewMarketProductFields: !!featureFlags.PRODUCT_PERPETUALS,
includeUpdateMarketStates: !!featureFlags.UPDATE_MARKET_STATE,
includeUpdateReferralPrograms: !!featureFlags.REFERRALS,
},
});
const {
@@ -204,18 +212,15 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
useRefreshAfterEpoch(validatorsData?.epoch.timestamps.expiry, refetch);
const proposals = useMemo(() => {
if (!proposalsData?.proposalsConnection?.edges?.length) return [];
return proposalsData
? getNotRejectedProposals(
compact(
proposalsData.proposalsConnection.edges.map(
(edge) => edge?.proposalNode
)
const proposals = useMemo(
() =>
proposalsData
? getNotRejectedProposals(
removePaginationWrapper(proposalsData.proposalsConnection?.edges)
)
)
: [];
}, [proposalsData]);
: [],
[proposalsData]
);
const sortedProposals = useMemo(
() => 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 { type ReactNode } from 'react';
import { type ProposalInfoLabelVariant } from '../proposal-info-label';
import { type Proposal, type BatchProposal } from '../../types';
import { type Proposal } from '../../types';
export const CurrentProposalState = ({
proposal,
}: {
proposal: Proposal | BatchProposal;
}) => {
export const CurrentProposalState = ({ proposal }: { proposal: Proposal }) => {
const { t } = useTranslation();
let proposalStatus: ReactNode;
let variant = 'tertiary' as ProposalInfoLabelVariant;
@@ -79,22 +79,13 @@ export const ListAsset = ({
) {
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) {
return null;
}
if (errorAsset || errorBundle) {
return null;
}
if (errorAsset || errorBundle) return null;
const { assetSource, signatures, vegaAssetId, nonce } =
assetData.erc20ListAssetBundle;
return (
<div className="mb-8">
<h3 className="mb-2 text-xl">{t('ListAsset')}</h3>
@@ -2,53 +2,19 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SubHeading } from '../../../../components/heading';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import {
AssetDetail,
AssetDetailsTable,
useAssetQuery,
} from '@vegaprotocol/assets';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import {
type INewAssetFieldsFragment,
type IUpdateAssetFieldsFragment,
} from '../../__generated__/Proposals';
import { AssetDetail, AssetDetailsTable } from '@vegaprotocol/assets';
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
export const ProposalAssetDetails = ({
change,
assetId,
asset,
originalAsset,
}: {
change: IUpdateAssetFieldsFragment | INewAssetFieldsFragment;
assetId: string;
asset: AssetFieldsFragment;
originalAsset?: AssetFieldsFragment;
}) => {
const { t } = useTranslation();
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 (
<section data-testid="proposal-asset-details">
<CollapsibleToggle
@@ -6,46 +6,16 @@ import {
KeyValueTableRow,
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import { type Proposal, type BatchProposal } from '../../types';
import { type Proposal } from '../../types';
interface ProposalChangeTableProps {
proposal: Proposal | BatchProposal;
proposal: Proposal;
}
export const ProposalChangeTable = ({ proposal }: ProposalChangeTableProps) => {
const { t } = useTranslation();
const closingTimeRow =
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;
const terms = proposal?.terms;
return (
<RoundedWrapper paddingBottom={true}>
@@ -54,8 +24,22 @@ export const ProposalChangeTable = ({ proposal }: ProposalChangeTableProps) => {
{t('id')}
{proposal?.id}
</KeyValueTableRow>
{closingTimeRow}
{enactmentRow}
<KeyValueTableRow>
{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>
{t('proposedBy')}
<span style={{ wordBreak: 'break-word' }}>{proposal?.party.id}</span>
@@ -18,21 +18,20 @@ import {
nextWeek,
mockWalletContext,
createUserVoteQueryMock,
networkParamsQueryMock,
} from '../../test-helpers/mocks';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { BrowserRouter } from 'react-router-dom';
import { VoteState } from '../vote-details/use-user-vote';
import {
InstrumentDetailsDocument,
useNewTransferProposalDetails,
type InstrumentDetailsQuery,
type InstrumentDetailsQueryVariables,
} from '@vegaprotocol/proposals';
import { useNewTransferProposalDetails } from '@vegaprotocol/proposals';
import { type MockedResponse } from '@apollo/client/testing';
import { type Proposal } from '../../types';
jest.mock('@vegaprotocol/proposals', () => ({
...jest.requireActual('@vegaprotocol/proposals'),
useSuccessorMarketProposalDetails: () => ({
code: 'PARENT_CODE',
parentMarketId: 'PARENT_ID',
}),
useNewTransferProposalDetails: jest.fn(),
}));
@@ -45,7 +44,7 @@ const renderComponent = (
render(
<AppStateProvider>
<BrowserRouter>
<MockedProvider mocks={[networkParamsQueryMock, ...mocks]}>
<MockedProvider mocks={mocks}>
<VegaWalletContext.Provider value={mockWalletContext}>
<ProposalHeader
proposal={proposal}
@@ -62,39 +61,10 @@ describe('Proposal header', () => {
afterAll(() => {
jest.clearAllMocks();
});
it('Renders New market proposal', async () => {
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,
},
},
},
},
},
};
it('Renders New market proposal', () => {
useFeatureFlags.setState({ flags: { SUCCESSOR_MARKETS: true } });
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
rationale: {
title: 'New some market',
@@ -103,9 +73,6 @@ describe('Proposal header', () => {
terms: {
change: {
__typename: 'NewMarket',
successorConfiguration: {
parentMarketId,
},
instrument: {
__typename: 'InstrumentConfiguration',
name: 'Some market',
@@ -120,9 +87,7 @@ describe('Proposal header', () => {
},
},
},
}),
undefined,
[mock]
})
);
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
'New some market'
@@ -131,13 +96,14 @@ describe('Proposal header', () => {
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
'tGBP settled future.'
);
expect(
await screen.findByTestId('proposal-successor-info')
).toHaveTextContent(parentCode);
expect(screen.getByTestId('proposal-successor-info')).toHaveTextContent(
'PARENT_CODE'
);
});
it('Renders Update market proposal', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
rationale: {
title: 'New market id',
@@ -160,12 +126,13 @@ describe('Proposal header', () => {
screen.queryByTestId('proposal-description')
).not.toBeInTheDocument();
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
'Update to market: MarketId'
'Update to market ID: MarketId'
);
});
it('Renders New asset proposal - ERC20', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
rationale: {
title: 'New asset: Fake currency',
@@ -195,10 +162,8 @@ describe('Proposal header', () => {
it('Renders New asset proposal - BuiltInAsset', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
rationale: {
title: 'New asset',
},
terms: {
change: {
__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-details')).toHaveTextContent(
'Symbol: BIA. Max faucet amount mint: 300'
@@ -221,6 +188,7 @@ describe('Proposal header', () => {
it('Renders Update network', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
rationale: {
title: 'Network parameter',
@@ -250,6 +218,7 @@ describe('Proposal header', () => {
it('Renders Freeform proposal - short rationale', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
id: 'short',
rationale: {
@@ -271,6 +240,7 @@ describe('Proposal header', () => {
it('Renders Freeform proposal - long rationale (105 chars) - listing', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
id: 'long',
rationale: {
@@ -296,6 +266,7 @@ describe('Proposal header', () => {
// Remove once proposals have rationale and re-enable above tests
it('Renders Freeform proposal - id for title', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
id: 'freeform id',
rationale: {
@@ -352,6 +323,7 @@ describe('Proposal header', () => {
it('Renders proposal state: Enacted', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
state: ProposalState.STATE_ENACTED,
terms: {
@@ -364,6 +336,7 @@ describe('Proposal header', () => {
it('Renders proposal state: Passed', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
@@ -377,6 +350,7 @@ describe('Proposal header', () => {
it('Renders proposal state: Waiting for node vote', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
state: ProposalState.STATE_WAITING_FOR_NODE_VOTE,
terms: {
@@ -391,6 +365,7 @@ describe('Proposal header', () => {
it('Renders proposal state: Open', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
state: ProposalState.STATE_OPEN,
votes: {
@@ -459,6 +434,8 @@ describe('Proposal header', () => {
});
});
jest.mock('@vegaprotocol/proposals');
describe('<NewTransferSummary />', () => {
it('renders null if no details are provided', () => {
(useNewTransferProposalDetails as jest.Mock).mockReturnValue(null);
@@ -1,4 +1,4 @@
import { Trans, useTranslation } from 'react-i18next';
import { useTranslation } from 'react-i18next';
import {
CopyWithTooltip,
Lozenge,
@@ -8,13 +8,14 @@ import {
} from '@vegaprotocol/ui-toolkit';
import { shorten } from '@vegaprotocol/utils';
import { Heading, SubHeading } from '../../../../components/heading';
import { type ReactNode } from 'react';
import { truncateMiddle } from '../../../../lib/truncate-middle';
import { CurrentProposalState } from '../current-proposal-state';
import { ProposalInfoLabel } from '../proposal-info-label';
import {
useCancelTransferProposalDetails,
useInstrumentDetailsQuery,
useNewTransferProposalDetails,
useSuccessorMarketProposalDetails,
} from '@vegaprotocol/proposals';
import {
CONSOLE_MARKET_PAGE,
@@ -26,539 +27,217 @@ import Routes from '../../../routes';
import { Link } from 'react-router-dom';
import { type VoteState } from '../vote-details/use-user-vote';
import { VoteBreakdown } from '../vote-breakdown';
import {
GovernanceTransferKindMapping,
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 { getIndicatorStyle } from '../proposal/colours';
import { MarketName } from '../proposal/market-name';
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 />,
}}
/>
);
}
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="flex gap-3 items-center">
<span className={getIndicatorStyle(i + 1)}>{i + 1}</span>
<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;
}
}
};
import { GovernanceTransferKindMapping } from '@vegaprotocol/types';
import { type Proposal } from '../../types';
export const ProposalHeader = ({
proposal,
isListItem = true,
voteState,
}: {
proposal: Proposal | BatchProposal;
proposal: Proposal;
isListItem?: boolean;
voteState?: VoteState | null;
}) => {
const featureFlags = useFeatureFlags((state) => state.flags);
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 fallbackTitle = t(
proposal.__typename === 'Proposal'
? 'Unknown proposal'
: 'Unknown batch proposal'
);
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 (
<>
<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">
{(voteState === 'Yes' || voteState === 'No') && (
@@ -585,43 +264,50 @@ export const ProposalHeader = ({
<div data-testid="proposal-title" className="break-all">
{isListItem ? (
<header>
<SubHeading title={titleContent || fallbackTitle} />
<SubHeading
title={titleContent || fallbackTitle || t('Unknown proposal')}
/>
</header>
) : (
<Heading title={titleContent || fallbackTitle} />
<Heading
title={titleContent || fallbackTitle || t('Unknown proposal')}
/>
)}
</div>
<ProposalDetails proposal={proposal} />
{details && (
<div
data-testid="proposal-details"
className="break-words mb-6 text-vega-light-200"
>
{details}
</div>
)}
<VoteBreakdown proposal={proposal} />
</>
);
};
export const ParentMarketCode = ({
parentMarketId,
export const SuccessorCode = ({
proposalId,
}: {
parentMarketId: string;
proposalId?: string | null;
}) => {
const { t } = useTranslation();
const { data } = useInstrumentDetailsQuery({
variables: {
marketId: parentMarketId,
},
});
const successor = useSuccessorMarketProposalDetails(proposalId);
if (!data?.market?.tradableInstrument.instrument.code) return null;
return (
return successor.parentMarketId || successor.code ? (
<span className="block" data-testid="proposal-successor-info">
{t('Successor market to')}:{' '}
<Link
to={`${Routes.PROPOSALS}/${parentMarketId}`}
to={`${Routes.PROPOSALS}/${successor.parentMarketId}`}
className="hover:underline"
>
{data.market.tradableInstrument.instrument.code}
{successor.code || successor.parentMarketId}
</Link>
</span>
);
) : null;
};
export const NewTransferSummary = ({
@@ -3,8 +3,14 @@ import { useTranslation } from 'react-i18next';
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
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 [showDetails, setShowDetails] = useState(false);
@@ -5,11 +5,6 @@ import {
} from './proposal-market-changes';
import type { JsonValue } from '../../../../components/json-diff';
jest.mock('../proposal/market-name.tsx', () => ({
...jest.requireActual('../proposal/market-name.tsx'),
MarketName: jest.fn(),
}));
describe('applyImmutableKeysFromEarlierVersion', () => {
it('returns an empty object if any argument is not an object or null', () => {
const earlierVersion: JsonValue = null;
@@ -62,21 +57,33 @@ describe('applyImmutableKeysFromEarlierVersion', () => {
describe('ProposalMarketChanges', () => {
it('renders correctly', () => {
const { getByTestId } = render(
<ProposalMarketChanges marketId="market-id" updateProposalNode={null} />
<ProposalMarketChanges
originalProposal={{}}
latestEnactedProposal={{}}
updatedProposal={{}}
/>
);
expect(getByTestId('proposal-market-changes')).toBeInTheDocument();
});
it('JsonDiff is not visible when showChanges is false', () => {
const { queryByTestId } = render(
<ProposalMarketChanges marketId="market-id" updateProposalNode={null} />
<ProposalMarketChanges
originalProposal={{}}
latestEnactedProposal={{}}
updatedProposal={{}}
/>
);
expect(queryByTestId('json-diff')).not.toBeInTheDocument();
});
it('JsonDiff is visible when showChanges is true', async () => {
const { getByTestId } = render(
<ProposalMarketChanges marketId="market-id" updateProposalNode={null} />
<ProposalMarketChanges
originalProposal={{}}
latestEnactedProposal={{}}
updatedProposal={{}}
/>
);
fireEvent.click(getByTestId('proposal-market-changes-toggle'));
expect(getByTestId('json-diff')).toBeInTheDocument();
@@ -1,24 +1,12 @@
import cloneDeep from 'lodash/cloneDeep';
import set from 'lodash/set';
import get from 'lodash/get';
import compact from 'lodash/compact';
import orderBy from 'lodash/orderBy';
import { JsonDiff, type JsonValue } from '../../../../components/json-diff';
import { JsonDiff } from '../../../../components/json-diff';
import { useTranslation } from 'react-i18next';
import { useState } from 'react';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import { SubHeading } from '../../../../components/heading';
import {
useFetchProposal,
useFetchProposals,
flatten,
isBatchProposalNode,
isSingleProposalNode,
type ProposalNode,
type SingleProposalData,
type SubProposalData,
} from '../proposal/proposal-utils';
import { MarketName } from '../proposal/market-name';
import type { JsonValue } from '../../../../components/json-diff';
const immutableKeys = [
'decimalPlaces',
@@ -28,8 +16,8 @@ const immutableKeys = [
];
export const applyImmutableKeysFromEarlierVersion = (
earlierVersion: unknown,
updatedVersion: unknown
earlierVersion: JsonValue,
updatedVersion: JsonValue
) => {
if (
typeof earlierVersion !== 'object' ||
@@ -45,94 +33,26 @@ export const applyImmutableKeysFromEarlierVersion = (
// Overwrite the immutable keys in the updatedVersionCopy with the earlier values
immutableKeys.forEach((key) => {
const earlier = get(earlierVersion, key);
if (earlier) set(updatedVersionCopy, key, earlier);
set(updatedVersionCopy, key, get(earlierVersion, key));
});
return updatedVersionCopy;
};
interface ProposalMarketChangesProps {
marketId: string;
/** This are the changes from proposal */
updateProposalNode: ProposalNode | null;
indicator?: number;
originalProposal: JsonValue;
latestEnactedProposal: JsonValue | undefined;
updatedProposal: JsonValue;
}
export const ProposalMarketChanges = ({
marketId,
updateProposalNode,
indicator,
originalProposal,
latestEnactedProposal,
updatedProposal,
}: ProposalMarketChangesProps) => {
const { t } = useTranslation();
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 (
<section data-testid="proposal-market-changes">
<CollapsibleToggle
@@ -140,18 +60,25 @@ export const ProposalMarketChanges = ({
setToggleState={setShowChanges}
dataTestId={'proposal-market-changes-toggle'}
>
<SubHeading
title={
<>
{t('UpdateToMarket')}: <MarketName marketId={marketId} />
</>
}
/>
<SubHeading title={t('updatesToMarket')} />
</CollapsibleToggle>
{showChanges && (
<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>
)}
</section>
@@ -18,7 +18,6 @@ import {
getDataSourceSpecForTradingTermination,
getSigners,
MarginScalingFactorsPanel,
marketInfoProvider,
} from '@vegaprotocol/markets';
import {
Button,
@@ -29,8 +28,8 @@ import {
} from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import type { MarketInfo } from '@vegaprotocol/markets';
import { create } from 'zustand';
import { useDataProvider } from '@vegaprotocol/data-provider';
type MarketDataDialogState = {
isOpen: boolean;
@@ -49,29 +48,18 @@ export const useMarketDataDialogStore = create<MarketDataDialogState>(
const marketDataHeaderStyles =
'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 { isOpen, open, close } = useMarketDataDialogStore();
const [showDetails, setShowDetails] = useState(false);
const { data: marketData } = useDataProvider({
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) {
if (!marketData) {
return null;
}
@@ -7,6 +7,7 @@ import {
formatReferralRewardMultiplier,
ProposalReferralProgramDetails,
} from './proposal-referral-program-details';
import { generateProposal } from '../../test-helpers/generate-proposals';
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
useAppState: () => ({
@@ -58,65 +59,87 @@ describe('ProposalReferralProgramDetails helper functions', () => {
});
});
const mockChange = {
__typename: 'UpdateReferralProgram' as const,
benefitTiers: [
{
minimumEpochs: 6,
minimumRunningNotionalTakerVolume: '10000',
referralDiscountFactor: '0.001',
referralRewardFactor: '0.001',
const mockReferralProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateReferralProgram',
benefitTiers: [
{
minimumEpochs: 6,
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 />', () => {
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(
screen.queryByTestId('proposal-referral-program-details')
).toBeNull();
});
it('should not render if there are no relevant fields', () => {
const emptyChange = {};
// @ts-ignore change deliberately empty
render(<ProposalReferralProgramDetails change={emptyChange} />);
const incompleteProposal = generateProposal({
terms: {
change: {},
},
});
render(<ProposalReferralProgramDetails proposal={incompleteProposal} />);
expect(
screen.queryByTestId('proposal-referral-program-details')
).toBeNull();
});
it('should render relevant fields if present', () => {
render(<ProposalReferralProgramDetails change={mockChange} />);
render(<ProposalReferralProgramDetails proposal={mockReferralProposal} />);
expect(
screen.getByTestId('proposal-referral-program-window-length')
).toBeInTheDocument();
@@ -13,10 +13,10 @@ import {
} from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
import { useAppState } from '../../../../contexts/app-state/app-state-context';
import { type UpdateReferralProgramsFragment } from '../../__generated__/Proposals';
import { type Proposal } from '../../types';
interface ProposalReferralProgramDetailsProps {
change: UpdateReferralProgramsFragment | null;
proposal: Proposal | null;
}
export const formatEndOfProgramTimestamp = (value: string) => {
@@ -44,19 +44,20 @@ export const formatReferralRewardMultiplier = (value: string) => {
};
export const ProposalReferralProgramDetails = ({
change,
proposal,
}: ProposalReferralProgramDetailsProps) => {
const {
appState: { decimals },
} = useAppState();
const { t } = useTranslation();
if (proposal?.terms?.change?.__typename !== 'UpdateReferralProgram') {
return null;
}
if (change?.__typename !== 'UpdateReferralProgram') return null;
const benefitTiers = change?.benefitTiers.slice();
const stakingTiers = change?.stakingTiers.slice();
const windowLength = change?.windowLength;
const endOfProgramTimestamp = change?.endOfProgram;
const benefitTiers = proposal?.terms?.change?.benefitTiers.slice();
const stakingTiers = proposal?.terms?.change?.stakingTiers.slice();
const windowLength = proposal?.terms?.change?.windowLength;
const endOfProgramTimestamp = proposal?.terms?.change?.endOfProgram;
if (
!benefitTiers &&
@@ -6,14 +6,15 @@ import {
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { type Proposal } from '../../types';
export const ProposalCancelTransferDetails = ({
proposalId,
proposal,
}: {
proposalId: string;
proposal: Proposal;
}) => {
const { t } = useTranslation();
const details = useCancelTransferProposalDetails(proposalId);
const details = useCancelTransferProposalDetails(proposal?.id);
if (!details) {
return null;
@@ -19,16 +19,17 @@ import {
addDecimalsFormatNumberQuantum,
formatDateWithLocalTimezone,
} from '@vegaprotocol/utils';
import { type Proposal } from '../../types';
export const ProposalTransferDetails = ({
proposalId,
proposal,
}: {
proposalId: string;
proposal: Proposal;
}) => {
const { t } = useTranslation();
const [show, setShow] = useState(false);
const details = useNewTransferProposalDetails(proposalId);
const details = useNewTransferProposalDetails(proposal?.id);
if (!details) {
return null;
}
@@ -1,5 +1,6 @@
import { render, screen } from '@testing-library/react';
import { ProposalUpdateBenefitTiers } from './proposal-update-benefit-tiers-details';
import { generateProposal } from '../../test-helpers/generate-proposals';
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
useAppState: () => ({
@@ -9,71 +10,109 @@ jest.mock('../../../../contexts/app-state/app-state-context', () => ({
}),
}));
const mockChange1 = {
__typename: 'UpdateNetworkParameter' as const,
networkParameter: {
key: 'blah.blah.benefitTiers',
value: JSON.stringify({
tiers: [
{
minimum_quantum_balance: '10000',
reward_multiplier: '0.05',
},
{
minimum_quantum_balance: '500000000000',
reward_multiplier: '0.1',
},
{
minimum_quantum_balance: '10000000000000',
reward_multiplier: '10',
},
],
}),
const mockVestingBenefitTierProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateNetworkParameter',
networkParameter: {
key: 'blah.blah.benefitTiers',
value: JSON.stringify({
tiers: [
{
minimum_quantum_balance: '10000',
reward_multiplier: '0.05',
},
{
minimum_quantum_balance: '500000000000',
reward_multiplier: '0.1',
},
{
minimum_quantum_balance: '10000000000000',
reward_multiplier: '10',
},
],
}),
},
},
},
};
});
const mockChange2 = {
__typename: 'UpdateNetworkParameter' as const,
networkParameter: {
key: 'blah.blah.benefitTiers',
value: JSON.stringify({
tiers: [
{
minimum_activity_streak: '10000',
vesting_multiplier: '5',
reward_multiplier: '0.1',
},
{
minimum_activity_streak: '10000000000000',
vesting_multiplier: '100',
reward_multiplier: '10',
},
],
}),
const mockActivityStreakBenefitTierProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateNetworkParameter',
networkParameter: {
key: 'blah.blah.benefitTiers',
value: JSON.stringify({
tiers: [
{
minimum_activity_streak: '10000',
vesting_multiplier: '5',
reward_multiplier: '0.1',
},
{
minimum_activity_streak: '10000000000000',
vesting_multiplier: '100',
reward_multiplier: '10',
},
],
}),
},
},
},
};
});
describe('ProposalUpdateBenefitTiers', () => {
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();
});
it('should not render if there are relevant fields that are empty', () => {
const incompleteProposal = {
__typename: 'UpdateNetworkParameter' as const,
networkParameter: {
key: 'blah.blah.benefitTiers',
value: JSON.stringify({}),
const incompleteProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateNetworkParameter',
networkParameter: {
key: 'blah.blah.benefitTiers',
value: JSON.stringify({}),
},
},
},
};
});
render(<ProposalUpdateBenefitTiers change={incompleteProposal} />);
render(<ProposalUpdateBenefitTiers proposal={incompleteProposal} />);
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
});
it('should render a valid vesting benefit tier proposal', () => {
render(<ProposalUpdateBenefitTiers change={mockChange1} />);
render(
<ProposalUpdateBenefitTiers proposal={mockVestingBenefitTierProposal} />
);
// 3 tiers in the sample data
expect(screen.getByText('Tier 1')).toBeInTheDocument();
@@ -94,7 +133,11 @@ describe('ProposalUpdateBenefitTiers', () => {
});
it('should render a valid activity streak benefit tier proposal', () => {
render(<ProposalUpdateBenefitTiers change={mockChange2} />);
render(
<ProposalUpdateBenefitTiers
proposal={mockActivityStreakBenefitTierProposal}
/>
);
// 3 tiers in the sample data
expect(screen.getByText('Tier 1')).toBeInTheDocument();
@@ -11,7 +11,7 @@ import {
} from '../proposal-referral-program-details';
import { formatNumberPercentage } from '@vegaprotocol/utils';
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
type VestingBenefitTier = {
@@ -43,7 +43,7 @@ export const formatVolumeDiscountFactor = (value: string) => {
};
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.
*/
export const ProposalUpdateBenefitTiers = ({
change,
proposal,
}: ProposalReferralProgramDetailsProps) => {
const { t } = useTranslation();
if (
change?.__typename !== 'UpdateNetworkParameter' ||
change?.networkParameter.key.slice(-13) !== '.benefitTiers'
proposal?.terms?.change?.__typename !== 'UpdateNetworkParameter' ||
proposal?.terms?.change?.networkParameter.key.slice(-13) !== '.benefitTiers'
) {
return null;
}
const benefitTiersString = change?.networkParameter.value;
const benefitTiersString = proposal?.terms?.change?.networkParameter.value;
const benefitTiers = getBenefitTiers(benefitTiersString);
if (!benefitTiers) {
@@ -1,78 +1,86 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { ProposalUpdateMarketState } from './proposal-update-market-state';
import { generateProposal } from '../../test-helpers/generate-proposals';
import { MarketUpdateType } from '@vegaprotocol/types';
jest.mock('../proposal/market-name.tsx', () => ({
...jest.requireActual('../proposal/market-name.tsx'),
MarketName: jest.fn(),
}));
describe('<ProposalUpdateMarketState />', () => {
const suspendProposal = {
__typename: 'UpdateMarketState' as const,
market: {
id: '1',
decimalPlaces: 0,
tradableInstrument: {
instrument: {
name: 'suspendProposal Name',
code: 'suspendProposal Code',
product: {
__typename: 'Future' as const,
quoteName: 'USD',
const suspendProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateMarketState',
market: {
id: '1',
decimalPlaces: 0,
tradableInstrument: {
instrument: {
name: 'suspendProposal Name',
code: 'suspendProposal Code',
product: {
__typename: 'Future',
quoteName: 'USD',
},
},
},
},
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_SUSPEND,
},
},
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_SUSPEND,
};
});
const resumeProposal = {
__typename: 'UpdateMarketState' as const,
market: {
id: '1',
decimalPlaces: 0,
tradableInstrument: {
instrument: {
name: 'resumeProposal Name',
code: 'resumeProposal Code',
product: {
__typename: 'Future' as const,
quoteName: 'USD',
const resumeProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateMarketState',
market: {
id: '1',
decimalPlaces: 0,
tradableInstrument: {
instrument: {
name: 'resumeProposal Name',
code: 'resumeProposal Code',
product: {
__typename: 'Future',
quoteName: 'USD',
},
},
},
},
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_RESUME,
},
},
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_RESUME,
};
});
const terminateProposal = {
__typename: 'UpdateMarketState' as const,
market: {
id: '1',
decimalPlaces: 0,
tradableInstrument: {
instrument: {
name: 'terminateProposal Name',
code: 'terminateProposal Code',
product: {
__typename: 'Future' as const,
quoteName: 'USD',
const terminateProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateMarketState',
market: {
id: '1',
decimalPlaces: 0,
tradableInstrument: {
instrument: {
name: 'terminateProposal Name',
code: 'terminateProposal Code',
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', () => {
render(<ProposalUpdateMarketState change={null} />);
render(<ProposalUpdateMarketState proposal={null} />);
expect(screen.queryByTestId('proposal-update-market-state')).toBeNull();
});
it('should toggle details when CollapsibleToggle is clicked', () => {
render(<ProposalUpdateMarketState change={suspendProposal} />);
render(<ProposalUpdateMarketState proposal={suspendProposal} />);
expect(
screen.queryByTestId('proposal-update-market-state-table')
@@ -86,7 +94,7 @@ describe('<ProposalUpdateMarketState />', () => {
});
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'));
@@ -95,7 +103,7 @@ describe('<ProposalUpdateMarketState />', () => {
});
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'));
@@ -104,7 +112,7 @@ describe('<ProposalUpdateMarketState />', () => {
});
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'));
@@ -8,31 +8,29 @@ import { Row } from '@vegaprotocol/markets';
import { useState } from 'react';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import { SubHeading } from '../../../../components/heading';
import { type UpdateMarketStatesFragment } from '../../__generated__/Proposals';
import { MarketUpdateTypeMapping } from '@vegaprotocol/types';
import { MarketName } from '../proposal/market-name';
import { type Proposal } from '../../types';
interface ProposalUpdateMarketStateProps {
change: UpdateMarketStatesFragment | null;
proposal: Proposal | null;
}
export const ProposalUpdateMarketState = ({
change,
proposal,
}: ProposalUpdateMarketStateProps) => {
const { t } = useTranslation();
const [showDetails, setShowDetails] = useState(false);
let market;
let isTerminate = false;
if (!change || change.__typename !== 'UpdateMarketState') {
if (!proposal) {
return null;
}
const market = change?.market;
const isTerminate =
change?.updateType === 'MARKET_STATE_UPDATE_TYPE_TERMINATE';
let toggleTitle = t(change.updateType);
if (toggleTitle.length === 0) {
toggleTitle = t('MarketDetails');
if (proposal?.terms.change.__typename === 'UpdateMarketState') {
market = proposal?.terms?.change?.market;
isTerminate =
proposal?.terms?.change?.updateType ===
'MARKET_STATE_UPDATE_TYPE_TERMINATE';
}
return (
@@ -42,29 +40,17 @@ export const ProposalUpdateMarketState = ({
setToggleState={setShowDetails}
dataTestId="proposal-market-data-toggle"
>
<SubHeading
title={
<>
{toggleTitle}: <MarketName marketId={market?.id} />
</>
}
/>
<SubHeading title={t('MarketDetails')} />
</CollapsibleToggle>
{showDetails && (
<RoundedWrapper paddingBottom={true} marginBottomLarge={true}>
{change.__typename === 'UpdateMarketState' && (
{proposal?.terms.change.__typename === 'UpdateMarketState' && (
<KeyValueTable data-testid="proposal-update-market-state-table">
<KeyValueTableRow>
{t('marketId')}
{market?.id}
</KeyValueTableRow>
<KeyValueTableRow>
{t('State')}
<span className="bg-vega-green-650 px-1">
{MarketUpdateTypeMapping[change.updateType]}
</span>
</KeyValueTableRow>
<KeyValueTableRow>
{t('marketName')}
{market?.tradableInstrument?.instrument?.name}
@@ -73,10 +59,10 @@ export const ProposalUpdateMarketState = ({
{t('marketCode')}
{market?.tradableInstrument?.instrument?.code}
</KeyValueTableRow>
{isTerminate && market && (
{isTerminate && (
<Row
field="termination-price"
value={change?.price}
value={proposal?.terms?.change?.price}
assetSymbol={
market?.tradableInstrument?.instrument?.product
?.__typename === 'Future' ||
@@ -1,5 +1,6 @@
import { render, screen } from '@testing-library/react';
import { ProposalVolumeDiscountProgramDetails } from './proposal-volume-discount-program-details';
import { generateProposal } from '../../test-helpers/generate-proposals';
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
useAppState: () => ({
@@ -9,56 +10,95 @@ jest.mock('../../../../contexts/app-state/app-state-context', () => ({
}),
}));
const mockChange = {
__typename: 'UpdateVolumeDiscountProgram' as const,
benefitTiers: [
{
minimumRunningNotionalTakerVolume: '10000',
volumeDiscountFactor: '0.05',
const mockReferralProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateVolumeDiscountProgram',
benefitTiers: [
{
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', () => {
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(
screen.queryByTestId('proposal-volume-discount-program-details')
).toBeNull();
});
it('should render relevant fields if present', () => {
render(<ProposalVolumeDiscountProgramDetails change={mockChange} />);
render(
<ProposalVolumeDiscountProgramDetails proposal={mockReferralProposal} />
);
expect(
screen.getByTestId('proposal-volume-discount-program-window-length')
).toBeInTheDocument();
@@ -11,10 +11,10 @@ import {
} from '../proposal-referral-program-details';
import { formatNumberPercentage } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
import { type UpdateVolumeDiscountProgramsFragment } from '../../__generated__/Proposals';
import { type Proposal } from '../../types';
interface ProposalReferralProgramDetailsProps {
change: UpdateVolumeDiscountProgramsFragment | null;
proposal: Proposal | null;
}
export const formatVolumeDiscountFactor = (value: string) => {
@@ -22,16 +22,16 @@ export const formatVolumeDiscountFactor = (value: string) => {
};
export const ProposalVolumeDiscountProgramDetails = ({
change,
proposal,
}: ProposalReferralProgramDetailsProps) => {
const { t } = useTranslation();
if (change?.__typename !== 'UpdateVolumeDiscountProgram') {
if (proposal?.terms?.change?.__typename !== 'UpdateVolumeDiscountProgram') {
return null;
}
const benefitTiers = change.benefitTiers;
const windowLength = change?.windowLength;
const endOfProgramTimestamp = change?.endOfProgramTimestamp;
const benefitTiers = proposal?.terms?.change?.benefitTiers;
const windowLength = proposal?.terms?.change?.windowLength;
const endOfProgramTimestamp = proposal?.terms?.change?.endOfProgramTimestamp;
if (!benefitTiers && !windowLength && !endOfProgramTimestamp) {
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 after:bg-vega-yellow-400':
'yellow' === getColour(indicator, max),
'bg-vega-green-400 after:bg-vega-green-400':
'green' === getColour(indicator, max),
'bg-vega-blue-400 after:bg-vega-blue-400':
'blue' === getColour(indicator, max),
'bg-vega-purple-400 after:bg-vega-purple-400':
'purple' === getColour(indicator, max),
'bg-vega-pink-400 after:bg-vega-pink-400':
'pink' === getColour(indicator, max),
'bg-vega-orange-400 after:bg-vega-orange-400':
'orange' === getColour(indicator, max),
'bg-vega-red-400 after:bg-vega-red-400':
'red' === getColour(indicator, max),
'bg-vega-clight-600 after: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]',
'after:absolute after:z-[-1] after:top-1 after:right-[-11px] after:rounded-sm',
"after:w-[22.62px] after:h-[22.62px] after:rotate-45 after:content-['']"
);
@@ -1,14 +0,0 @@
import { useMarketInfoQuery } from '@vegaprotocol/markets';
export const MarketName = ({ marketId }: { marketId?: string }) => {
const { data } = useMarketInfoQuery({
variables: {
marketId: marketId || '',
},
skip: !marketId,
});
return (
<span>{data?.market?.tradableInstrument.instrument.code || marketId}</span>
);
};
@@ -1,133 +0,0 @@
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 { getIndicatorStyle } from './colours';
import { type ProposalNode } from './proposal-utils';
export const ProposalChangeDetails = ({
proposal,
terms,
restData,
indicator,
}: {
proposal: Proposal | BatchProposal;
terms: ProposalTermsFieldsFragment;
restData: ProposalNode | null;
indicator?: number;
}) => {
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} />;
}
break;
}
case 'NewFreeform':
case 'NewSpotMarket':
case 'UpdateSpotMarket':
default: {
break;
}
}
if (indicator != null) {
details = (
<div className="flex gap-3 mb-3">
<div className={getIndicatorStyle(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 { Proposal } from './proposal';
import { ProposalState } from '@vegaprotocol/types';
import { mockNetworkParams } from '../../test-helpers/mocks';
import { type Proposal as IProposal } from '../../types';
jest.mock('@vegaprotocol/network-parameters', () => ({
@@ -37,12 +38,6 @@ jest.mock('../list-asset', () => ({
ListAsset: () => <div data-testid="proposal-list-asset"></div>,
}));
jest.mock('./proposal-change-details', () => ({
ProposalChangeDetails: () => (
<div data-testid="proposal-change-details"></div>
),
}));
const vegaWalletConfig: VegaWalletConfig = {
network: 'TESTNET',
vegaUrl: 'https://vega.xyz',
@@ -61,7 +56,11 @@ const renderComponent = (proposal: IProposal) => {
<MemoryRouter>
<MockedProvider>
<VegaWalletProvider config={vegaWalletConfig}>
<Proposal restData={null} proposal={proposal} />
<Proposal
restData={{}}
proposal={proposal}
networkParams={mockNetworkParams}
/>
</VegaWalletProvider>
</MockedProvider>
</MemoryRouter>
@@ -100,6 +99,27 @@ it('renders each section', async () => {
expect(await screen.findByTestId('proposal-header')).toBeInTheDocument();
expect(screen.getByTestId('proposal-change-table')).toBeInTheDocument();
expect(screen.getByTestId('proposal-json')).toBeInTheDocument();
expect(screen.getByTestId('proposal-change-details')).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 { ProposalChangeTable } from '../proposal-change-table';
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 { ListAsset } from '../list-asset';
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 { 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 { useUserVote } from '../vote-details/use-user-vote';
import { type Proposal as IProposal, type BatchProposal } from '../../types';
import { ProposalChangeDetails } from './proposal-change-details';
import { type JsonValue } from 'type-fest';
import {
ProposalCancelTransferDetails,
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 {
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
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 { submit, Dialog, finalizedVote, transaction } = useVoteSubmit();
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 (
<section data-testid="proposal">
<div className="flex items-center gap-1 mb-6">
@@ -56,37 +181,91 @@ export const Proposal = ({ proposal, restData }: ProposalProps) => {
<ProposalChangeTable proposal={proposal} />
</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">
<ProposalDescription description={proposal.rationale.description} />
</div>
<div className="mb-4">
{proposal.__typename === 'Proposal' ? (
<ProposalChangeDetails
proposal={proposal}
terms={proposal.terms}
restData={restData}
{marketData && (
<div className="mb-4">
<ProposalMarketData
marketData={marketData}
parentMarketData={parentMarketData ? parentMarketData : undefined}
/>
) : proposal.__typename === 'BatchProposal' ? (
proposal.subProposals?.map((p, i) => {
if (!p?.terms) return null;
return (
<ProposalChangeDetails
indicator={i + 1}
key={i}
proposal={proposal}
terms={p.terms}
restData={restData}
/>
);
})
) : null}
</div>
</div>
)}
{proposal.terms.change.__typename === 'UpdateMarketState' && (
<div className="mb-4">
<ProposalUpdateMarketState proposal={proposal} />
</div>
)}
{proposal.terms.change.__typename === 'UpdateMarket' && (
<div className="mb-4">
<ProposalMarketChanges
originalProposal={
originalMarketProposalRestData?.data?.proposal?.terms?.newMarket
?.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">
<RoundedWrapper paddingBottom={true}>
<UserVote
proposal={proposal}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={
networkParams?.spam_protection_voting_min_tokens
}
submit={submit}
dialog={Dialog}
transaction={transaction}
@@ -97,7 +276,7 @@ export const Proposal = ({ proposal, restData }: ProposalProps) => {
</div>
<div className="mb-6">
<ProposalJson proposal={restData?.proposal as unknown as JsonValue} />
<ProposalJson proposal={restData?.data?.proposal} />
</div>
</section>
);
@@ -1,25 +1,149 @@
import { BrowserRouter as Router } from 'react-router-dom';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
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 { 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 { 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(
<Router>
<VegaWalletContext.Provider value={mockWalletContext}>
<ProposalsListItemDetails id={id} />
</VegaWalletContext.Provider>
<MockedProvider mocks={mocks}>
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<ProposalsListItemDetails proposal={proposal} />
</VegaWalletContext.Provider>
</AppStateProvider>
</MockedProvider>
</Router>
);
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(0);
});
afterAll(() => {
jest.useRealTimers();
});
describe('Proposals list item details', () => {
it('links to single proposal page', () => {
const proposalId = 'proposal-id';
renderComponent(proposalId);
expect(screen.getByRole('link')).toHaveAttribute(
'href',
expect.stringContaining(proposalId)
it('Renders proposal state: Enacted', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_ENACTED,
terms: {
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 { Button } from '@vegaprotocol/ui-toolkit';
import { differenceInHours, format, formatDistanceToNowStrict } from 'date-fns';
import { useTranslation } from 'react-i18next';
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
import {
ProposalRejectionReasonMapping,
ProposalState,
} from '@vegaprotocol/types';
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 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 (
<div className="mt-4 items-start text-sm">
<Link to={`${Routes.PROPOSALS}/${id}`}>
<Button data-testid="view-proposal-btn">{t('viewDetails')}</Button>
</Link>
<div className="flex items-center gap-2 text-vega-light-300 mb-2">
{voteDetails && <span data-testid="vote-details">{voteDetails}</span>}
{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>
);
};
@@ -2,10 +2,10 @@ import { RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { ProposalHeader } from '../proposal-detail-header/proposal-header';
import { ProposalsListItemDetails } from './proposals-list-item-details';
import { useUserVote } from '../vote-details/use-user-vote';
import { type Proposal, type BatchProposal } from '../../types';
import { type Proposal } from '../../types';
interface ProposalsListItemProps {
proposal?: Proposal | BatchProposal;
proposal?: Proposal | null;
}
export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => {
@@ -16,7 +16,7 @@ export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => {
<li id={proposal.id} data-testid="proposals-list-item">
<RoundedWrapper paddingBottom={true} heightFull={true}>
<ProposalHeader proposal={proposal} voteState={voteState} />
<ProposalsListItemDetails id={proposal.id} />
<ProposalsListItemDetails proposal={proposal} />
</RoundedWrapper>
</li>
);
@@ -3,12 +3,14 @@ import {
generateProtocolUpgradeProposal,
} 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 { ProposalsList } from './proposals-list';
import { ProposalState } from '@vegaprotocol/types';
import { fireEvent, render, screen, within } from '@testing-library/react';
import {
mockWalletContext,
networkParamsQueryMock,
lastWeek,
nextWeek,
@@ -18,16 +20,6 @@ import {
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
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({
id: 'proposal1',
state: ProposalState.STATE_OPEN,
@@ -77,10 +69,12 @@ const renderComponent = (
<Router>
<MockedProvider mocks={[networkParamsQueryMock]}>
<AppStateProvider>
<ProposalsList
proposals={proposals}
protocolUpgradeProposals={protocolUpgradeProposals || []}
/>
<VegaWalletContext.Provider value={mockWalletContext}>
<ProposalsList
proposals={proposals}
protocolUpgradeProposals={protocolUpgradeProposals || []}
/>
</VegaWalletContext.Provider>
</AppStateProvider>
</MockedProvider>
</Router>
@@ -94,6 +88,10 @@ afterAll(() => {
jest.useRealTimers();
});
jest.mock('../vote-details/use-user-vote', () => ({
useUserVote: jest.fn().mockImplementation(() => ({ voteState: 'NotCast' })),
}));
describe('Proposals list', () => {
it('Render a page title and link to the make proposal form', async () => {
render(renderComponent([]));
@@ -11,20 +11,19 @@ import { Button, Toggle } from '@vegaprotocol/ui-toolkit';
import { Link } from 'react-router-dom';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { ExternalLinks } from '@vegaprotocol/environment';
import { type ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { type BatchProposal, type Proposal } from '../../types';
type Proposals = Array<Proposal | BatchProposal>;
import { type Proposal } from '../../types';
interface ProposalsListProps {
proposals: Proposals;
proposals: Proposal[];
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
lastBlockHeight?: string;
}
interface SortedProposalsProps {
open: Proposals;
closed: Proposals;
open: Proposal[];
closed: Proposal[];
}
interface SortedProtocolUpgradeProposalsProps {
@@ -32,26 +31,15 @@ interface SortedProtocolUpgradeProposalsProps {
closed: ProtocolUpgradeProposalFieldsFragment[];
}
export const orderByDate = (arr: Proposals) =>
export const orderByDate = (arr: Proposal[]) =>
orderBy(
arr,
[
(p) => {
if (p.__typename === 'BatchProposal') {
// Batch proposals can have different enactment dates, this could be improved by ordering
// by soonest enactment date in the batch
return new Date(p.batchTerms?.closingDatetime || p.datetime);
}
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) =>
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(),
(p) => new Date(p?.datetime).getTime(),
],
['asc', 'asc']
@@ -88,40 +76,18 @@ export const ProposalsList = ({
const sortedProposals: SortedProposalsProps = useMemo(() => {
const initialSorting = proposals.reduce(
(acc, proposal) => {
if (proposal.__typename === 'Proposal') {
if (isFuture(new Date(proposal?.terms.closingDatetime))) {
acc.open.push(proposal);
} else {
acc.closed.push(proposal);
}
return acc;
(acc: SortedProposalsProps, proposal) => {
if (isFuture(new Date(proposal?.terms.closingDatetime))) {
acc.open.push(proposal);
} else {
acc.closed.push(proposal);
}
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;
},
{
open: [],
closed: [],
} as SortedProposalsProps
}
);
return {
open:
@@ -155,7 +121,7 @@ export const ProposalsList = ({
};
}, [protocolUpgradeProposals, lastBlockHeight]);
const filterPredicate = (p: Proposal | BatchProposal) =>
const filterPredicate = (p: ProposalFieldsFragment | Proposal) =>
p?.id?.includes(filterString) ||
p?.party?.id?.toString().includes(filterString);
@@ -1,8 +1,17 @@
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 { ProposalState } from '@vegaprotocol/types';
import { render, screen } from '@testing-library/react';
import { nextWeek, lastMonth } from '../../test-helpers/mocks';
import { render, screen, waitFor, within } from '@testing-library/react';
import {
mockWalletContext,
networkParamsQueryMock,
nextWeek,
lastMonth,
} from '../../test-helpers/mocks';
import { type Proposal } from '../../types';
const rejectedProposalClosesNextWeek = generateProposal({
@@ -27,15 +36,31 @@ const rejectedProposalClosedLastMonth = generateProposal({
});
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', () => ({
ProposalsListItem: () => <div data-testid="proposals-list-item" />,
beforeAll(() => {
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', () => {
it('Renders a list of proposals', () => {
it('Renders a list of proposals', async () => {
render(
renderComponent([
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 () => {
render(renderComponent([]));
expect(screen.getByTestId('no-rejected-proposals')).toBeInTheDocument();
expect(screen.queryByTestId('rejected-proposals')).not.toBeInTheDocument();
await waitFor(() => {
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 { ProposalsListItem } from '../proposals-list-item';
import { ProposalsListFilter } from '../proposals-list-filter';
import { type BatchProposal, type Proposal } from '../../types';
import { type Proposal } from '../../types';
interface ProposalsListProps {
proposals: Array<Proposal | BatchProposal>;
proposals: Proposal[];
}
export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
const { t } = useTranslation();
const [filterString, setFilterString] = useState('');
const filterPredicate = (p: Proposal | BatchProposal) =>
const filterPredicate = (p: Proposal) =>
p?.id?.includes(filterString) ||
p?.party?.id?.toString().includes(filterString);
@@ -64,7 +64,7 @@ describe('VoteBreakdown', () => {
jest.useRealTimers();
});
it('Renders majority reached', async () => {
it('Renders majority reached', () => {
const yesVotes = 100;
const noVotes = 0;
@@ -82,10 +82,10 @@ describe('VoteBreakdown', () => {
},
})
);
expect(await screen.findByTestId('token-majority-met')).toBeInTheDocument();
expect(screen.getByTestId('token-majority-met')).toBeInTheDocument();
});
it('Renders majority not reached', async () => {
it('Renders majority not reached', () => {
const yesVotes = 20;
const noVotes = 80;
@@ -103,12 +103,10 @@ describe('VoteBreakdown', () => {
},
})
);
expect(
await screen.findByTestId('token-majority-not-met')
).toBeInTheDocument();
expect(screen.getByTestId('token-majority-not-met')).toBeInTheDocument();
});
it('Renders participation reached', async () => {
it('Renders participation reached', () => {
const yesVotes = 1000;
const noVotes = 0;
@@ -126,12 +124,10 @@ describe('VoteBreakdown', () => {
},
})
);
expect(
await screen.findByTestId('token-participation-met')
).toBeInTheDocument();
expect(screen.getByTestId('token-participation-met')).toBeInTheDocument();
});
it('Renders participation not reached', async () => {
it('Renders participation not reached', () => {
const yesVotes = 0;
const noVotes = 0;
@@ -149,11 +145,11 @@ describe('VoteBreakdown', () => {
})
);
expect(
await screen.findByTestId('token-participation-not-met')
screen.getByTestId('token-participation-not-met')
).toBeInTheDocument();
});
it('Renders proposal state: Update market proposal - Currently expected to pass by LP vote', async () => {
it('Renders proposal state: Update market proposal - Currently expected to pass by LP vote', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
@@ -174,12 +170,12 @@ describe('VoteBreakdown', () => {
},
})
);
expect(await screen.findByTestId('vote-status')).toHaveTextContent(
expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Currently expected to pass by liquidity vote'
);
});
it('Renders proposal state: Update market proposal - Currently expected to pass by token vote', async () => {
it('Renders proposal state: Update market proposal - Currently expected to pass by token vote', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
@@ -200,12 +196,12 @@ describe('VoteBreakdown', () => {
},
})
);
expect(await screen.findByTestId('vote-status')).toHaveTextContent(
expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Currently expected to pass by token vote'
);
});
it('Renders proposal state: Update market proposal - Currently expected to fail', async () => {
it('Renders proposal state: Update market proposal - Currently expected to fail', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
@@ -226,12 +222,12 @@ describe('VoteBreakdown', () => {
},
})
);
expect(await screen.findByTestId('vote-status')).toHaveTextContent(
expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Currently expected to fail'
);
});
it('Progress bar displays status - token majority', async () => {
it('Progress bar displays status - token majority', () => {
const yesVotes = 80;
const noVotes = 20;
@@ -250,13 +246,13 @@ describe('VoteBreakdown', () => {
})
);
const element = await screen.findByTestId('token-majority-progress');
const element = screen.getByTestId('token-majority-progress');
const style = window.getComputedStyle(element);
expect(style.width).toBe(`${yesVotes}%`);
});
it('Progress bar displays status - token participation', async () => {
it('Progress bar displays status - token participation', () => {
const yesVotes = 40;
const noVotes = 20;
const totalVotes = yesVotes + noVotes;
@@ -278,13 +274,13 @@ describe('VoteBreakdown', () => {
})
);
const element = await screen.findByTestId('token-participation-progress');
const element = screen.getByTestId('token-participation-progress');
const style = window.getComputedStyle(element);
expect(style.width).toBe(`${expectedProgress}%`);
});
it('Progress bar displays status - LP majority', async () => {
it('Progress bar displays status - LP majority', () => {
const yesVotesLP = 0.8;
const noVotesLP = 0.2;
const expectedProgress = (yesVotesLP / (yesVotesLP + noVotesLP)) * 100; // 80%
@@ -311,12 +307,12 @@ describe('VoteBreakdown', () => {
})
);
const element = await screen.findByTestId('lp-majority-progress');
const element = screen.getByTestId('lp-majority-progress');
const style = window.getComputedStyle(element);
expect(style.width).toBe(`${expectedProgress}%`);
});
it('Progress bar displays status - LP participation', async () => {
it('Progress bar displays status - LP participation', () => {
const yesVotesLP = 400;
const noVotesLP = 600;
const totalVotesLP = yesVotesLP + noVotesLP;
@@ -345,7 +341,7 @@ describe('VoteBreakdown', () => {
})
);
const element = await screen.findByTestId('lp-participation-progress');
const element = screen.getByTestId('lp-participation-progress');
const style = window.getComputedStyle(element);
expect(style.width).toBe(`${expectedProgress}%`);
});
@@ -1,22 +1,13 @@
import compact from 'lodash/compact';
import countBy from 'lodash/countBy';
import { useState, type ReactNode } from 'react';
import { type ReactNode } from 'react';
import classNames from 'classnames';
import BigNumber from 'bignumber.js';
import { Trans, useTranslation } from 'react-i18next';
import { useTranslation } from 'react-i18next';
import { useVoteInformation } from '../../hooks';
import { Tooltip, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '@vegaprotocol/utils';
import { ProposalState } from '@vegaprotocol/types';
import { CompactNumber } from '@vegaprotocol/react-helpers';
import { type Proposal, type BatchProposal } from '../../types';
import {
type ProposalTermsFieldsFragment,
type VoteFieldsFragment,
} from '../../__generated__/Proposals';
import { useBatchVoteInformation } from '../../hooks/use-vote-information';
import { getIndicatorStyle } from '../proposal/colours';
import { MarketName } from '../proposal/market-name';
import { type Proposal } from '../../types';
export const CompactVotes = ({ number }: { number: BigNumber }) => (
<CompactNumber
@@ -27,6 +18,10 @@ export const CompactVotes = ({ number }: { number: BigNumber }) => (
/>
);
interface VoteBreakdownProps {
proposal: Proposal;
}
interface VoteProgressProps {
percentageFor: BigNumber;
colourfulBg?: boolean;
@@ -41,9 +36,8 @@ const VoteProgress = ({
children,
}: VoteProgressProps) => {
const containerClasses = classNames(
'relative h-2 rounded-md overflow-hidden',
// 'border border-vega-dark-300',
colourfulBg ? 'bg-vega-red' : 'bg-vega-dark-200'
'relative h-10 rounded-md border border-vega-dark-300 overflow-hidden',
colourfulBg ? 'bg-vega-pink' : 'bg-vega-dark-400'
);
const progressClasses = classNames(
@@ -52,19 +46,17 @@ const VoteProgress = ({
);
const textClasses = classNames(
'w-full flex items-center justify-start text-white text-sm pb-1'
'absolute top-0 left-0 w-full h-full flex items-center justify-start px-3 text-black'
);
return (
<div>
<div className={containerClasses}>
<div
className={progressClasses}
style={{ width: `${percentageFor}%` }}
data-testid={testId}
></div>
<div className={textClasses}>{children}</div>
<div className={containerClasses}>
<div
className={progressClasses}
style={{ width: `${percentageFor}%` }}
data-testid={testId}
/>
</div>
</div>
);
};
@@ -83,22 +75,14 @@ const Status = ({ reached, threshold, text, testId }: StatusProps) => {
<div data-testid={testId}>
{reached ? (
<div className="flex items-center gap-2">
<VegaIcon
name={VegaIconNames.TICK}
className="text-vega-green"
size={20}
/>
<Icon name="tick" size={4} />
<span>
{threshold.toString()}% {text} {t('met')}
</span>
</div>
) : (
<div className="flex items-center gap-2">
<VegaIcon
name={VegaIconNames.CROSS}
className="text-vega-red"
size={20}
/>
<Icon name="cross" size={4} />
<span>
{threshold.toString()}% {text} {t('not met')}
</span>
@@ -108,256 +92,7 @@ const Status = ({ reached, threshold, text, testId }: StatusProps) => {
);
};
export const VoteBreakdown = ({
proposal,
}: {
proposal: Proposal | BatchProposal;
}) => {
if (proposal.__typename === 'Proposal') {
return <VoteBreakdownNormal proposal={proposal} />;
}
if (proposal.__typename === 'BatchProposal') {
return <VoteBreakdownBatch proposal={proposal} />;
}
return null;
};
const VoteBreakdownBatch = ({ proposal }: { proposal: BatchProposal }) => {
const [fullBreakdown, setFullBreakdown] = useState(false);
const { t } = useTranslation();
const voteInfo = useBatchVoteInformation({
terms: compact(
proposal.subProposals ? proposal.subProposals.map((p) => p?.terms) : []
),
votes: proposal.votes,
});
if (!voteInfo) return null;
const batchWillPass = voteInfo.every((i) => i.willPass);
const passingCount = countBy(voteInfo, (v) => v.willPass);
if (proposal.state === ProposalState.STATE_OPEN) {
return (
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-2">
{batchWillPass ? (
<p className="flex gap-2 m-0 items-center">
<VegaIcon
name={VegaIconNames.TICK}
className="text-vega-green"
size={20}
/>
{t(
'Currently expected to pass: conditions met for {{count}} of {{total}} proposals',
{
count: passingCount['true'] || 0,
total: voteInfo.length,
}
)}
</p>
) : (
<p className="flex gap-2 m-0 items-center">
<VegaIcon
name={VegaIconNames.CROSS}
className="text-vega-red"
size={20}
/>
{t(
'Currently expected to fail: {{count}} of {{total}} proposals are passing',
{
count: passingCount['true'] || 0,
total: voteInfo.length,
}
)}
</p>
)}
<button
className="underline"
onClick={() => setFullBreakdown((x) => !x)}
>
{fullBreakdown ? 'Hide vote breakdown' : 'Show vote breakdown'}
</button>
</div>
{fullBreakdown && (
<div>
{proposal.subProposals?.map((p, i) => {
if (!p?.terms) return null;
return (
<VoteBreakdownBatchSubProposal
indicator={i + 1}
key={i}
proposal={proposal}
votes={proposal.votes}
terms={p.terms}
/>
);
})}
</div>
)}
</div>
);
} else if (
proposal.state === ProposalState.STATE_DECLINED ||
proposal.state === ProposalState.STATE_PASSED
) {
return (
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-2">
{batchWillPass ? (
<p className="flex gap-2 m-0 items-center">
<VegaIcon
name={VegaIconNames.TICK}
className="text-vega-green"
size={20}
/>
{t(
'Proposal passed: conditions met for {{count}} of {{total}} proposals',
{
count: passingCount['true'] || 0,
total: voteInfo.length,
}
)}
</p>
) : (
<p className="flex gap-2 m-0 items-center">
<VegaIcon
name={VegaIconNames.CROSS}
className="text-vega-red"
size={20}
/>
{t('Proposal failed: {{count}} of {{total}} proposals passed', {
count: passingCount['true'] || 0,
total: voteInfo.length,
})}
</p>
)}
<button
className="underline"
onClick={() => setFullBreakdown((x) => !x)}
>
{fullBreakdown ? 'Hide vote breakdown' : 'Show vote breakdown'}
</button>
</div>
{fullBreakdown && (
<div>
{proposal.subProposals?.map((p, i) => {
if (!p?.terms) return null;
return (
<VoteBreakdownBatchSubProposal
indicator={i + 1}
key={i}
proposal={proposal}
votes={proposal.votes}
terms={p.terms}
/>
);
})}
</div>
)}
</div>
);
}
return null;
};
const VoteBreakdownBatchSubProposal = ({
proposal,
votes,
terms,
indicator,
}: {
proposal: BatchProposal;
votes: VoteFieldsFragment;
terms: ProposalTermsFieldsFragment;
indicator?: number;
}) => {
const { t } = useTranslation();
const voteInfo = useVoteInformation({
votes,
terms,
});
const isProposalOpen = proposal?.state === ProposalState.STATE_OPEN;
const isUpdateMarket = terms?.change?.__typename === 'UpdateMarket';
let marketId = undefined;
if (terms?.change?.__typename === 'UpdateMarket') {
marketId = terms.change.marketId;
}
if (terms?.change?.__typename === 'UpdateMarketState') {
marketId = terms.change.market.id;
}
const marketName = marketId ? (
<>
: <MarketName marketId={marketId} />
</>
) : null;
const indicatorElement = indicator && (
<span className={getIndicatorStyle(indicator)}>{indicator}</span>
);
return (
<div className="mb-6">
<div className="flex items-baseline gap-3 mb-3">
{indicatorElement}
<h4>
{t(terms.change.__typename)} {marketName}
</h4>
</div>
<div className="rounded-sm bg-vega-dark-100 p-3">
<VoteBreakDownUI
voteInfo={voteInfo}
isProposalOpen={isProposalOpen}
isUpdateMarket={isUpdateMarket}
/>
</div>
</div>
);
};
const VoteBreakdownNormal = ({ proposal }: { proposal: Proposal }) => {
const voteInfo = useVoteInformation({
votes: proposal.votes,
terms: proposal.terms,
});
const isProposalOpen = proposal?.state === ProposalState.STATE_OPEN;
const isUpdateMarket = proposal?.terms?.change?.__typename === 'UpdateMarket';
return (
<div className="mb-6">
<VoteBreakDownUI
voteInfo={voteInfo}
isProposalOpen={isProposalOpen}
isUpdateMarket={isUpdateMarket}
/>
</div>
);
};
const VoteBreakDownUI = ({
voteInfo,
isProposalOpen,
isUpdateMarket,
}: {
voteInfo: ReturnType<typeof useVoteInformation>;
isProposalOpen: boolean;
isUpdateMarket: boolean;
}) => {
const defaultDP = 2;
const { t } = useTranslation();
if (!voteInfo) return null;
export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
const {
totalTokensPercentage,
participationMet,
@@ -366,6 +101,7 @@ const VoteBreakDownUI = ({
noPercentage,
noLPPercentage,
yesPercentage,
yesLPPercentage,
yesTokens,
noTokens,
totalEquityLikeShareWeight,
@@ -378,9 +114,12 @@ const VoteBreakDownUI = ({
majorityLPMet,
willPassByTokenVote,
willPassByLPVote,
lpVoteWeight,
} = voteInfo;
} = useVoteInformation({ proposal });
const { t } = useTranslation();
const defaultDP = 2;
const isProposalOpen = proposal?.state === ProposalState.STATE_OPEN;
const isUpdateMarket = proposal?.terms?.change?.__typename === 'UpdateMarket';
const participationThresholdProgress = BigNumber.min(
totalTokensPercentage.dividedBy(requiredParticipation).multipliedBy(100),
new BigNumber(100)
@@ -403,52 +142,127 @@ const VoteBreakDownUI = ({
'flex justify-between flex-wrap gap-6'
);
const sectionClasses = classNames('min-w-[300px] flex-1 flex-grow');
const headingClasses = classNames('mb-2 text-sm text-white font-bold');
const headingClasses = classNames('mb-2 text-vega-dark-400');
const progressDetailsClasses = classNames(
'flex justify-between flex-wrap mt-2 text-sm'
);
return (
<div>
<div className="mb-6">
{isProposalOpen && (
<div
data-testid="vote-status"
className="flex items-center gap-2 mb-2 text-bold"
className="flex items-center gap-1 mb-2 text-bold"
>
<span>
{willPass ? (
<VegaIcon
name={VegaIconNames.TICK}
size={20}
className="text-vega-green"
/>
<Icon name="tick" size={5} className="text-vega-green" />
) : (
<VegaIcon
name={VegaIconNames.CROSS}
size={20}
className="text-vega-red"
/>
<Icon name="cross" size={5} className="text-vega-pink" />
)}
</span>
<span>{t('currentlySetTo')} </span>
{willPass ? (
<p className="m-0">
<Trans
i18nKey={'Currently expected to <0>pass</0>'}
components={[<span className="text-vega-green" />]}
/>
<span>
<span className="text-vega-green">{t('pass')}</span>
{isUpdateMarket && <span> {updateMarketVotePassMethod}</span>}
</p>
</span>
) : (
<p className="m-0">
<Trans
i18nKey={'Currently expected to <0>fail</0>'}
components={[<span className="text-vega-red" />]}
/>
</p>
<span className="text-vega-pink">{t('fail')}</span>
)}
</div>
)}
{isUpdateMarket && (
<div className="mb-4">
<h3 className={headingClasses}>{t('liquidityProviderVote')}</h3>
<div className={sectionWrapperClasses}>
<section
className={sectionClasses}
data-testid="lp-majority-breakdown"
>
<VoteProgress
percentageFor={yesLPPercentage}
colourfulBg={true}
testId="lp-majority-progress"
>
<Status
reached={majorityLPMet}
threshold={requiredMajorityLPPercentage}
text={t('majorityThreshold')}
testId={
majorityLPMet ? 'lp-majority-met' : 'lp-majority-not-met'
}
/>
</VoteProgress>
<div className={progressDetailsClasses}>
<div className="flex items-center gap-1">
<span>{t('liquidityProviderVotesFor')}:</span>
<Tooltip
description={
<span>{yesLPPercentage.toFixed(defaultDP)}%</span>
}
>
<button>{yesLPPercentage.toFixed(1)}%</button>
</Tooltip>
</div>
<div className="flex items-center gap-1">
<span>{t('liquidityProviderVotesAgainst')}:</span>
<span>
<Tooltip
description={
<span>{noLPPercentage.toFixed(defaultDP)}%</span>
}
>
<button>{noLPPercentage.toFixed(1)}%</button>
</Tooltip>
</span>
</div>
</div>
</section>
<section
className={sectionClasses}
data-testid="lp-participation-breakdown"
>
<VoteProgress
percentageFor={
lpParticipationThresholdProgress || new BigNumber(0)
}
testId="lp-participation-progress"
>
<Status
reached={participationLPMet}
threshold={requiredParticipationLP || new BigNumber(1)}
text={t('participationThreshold')}
testId={
participationLPMet
? 'lp-participation-met'
: 'lp-participation-not-met'
}
/>
</VoteProgress>
<div className="flex mt-2 text-sm">
<div className="flex items-center gap-1">
<span>{t('totalLiquidityProviderTokensVoted')}:</span>
<Tooltip
description={formatNumber(
totalEquityLikeShareWeight,
defaultDP
)}
>
<span>{totalEquityLikeShareWeight.toFixed(1)}%</span>
</Tooltip>
</div>
</div>
</section>
</div>
</div>
)}
{isUpdateMarket && <h3 className={headingClasses}>{t('tokenVote')}</h3>}
<div className={sectionWrapperClasses}>
<section
@@ -548,97 +362,6 @@ const VoteBreakDownUI = ({
</div>
</section>
</div>
{/** Liquidity provider vote */}
{isUpdateMarket && (
<div className="mt-3">
<h3 className={headingClasses}>{t('liquidityProviderVote')}</h3>
<div className={sectionWrapperClasses}>
<section
className={sectionClasses}
data-testid="lp-majority-breakdown"
>
<VoteProgress
percentageFor={lpVoteWeight}
colourfulBg={true}
testId="lp-majority-progress"
>
<Status
reached={majorityLPMet}
threshold={requiredMajorityLPPercentage}
text={t('majorityThreshold')}
testId={
majorityLPMet ? 'lp-majority-met' : 'lp-majority-not-met'
}
/>
</VoteProgress>
<div className={progressDetailsClasses}>
<div className="flex items-center gap-1">
<span>{t('liquidityProviderVotesFor')}:</span>
<Tooltip
description={
<span>{lpVoteWeight.toFixed(defaultDP)}%</span>
}
>
<button>{lpVoteWeight.toFixed(1)}%</button>
</Tooltip>
</div>
<div className="flex items-center gap-1">
<span>{t('liquidityProviderVotesAgainst')}:</span>
<span>
<Tooltip
description={
<span>{noLPPercentage.toFixed(defaultDP)}%</span>
}
>
<button>{noLPPercentage.toFixed(1)}%</button>
</Tooltip>
</span>
</div>
</div>
</section>
<section
className={sectionClasses}
data-testid="lp-participation-breakdown"
>
<VoteProgress
percentageFor={
lpParticipationThresholdProgress || new BigNumber(0)
}
testId="lp-participation-progress"
>
<Status
reached={participationLPMet}
threshold={requiredParticipationLP || new BigNumber(1)}
text={t('participationThreshold')}
testId={
participationLPMet
? 'lp-participation-met'
: 'lp-participation-not-met'
}
/>
</VoteProgress>
<div className="flex mt-2 text-sm">
<div className="flex items-center gap-1">
<span>{t('totalLiquidityProviderTokensVoted')}:</span>
<Tooltip
description={formatNumber(
totalEquityLikeShareWeight,
defaultDP
)}
>
<span>{totalEquityLikeShareWeight.toFixed(1)}%</span>
</Tooltip>
</div>
</div>
</section>
</div>
</div>
)}
</div>
);
};
@@ -8,10 +8,12 @@ import { SubHeading } from '../../../../components/heading';
import { type VoteValue } from '@vegaprotocol/types';
import { type DialogProps, type VegaTxState } from '@vegaprotocol/proposals';
import { type VoteState } from './use-user-vote';
import { type Proposal, type BatchProposal } from '../../types';
import { type Proposal } from '../../types';
interface UserVoteProps {
proposal: Proposal | BatchProposal;
proposal: Proposal;
minVoterBalance: string | null | undefined;
spamProtectionMinTokens: string | null | undefined;
transaction: VegaTxState | null;
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
dialog: (props: DialogProps) => JSX.Element;
@@ -21,6 +23,8 @@ interface UserVoteProps {
export const UserVote = ({
proposal,
minVoterBalance,
spamProtectionMinTokens,
submit,
transaction,
dialog,
@@ -42,17 +46,12 @@ export const UserVote = ({
{pubKey ? (
proposal && (
<VoteButtonsContainer
changeType={
proposal.__typename === 'BatchProposal'
? // @ts-ignore should not be null/undefined
proposal.subProposals[0]?.terms?.change.__typename
: // @ts-ignore should not be null/undefined
proposal.terms?.change.__typename
}
voteState={voteState}
voteDatetime={voteDatetime}
proposalState={proposal.state}
proposalId={proposal.id ?? ''}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={spamProtectionMinTokens}
className="flex"
submit={submit}
transaction={transaction}
@@ -19,18 +19,14 @@ import { VoteTransactionDialog } from './vote-transaction-dialog';
import { useVoteButtonsQuery } from './__generated__/Stake';
import type { DialogProps, VegaTxState } from '@vegaprotocol/proposals';
import { filterAcceptableGraphqlErrors } from '../../../../lib/party';
import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { type ProposalChangeType } from '../../types';
interface VoteButtonsContainerProps {
changeType: ProposalChangeType;
voteState: VoteState | null;
voteDatetime: Date | null;
proposalId: string | null;
proposalState: ProposalState;
minVoterBalance: string | null | undefined;
spamProtectionMinTokens: string | null | undefined;
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
transaction: VegaTxState | null;
dialog: (props: DialogProps) => JSX.Element;
@@ -39,94 +35,20 @@ interface VoteButtonsContainerProps {
export const VoteButtonsContainer = (props: VoteButtonsContainerProps) => {
const { pubKey } = useVegaWallet();
const {
appState: { decimals },
} = useAppState();
const { data, loading, error } = useVoteButtonsQuery({
variables: { partyId: pubKey || '' },
skip: !pubKey,
});
const { params: networkParams } = useNetworkParams([
NetworkParams.governance_proposal_market_minVoterBalance,
NetworkParams.governance_proposal_updateMarket_minVoterBalance,
NetworkParams.governance_proposal_asset_minVoterBalance,
NetworkParams.governance_proposal_updateAsset_minVoterBalance,
NetworkParams.governance_proposal_updateNetParam_minVoterBalance,
NetworkParams.governance_proposal_freeform_minVoterBalance,
NetworkParams.governance_proposal_referralProgram_minVoterBalance,
NetworkParams.governance_proposal_VolumeDiscountProgram_minVoterBalance,
NetworkParams.governance_proposal_transfer_minVoterBalance,
NetworkParams.spam_protection_voting_min_tokens,
NetworkParams.governance_proposal_market_requiredMajority,
NetworkParams.governance_proposal_updateMarket_requiredMajority,
NetworkParams.governance_proposal_updateMarket_requiredMajorityLP,
NetworkParams.governance_proposal_asset_requiredMajority,
NetworkParams.governance_proposal_updateAsset_requiredMajority,
NetworkParams.governance_proposal_updateNetParam_requiredMajority,
NetworkParams.governance_proposal_freeform_requiredMajority,
NetworkParams.governance_proposal_referralProgram_requiredMajority,
NetworkParams.governance_proposal_VolumeDiscountProgram_requiredMajority,
NetworkParams.governance_proposal_transfer_requiredMajority,
]);
let minVoterBalance = null;
if (networkParams) {
switch (props.changeType) {
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 'CancelTransfer':
case 'NewTransfer':
minVoterBalance =
networkParams.governance_proposal_transfer_requiredMajority;
break;
case 'UpdateReferralProgram':
minVoterBalance =
networkParams.governance_proposal_referralProgram_minVoterBalance;
break;
case 'UpdateVolumeDiscountProgram':
minVoterBalance =
networkParams.governance_proposal_VolumeDiscountProgram_minVoterBalance;
break;
}
}
const filteredErrors = filterAcceptableGraphqlErrors(error);
return (
<AsyncRenderer loading={loading} error={filteredErrors} data={data}>
<VoteButtons
{...props}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={
networkParams.spam_protection_voting_min_tokens
}
currentStakeAvailable={toBigNum(
data?.party?.stakingSummary.currentStakeAvailable || 0,
decimals
@@ -136,17 +58,8 @@ export const VoteButtonsContainer = (props: VoteButtonsContainerProps) => {
);
};
interface VoteButtonsProps {
voteState: VoteState | null;
voteDatetime: Date | null;
proposalId: string | null;
proposalState: ProposalState;
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
transaction: VegaTxState | null;
dialog: (props: DialogProps) => JSX.Element;
interface VoteButtonsProps extends VoteButtonsContainerProps {
currentStakeAvailable: BigNumber;
minVoterBalance: string | null;
spamProtectionMinTokens: string | null;
}
export const VoteButtons = ({
@@ -1,6 +1,7 @@
import { renderHook } from '@testing-library/react';
import { BigNumber } from '../../../lib/bignumber';
import { useProposalNetworkParams } from './use-proposal-network-params';
import { generateProposal } from '../test-helpers/generate-proposals';
jest.mock('@vegaprotocol/network-parameters', () => ({
...jest.requireActual('@vegaprotocol/network-parameters'),
@@ -28,84 +29,118 @@ jest.mock('@vegaprotocol/network-parameters', () => ({
describe('use-proposal-network-params', () => {
it('returns the correct params for an update market proposal', () => {
const proposal = generateProposal({
terms: {
change: {
__typename: 'UpdateMarket',
},
},
});
const {
result: { current },
} = renderHook(() => useProposalNetworkParams());
const expectedObj = {
requiredMajority: expect.any(BigNumber),
requiredMajorityLP: expect.any(BigNumber),
requiredParticipation: expect.any(BigNumber),
requiredParticipationLP: expect.any(BigNumber),
};
} = renderHook(() => useProposalNetworkParams({ proposal }));
expect(current).toEqual({
NewMarket: expectedObj,
NewSpotMarket: expectedObj,
UpdateMarket: expectedObj,
UpdateMarketState: expectedObj,
UpdateSpotMarket: expectedObj,
UpdateNetworkParameter: expectedObj,
NewAsset: expectedObj,
UpdateAsset: expectedObj,
NewFreeform: expectedObj,
UpdateReferralProgram: expectedObj,
UpdateVolumeDiscountProgram: expectedObj,
NewTransfer: expectedObj,
CancelTransfer: expectedObj,
requiredMajority: '0.1',
requiredMajorityLP: '0.2',
requiredParticipation: new BigNumber(0.15),
requiredParticipationLP: new BigNumber(0.25),
});
});
it('returns the correct values for the proposal change type', () => {
it('returns the correct params for a market proposal', () => {
const proposal = generateProposal({
terms: {
change: {
__typename: 'NewMarket',
},
},
});
const {
result: { current },
} = renderHook(() => useProposalNetworkParams());
} = renderHook(() => useProposalNetworkParams({ proposal }));
expect(current?.UpdateMarket.requiredMajority.toString()).toEqual('0.1');
expect(current?.UpdateMarket.requiredMajorityLP.toString()).toEqual('0.2');
expect(current?.UpdateMarket.requiredParticipation.toString()).toEqual(
'0.15'
);
expect(current?.UpdateMarket.requiredParticipationLP.toString()).toEqual(
'0.25'
);
expect(current).toEqual({
requiredMajority: '0.3',
requiredParticipation: new BigNumber(0.35),
});
});
expect(current?.NewMarket.requiredMajority.toString()).toEqual('0.3');
expect(current?.NewMarket.requiredMajorityLP.toString()).toEqual('0');
expect(current?.NewMarket.requiredParticipation.toString()).toEqual('0.35');
expect(current?.NewMarket.requiredParticipationLP.toString()).toEqual('0');
it('returns the correct params for an asset proposal', () => {
const proposal = generateProposal({
terms: {
change: {
__typename: 'NewAsset',
},
},
});
expect(current?.NewAsset.requiredMajority.toString()).toEqual('0.4');
expect(current?.NewAsset.requiredMajorityLP.toString()).toEqual('0');
expect(current?.NewAsset.requiredParticipation.toString()).toEqual('0.45');
expect(current?.NewAsset.requiredParticipationLP.toString()).toEqual('0');
const {
result: { current },
} = renderHook(() => useProposalNetworkParams({ proposal }));
expect(current?.UpdateAsset.requiredMajority.toString()).toEqual('0.5');
expect(current?.UpdateAsset.requiredMajorityLP.toString()).toEqual('0');
expect(current?.UpdateAsset.requiredParticipation.toString()).toEqual(
'0.55'
);
expect(current).toEqual({
requiredMajority: '0.4',
requiredParticipation: new BigNumber(0.45),
});
});
expect(current?.UpdateNetworkParameter.requiredMajority.toString()).toEqual(
'0.6'
);
expect(
current?.UpdateNetworkParameter.requiredMajorityLP.toString()
).toEqual('0');
expect(
current?.UpdateNetworkParameter.requiredParticipation.toString()
).toEqual('0.65');
expect(
current?.UpdateNetworkParameter.requiredParticipationLP.toString()
).toEqual('0');
it('returns the correct params for an update asset proposal', () => {
const proposal = generateProposal({
terms: {
change: {
__typename: 'UpdateAsset',
},
},
});
expect(current?.NewFreeform.requiredMajority.toString()).toEqual('0.7');
expect(current?.NewFreeform.requiredMajorityLP.toString()).toEqual('0');
expect(current?.NewFreeform.requiredParticipation.toString()).toEqual(
'0.75'
);
expect(current?.NewFreeform.requiredParticipationLP.toString()).toEqual(
'0'
);
const {
result: { current },
} = renderHook(() => useProposalNetworkParams({ proposal }));
expect(current).toEqual({
requiredMajority: '0.5',
requiredParticipation: new BigNumber(0.55),
});
});
it('returns the correct params for a network params proposal', () => {
const proposal = generateProposal({
terms: {
change: {
__typename: 'UpdateNetworkParameter',
},
},
});
const {
result: { current },
} = renderHook(() => useProposalNetworkParams({ proposal }));
expect(current).toEqual({
requiredMajority: '0.6',
requiredParticipation: new BigNumber(0.65),
});
});
it('returns the correct params for a freeform proposal', () => {
const proposal = generateProposal({
terms: {
change: {
__typename: 'NewFreeform',
},
},
});
const {
result: { current },
} = renderHook(() => useProposalNetworkParams({ proposal }));
expect(current).toEqual({
requiredMajority: '0.7',
requiredParticipation: new BigNumber(0.75),
});
});
});
@@ -3,33 +3,35 @@ import {
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { BigNumber } from '../../../lib/bignumber';
import { type ProposalChangeType } from '../types';
import { type Proposal } from '../types';
const REQUIRED_PARAMS = [
NetworkParams.governance_proposal_updateMarket_requiredMajority,
NetworkParams.governance_proposal_updateMarket_requiredMajorityLP,
NetworkParams.governance_proposal_updateMarket_requiredParticipation,
NetworkParams.governance_proposal_updateMarket_requiredParticipationLP,
NetworkParams.governance_proposal_market_requiredMajority,
NetworkParams.governance_proposal_market_requiredParticipation,
NetworkParams.governance_proposal_updateAsset_requiredMajority,
NetworkParams.governance_proposal_referralProgram_requiredMajority,
NetworkParams.governance_proposal_referralProgram_requiredParticipation,
NetworkParams.governance_proposal_updateAsset_requiredParticipation,
NetworkParams.governance_proposal_asset_requiredMajority,
NetworkParams.governance_proposal_asset_requiredParticipation,
NetworkParams.governance_proposal_updateNetParam_requiredMajority,
NetworkParams.governance_proposal_updateNetParam_requiredParticipation,
NetworkParams.governance_proposal_freeform_requiredMajority,
NetworkParams.governance_proposal_freeform_requiredParticipation,
NetworkParams.governance_proposal_VolumeDiscountProgram_requiredMajority,
NetworkParams.governance_proposal_VolumeDiscountProgram_requiredParticipation,
NetworkParams.governance_proposal_transfer_requiredParticipation,
NetworkParams.governance_proposal_transfer_requiredMajority,
];
export const useProposalNetworkParams = () => {
const { params } = useNetworkParams(REQUIRED_PARAMS);
export const useProposalNetworkParams = ({
proposal,
}: {
proposal: Proposal;
}) => {
const { params } = useNetworkParams([
NetworkParams.governance_proposal_updateMarket_requiredMajority,
NetworkParams.governance_proposal_updateMarket_requiredMajorityLP,
NetworkParams.governance_proposal_updateMarket_requiredParticipation,
NetworkParams.governance_proposal_updateMarket_requiredParticipationLP,
NetworkParams.governance_proposal_market_requiredMajority,
NetworkParams.governance_proposal_market_requiredParticipation,
NetworkParams.governance_proposal_updateAsset_requiredMajority,
NetworkParams.governance_proposal_referralProgram_requiredMajority,
NetworkParams.governance_proposal_referralProgram_requiredParticipation,
NetworkParams.governance_proposal_updateAsset_requiredParticipation,
NetworkParams.governance_proposal_asset_requiredMajority,
NetworkParams.governance_proposal_asset_requiredParticipation,
NetworkParams.governance_proposal_updateNetParam_requiredMajority,
NetworkParams.governance_proposal_updateNetParam_requiredParticipation,
NetworkParams.governance_proposal_freeform_requiredMajority,
NetworkParams.governance_proposal_freeform_requiredParticipation,
NetworkParams.governance_proposal_VolumeDiscountProgram_requiredMajority,
NetworkParams.governance_proposal_VolumeDiscountProgram_requiredParticipation,
NetworkParams.governance_proposal_transfer_requiredParticipation,
NetworkParams.governance_proposal_transfer_requiredMajority,
]);
const fallback = {
requiredMajority: new BigNumber(1),
@@ -39,152 +41,86 @@ export const useProposalNetworkParams = () => {
};
if (!params) {
return;
return fallback;
}
const result: Record<
ProposalChangeType,
{
requiredMajority: BigNumber;
requiredParticipation: BigNumber;
requiredMajorityLP: BigNumber;
requiredParticipationLP: BigNumber;
}
> = {
NewMarket: {
...fallback,
requiredMajority: new BigNumber(
params.governance_proposal_market_requiredMajority || 1
),
requiredParticipation: new BigNumber(
params.governance_proposal_market_requiredParticipation || 1
),
},
NewSpotMarket: {
...fallback,
requiredMajority: new BigNumber(
params.governance_proposal_market_requiredMajority || 1
),
requiredParticipation: new BigNumber(
params.governance_proposal_market_requiredParticipation || 1
),
},
UpdateMarket: {
requiredMajority: new BigNumber(
params.governance_proposal_updateMarket_requiredMajority || 1
),
requiredMajorityLP: new BigNumber(
params.governance_proposal_updateMarket_requiredMajorityLP || 0
),
requiredParticipation: new BigNumber(
params.governance_proposal_updateMarket_requiredParticipation || 1
),
requiredParticipationLP: new BigNumber(
params.governance_proposal_updateMarket_requiredParticipationLP || 0
),
},
UpdateMarketState: {
requiredMajority: new BigNumber(
params.governance_proposal_updateMarket_requiredMajority || 1
),
requiredMajorityLP: new BigNumber(
params.governance_proposal_updateMarket_requiredMajorityLP || 0
),
requiredParticipation: new BigNumber(
params.governance_proposal_updateMarket_requiredParticipation || 1
),
requiredParticipationLP: new BigNumber(
params.governance_proposal_updateMarket_requiredParticipationLP || 0
),
},
UpdateSpotMarket: {
requiredMajority: new BigNumber(
params.governance_proposal_updateMarket_requiredMajority || 1
),
requiredMajorityLP: new BigNumber(
params.governance_proposal_updateMarket_requiredMajorityLP || 0
),
requiredParticipation: new BigNumber(
params.governance_proposal_updateMarket_requiredParticipation || 1
),
requiredParticipationLP: new BigNumber(
params.governance_proposal_updateMarket_requiredParticipationLP || 0
),
},
UpdateNetworkParameter: {
...fallback,
requiredMajority: new BigNumber(
params.governance_proposal_updateNetParam_requiredMajority || 1
),
requiredParticipation: new BigNumber(
params.governance_proposal_updateNetParam_requiredParticipation || 1
),
},
NewAsset: {
...fallback,
requiredMajority: new BigNumber(
params.governance_proposal_asset_requiredMajority || 1
),
requiredParticipation: new BigNumber(
params.governance_proposal_asset_requiredParticipation || 1
),
},
UpdateAsset: {
...fallback,
requiredMajority: new BigNumber(
params.governance_proposal_updateAsset_requiredMajority || 1
),
requiredParticipation: new BigNumber(
params.governance_proposal_updateAsset_requiredParticipation || 1
),
},
NewFreeform: {
...fallback,
requiredMajority: new BigNumber(
params.governance_proposal_freeform_requiredMajority || 1
),
requiredParticipation: new BigNumber(
params.governance_proposal_freeform_requiredParticipation || 1
),
},
UpdateReferralProgram: {
...fallback,
requiredMajority: new BigNumber(
params.governance_proposal_referralProgram_requiredMajority || 1
),
requiredParticipation: new BigNumber(
params.governance_proposal_referralProgram_requiredParticipation || 1
),
},
UpdateVolumeDiscountProgram: {
...fallback,
requiredMajority: new BigNumber(
params.governance_proposal_VolumeDiscountProgram_requiredMajority || 1
),
requiredParticipation: new BigNumber(
params.governance_proposal_VolumeDiscountProgram_requiredParticipation ||
1
),
},
NewTransfer: {
...fallback,
requiredMajority: new BigNumber(
params.governance_proposal_transfer_requiredMajority || 1
),
requiredParticipation: new BigNumber(
params.governance_proposal_transfer_requiredParticipation || 1
),
},
CancelTransfer: {
...fallback,
requiredMajority: new BigNumber(
params.governance_proposal_transfer_requiredMajority || 1
),
requiredParticipation: new BigNumber(
params.governance_proposal_transfer_requiredParticipation || 1
),
},
};
return result;
switch (proposal?.terms.change.__typename) {
case 'UpdateMarket':
case 'UpdateMarketState':
return {
requiredMajority:
params.governance_proposal_updateMarket_requiredMajority,
requiredMajorityLP:
params.governance_proposal_updateMarket_requiredMajorityLP,
requiredParticipation: new BigNumber(
params.governance_proposal_updateMarket_requiredParticipation
),
requiredParticipationLP: new BigNumber(
params.governance_proposal_updateMarket_requiredParticipationLP
),
};
case 'UpdateNetworkParameter':
return {
requiredMajority:
params.governance_proposal_updateNetParam_requiredMajority,
requiredParticipation: new BigNumber(
params.governance_proposal_updateNetParam_requiredParticipation
),
};
case 'NewAsset':
return {
requiredMajority: params.governance_proposal_asset_requiredMajority,
requiredParticipation: new BigNumber(
params.governance_proposal_asset_requiredParticipation
),
};
case 'UpdateAsset':
return {
requiredMajority:
params.governance_proposal_updateAsset_requiredMajority,
requiredParticipation: new BigNumber(
params.governance_proposal_updateAsset_requiredParticipation
),
};
case 'NewMarket':
return {
requiredMajority: params.governance_proposal_market_requiredMajority,
requiredParticipation: new BigNumber(
params.governance_proposal_market_requiredParticipation
),
};
case 'NewFreeform':
return {
requiredMajority: params.governance_proposal_freeform_requiredMajority,
requiredParticipation: new BigNumber(
params.governance_proposal_freeform_requiredParticipation
),
};
case 'UpdateReferralProgram':
return {
requiredMajority:
params.governance_proposal_referralProgram_requiredMajority,
requiredParticipation: new BigNumber(
params.governance_proposal_referralProgram_requiredParticipation
),
};
case 'UpdateVolumeDiscountProgram':
return {
requiredMajority:
params.governance_proposal_VolumeDiscountProgram_requiredMajority,
requiredParticipation: new BigNumber(
params.governance_proposal_VolumeDiscountProgram_requiredParticipation
),
};
case 'NewTransfer':
case 'CancelTransfer':
return {
requiredMajority: params.governance_proposal_transfer_requiredMajority,
requiredParticipation: new BigNumber(
params.governance_proposal_transfer_requiredParticipation
),
};
default:
return fallback;
}
};
@@ -79,35 +79,33 @@ describe('use-vote-information', () => {
const {
result: { current },
} = renderHook(() =>
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
);
} = renderHook(() => useVoteInformation({ proposal }));
expect(current?.requiredMajorityLPPercentage).toEqual(new BigNumber(50));
expect(current?.requiredMajorityPercentage).toEqual(new BigNumber(50));
expect(current?.noTokens).toEqual(new BigNumber(60));
expect(current?.noVotes).toEqual(new BigNumber(60));
expect(current?.noEquityLikeShareWeight).toEqual(new BigNumber(70));
expect(current?.yesTokens).toEqual(new BigNumber(40));
expect(current?.yesVotes).toEqual(new BigNumber(40));
expect(current?.yesEquityLikeShareWeight).toEqual(new BigNumber(30));
expect(current?.totalTokensVoted).toEqual(new BigNumber(100));
expect(current?.totalVotes).toEqual(new BigNumber(100));
expect(current?.totalEquityLikeShareWeight).toEqual(new BigNumber(100));
expect(current?.yesPercentage).toEqual(new BigNumber(40));
expect(current?.yesLPPercentage).toEqual(new BigNumber(30));
expect(current?.noPercentage).toEqual(new BigNumber(60));
expect(current?.noLPPercentage).toEqual(new BigNumber(70));
expect(current?.requiredParticipation).toEqual(new BigNumber(50));
expect(current?.participationMet).toEqual(true);
expect(current?.requiredParticipationLP).toEqual(new BigNumber(50));
expect(current?.participationLPMet).toEqual(true);
expect(current?.majorityMet).toEqual(false);
expect(current?.majorityLPMet).toEqual(false);
expect(current?.totalTokensPercentage).toEqual(new BigNumber(100));
expect(current?.totalLPTokensPercentage).toEqual(new BigNumber(100));
expect(current?.willPassByTokenVote).toEqual(false);
expect(current?.willPassByLPVote).toEqual(false);
expect(current.requiredMajorityPercentage).toEqual(new BigNumber(50));
expect(current.requiredMajorityLPPercentage).toEqual(new BigNumber(50));
expect(current.noTokens).toEqual(new BigNumber(60));
expect(current.noVotes).toEqual(new BigNumber(60));
expect(current.noEquityLikeShareWeight).toEqual(new BigNumber(70));
expect(current.yesTokens).toEqual(new BigNumber(40));
expect(current.yesVotes).toEqual(new BigNumber(40));
expect(current.yesEquityLikeShareWeight).toEqual(new BigNumber(30));
expect(current.totalTokensVoted).toEqual(new BigNumber(100));
expect(current.totalVotes).toEqual(new BigNumber(100));
expect(current.totalEquityLikeShareWeight).toEqual(new BigNumber(100));
expect(current.yesPercentage).toEqual(new BigNumber(40));
expect(current.yesLPPercentage).toEqual(new BigNumber(30));
expect(current.noPercentage).toEqual(new BigNumber(60));
expect(current.noLPPercentage).toEqual(new BigNumber(70));
expect(current.requiredParticipation).toEqual(new BigNumber(50));
expect(current.participationMet).toEqual(true);
expect(current.requiredParticipationLP).toEqual(new BigNumber(50));
expect(current.participationLPMet).toEqual(true);
expect(current.majorityMet).toEqual(false);
expect(current.majorityLPMet).toEqual(false);
expect(current.totalTokensPercentage).toEqual(new BigNumber(100));
expect(current.totalLPTokensPercentage).toEqual(new BigNumber(100));
expect(current.willPassByTokenVote).toEqual(false);
expect(current.willPassByLPVote).toEqual(false);
});
it('correctly returns majority, participation and will-pass status for a proposal with no votes', () => {
@@ -125,13 +123,11 @@ describe('use-vote-information', () => {
const {
result: { current },
} = renderHook(() =>
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
);
} = renderHook(() => useVoteInformation({ proposal }));
expect(current?.participationMet).toEqual(false);
expect(current?.majorityMet).toEqual(false);
expect(current?.willPassByTokenVote).toEqual(false);
expect(current.participationMet).toEqual(false);
expect(current.majorityMet).toEqual(false);
expect(current.willPassByTokenVote).toEqual(false);
});
it('correctly shows lack of participation for a failing proposal lacking votes', () => {
@@ -149,11 +145,9 @@ describe('use-vote-information', () => {
const {
result: { current },
} = renderHook(() =>
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
);
} = renderHook(() => useVoteInformation({ proposal }));
expect(current?.participationMet).toEqual(false);
expect(current.participationMet).toEqual(false);
});
it('correctly shows participation but lack of majority for a failing proposal with enough votes but not enough majority', () => {
@@ -171,13 +165,11 @@ describe('use-vote-information', () => {
const {
result: { current },
} = renderHook(() =>
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
);
} = renderHook(() => useVoteInformation({ proposal }));
expect(current?.participationMet).toEqual(true);
expect(current?.majorityMet).toEqual(false);
expect(current?.willPassByTokenVote).toEqual(false);
expect(current.participationMet).toEqual(true);
expect(current.majorityMet).toEqual(false);
expect(current.willPassByTokenVote).toEqual(false);
});
it('correctly shows participation, majority and will-pass data for successful proposal', () => {
@@ -195,13 +187,11 @@ describe('use-vote-information', () => {
const {
result: { current },
} = renderHook(() =>
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
);
} = renderHook(() => useVoteInformation({ proposal }));
expect(current?.participationMet).toEqual(true);
expect(current?.majorityMet).toEqual(true);
expect(current?.willPassByTokenVote).toEqual(true);
expect(current.participationMet).toEqual(true);
expect(current.majorityMet).toEqual(true);
expect(current.willPassByTokenVote).toEqual(true);
});
it('correctly shows whether an update market proposal will pass by token or LP vote - both failing', () => {
@@ -231,12 +221,10 @@ describe('use-vote-information', () => {
const {
result: { current },
} = renderHook(() =>
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
);
} = renderHook(() => useVoteInformation({ proposal }));
expect(current?.willPassByTokenVote).toEqual(false);
expect(current?.willPassByLPVote).toEqual(false);
expect(current.willPassByTokenVote).toEqual(false);
expect(current.willPassByLPVote).toEqual(false);
});
it('correctly shows whether an update market proposal failing token but passing LP voting', () => {
@@ -266,81 +254,9 @@ describe('use-vote-information', () => {
const {
result: { current },
} = renderHook(() =>
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
);
} = renderHook(() => useVoteInformation({ proposal }));
expect(current?.willPassByTokenVote).toEqual(false);
expect(current?.willPassByLPVote).toEqual(true);
});
it('mainnet recreation: only yes LP votes equal passing', () => {
const yesVotes = 0;
const noVotes = 70;
const yesEquityLikeShareWeight = '0.21';
const noEquityLikeShareWeight = '0';
const fixedTokenValue = 1000000000000000000;
const proposal = generateProposal({
terms: {
change: {
__typename: 'UpdateMarket',
marketId: '12345',
},
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(
yesVotes,
fixedTokenValue,
yesEquityLikeShareWeight
),
no: generateNoVotes(noVotes, fixedTokenValue, noEquityLikeShareWeight),
},
});
const {
result: { current },
} = renderHook(() =>
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
);
expect(current?.willPassByTokenVote).toEqual(false);
expect(current?.willPassByLPVote).toEqual(true);
});
it('mainnet recreation: mixed yes and no LP votes equal failing', () => {
const yesVotes = 0;
const noVotes = 70;
const yesEquityLikeShareWeight = '0.21';
const noEquityLikeShareWeight = '0.22';
const fixedTokenValue = 1000000000000000000;
const proposal = generateProposal({
terms: {
change: {
__typename: 'UpdateMarket',
marketId: '12345',
},
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(
yesVotes,
fixedTokenValue,
yesEquityLikeShareWeight
),
no: generateNoVotes(noVotes, fixedTokenValue, noEquityLikeShareWeight),
},
});
const {
result: { current },
} = renderHook(() =>
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
);
expect(current?.willPassByTokenVote).toEqual(false);
expect(current?.willPassByLPVote).toEqual(false);
expect(current.willPassByTokenVote).toEqual(false);
expect(current.willPassByLPVote).toEqual(true);
});
});
@@ -1,169 +1,25 @@
import { useMemo } from 'react';
import { useAppState } from '../../../contexts/app-state/app-state-context';
import { BigNumber } from '../../../lib/bignumber';
import { useProposalNetworkParams } from './use-proposal-network-params';
import { addDecimal } from '@vegaprotocol/utils';
import {
type ProposalTermsFieldsFragment,
type ProposalFieldsFragment,
type VoteFieldsFragment,
} from '../__generated__/Proposals';
import { type ProposalChangeType } from '../types';
import { type Proposal } from '../types';
export const useVoteInformation = ({
votes,
terms,
}: {
votes: VoteFieldsFragment;
terms: ProposalTermsFieldsFragment;
}) => {
export const useVoteInformation = ({ proposal }: { proposal: Proposal }) => {
const {
appState: { totalSupply, decimals },
} = useAppState();
const params = useProposalNetworkParams();
if (!params) return;
const paramsForChange = params[terms.change.__typename];
return getVoteData(
terms.change.__typename,
paramsForChange,
votes,
totalSupply,
decimals
);
};
export const useBatchVoteInformation = ({
votes,
terms,
}: {
votes: VoteFieldsFragment;
terms: ProposalTermsFieldsFragment[];
}) => {
const {
appState: { totalSupply, decimals },
} = useAppState();
const params = useProposalNetworkParams();
if (!params) return;
return terms.map((t) => {
const paramsForChange = params[t.change.__typename];
return getVoteData(
t.change.__typename,
paramsForChange,
votes,
totalSupply,
decimals
);
requiredMajority,
requiredParticipation,
requiredMajorityLP,
requiredParticipationLP,
} = useProposalNetworkParams({
proposal,
});
};
const getVoteData = (
changeType: ProposalChangeType,
params: {
requiredMajority: BigNumber;
requiredMajorityLP: BigNumber;
requiredParticipation: BigNumber;
requiredParticipationLP: BigNumber;
},
votes: ProposalFieldsFragment['votes'],
totalSupply: BigNumber,
decimals: number
) => {
const requiredMajorityPercentage = params.requiredMajority
? new BigNumber(params.requiredMajority).times(100)
: new BigNumber(100);
const requiredMajorityLPPercentage = params.requiredMajorityLP
? new BigNumber(params.requiredMajorityLP).times(100)
: new BigNumber(100);
const noTokens = new BigNumber(
addDecimal(votes.no.totalTokens ?? 0, decimals)
);
const noEquityLikeShareWeight = !votes.no.totalEquityLikeShareWeight
? new BigNumber(0)
: new BigNumber(votes.no.totalEquityLikeShareWeight).times(100);
const yesTokens = new BigNumber(
addDecimal(votes.yes.totalTokens ?? 0, decimals)
);
const yesEquityLikeShareWeight = !votes.yes.totalEquityLikeShareWeight
? new BigNumber(0)
: new BigNumber(votes.yes.totalEquityLikeShareWeight).times(100);
const totalTokensVoted = yesTokens.plus(noTokens);
const totalEquityLikeShareWeight = yesEquityLikeShareWeight.plus(
noEquityLikeShareWeight
);
const yesPercentage = totalTokensVoted.isZero()
? new BigNumber(0)
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
const yesLPPercentage = yesEquityLikeShareWeight;
const noPercentage = totalTokensVoted.isZero()
? new BigNumber(0)
: noTokens.multipliedBy(100).dividedBy(totalTokensVoted);
const noLPPercentage = totalEquityLikeShareWeight.isZero()
? new BigNumber(0)
: noEquityLikeShareWeight
.multipliedBy(100)
.dividedBy(totalEquityLikeShareWeight);
const participationMet = totalTokensVoted.isGreaterThan(
totalSupply.multipliedBy(params.requiredParticipation)
);
const lpVoteWeight = yesEquityLikeShareWeight
.dividedBy(totalEquityLikeShareWeight)
.multipliedBy(100);
const participationLPMet = params.requiredParticipationLP
? lpVoteWeight.isGreaterThan(params.requiredParticipationLP)
: false;
const majorityMet = yesPercentage.isGreaterThanOrEqualTo(
requiredMajorityPercentage
);
const majorityLPMet = lpVoteWeight.isGreaterThanOrEqualTo(
requiredMajorityLPPercentage
);
const totalTokensPercentage = totalTokensVoted
.multipliedBy(100)
.dividedBy(totalSupply);
const totalLPTokensPercentage = totalEquityLikeShareWeight;
const willPassByTokenVote =
participationMet &&
new BigNumber(yesPercentage).isGreaterThanOrEqualTo(
requiredMajorityPercentage
);
const willPassByLPVote =
participationLPMet &&
lpVoteWeight.isGreaterThanOrEqualTo(requiredMajorityLPPercentage);
let willPass = false;
if (changeType === 'UpdateMarket' || changeType === 'UpdateMarketState') {
willPass = willPassByTokenVote || willPassByLPVote;
} else {
willPass = willPassByTokenVote;
}
return {
const {
requiredMajorityPercentage,
requiredMajorityLPPercentage,
noTokens,
@@ -184,18 +40,152 @@ const getVoteData = (
totalLPTokensPercentage,
willPassByTokenVote,
willPassByLPVote,
lpVoteWeight: lpVoteWeight.isNaN() ? new BigNumber(0) : lpVoteWeight,
yesVotes: new BigNumber(votes.yes.totalNumber ?? 0),
noVotes: new BigNumber(votes.no.totalNumber ?? 0),
totalVotes: new BigNumber(votes.yes.totalNumber ?? 0).plus(
votes.no.totalNumber ?? 0
} = useMemo(() => {
const requiredMajorityPercentage = requiredMajority
? new BigNumber(requiredMajority).times(100)
: new BigNumber(100);
const requiredMajorityLPPercentage = requiredMajorityLP
? new BigNumber(requiredMajorityLP).times(100)
: new BigNumber(100);
const noTokens = new BigNumber(
addDecimal(proposal?.votes.no.totalTokens ?? 0, decimals)
);
const noEquityLikeShareWeight = !proposal?.votes.no
.totalEquityLikeShareWeight
? new BigNumber(0)
: new BigNumber(proposal.votes.no.totalEquityLikeShareWeight).times(100);
const yesTokens = new BigNumber(
addDecimal(proposal?.votes.yes.totalTokens ?? 0, decimals)
);
const yesEquityLikeShareWeight = !proposal?.votes.yes
.totalEquityLikeShareWeight
? new BigNumber(0)
: new BigNumber(proposal.votes.yes.totalEquityLikeShareWeight).times(100);
const totalTokensVoted = yesTokens.plus(noTokens);
const totalEquityLikeShareWeight = yesEquityLikeShareWeight.plus(
noEquityLikeShareWeight
);
const yesPercentage = totalTokensVoted.isZero()
? new BigNumber(0)
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
const yesLPPercentage = yesEquityLikeShareWeight;
const noPercentage = totalTokensVoted.isZero()
? new BigNumber(0)
: noTokens.multipliedBy(100).dividedBy(totalTokensVoted);
const noLPPercentage = totalEquityLikeShareWeight.isZero()
? new BigNumber(0)
: noEquityLikeShareWeight
.multipliedBy(100)
.dividedBy(totalEquityLikeShareWeight);
const participationMet = totalTokensVoted.isGreaterThan(
totalSupply.multipliedBy(requiredParticipation)
);
const participationLPMet = requiredParticipationLP
? totalEquityLikeShareWeight.isGreaterThan(requiredParticipationLP)
: false;
const majorityMet = yesPercentage.isGreaterThanOrEqualTo(
requiredMajorityPercentage
);
const majorityLPMet = yesLPPercentage.isGreaterThanOrEqualTo(
requiredMajorityLPPercentage
);
const totalTokensPercentage = totalTokensVoted
.multipliedBy(100)
.dividedBy(totalSupply);
const totalLPTokensPercentage = totalEquityLikeShareWeight;
const willPassByTokenVote =
participationMet &&
new BigNumber(yesPercentage).isGreaterThanOrEqualTo(
requiredMajorityPercentage
);
const willPassByLPVote =
participationLPMet &&
new BigNumber(yesLPPercentage).isGreaterThanOrEqualTo(
requiredMajorityLPPercentage
);
return {
requiredMajorityPercentage,
requiredMajorityLPPercentage,
noTokens,
noEquityLikeShareWeight,
yesTokens,
yesEquityLikeShareWeight,
totalTokensVoted,
totalEquityLikeShareWeight,
yesPercentage,
yesLPPercentage,
noPercentage,
noLPPercentage,
participationMet,
participationLPMet,
majorityMet,
majorityLPMet,
totalTokensPercentage,
totalLPTokensPercentage,
willPassByTokenVote,
willPassByLPVote,
};
}, [
decimals,
proposal?.votes.no.totalEquityLikeShareWeight,
proposal?.votes.no.totalTokens,
proposal?.votes.yes.totalEquityLikeShareWeight,
proposal?.votes.yes.totalTokens,
requiredMajority,
requiredMajorityLP,
requiredParticipation,
requiredParticipationLP,
totalSupply,
]);
return {
willPassByTokenVote,
willPassByLPVote,
totalTokensPercentage,
totalLPTokensPercentage,
participationMet,
participationLPMet,
totalTokensVoted,
totalEquityLikeShareWeight,
noPercentage,
noLPPercentage,
yesPercentage,
yesLPPercentage,
noTokens,
noEquityLikeShareWeight,
yesTokens,
yesEquityLikeShareWeight,
yesVotes: new BigNumber(proposal?.votes.yes.totalNumber ?? 0),
noVotes: new BigNumber(proposal?.votes.no.totalNumber ?? 0),
totalVotes: new BigNumber(proposal?.votes.yes.totalNumber ?? 0).plus(
proposal?.votes.no.totalNumber ?? 0
),
requiredParticipation: new BigNumber(params.requiredParticipation).times(
100
),
requiredParticipationLP: new BigNumber(
params.requiredParticipationLP
).times(100),
willPass,
requiredMajorityPercentage,
requiredMajorityLPPercentage,
requiredParticipation: new BigNumber(requiredParticipation).times(100),
requiredParticipationLP:
requiredParticipationLP &&
new BigNumber(requiredParticipationLP).times(100),
majorityMet,
majorityLPMet,
};
};
@@ -0,0 +1,442 @@
fragment NewMarketProductField on Proposal {
terms {
change {
... on NewMarket {
instrument {
product {
__typename
}
}
}
}
}
}
fragment UpdateMarketState on Proposal {
terms {
change {
... on UpdateMarketState {
updateType
market {
decimalPlaces
id
tradableInstrument {
instrument {
product {
__typename
... on Future {
quoteName
}
... on Perpetual {
quoteName
}
}
name
code
}
}
}
updateType
price
}
}
}
}
fragment UpdateReferralProgram on Proposal {
terms {
change {
... on UpdateReferralProgram {
benefitTiers {
minimumEpochs
minimumRunningNotionalTakerVolume
referralDiscountFactor
referralRewardFactor
}
endOfProgram: endOfProgramTimestamp
windowLength
stakingTiers {
minimumStakedTokens
referralRewardMultiplier
}
}
}
}
}
fragment UpdateVolumeDiscountProgram on Proposal {
terms {
change {
... on UpdateVolumeDiscountProgram {
benefitTiers {
minimumRunningNotionalTakerVolume
volumeDiscountFactor
}
endOfProgramTimestamp
windowLength
}
}
}
}
query Proposal(
$proposalId: ID!
$includeNewMarketProductField: Boolean!
$includeUpdateMarketState: Boolean!
$includeUpdateReferralProgram: Boolean!
) {
proposal(id: $proposalId) {
... on Proposal {
id
rationale {
title
description
}
reference
state
datetime
rejectionReason
party {
id
}
errorDetails
...NewMarketProductField @include(if: $includeNewMarketProductField)
...UpdateMarketState @include(if: $includeUpdateMarketState)
...UpdateReferralProgram @include(if: $includeUpdateReferralProgram)
...UpdateVolumeDiscountProgram
terms {
closingDatetime
enactmentDatetime
change {
... on NewMarket {
decimalPlaces
metadata
riskParameters {
... on LogNormalRiskModel {
riskAversionParameter
tau
params {
mu
r
sigma
}
}
... on SimpleRiskModel {
params {
factorLong
factorShort
}
}
}
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
}
... on UpdateMarket {
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
}
}
}
}
}
}
... on NewAsset {
name
symbol
decimals
quantum
source {
... on BuiltinAsset {
maxFaucetAmountMint
}
... on ERC20 {
contractAddress
lifetimeLimit
withdrawThreshold
}
}
}
... on UpdateNetworkParameter {
networkParameter {
key
value
}
}
... on UpdateAsset {
quantum
assetId
source {
... on UpdateERC20 {
lifetimeLimit
withdrawThreshold
}
}
}
}
}
votes {
yes {
totalTokens
totalNumber
totalEquityLikeShareWeight
}
no {
totalTokens
totalNumber
totalEquityLikeShareWeight
}
}
}
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,69 @@
import { render, screen, waitFor } from '@testing-library/react';
import { generateProposal } from '../test-helpers/generate-proposals';
import type { ProposalQuery } from './__generated__/Proposal';
import { ProposalContainer } from './proposal-container';
import { MockedProvider } from '@apollo/client/testing';
import { MemoryRouter, Route, Routes } from 'react-router-dom';
import { ProposalDocument } from './__generated__/Proposal';
jest.mock('@vegaprotocol/data-provider', () => ({
...jest.requireActual('@vegaprotocol/data-provider'),
useDataProvider: jest.fn(() => ({ data: [], loading: false })),
}));
jest.mock('../components/proposal', () => ({
Proposal: () => <div data-testid="proposal" />,
}));
jest.mock('../components/proposal-not-found', () => ({
ProposalNotFound: () => <div data-testid="proposal-not-found" />,
}));
const renderComponent = (
proposal: ProposalQuery['proposal'] | null,
id: string
) => {
return (
<MemoryRouter initialEntries={[`/governance/${id}`]}>
<MockedProvider
mocks={[
{
request: {
query: ProposalDocument,
variables: {
proposalId: id,
},
},
result: { data: { proposal } },
},
]}
>
<Routes>
<Route
path={`/governance/:proposalId`}
element={<ProposalContainer />}
/>
</Routes>
</MockedProvider>
</MemoryRouter>
);
};
// These tests are broken due to schema changes. NewMarket.futureProduct -> NewMarket.product union
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('Proposal container', () => {
it('Renders not found if the proposal is not found', async () => {
render(renderComponent(null, 'foo'));
await waitFor(() => {
expect(screen.getByTestId('proposal-not-found')).toBeInTheDocument();
});
});
it('Renders proposal details if proposal is found', async () => {
const proposal = generateProposal({ id: 'foo' });
render(renderComponent(proposal as ProposalQuery['proposal'], 'foo'));
await waitFor(() => {
expect(screen.getByTestId('proposal')).toBeInTheDocument();
});
});
});
@@ -1,38 +1,260 @@
import { useParams } from 'react-router-dom';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { Proposal } from '../components/proposal';
import { ProposalNotFound } from '../components/proposal-not-found';
import { useProposalQuery } from '../__generated__/Proposals';
import { useFetchProposal } from '../components/proposal/proposal-utils';
import { useProposalQuery } from './__generated__/Proposal';
import { useFetch } from '@vegaprotocol/react-helpers';
import { ENV } from '../../../config';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { marketInfoProvider } from '@vegaprotocol/markets';
import { useAssetQuery } from '@vegaprotocol/assets';
import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { useParentMarketIdQuery } from '@vegaprotocol/markets';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { useSuccessorMarketProposalDetails } from '@vegaprotocol/proposals';
import { type Proposal as IProposal } from '../types';
export const ProposalContainer = () => {
const featureFlags = useFeatureFlags((state) => state.flags);
const [
mostRecentlyEnactedAssociatedMarketProposal,
setMostRecentlyEnactedAssociatedMarketProposal,
] = useState(undefined);
const params = useParams<{ proposalId: string }>();
const { data: restData, loading: restLoading } = useFetchProposal({
proposalId: params.proposalId,
});
const {
params: networkParams,
loading: networkParamsLoading,
error: networkParamsError,
} = useNetworkParams([
NetworkParams.governance_proposal_market_minVoterBalance,
NetworkParams.governance_proposal_updateMarket_minVoterBalance,
NetworkParams.governance_proposal_asset_minVoterBalance,
NetworkParams.governance_proposal_updateAsset_minVoterBalance,
NetworkParams.governance_proposal_updateNetParam_minVoterBalance,
NetworkParams.governance_proposal_freeform_minVoterBalance,
NetworkParams.governance_proposal_referralProgram_minVoterBalance,
NetworkParams.governance_proposal_VolumeDiscountProgram_minVoterBalance,
NetworkParams.spam_protection_voting_min_tokens,
NetworkParams.governance_proposal_market_requiredMajority,
NetworkParams.governance_proposal_updateMarket_requiredMajority,
NetworkParams.governance_proposal_updateMarket_requiredMajorityLP,
NetworkParams.governance_proposal_asset_requiredMajority,
NetworkParams.governance_proposal_updateAsset_requiredMajority,
NetworkParams.governance_proposal_updateNetParam_requiredMajority,
NetworkParams.governance_proposal_freeform_requiredMajority,
NetworkParams.governance_proposal_referralProgram_requiredMajority,
NetworkParams.governance_proposal_VolumeDiscountProgram_requiredMajority,
]);
const { data, loading, error } = useProposalQuery({
const {
state: { data: restData, loading: restLoading, error: restError },
} = useFetch(`${ENV.rest}governance?proposalId=${params.proposalId}`);
const { data, loading, error, refetch } = useProposalQuery({
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
variables: {
proposalId: params.proposalId || '',
includeNewMarketProductField: !!featureFlags.PRODUCT_PERPETUALS,
includeUpdateMarketState: !!featureFlags.UPDATE_MARKET_STATE,
includeUpdateReferralProgram: !!featureFlags.REFERRALS,
},
skip: !params.proposalId,
pollInterval: 2000,
});
const proposal = data?.proposal as IProposal;
const successor = useSuccessorMarketProposalDetails(params.proposalId);
const isSuccessor = !!successor?.parentMarketId || !!successor.code;
const {
state: {
data: originalMarketProposalRestData,
loading: originalMarketProposalRestLoading,
error: originalMarketProposalRestError,
},
} = useFetch(
`${ENV.rest}governance?proposalId=${
proposal?.terms.change.__typename === 'UpdateMarket' &&
proposal.terms.change.marketId
}`,
undefined,
true,
proposal?.terms.change.__typename !== 'UpdateMarket'
);
const {
state: {
data: previouslyEnactedMarketProposalsRestData,
loading: previouslyEnactedMarketProposalsRestLoading,
error: previouslyEnactedMarketProposalsRestError,
},
} = useFetch(
`${ENV.rest}governances?proposalState=STATE_ENACTED&proposalType=TYPE_UPDATE_MARKET`,
undefined,
true,
proposal?.terms.change.__typename !== 'UpdateMarket'
);
const {
data: marketData,
loading: marketLoading,
error: marketError,
} = useDataProvider({
dataProvider: marketInfoProvider,
skipUpdates: true,
variables: {
marketId: proposal?.id || '',
skip: !proposal?.id,
},
});
const {
data: parentMarketId,
loading: parentMarketIdLoading,
error: parentMarketIdError,
} = useParentMarketIdQuery({
variables: {
marketId: marketData?.id || '',
},
skip: !featureFlags.SUCCESSOR_MARKETS || !isSuccessor || !marketData?.id,
});
const {
data: parentMarketData,
loading: parentMarketLoading,
error: parentMarketError,
} = useDataProvider({
dataProvider: marketInfoProvider,
skipUpdates: true,
variables: {
marketId: parentMarketId?.market?.parentMarketID || '',
skip:
!featureFlags.SUCCESSOR_MARKETS ||
!isSuccessor ||
!parentMarketId?.market?.parentMarketID,
},
});
const {
data: assetData,
loading: assetLoading,
error: assetError,
} = useAssetQuery({
fetchPolicy: 'network-only',
variables: {
assetId:
(proposal?.terms.change.__typename === 'NewAsset' && proposal?.id) ||
(proposal?.terms.change.__typename === 'UpdateAsset' &&
proposal.terms.change.assetId) ||
'',
},
skip: !['NewAsset', 'UpdateAsset'].includes(
proposal?.terms?.change?.__typename || ''
),
});
useEffect(() => {
if (
previouslyEnactedMarketProposalsRestData &&
proposal?.terms.change.__typename === 'UpdateMarket'
) {
const change = proposal?.terms?.change as { marketId: string };
const filteredProposals =
// @ts-ignore rest data is not typed
previouslyEnactedMarketProposalsRestData.connection.edges.filter(
// @ts-ignore rest data is not typed
({ node }) =>
node?.proposal?.terms?.updateMarket?.marketId === change.marketId
);
const sortedProposals = filteredProposals.sort(
// @ts-ignore rest data is not typed
(a, b) =>
new Date(a?.node?.terms?.enactmentTimestamp).getTime() -
new Date(b?.node?.terms?.enactmentTimestamp).getTime()
);
setMostRecentlyEnactedAssociatedMarketProposal(
sortedProposals[sortedProposals.length - 1]
);
}
}, [
previouslyEnactedMarketProposalsRestData,
params.proposalId,
proposal?.terms.change.__typename,
proposal?.terms.change,
]);
useEffect(() => {
const interval = setInterval(refetch, 2000);
return () => clearInterval(interval);
}, [refetch]);
return (
<AsyncRenderer
loading={Boolean(loading || restLoading)}
error={error}
loading={
loading ||
marketLoading ||
assetLoading ||
networkParamsLoading ||
parentMarketIdLoading ||
parentMarketLoading ||
(restLoading ? (restLoading as boolean) : false) ||
(originalMarketProposalRestLoading
? (originalMarketProposalRestLoading as boolean)
: false) ||
(previouslyEnactedMarketProposalsRestLoading
? (previouslyEnactedMarketProposalsRestLoading as boolean)
: false)
}
error={
error ||
marketError ||
assetError ||
networkParamsError ||
parentMarketIdError ||
parentMarketError ||
restError ||
originalMarketProposalRestError ||
previouslyEnactedMarketProposalsRestError
}
data={{
...data,
...networkParams,
...(marketData ? { newMarketData: marketData } : {}),
...(parentMarketData ? { parentMarketData } : {}),
...(assetData ? { assetData } : {}),
...(restData ? { restData } : {}),
...(originalMarketProposalRestData
? { originalMarketProposalRestData }
: {}),
...(previouslyEnactedMarketProposalsRestData
? { previouslyEnactedMarketProposalsRestData }
: {}),
}}
>
{data?.proposal ? (
<Proposal proposal={data.proposal} restData={restData} />
<Proposal
proposal={proposal}
networkParams={networkParams}
restData={restData}
marketData={marketData}
parentMarketData={parentMarketData}
assetData={assetData}
originalMarketProposalRestData={originalMarketProposalRestData}
mostRecentlyEnactedAssociatedMarketProposal={
mostRecentlyEnactedAssociatedMarketProposal
}
/>
) : (
<ProposalNotFound />
)}
@@ -0,0 +1,186 @@
fragment NewMarketProductFields on Proposal {
terms {
change {
... on NewMarket {
instrument {
product {
__typename
}
}
}
}
}
}
fragment UpdateMarketStates on Proposal {
terms {
change {
... on UpdateMarketState {
updateType
market {
decimalPlaces
id
tradableInstrument {
instrument {
product {
__typename
... on Future {
quoteName
}
... on Perpetual {
quoteName
}
}
name
code
}
}
}
updateType
price
}
}
}
}
fragment UpdateReferralPrograms on Proposal {
terms {
change {
... on UpdateReferralProgram {
benefitTiers {
minimumEpochs
minimumRunningNotionalTakerVolume
referralDiscountFactor
referralRewardFactor
}
endOfProgram: endOfProgramTimestamp
windowLength
stakingTiers {
minimumStakedTokens
referralRewardMultiplier
}
}
}
}
}
fragment UpdateVolumeDiscountPrograms on Proposal {
terms {
change {
... on UpdateVolumeDiscountProgram {
benefitTiers {
minimumRunningNotionalTakerVolume
volumeDiscountFactor
}
endOfProgramTimestamp
windowLength
}
}
}
}
fragment ProposalFields on Proposal {
id
rationale {
title
description
}
reference
state
datetime
rejectionReason
party {
id
}
errorDetails
terms {
closingDatetime
enactmentDatetime
change {
... on NewMarket {
instrument {
name
code
product {
... on FutureProduct {
settlementAsset {
symbol
}
}
... on PerpetualProduct {
settlementAsset {
symbol
}
}
}
}
}
... on UpdateMarket {
marketId
}
... on NewAsset {
__typename
name
symbol
decimals
quantum
source {
... on BuiltinAsset {
maxFaucetAmountMint
}
... on ERC20 {
contractAddress
withdrawThreshold
lifetimeLimit
}
}
}
... on UpdateNetworkParameter {
networkParameter {
key
value
}
}
... on UpdateAsset {
quantum
assetId
source {
... on UpdateERC20 {
lifetimeLimit
withdrawThreshold
}
}
}
}
}
votes {
yes {
totalTokens
totalNumber
totalEquityLikeShareWeight
}
no {
totalTokens
totalNumber
totalEquityLikeShareWeight
}
}
}
query Proposals(
$includeNewMarketProductFields: Boolean!
$includeUpdateMarketStates: Boolean!
$includeUpdateReferralPrograms: Boolean!
) {
proposalsConnection {
edges {
node {
...ProposalFields
...NewMarketProductFields @include(if: $includeNewMarketProductFields)
...UpdateMarketStates @include(if: $includeUpdateMarketStates)
...UpdateReferralPrograms @include(if: $includeUpdateReferralPrograms)
...UpdateVolumeDiscountPrograms
}
}
}
}
@@ -0,0 +1,247 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type NewMarketProductFieldsFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', product?: { __typename: 'FutureProduct' } | { __typename: 'PerpetualProduct' } | { __typename: 'SpotProduct' } | null } } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } };
export type UpdateMarketStatesFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', decimalPlaces: number, id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename: 'Future', quoteName: string } | { __typename: 'Perpetual', quoteName: string } | { __typename: 'Spot' } } } } } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } };
export type UpdateReferralProgramsFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram', windowLength: number, endOfProgram: any, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } };
export type UpdateVolumeDiscountProgramsFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram', endOfProgramTimestamp: any, windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } } };
export type ProposalFieldsFragment = { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, product?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | { __typename?: 'PerpetualProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | { __typename?: 'SpotProduct' } | null } } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } };
export type ProposalsQueryVariables = Types.Exact<{
includeNewMarketProductFields: Types.Scalars['Boolean'];
includeUpdateMarketStates: Types.Scalars['Boolean'];
includeUpdateReferralPrograms: Types.Scalars['Boolean'];
}>;
export type ProposalsQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, product?: { __typename: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | { __typename: 'PerpetualProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | { __typename: 'SpotProduct' } | null } } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', decimalPlaces: number, id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename: 'Future', quoteName: string } | { __typename: 'Perpetual', quoteName: string } | { __typename: 'Spot' } } } } } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram', windowLength: number, endOfProgram: any, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram', endOfProgramTimestamp: any, windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } } } | null> | null } | null };
export const NewMarketProductFieldsFragmentDoc = gql`
fragment NewMarketProductFields on Proposal {
terms {
change {
... on NewMarket {
instrument {
product {
__typename
}
}
}
}
}
}
`;
export const UpdateMarketStatesFragmentDoc = gql`
fragment UpdateMarketStates on Proposal {
terms {
change {
... on UpdateMarketState {
updateType
market {
decimalPlaces
id
tradableInstrument {
instrument {
product {
__typename
... on Future {
quoteName
}
... on Perpetual {
quoteName
}
}
name
code
}
}
}
updateType
price
}
}
}
}
`;
export const UpdateReferralProgramsFragmentDoc = gql`
fragment UpdateReferralPrograms on Proposal {
terms {
change {
... on UpdateReferralProgram {
benefitTiers {
minimumEpochs
minimumRunningNotionalTakerVolume
referralDiscountFactor
referralRewardFactor
}
endOfProgram: endOfProgramTimestamp
windowLength
stakingTiers {
minimumStakedTokens
referralRewardMultiplier
}
}
}
}
}
`;
export const UpdateVolumeDiscountProgramsFragmentDoc = gql`
fragment UpdateVolumeDiscountPrograms on Proposal {
terms {
change {
... on UpdateVolumeDiscountProgram {
benefitTiers {
minimumRunningNotionalTakerVolume
volumeDiscountFactor
}
endOfProgramTimestamp
windowLength
}
}
}
}
`;
export const ProposalFieldsFragmentDoc = gql`
fragment ProposalFields on Proposal {
id
rationale {
title
description
}
reference
state
datetime
rejectionReason
party {
id
}
errorDetails
terms {
closingDatetime
enactmentDatetime
change {
... on NewMarket {
instrument {
name
code
product {
... on FutureProduct {
settlementAsset {
symbol
}
}
... on PerpetualProduct {
settlementAsset {
symbol
}
}
}
}
}
... on UpdateMarket {
marketId
}
... on NewAsset {
__typename
name
symbol
decimals
quantum
source {
... on BuiltinAsset {
maxFaucetAmountMint
}
... on ERC20 {
contractAddress
withdrawThreshold
lifetimeLimit
}
}
}
... on UpdateNetworkParameter {
networkParameter {
key
value
}
}
... on UpdateAsset {
quantum
assetId
source {
... on UpdateERC20 {
lifetimeLimit
withdrawThreshold
}
}
}
}
}
votes {
yes {
totalTokens
totalNumber
totalEquityLikeShareWeight
}
no {
totalTokens
totalNumber
totalEquityLikeShareWeight
}
}
}
`;
export const ProposalsDocument = gql`
query Proposals($includeNewMarketProductFields: Boolean!, $includeUpdateMarketStates: Boolean!, $includeUpdateReferralPrograms: Boolean!) {
proposalsConnection {
edges {
node {
...ProposalFields
...NewMarketProductFields @include(if: $includeNewMarketProductFields)
...UpdateMarketStates @include(if: $includeUpdateMarketStates)
...UpdateReferralPrograms @include(if: $includeUpdateReferralPrograms)
...UpdateVolumeDiscountPrograms
}
}
}
}
${ProposalFieldsFragmentDoc}
${NewMarketProductFieldsFragmentDoc}
${UpdateMarketStatesFragmentDoc}
${UpdateReferralProgramsFragmentDoc}
${UpdateVolumeDiscountProgramsFragmentDoc}`;
/**
* __useProposalsQuery__
*
* To run a query within a React component, call `useProposalsQuery` and pass it any options that fit your needs.
* When your component renders, `useProposalsQuery` 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 } = useProposalsQuery({
* variables: {
* includeNewMarketProductFields: // value for 'includeNewMarketProductFields'
* includeUpdateMarketStates: // value for 'includeUpdateMarketStates'
* includeUpdateReferralPrograms: // value for 'includeUpdateReferralPrograms'
* },
* });
*/
export function useProposalsQuery(baseOptions: Apollo.QueryHookOptions<ProposalsQuery, ProposalsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ProposalsQuery, ProposalsQueryVariables>(ProposalsDocument, options);
}
export function useProposalsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ProposalsQuery, ProposalsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ProposalsQuery, ProposalsQueryVariables>(ProposalsDocument, options);
}
export type ProposalsQueryHookResult = ReturnType<typeof useProposalsQuery>;
export type ProposalsLazyQueryHookResult = ReturnType<typeof useProposalsLazyQuery>;
export type ProposalsQueryResult = Apollo.QueryResult<ProposalsQuery, ProposalsQueryVariables>;
@@ -1,27 +1,31 @@
import flow from 'lodash/flow';
import compact from 'lodash/compact';
import { Callout, Intent, Splash } from '@vegaprotocol/ui-toolkit';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { SplashLoader } from '../../../components/splash-loader';
import { ProposalsList } from '../components/proposals-list';
import { getNodes } from '@vegaprotocol/utils';
import { getNodes, removePaginationWrapper } from '@vegaprotocol/utils';
import {
ProposalState,
ProtocolUpgradeProposalStatus,
} from '@vegaprotocol/types';
import { type NodeConnection, type NodeEdge } from '@vegaprotocol/utils';
import { useProposalsQuery } from '../__generated__/Proposals';
import {
useProposalsQuery,
type ProposalFieldsFragment,
} from './__generated__/Proposals';
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { useProtocolUpgradeProposalsQuery } from '@vegaprotocol/proposals';
import { type BatchProposal, type Proposal } from '../types';
import { useFeatureFlags } from '@vegaprotocol/environment';
export function getNotRejectedProposals(
data?: Array<Proposal | BatchProposal>
) {
if (!data) return [];
return data.filter((p) => p.state !== ProposalState.STATE_REJECTED);
export function getNotRejectedProposals(data?: ProposalFieldsFragment[]) {
return flow([
(data) =>
data.filter(
(p: ProposalFieldsFragment) => p?.state !== ProposalState.STATE_REJECTED
),
])(data);
}
export function getNotRejectedProtocolUpgradeProposals<
@@ -39,11 +43,17 @@ export function getNotRejectedProtocolUpgradeProposals<
}
export const ProposalsContainer = () => {
const featureFlags = useFeatureFlags((state) => state.flags);
const { t } = useTranslation();
const { data, loading, error } = useProposalsQuery({
pollInterval: 5000,
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
variables: {
includeNewMarketProductFields: !!featureFlags.PRODUCT_PERPETUALS,
includeUpdateMarketStates: !!featureFlags.UPDATE_MARKET_STATE,
includeUpdateReferralPrograms: !!featureFlags.REFERRALS,
},
});
const {
@@ -59,7 +69,7 @@ export const ProposalsContainer = () => {
const proposals = useMemo(
() =>
getNotRejectedProposals(
compact(data?.proposalsConnection?.edges?.map((e) => e?.proposalNode))
removePaginationWrapper(data?.proposalsConnection?.edges)
),
[data]
);

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