Compare commits

..
96 changed files with 3921 additions and 3698 deletions
@@ -2,7 +2,7 @@ export const proposalsData = {
proposalsConnection: {
edges: [
{
node: {
proposalNode: {
id: 'e8ba9d268e12514644fd1fc7ff289292f4ce6489cc32cc73133aea52c04aef89',
rationale: {
title: 'Add asset Wrapped Ether',
@@ -56,7 +56,7 @@ export const proposalsData = {
__typename: 'ProposalEdge',
},
{
node: {
proposalNode: {
id: 'd848fc7881f13d366df5f61ab139d5fcfa72bf838151bb51b54381870e357931',
rationale: {
title: 'Add asset Dai Stablecoin',
@@ -110,60 +110,7 @@ export const proposalsData = {
__typename: 'ProposalEdge',
},
{
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: {
proposalNode: {
id: 'bc70383f0e9515b15542cf4c63590cd2ca46b3363ba7c4a72af0e62112b3951b',
rationale: {
title: 'USDC-III',
@@ -217,60 +164,7 @@ export const proposalsData = {
__typename: 'ProposalEdge',
},
{
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: {
proposalNode: {
id: '9c48796e7988769ededc2b2b02220b00e93f65f23e8141bf1fd23a6983d95943',
rationale: {
title: 'Update governance.proposal.asset.requiredMajority',
@@ -236,7 +236,7 @@ context(
});
// 1002-STKE-041 1002-STKE-053
it(
it.skip(
'Able to remove part of a stake against a validator',
// @ts-ignore clash between jest and cypress
{ tags: '@smoke' },
@@ -7,7 +7,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
describe('Links and buttons', function () {
it('should have link for proposal page', function () {
it.skip('should have link for proposal page', function () {
cy.getByTestId('home-proposals').within(() => {
cy.get('[href="/proposals"]')
.should('exist')
@@ -27,7 +27,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
cy.getByTestId('app-announcement').should('not.exist');
});
it('should show open or enacted proposals without proposal summary', function () {
it.skip('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();
@@ -51,7 +51,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
});
it('should have external link for governance', function () {
it.skip('should have external link for governance', function () {
cy.getByTestId('home-proposals').within(() => {
cy.getByTestId('external-link')
.should('have.attr', 'href')
@@ -59,7 +59,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
});
it('should have link for validator page', function () {
it.skip('should have link for validator page', function () {
cy.getByTestId('home-validators').within(() => {
cy.get('[href="/validators"]')
.first()
@@ -68,7 +68,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
});
it('should have external link for validators', function () {
it.skip('should have external link for validators', function () {
cy.getByTestId('home-validators').within(() => {
cy.getByTestId('external-link')
.should('have.attr', 'href')
@@ -79,21 +79,21 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
});
it('should have information on active nodes', function () {
it.skip('should have information on active nodes', function () {
cy.getByTestId('node-information')
.first()
.should('contain.text', '2')
.and('contain.text', 'active nodes');
});
it('should have information on consensus nodes', function () {
it.skip('should have information on consensus nodes', function () {
cy.getByTestId('node-information')
.last()
.should('contain.text', '2')
.and('contain.text', 'consensus nodes');
});
it('should contain link to specific validators', function () {
it.skip('should contain link to specific validators', function () {
cy.getByTestId('validators')
.should('have.length', '2')
.each(($validator) => {
@@ -101,7 +101,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
});
it('should have link for rewards page', function () {
it.skip('should have link for rewards page', function () {
cy.getByTestId('home-rewards').within(() => {
cy.get('[href="/rewards"]')
.first()
@@ -110,7 +110,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
});
it('should have link for withdrawal page', function () {
it.skip('should have link for withdrawal page', function () {
cy.getByTestId('home-vega-token').within(() => {
cy.get('[href="/token/withdraw"]')
.first()
@@ -132,7 +132,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
});
// 0006-NETW-003 0006-NETW-008 0006-NETW-009 0006-NETW-010 0006-NETW-012 0006-NETW-013 0006-NETW-017 0006-NETW-018 0006-NETW-019 0006-NETW-020
it('should have option to switch to different network node', function () {
it.skip('should have option to switch to different network node', function () {
cy.getByTestId('git-network-data').within(() => {
cy.getByTestId('link').click();
});
@@ -189,7 +189,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
cy.viewport('iphone-xr');
});
it('should have burger button', () => {
it.skip('should have burger button', () => {
cy.getByTestId('button-menu-drawer').should('be.visible').click();
cy.getByTestId('menu-drawer').should('be.visible');
});
@@ -33,12 +33,12 @@ context(
verifyTabHighlighted(navigation.proposals);
});
it('should have GOVERNANCE header visible', function () {
it.skip('should have GOVERNANCE header visible', function () {
verifyPageHeader('Proposals');
});
// 3002-PROP-023 3004-PMAC-002 3005-PASN-002 3006-PASC-002 3007-PNEC-002 3008-PFRO-003
it('new proposal page should have button for link to more information on proposals', function () {
it.skip('new proposal page should have button for link to more information on proposals', function () {
cy.getByTestId('new-proposal-link').click();
cy.url().should('include', '/proposals/propose/raw');
cy.contains('To see Explorer data on proposals visit').within(() => {
@@ -73,7 +73,7 @@ context(
navigateTo(navigation.proposals);
});
it('should be able to see a working link for - find out more about Vega governance', function () {
it.skip('should be able to see a working link for - find out more about Vega governance', function () {
// 3001-VOTE-001 // 3002-PROP-001
cy.getByTestId(proposalDocumentationLink)
.should('be.visible')
@@ -46,11 +46,11 @@ context('Validators Page - verify elements on page', function () {
// @ts-ignore clash between jest and cypress
describe('with wallets disconnected', { tags: '@smoke' }, function () {
it('Should have validators tab highlighted', function () {
it.skip('Should have validators tab highlighted', function () {
verifyTabHighlighted(navigation.validators);
});
it('Should have validators ON VEGA header visible', function () {
it.skip('Should have validators ON VEGA header visible', function () {
verifyPageHeader('Validators');
});
@@ -192,7 +192,7 @@ context('Validators Page - verify elements on page', function () {
});
// 1002-STKE-006
it('Should be able to see validator name', function () {
it.skip('Should be able to see validator name', function () {
cy.getByTestId(validatorTitle).should('not.be.empty');
});
@@ -1,5 +1,5 @@
import classnames from 'classnames';
import { Icon } from '@vegaprotocol/ui-toolkit';
import { VegaIcon, VegaIconNames } 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">
<Icon name="chevron-down" size={8} />
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={20} />
</div>
</div>
</button>
+19 -24
View File
@@ -1,3 +1,4 @@
import compact from 'lodash/compact';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
@@ -12,10 +13,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, useFeatureFlags } from '@vegaprotocol/environment';
import { ExternalLinks } from '@vegaprotocol/environment';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import { useNodesQuery } from '../staking/home/__generated__/Nodes';
import { useProposalsQuery } from '../proposals/proposals/__generated__/Proposals';
import { useProposalsQuery } from '../proposals/__generated__/Proposals';
import {
getNotRejectedProposals,
getNotRejectedProtocolUpgradeProposals,
@@ -31,7 +32,7 @@ import {
orderByUpgradeBlockHeight,
} from '../proposals/components/proposals-list/proposals-list';
import { BigNumber } from '../../lib/bignumber';
import { type Proposal } from '../proposals/types';
import { type Proposal, type BatchProposal } from '../proposals/types';
const nodesToShow = 6;
@@ -39,7 +40,7 @@ const HomeProposals = ({
proposals,
protocolUpgradeProposals,
}: {
proposals: Proposal[];
proposals: Array<Proposal | BatchProposal>;
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
}) => {
const { t } = useTranslation();
@@ -60,12 +61,9 @@ const HomeProposals = ({
<ProtocolUpgradeProposalsListItem key={index} proposal={proposal} />
))}
{proposals.map(
(proposal) =>
proposal?.id && (
<ProposalsListItem key={proposal.id} proposal={proposal} />
)
)}
{compact(proposals).map((proposal) => {
return <ProposalsListItem key={proposal.id} proposal={proposal} />;
})}
</ul>
<div className="mt-6">
@@ -175,7 +173,6 @@ export const ValidatorDetailsLink = ({
};
const GovernanceHome = ({ name }: RouteChildProps) => {
const featureFlags = useFeatureFlags((state) => state.flags);
useDocumentTitle(name);
const { t } = useTranslation();
const {
@@ -186,11 +183,6 @@ 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 {
@@ -212,15 +204,18 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
useRefreshAfterEpoch(validatorsData?.epoch.timestamps.expiry, refetch);
const proposals = useMemo(
() =>
proposalsData
? getNotRejectedProposals(
removePaginationWrapper(proposalsData.proposalsConnection?.edges)
const proposals = useMemo(() => {
if (!proposalsData?.proposalsConnection?.edges?.length) return [];
return proposalsData
? getNotRejectedProposals(
compact(
proposalsData.proposalsConnection.edges.map(
(edge) => edge?.proposalNode
)
)
: [],
[proposalsData]
);
)
: [];
}, [proposalsData]);
const sortedProposals = useMemo(
() => orderByDate(proposals).reverse(),
@@ -0,0 +1,493 @@
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,9 +3,13 @@ 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 } from '../../types';
import { type Proposal, type BatchProposal } from '../../types';
export const CurrentProposalState = ({ proposal }: { proposal: Proposal }) => {
export const CurrentProposalState = ({
proposal,
}: {
proposal: Proposal | BatchProposal;
}) => {
const { t } = useTranslation();
let proposalStatus: ReactNode;
let variant = 'tertiary' as ProposalInfoLabelVariant;
@@ -79,13 +79,22 @@ 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,19 +2,53 @@ import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SubHeading } from '../../../../components/heading';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import { AssetDetail, AssetDetailsTable } from '@vegaprotocol/assets';
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import {
AssetDetail,
AssetDetailsTable,
useAssetQuery,
} from '@vegaprotocol/assets';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import {
type INewAssetFieldsFragment,
type IUpdateAssetFieldsFragment,
} from '../../__generated__/Proposals';
export const ProposalAssetDetails = ({
asset,
originalAsset,
change,
assetId,
}: {
asset: AssetFieldsFragment;
originalAsset?: AssetFieldsFragment;
change: IUpdateAssetFieldsFragment | INewAssetFieldsFragment;
assetId: string;
}) => {
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,16 +6,46 @@ import {
KeyValueTableRow,
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import { type Proposal } from '../../types';
import { type Proposal, type BatchProposal } from '../../types';
interface ProposalChangeTableProps {
proposal: Proposal;
proposal: Proposal | BatchProposal;
}
export const ProposalChangeTable = ({ proposal }: ProposalChangeTableProps) => {
const { t } = useTranslation();
const terms = proposal?.terms;
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;
return (
<RoundedWrapper paddingBottom={true}>
@@ -24,22 +54,8 @@ export const ProposalChangeTable = ({ proposal }: ProposalChangeTableProps) => {
{t('id')}
{proposal?.id}
</KeyValueTableRow>
<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}
{closingTimeRow}
{enactmentRow}
<KeyValueTableRow>
{t('proposedBy')}
<span style={{ wordBreak: 'break-word' }}>{proposal?.party.id}</span>
@@ -18,20 +18,21 @@ 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 { useNewTransferProposalDetails } from '@vegaprotocol/proposals';
import {
InstrumentDetailsDocument,
useNewTransferProposalDetails,
type InstrumentDetailsQuery,
type InstrumentDetailsQueryVariables,
} 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(),
}));
@@ -44,7 +45,7 @@ const renderComponent = (
render(
<AppStateProvider>
<BrowserRouter>
<MockedProvider mocks={mocks}>
<MockedProvider mocks={[networkParamsQueryMock, ...mocks]}>
<VegaWalletContext.Provider value={mockWalletContext}>
<ProposalHeader
proposal={proposal}
@@ -61,10 +62,39 @@ describe('Proposal header', () => {
afterAll(() => {
jest.clearAllMocks();
});
it('Renders New market proposal', () => {
useFeatureFlags.setState({ flags: { SUCCESSOR_MARKETS: true } });
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,
},
},
},
},
},
};
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
rationale: {
title: 'New some market',
@@ -73,6 +103,9 @@ describe('Proposal header', () => {
terms: {
change: {
__typename: 'NewMarket',
successorConfiguration: {
parentMarketId,
},
instrument: {
__typename: 'InstrumentConfiguration',
name: 'Some market',
@@ -87,7 +120,9 @@ describe('Proposal header', () => {
},
},
},
})
}),
undefined,
[mock]
);
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
'New some market'
@@ -96,14 +131,13 @@ describe('Proposal header', () => {
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
'tGBP settled future.'
);
expect(screen.getByTestId('proposal-successor-info')).toHaveTextContent(
'PARENT_CODE'
);
expect(
await screen.findByTestId('proposal-successor-info')
).toHaveTextContent(parentCode);
});
it('Renders Update market proposal', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
rationale: {
title: 'New market id',
@@ -132,7 +166,6 @@ describe('Proposal header', () => {
it('Renders New asset proposal - ERC20', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
rationale: {
title: 'New asset: Fake currency',
@@ -162,8 +195,10 @@ 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',
@@ -177,9 +212,7 @@ describe('Proposal header', () => {
},
})
);
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
'New asset proposal'
);
expect(screen.getByTestId('proposal-title')).toHaveTextContent('New asset');
expect(screen.getByTestId('proposal-type')).toHaveTextContent('New asset');
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
'Symbol: BIA. Max faucet amount mint: 300'
@@ -188,7 +221,6 @@ describe('Proposal header', () => {
it('Renders Update network', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
rationale: {
title: 'Network parameter',
@@ -218,7 +250,6 @@ describe('Proposal header', () => {
it('Renders Freeform proposal - short rationale', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
id: 'short',
rationale: {
@@ -240,7 +271,6 @@ 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: {
@@ -266,7 +296,6 @@ 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: {
@@ -323,7 +352,6 @@ describe('Proposal header', () => {
it('Renders proposal state: Enacted', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
state: ProposalState.STATE_ENACTED,
terms: {
@@ -336,7 +364,6 @@ describe('Proposal header', () => {
it('Renders proposal state: Passed', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
@@ -350,7 +377,6 @@ 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: {
@@ -365,7 +391,6 @@ describe('Proposal header', () => {
it('Renders proposal state: Open', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
state: ProposalState.STATE_OPEN,
votes: {
@@ -434,8 +459,6 @@ 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 { useTranslation } from 'react-i18next';
import { Trans, useTranslation } from 'react-i18next';
import {
CopyWithTooltip,
Lozenge,
@@ -8,14 +8,13 @@ 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,
@@ -27,217 +26,510 @@ 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 } from '@vegaprotocol/types';
import { type Proposal } from '../../types';
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';
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" className="flex gap-1">
{proposal.subProposals?.map((subProposal, i) => {
if (!subProposal?.terms) return null;
return <ProposalTypeTag key={i} terms={subProposal.terms} />;
})}
</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': {
return (
<span>
{featureFlags.UPDATE_MARKET_STATE &&
terms.change?.market?.id &&
terms.change.updateType ? (
<>
{t(terms.change.updateType)}:{' '}
{truncateMiddle(terms.change.market.id)}
</>
) : null}
</span>
);
}
case 'UpdateMarket': {
return (
<>
<span>{t('UpdateToMarket')}:</span>{' '}
<span className="inline-flex items-start gap-2">
<span className="break-all">{terms.change.marketId} </span>
<span className="inline-flex items-end gap-0">
<CopyWithTooltip
text={terms.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',
// @ts-ignore ts doesn't like this field even though its already a string above???
terms.change.marketId
)
);
window.open(marketPageLink, '_blank');
}}
>
<VegaIcon size={20} name={VegaIconNames.OPEN_EXTERNAL} />
</button>
</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}>
<div>{renderDetails(p.terms)}</div>
<SubProposalStateText
state={proposal.state}
enactmentDatetime={p.terms.enactmentDatetime}
/>
</li>
);
})}
</ul>
<BatchProposalStateText
state={proposal.state}
closingDatetime={proposal.batchTerms?.closingDatetime}
rejectionReason={proposal.rejectionReason}
/>
</div>
);
}
return (
<div
data-testid="proposal-details"
className="break-words mb-6 text-vega-light-200"
>
{details}
</div>
);
};
const VoteStateText = ({
state,
closingDatetime,
enactmentDatetime,
rejectionReason,
}: {
state: ProposalState;
closingDatetime: string;
enactmentDatetime: string;
rejectionReason: ProposalRejectionReason | null | undefined;
}) => {
const { t } = useTranslation();
const nowToCloseInHours = differenceInHours(
new Date(closingDatetime),
new Date()
);
const props = {
'data-testid': 'vote-details',
};
switch (state) {
case ProposalState.STATE_ENACTED: {
return (
<p {...props}>
{t('enactedOn{{date}}', {
enactmentDate:
enactmentDatetime &&
format(new Date(enactmentDatetime), DATE_FORMAT_DETAILED),
})}
</p>
);
}
case ProposalState.STATE_PASSED:
case ProposalState.STATE_WAITING_FOR_NODE_VOTE: {
return (
<p {...props}>
{t('enactsOn{{date}}', {
enactmentDate:
enactmentDatetime &&
format(new Date(enactmentDatetime), DATE_FORMAT_DETAILED),
})}
</p>
);
}
case ProposalState.STATE_OPEN: {
return (
<p {...props}>
<span className={nowToCloseInHours < 6 ? 'text-vega-orange' : ''}>
{t('{{time}} left to vote', {
time: formatDistanceToNowStrict(new Date(closingDatetime)),
})}
</span>
</p>
);
}
case ProposalState.STATE_DECLINED: {
return <p {...props}>{t(state)}</p>;
}
case ProposalState.STATE_REJECTED: {
const props = { 'data-testid': 'vote-status' };
if (rejectionReason) {
return (
<p {...props}>{t(ProposalRejectionReasonMapping[rejectionReason])}</p>
);
}
return <p {...props}>{t('Proposal rejected')}</p>;
}
default: {
return null;
}
}
};
/**
* Renders state details relevant to the sub proposal, namely the enactment
* date and time
*/
const SubProposalStateText = ({
state,
enactmentDatetime,
}: {
state: ProposalState;
enactmentDatetime: string;
}) => {
const { t } = useTranslation();
const props = {
'data-testid': 'vote-details',
className: 'm-0',
};
switch (state) {
case ProposalState.STATE_ENACTED: {
return (
<p {...props}>
{t('enactedOn{{date}}', {
enactmentDate:
enactmentDatetime &&
format(new Date(enactmentDatetime), DATE_FORMAT_DETAILED),
})}
</p>
);
}
case ProposalState.STATE_OPEN:
case ProposalState.STATE_PASSED:
case ProposalState.STATE_WAITING_FOR_NODE_VOTE: {
return (
<p {...props}>
{t('enactsOn{{date}}', {
enactmentDate:
enactmentDatetime &&
format(new Date(enactmentDatetime), DATE_FORMAT_DETAILED),
})}
</p>
);
}
case ProposalState.STATE_REJECTED:
case ProposalState.STATE_DECLINED: {
// If voting is still open we render a single clost time for all sub proposals
return null;
}
default: {
return null;
}
}
};
/**
* Renders state details relevant for the entire batch. IE. if the proposal was
* rejected or declined, or the vote close time. Does not render enactment times as
* those are relevant to the sub proposal
*/
const BatchProposalStateText = ({
state,
closingDatetime,
rejectionReason,
}: {
state: ProposalState;
closingDatetime: string;
rejectionReason: ProposalRejectionReason | null | undefined;
}) => {
const { t } = useTranslation();
const nowToCloseInHours = differenceInHours(
new Date(closingDatetime),
new Date()
);
const props = {
'data-testid': 'vote-details',
};
switch (state) {
case ProposalState.STATE_ENACTED:
case ProposalState.STATE_PASSED:
case ProposalState.STATE_WAITING_FOR_NODE_VOTE: {
return null;
}
case ProposalState.STATE_OPEN: {
return (
<p {...props}>
<span className={nowToCloseInHours < 6 ? 'text-vega-orange' : ''}>
{t('{{time}} left to vote', {
time: formatDistanceToNowStrict(new Date(closingDatetime)),
})}
</span>
</p>
);
}
case ProposalState.STATE_DECLINED: {
return <p {...props}>{t(state)}</p>;
}
case ProposalState.STATE_REJECTED: {
const props = { 'data-testid': 'vote-status' };
if (rejectionReason) {
return (
<p {...props}>{t(ProposalRejectionReasonMapping[rejectionReason])}</p>
);
}
return <p {...props}>{t('Proposal rejected')}</p>;
}
default: {
return null;
}
}
};
export const ProposalHeader = ({
proposal,
isListItem = true,
voteState,
}: {
proposal: Proposal;
proposal: Proposal | BatchProposal;
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">
<div data-testid="proposal-type">
<ProposalInfoLabel variant="secondary">
{t(`${proposalType}`)}
</ProposalInfoLabel>
</div>
<ProposalTypeTags proposal={proposal} />
<div className="flex items-center gap-6">
{(voteState === 'Yes' || voteState === 'No') && (
@@ -264,50 +556,43 @@ export const ProposalHeader = ({
<div data-testid="proposal-title" className="break-all">
{isListItem ? (
<header>
<SubHeading
title={titleContent || fallbackTitle || t('Unknown proposal')}
/>
<SubHeading title={titleContent || fallbackTitle} />
</header>
) : (
<Heading
title={titleContent || fallbackTitle || t('Unknown proposal')}
/>
<Heading title={titleContent || fallbackTitle} />
)}
</div>
{details && (
<div
data-testid="proposal-details"
className="break-words mb-6 text-vega-light-200"
>
{details}
</div>
)}
<ProposalDetails proposal={proposal} />
<VoteBreakdown proposal={proposal} />
</>
);
};
export const SuccessorCode = ({
proposalId,
export const ParentMarketCode = ({
parentMarketId,
}: {
proposalId?: string | null;
parentMarketId: string;
}) => {
const { t } = useTranslation();
const successor = useSuccessorMarketProposalDetails(proposalId);
const { data } = useInstrumentDetailsQuery({
variables: {
marketId: parentMarketId,
},
});
return successor.parentMarketId || successor.code ? (
if (!data?.market?.tradableInstrument.instrument.code) return null;
return (
<span className="block" data-testid="proposal-successor-info">
{t('Successor market to')}:{' '}
<Link
to={`${Routes.PROPOSALS}/${successor.parentMarketId}`}
to={`${Routes.PROPOSALS}/${parentMarketId}`}
className="hover:underline"
>
{successor.code || successor.parentMarketId}
{data.market.tradableInstrument.instrument.code}
</Link>
</span>
) : null;
);
};
export const NewTransferSummary = ({
@@ -3,13 +3,15 @@ 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';
import {
type BatchProposalFieldsFragment,
type ProposalFieldsFragment,
} from '../../__generated__/Proposals';
export const ProposalJson = ({
proposal,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
proposal: ProposalFieldsFragment | BatchProposalFieldsFragment;
}) => {
const { t } = useTranslation();
const [showDetails, setShowDetails] = useState(false);
@@ -57,33 +57,21 @@ describe('applyImmutableKeysFromEarlierVersion', () => {
describe('ProposalMarketChanges', () => {
it('renders correctly', () => {
const { getByTestId } = render(
<ProposalMarketChanges
originalProposal={{}}
latestEnactedProposal={{}}
updatedProposal={{}}
/>
<ProposalMarketChanges marketId="market-id" updatedProposal={{}} />
);
expect(getByTestId('proposal-market-changes')).toBeInTheDocument();
});
it('JsonDiff is not visible when showChanges is false', () => {
const { queryByTestId } = render(
<ProposalMarketChanges
originalProposal={{}}
latestEnactedProposal={{}}
updatedProposal={{}}
/>
<ProposalMarketChanges marketId="market-id" updatedProposal={{}} />
);
expect(queryByTestId('json-diff')).not.toBeInTheDocument();
});
it('JsonDiff is visible when showChanges is true', async () => {
const { getByTestId } = render(
<ProposalMarketChanges
originalProposal={{}}
latestEnactedProposal={{}}
updatedProposal={{}}
/>
<ProposalMarketChanges marketId="market-id" updatedProposal={{}} />
);
fireEvent.click(getByTestId('proposal-market-changes-toggle'));
expect(getByTestId('json-diff')).toBeInTheDocument();
@@ -7,6 +7,8 @@ import { useState } from 'react';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import { SubHeading } from '../../../../components/heading';
import type { JsonValue } from '../../../../components/json-diff';
import { useFetch } from '@vegaprotocol/react-helpers';
import { ENV } from '../../../../config';
const immutableKeys = [
'decimalPlaces',
@@ -40,19 +42,51 @@ export const applyImmutableKeysFromEarlierVersion = (
};
interface ProposalMarketChangesProps {
originalProposal: JsonValue;
latestEnactedProposal: JsonValue | undefined;
marketId: string;
updatedProposal: JsonValue;
}
export const ProposalMarketChanges = ({
originalProposal,
latestEnactedProposal,
marketId,
updatedProposal,
}: ProposalMarketChangesProps) => {
const { t } = useTranslation();
const [showChanges, setShowChanges] = useState(false);
const {
state: { data },
} = useFetch(`${ENV.rest}governance?proposalId=${marketId}`, undefined, true);
const {
state: { data: enactedProposalData },
} = useFetch(
`${ENV.rest}governances?proposalState=STATE_ENACTED&proposalType=TYPE_UPDATE_MARKET`,
undefined,
true
);
// @ts-ignore no types here :-/
const enacted = enactedProposalData?.connection?.edges
.filter(
// @ts-ignore no type here
({ node }) => node?.proposal?.terms?.updateMarket?.marketId === marketId
)
// @ts-ignore no type here
.sort((a, b) => {
return (
new Date(a?.node?.terms?.enactmentTimestamp).getTime() -
new Date(b?.node?.terms?.enactmentTimestamp).getTime()
);
});
const latestEnactedProposal = enacted?.length
? enacted[enacted.length - 1]
: undefined;
const originalProposal =
// @ts-ignore no types with useFetch TODO: check this is good
data?.data?.proposal?.terms?.newMarket?.changes;
return (
<section data-testid="proposal-market-changes">
<CollapsibleToggle
@@ -18,6 +18,7 @@ import {
getDataSourceSpecForTradingTermination,
getSigners,
MarginScalingFactorsPanel,
marketInfoProvider,
} from '@vegaprotocol/markets';
import {
Button,
@@ -28,8 +29,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;
@@ -48,18 +49,29 @@ 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 = ({
marketData,
parentMarketData,
}: {
marketData: MarketInfo;
parentMarketData?: MarketInfo;
}) => {
export const ProposalMarketData = ({ proposalId }: { proposalId: string }) => {
const { t } = useTranslation();
const { isOpen, open, close } = useMarketDataDialogStore();
const [showDetails, setShowDetails] = useState(false);
if (!marketData) {
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) {
return null;
}
@@ -7,7 +7,6 @@ import {
formatReferralRewardMultiplier,
ProposalReferralProgramDetails,
} from './proposal-referral-program-details';
import { generateProposal } from '../../test-helpers/generate-proposals';
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
useAppState: () => ({
@@ -59,87 +58,65 @@ describe('ProposalReferralProgramDetails helper functions', () => {
});
});
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',
},
],
const mockChange = {
__typename: 'UpdateReferralProgram' as const,
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',
},
],
};
describe('<ProposalReferralProgramDetails />', () => {
it('should not render if proposal is 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} />);
render(<ProposalReferralProgramDetails change={null} />);
expect(
screen.queryByTestId('proposal-referral-program-details')
).toBeNull();
});
it('should not render if there are no relevant fields', () => {
const incompleteProposal = generateProposal({
terms: {
change: {},
},
});
render(<ProposalReferralProgramDetails proposal={incompleteProposal} />);
const emptyChange = {};
// @ts-ignore change deliberately empty
render(<ProposalReferralProgramDetails change={emptyChange} />);
expect(
screen.queryByTestId('proposal-referral-program-details')
).toBeNull();
});
it('should render relevant fields if present', () => {
render(<ProposalReferralProgramDetails proposal={mockReferralProposal} />);
render(<ProposalReferralProgramDetails change={mockChange} />);
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 Proposal } from '../../types';
import { type UpdateReferralProgramsFragment } from '../../__generated__/Proposals';
interface ProposalReferralProgramDetailsProps {
proposal: Proposal | null;
change: UpdateReferralProgramsFragment | null;
}
export const formatEndOfProgramTimestamp = (value: string) => {
@@ -44,20 +44,19 @@ export const formatReferralRewardMultiplier = (value: string) => {
};
export const ProposalReferralProgramDetails = ({
proposal,
change,
}: ProposalReferralProgramDetailsProps) => {
const {
appState: { decimals },
} = useAppState();
const { t } = useTranslation();
if (proposal?.terms?.change?.__typename !== 'UpdateReferralProgram') {
return null;
}
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 (change?.__typename !== 'UpdateReferralProgram') return null;
const benefitTiers = change?.benefitTiers.slice();
const stakingTiers = change?.stakingTiers.slice();
const windowLength = change?.windowLength;
const endOfProgramTimestamp = change?.endOfProgram;
if (
!benefitTiers &&
@@ -6,15 +6,14 @@ import {
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { type Proposal } from '../../types';
export const ProposalCancelTransferDetails = ({
proposal,
proposalId,
}: {
proposal: Proposal;
proposalId: string;
}) => {
const { t } = useTranslation();
const details = useCancelTransferProposalDetails(proposal?.id);
const details = useCancelTransferProposalDetails(proposalId);
if (!details) {
return null;
@@ -19,17 +19,16 @@ import {
addDecimalsFormatNumberQuantum,
formatDateWithLocalTimezone,
} from '@vegaprotocol/utils';
import { type Proposal } from '../../types';
export const ProposalTransferDetails = ({
proposal,
proposalId,
}: {
proposal: Proposal;
proposalId: string;
}) => {
const { t } = useTranslation();
const [show, setShow] = useState(false);
const details = useNewTransferProposalDetails(proposal?.id);
const details = useNewTransferProposalDetails(proposalId);
if (!details) {
return null;
}
@@ -1,6 +1,5 @@
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: () => ({
@@ -10,109 +9,71 @@ jest.mock('../../../../contexts/app-state/app-state-context', () => ({
}),
}));
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 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 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',
},
],
}),
},
},
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',
},
],
}),
},
});
};
describe('ProposalUpdateBenefitTiers', () => {
it('should not render if proposal is 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} />);
render(<ProposalUpdateBenefitTiers change={null} />);
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
});
it('should not render if there are relevant fields that are empty', () => {
const incompleteProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateNetworkParameter',
networkParameter: {
key: 'blah.blah.benefitTiers',
value: JSON.stringify({}),
},
},
const incompleteProposal = {
__typename: 'UpdateNetworkParameter' as const,
networkParameter: {
key: 'blah.blah.benefitTiers',
value: JSON.stringify({}),
},
});
};
render(<ProposalUpdateBenefitTiers proposal={incompleteProposal} />);
render(<ProposalUpdateBenefitTiers change={incompleteProposal} />);
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
});
it('should render a valid vesting benefit tier proposal', () => {
render(
<ProposalUpdateBenefitTiers proposal={mockVestingBenefitTierProposal} />
);
render(<ProposalUpdateBenefitTiers change={mockChange1} />);
// 3 tiers in the sample data
expect(screen.getByText('Tier 1')).toBeInTheDocument();
@@ -133,11 +94,7 @@ describe('ProposalUpdateBenefitTiers', () => {
});
it('should render a valid activity streak benefit tier proposal', () => {
render(
<ProposalUpdateBenefitTiers
proposal={mockActivityStreakBenefitTierProposal}
/>
);
render(<ProposalUpdateBenefitTiers change={mockChange2} />);
// 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 Proposal } from '../../types';
import { type IUpdateNetworkParameterFieldsFragment } from '../../__generated__/Proposals';
// 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 {
proposal: Proposal | null;
change: IUpdateNetworkParameterFieldsFragment | null;
}
/**
@@ -55,17 +55,17 @@ interface ProposalReferralProgramDetailsProps {
* It only renders known fields so that they can be formatted correctly.
*/
export const ProposalUpdateBenefitTiers = ({
proposal,
change,
}: ProposalReferralProgramDetailsProps) => {
const { t } = useTranslation();
if (
proposal?.terms?.change?.__typename !== 'UpdateNetworkParameter' ||
proposal?.terms?.change?.networkParameter.key.slice(-13) !== '.benefitTiers'
change?.__typename !== 'UpdateNetworkParameter' ||
change?.networkParameter.key.slice(-13) !== '.benefitTiers'
) {
return null;
}
const benefitTiersString = proposal?.terms?.change?.networkParameter.value;
const benefitTiersString = change?.networkParameter.value;
const benefitTiers = getBenefitTiers(benefitTiersString);
if (!benefitTiers) {
@@ -1,86 +1,73 @@
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';
describe('<ProposalUpdateMarketState />', () => {
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',
},
},
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',
},
},
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_SUSPEND,
},
},
});
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_SUSPEND,
};
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',
},
},
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',
},
},
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_RESUME,
},
},
});
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_RESUME,
};
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',
},
},
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',
},
},
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 proposal={null} />);
render(<ProposalUpdateMarketState change={null} />);
expect(screen.queryByTestId('proposal-update-market-state')).toBeNull();
});
it('should toggle details when CollapsibleToggle is clicked', () => {
render(<ProposalUpdateMarketState proposal={suspendProposal} />);
render(<ProposalUpdateMarketState change={suspendProposal} />);
expect(
screen.queryByTestId('proposal-update-market-state-table')
@@ -94,7 +81,7 @@ describe('<ProposalUpdateMarketState />', () => {
});
it('should display suspend market information when showDetails is true', () => {
render(<ProposalUpdateMarketState proposal={suspendProposal} />);
render(<ProposalUpdateMarketState change={suspendProposal} />);
fireEvent.click(screen.getByTestId('proposal-market-data-toggle'));
@@ -103,7 +90,7 @@ describe('<ProposalUpdateMarketState />', () => {
});
it('should display resume market information when showDetails is true', () => {
render(<ProposalUpdateMarketState proposal={resumeProposal} />);
render(<ProposalUpdateMarketState change={resumeProposal} />);
fireEvent.click(screen.getByTestId('proposal-market-data-toggle'));
@@ -112,7 +99,7 @@ describe('<ProposalUpdateMarketState />', () => {
});
it('should display terminate market information when showDetails is true', () => {
render(<ProposalUpdateMarketState proposal={terminateProposal} />);
render(<ProposalUpdateMarketState change={terminateProposal} />);
fireEvent.click(screen.getByTestId('proposal-market-data-toggle'));
@@ -8,29 +8,27 @@ import { Row } from '@vegaprotocol/markets';
import { useState } from 'react';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import { SubHeading } from '../../../../components/heading';
import { type Proposal } from '../../types';
import { type UpdateMarketStatesFragment } from '../../__generated__/Proposals';
interface ProposalUpdateMarketStateProps {
proposal: Proposal | null;
change: UpdateMarketStatesFragment | null;
}
export const ProposalUpdateMarketState = ({
proposal,
change,
}: ProposalUpdateMarketStateProps) => {
const { t } = useTranslation();
const [showDetails, setShowDetails] = useState(false);
let market;
let isTerminate = false;
if (!proposal) {
if (!change) {
return null;
}
if (proposal?.terms.change.__typename === 'UpdateMarketState') {
market = proposal?.terms?.change?.market;
isTerminate =
proposal?.terms?.change?.updateType ===
'MARKET_STATE_UPDATE_TYPE_TERMINATE';
if (change.__typename === 'UpdateMarketState') {
market = change?.market;
isTerminate = change?.updateType === 'MARKET_STATE_UPDATE_TYPE_TERMINATE';
}
return (
@@ -45,7 +43,7 @@ export const ProposalUpdateMarketState = ({
{showDetails && (
<RoundedWrapper paddingBottom={true} marginBottomLarge={true}>
{proposal?.terms.change.__typename === 'UpdateMarketState' && (
{change.__typename === 'UpdateMarketState' && (
<KeyValueTable data-testid="proposal-update-market-state-table">
<KeyValueTableRow>
{t('marketId')}
@@ -59,10 +57,10 @@ export const ProposalUpdateMarketState = ({
{t('marketCode')}
{market?.tradableInstrument?.instrument?.code}
</KeyValueTableRow>
{isTerminate && (
{isTerminate && market && (
<Row
field="termination-price"
value={proposal?.terms?.change?.price}
value={change?.price}
assetSymbol={
market?.tradableInstrument?.instrument?.product
?.__typename === 'Future' ||
@@ -1,6 +1,5 @@
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: () => ({
@@ -10,95 +9,56 @@ jest.mock('../../../../contexts/app-state/app-state-context', () => ({
}),
}));
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,
const mockChange = {
__typename: 'UpdateVolumeDiscountProgram' as const,
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,
};
describe('ProposalVolumeDiscountProgramDetails', () => {
it('should not render if proposal is 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} />
);
render(<ProposalVolumeDiscountProgramDetails change={null} />);
expect(
screen.queryByTestId('proposal-volume-discount-program-details')
).toBeNull();
});
it('should render relevant fields if present', () => {
render(
<ProposalVolumeDiscountProgramDetails proposal={mockReferralProposal} />
);
render(<ProposalVolumeDiscountProgramDetails change={mockChange} />);
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 Proposal } from '../../types';
import { type UpdateVolumeDiscountProgramsFragment } from '../../__generated__/Proposals';
interface ProposalReferralProgramDetailsProps {
proposal: Proposal | null;
change: UpdateVolumeDiscountProgramsFragment | null;
}
export const formatVolumeDiscountFactor = (value: string) => {
@@ -22,16 +22,16 @@ export const formatVolumeDiscountFactor = (value: string) => {
};
export const ProposalVolumeDiscountProgramDetails = ({
proposal,
change,
}: ProposalReferralProgramDetailsProps) => {
const { t } = useTranslation();
if (proposal?.terms?.change?.__typename !== 'UpdateVolumeDiscountProgram') {
if (change?.__typename !== 'UpdateVolumeDiscountProgram') {
return null;
}
const benefitTiers = proposal?.terms?.change?.benefitTiers;
const windowLength = proposal?.terms?.change?.windowLength;
const endOfProgramTimestamp = proposal?.terms?.change?.endOfProgramTimestamp;
const benefitTiers = change.benefitTiers;
const windowLength = change?.windowLength;
const endOfProgramTimestamp = change?.endOfProgramTimestamp;
if (!benefitTiers && !windowLength && !endOfProgramTimestamp) {
return null;
@@ -0,0 +1,117 @@
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';
export const ProposalChangeDetails = ({
proposal,
terms,
restData,
}: {
proposal: Proposal | BatchProposal;
terms: ProposalTermsFieldsFragment;
// eslint-disable-next-line
restData: any;
}) => {
switch (terms.change.__typename) {
case 'NewAsset': {
if (proposal.id && terms.change.source.__typename === 'ERC20') {
return (
<div>
<ListAsset
assetId={proposal.id}
withdrawalThreshold={terms.change.source.withdrawThreshold}
lifetimeLimit={terms.change.source.lifetimeLimit}
/>
<ProposalAssetDetails change={terms.change} assetId={proposal.id} />
</div>
);
}
return null;
}
case 'UpdateAsset': {
if (proposal.id) {
return (
<ProposalAssetDetails
change={terms.change}
assetId={terms.change.assetId}
/>
);
}
return null;
}
case 'NewMarket': {
if (proposal.id) {
return <ProposalMarketData proposalId={proposal.id} />;
}
return null;
}
case 'UpdateMarket': {
if (proposal.id) {
return (
<div className="flex flex-col gap-4">
<ProposalMarketData proposalId={proposal.id} />
<ProposalMarketChanges
marketId={terms.change.marketId}
updatedProposal={
restData?.data?.proposal?.terms?.updateMarket?.changes
}
/>
</div>
);
}
return null;
}
case 'NewTransfer': {
if (proposal.id) {
return <ProposalTransferDetails proposalId={proposal.id} />;
}
return null;
}
case 'CancelTransfer': {
if (proposal.id) {
return <ProposalCancelTransferDetails proposalId={proposal.id} />;
}
return null;
}
case 'UpdateMarketState': {
return <ProposalUpdateMarketState change={terms.change} />;
}
case 'UpdateReferralProgram': {
return <ProposalReferralProgramDetails change={terms.change} />;
}
case 'UpdateVolumeDiscountProgram': {
return <ProposalVolumeDiscountProgramDetails change={terms.change} />;
}
case 'UpdateNetworkParameter': {
if (
terms.change.networkParameter.key === 'rewards.vesting.benefitTiers' ||
terms.change.networkParameter.key ===
'rewards.activityStreak.benefitTiers'
) {
return <ProposalUpdateBenefitTiers change={terms.change} />;
}
return null;
}
case 'NewFreeform':
case 'NewSpotMarket':
case 'UpdateSpotMarket': {
return null;
}
default: {
return null;
}
}
};
@@ -6,7 +6,6 @@ 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', () => ({
@@ -38,6 +37,12 @@ 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',
@@ -56,11 +61,7 @@ const renderComponent = (proposal: IProposal) => {
<MemoryRouter>
<MockedProvider>
<VegaWalletProvider config={vegaWalletConfig}>
<Proposal
restData={{}}
proposal={proposal}
networkParams={mockNetworkParams}
/>
<Proposal restData={{}} proposal={proposal} />
</VegaWalletProvider>
</MockedProvider>
</MemoryRouter>
@@ -99,27 +100,6 @@ 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,152 +5,25 @@ 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 { 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 {
ProposalCancelTransferDetails,
ProposalTransferDetails,
} from '../proposal-transfer';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers';
import { type Proposal as IProposal } from '../../types';
import { type Proposal as IProposal, type BatchProposal } from '../../types';
import { ProposalChangeDetails } from './proposal-change-details';
export interface ProposalProps {
proposal: IProposal;
networkParams: Partial<NetworkParamsResult>;
marketData?: MarketInfo | null;
parentMarketData?: MarketInfo | null;
assetData?: AssetQuery | null;
proposal: IProposal | BatchProposal;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
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,
networkParams,
restData,
marketData,
parentMarketData,
assetData,
originalMarketProposalRestData,
mostRecentlyEnactedAssociatedMarketProposal,
}: ProposalProps) => {
const featureFlags = useFeatureFlags((state) => state.flags);
export const Proposal = ({ proposal, restData }: ProposalProps) => {
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">
@@ -181,91 +54,36 @@ export const Proposal = ({
<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>
{marketData && (
<div className="mb-4">
<ProposalMarketData
marketData={marketData}
parentMarketData={parentMarketData ? parentMarketData : undefined}
<div className="mb-4">
{proposal.__typename === 'Proposal' ? (
<ProposalChangeDetails
proposal={proposal}
terms={proposal.terms}
restData={restData}
/>
</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}
) : proposal.__typename === 'BatchProposal' ? (
proposal.subProposals?.map((p, i) => {
if (!p?.terms) return null;
return (
<ProposalChangeDetails
key={i}
proposal={proposal}
terms={p.terms}
restData={restData}
/>
);
})
) : null}
</div>
<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}
@@ -1,149 +1,25 @@
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 { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
import {
mockWalletContext,
networkParamsQueryMock,
fiveMinutes,
fiveHours,
fiveDays,
lastWeek,
nextWeek,
} from '../../test-helpers/mocks';
import { type Proposal } from '../../types';
import { mockWalletContext } from '../../test-helpers/mocks';
const renderComponent = (
proposal: Proposal,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mocks: MockedResponse<any>[] = [networkParamsQueryMock]
) =>
const renderComponent = (id: string) =>
render(
<Router>
<MockedProvider mocks={mocks}>
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<ProposalsListItemDetails proposal={proposal} />
</VegaWalletContext.Provider>
</AppStateProvider>
</MockedProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<ProposalsListItemDetails id={id} />
</VegaWalletContext.Provider>
</Router>
);
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(0);
});
afterAll(() => {
jest.useRealTimers();
});
describe('Proposals list item details', () => {
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'
it('links to single proposal page', () => {
const proposalId = 'proposal-id';
renderComponent(proposalId);
expect(screen.getByRole('link')).toHaveAttribute(
'href',
expect.stringContaining(proposalId)
);
});
});
@@ -1,101 +1,16 @@
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 = ({
proposal,
}: {
proposal: Proposal;
}) => {
export const ProposalsListItemDetails = ({ id }: { id: string }) => {
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">
<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>
)}
<Link to={`${Routes.PROPOSALS}/${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 } from '../../types';
import { type Proposal, type BatchProposal } from '../../types';
interface ProposalsListItemProps {
proposal?: Proposal | null;
proposal?: Proposal | BatchProposal;
}
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 proposal={proposal} />
<ProposalsListItemDetails id={proposal.id} />
</RoundedWrapper>
</li>
);
@@ -3,14 +3,12 @@ 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,
@@ -20,6 +18,16 @@ 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,
@@ -69,12 +77,10 @@ const renderComponent = (
<Router>
<MockedProvider mocks={[networkParamsQueryMock]}>
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<ProposalsList
proposals={proposals}
protocolUpgradeProposals={protocolUpgradeProposals || []}
/>
</VegaWalletContext.Provider>
<ProposalsList
proposals={proposals}
protocolUpgradeProposals={protocolUpgradeProposals || []}
/>
</AppStateProvider>
</MockedProvider>
</Router>
@@ -88,10 +94,6 @@ 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,19 +11,20 @@ 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 Proposal } from '../../types';
import { type BatchProposal, type Proposal } from '../../types';
type Proposals = Array<Proposal | BatchProposal>;
interface ProposalsListProps {
proposals: Proposal[];
proposals: Proposals;
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
lastBlockHeight?: string;
}
interface SortedProposalsProps {
open: Proposal[];
closed: Proposal[];
open: Proposals;
closed: Proposals;
}
interface SortedProtocolUpgradeProposalsProps {
@@ -31,15 +32,26 @@ interface SortedProtocolUpgradeProposalsProps {
closed: ProtocolUpgradeProposalFieldsFragment[];
}
export const orderByDate = (arr: Proposal[]) =>
export const orderByDate = (arr: Proposals) =>
orderBy(
arr,
[
(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) => {
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) => new Date(p?.datetime).getTime(),
],
['asc', 'asc']
@@ -76,18 +88,40 @@ export const ProposalsList = ({
const sortedProposals: SortedProposalsProps = useMemo(() => {
const initialSorting = proposals.reduce(
(acc: SortedProposalsProps, proposal) => {
if (isFuture(new Date(proposal?.terms.closingDatetime))) {
acc.open.push(proposal);
} else {
acc.closed.push(proposal);
(acc, proposal) => {
if (proposal.__typename === 'Proposal') {
if (isFuture(new Date(proposal?.terms.closingDatetime))) {
acc.open.push(proposal);
} else {
acc.closed.push(proposal);
}
return acc;
}
if (proposal.__typename === 'BatchProposal') {
if (
// this could be improved by sorting by soonest enactment date of all the
// sub proposals
isFuture(
new Date(
proposal.batchTerms?.closingDatetime || proposal.datetime
)
)
) {
acc.open.push(proposal);
} else {
acc.closed.push(proposal);
}
return acc;
}
return acc;
},
{
open: [],
closed: [],
}
} as SortedProposalsProps
);
return {
open:
@@ -121,7 +155,7 @@ export const ProposalsList = ({
};
}, [protocolUpgradeProposals, lastBlockHeight]);
const filterPredicate = (p: ProposalFieldsFragment | Proposal) =>
const filterPredicate = (p: Proposal | BatchProposal) =>
p?.id?.includes(filterString) ||
p?.party?.id?.toString().includes(filterString);
@@ -1,17 +1,8 @@
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, waitFor, within } from '@testing-library/react';
import {
mockWalletContext,
networkParamsQueryMock,
nextWeek,
lastMonth,
} from '../../test-helpers/mocks';
import { render, screen } from '@testing-library/react';
import { nextWeek, lastMonth } from '../../test-helpers/mocks';
import { type Proposal } from '../../types';
const rejectedProposalClosesNextWeek = generateProposal({
@@ -36,31 +27,15 @@ const rejectedProposalClosedLastMonth = generateProposal({
});
const renderComponent = (proposals: Proposal[]) => (
<Router>
<MockedProvider mocks={[networkParamsQueryMock]}>
<AppStateProvider>
<VegaWalletContext.Provider value={mockWalletContext}>
<RejectedProposalsList proposals={proposals} />
</VegaWalletContext.Provider>
</AppStateProvider>
</MockedProvider>
</Router>
<RejectedProposalsList proposals={proposals} />
);
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(0);
});
afterAll(() => {
jest.useRealTimers();
});
jest.mock('../vote-details/use-user-vote', () => ({
useUserVote: jest.fn().mockImplementation(() => ({ voteState: 'NotCast' })),
jest.mock('../proposals-list-item', () => ({
ProposalsListItem: () => <div data-testid="proposals-list-item" />,
}));
describe('Rejected proposals list', () => {
it('Renders a list of proposals', async () => {
it('Renders a list of proposals', () => {
render(
renderComponent([
rejectedProposalClosedLastMonth,
@@ -68,25 +43,13 @@ describe('Rejected proposals list', () => {
])
);
await waitFor(() => {
const rejectedProposals = within(
screen.getByTestId('rejected-proposals')
);
const rejectedProposalsItems = rejectedProposals.getAllByTestId(
'proposals-list-item'
);
expect(rejectedProposalsItems).toHaveLength(2);
});
expect(screen.getAllByTestId('proposals-list-item')).toHaveLength(2);
});
it('Displays text when there are no proposals', async () => {
render(renderComponent([]));
await waitFor(() => {
expect(screen.getByTestId('no-rejected-proposals')).toBeInTheDocument();
expect(
screen.queryByTestId('rejected-proposals')
).not.toBeInTheDocument();
});
expect(screen.getByTestId('no-rejected-proposals')).toBeInTheDocument();
expect(screen.queryByTestId('rejected-proposals')).not.toBeInTheDocument();
});
});
@@ -3,17 +3,17 @@ import { useTranslation } from 'react-i18next';
import { Heading } from '../../../../components/heading';
import { ProposalsListItem } from '../proposals-list-item';
import { ProposalsListFilter } from '../proposals-list-filter';
import { type Proposal } from '../../types';
import { type BatchProposal, type Proposal } from '../../types';
interface ProposalsListProps {
proposals: Proposal[];
proposals: Array<Proposal | BatchProposal>;
}
export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
const { t } = useTranslation();
const [filterString, setFilterString] = useState('');
const filterPredicate = (p: Proposal) =>
const filterPredicate = (p: Proposal | BatchProposal) =>
p?.id?.includes(filterString) ||
p?.party?.id?.toString().includes(filterString);
@@ -64,7 +64,7 @@ describe('VoteBreakdown', () => {
jest.useRealTimers();
});
it('Renders majority reached', () => {
it('Renders majority reached', async () => {
const yesVotes = 100;
const noVotes = 0;
@@ -82,10 +82,10 @@ describe('VoteBreakdown', () => {
},
})
);
expect(screen.getByTestId('token-majority-met')).toBeInTheDocument();
expect(await screen.findByTestId('token-majority-met')).toBeInTheDocument();
});
it('Renders majority not reached', () => {
it('Renders majority not reached', async () => {
const yesVotes = 20;
const noVotes = 80;
@@ -103,10 +103,12 @@ describe('VoteBreakdown', () => {
},
})
);
expect(screen.getByTestId('token-majority-not-met')).toBeInTheDocument();
expect(
await screen.findByTestId('token-majority-not-met')
).toBeInTheDocument();
});
it('Renders participation reached', () => {
it('Renders participation reached', async () => {
const yesVotes = 1000;
const noVotes = 0;
@@ -124,10 +126,12 @@ describe('VoteBreakdown', () => {
},
})
);
expect(screen.getByTestId('token-participation-met')).toBeInTheDocument();
expect(
await screen.findByTestId('token-participation-met')
).toBeInTheDocument();
});
it('Renders participation not reached', () => {
it('Renders participation not reached', async () => {
const yesVotes = 0;
const noVotes = 0;
@@ -145,11 +149,11 @@ describe('VoteBreakdown', () => {
})
);
expect(
screen.getByTestId('token-participation-not-met')
await screen.findByTestId('token-participation-not-met')
).toBeInTheDocument();
});
it('Renders proposal state: Update market proposal - Currently expected to pass by LP vote', () => {
it('Renders proposal state: Update market proposal - Currently expected to pass by LP vote', async () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
@@ -170,12 +174,12 @@ describe('VoteBreakdown', () => {
},
})
);
expect(screen.getByTestId('vote-status')).toHaveTextContent(
expect(await screen.findByTestId('vote-status')).toHaveTextContent(
'Currently expected to pass by liquidity vote'
);
});
it('Renders proposal state: Update market proposal - Currently expected to pass by token vote', () => {
it('Renders proposal state: Update market proposal - Currently expected to pass by token vote', async () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
@@ -196,12 +200,12 @@ describe('VoteBreakdown', () => {
},
})
);
expect(screen.getByTestId('vote-status')).toHaveTextContent(
expect(await screen.findByTestId('vote-status')).toHaveTextContent(
'Currently expected to pass by token vote'
);
});
it('Renders proposal state: Update market proposal - Currently expected to fail', () => {
it('Renders proposal state: Update market proposal - Currently expected to fail', async () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
@@ -222,12 +226,12 @@ describe('VoteBreakdown', () => {
},
})
);
expect(screen.getByTestId('vote-status')).toHaveTextContent(
expect(await screen.findByTestId('vote-status')).toHaveTextContent(
'Currently expected to fail'
);
});
it('Progress bar displays status - token majority', () => {
it('Progress bar displays status - token majority', async () => {
const yesVotes = 80;
const noVotes = 20;
@@ -246,13 +250,13 @@ describe('VoteBreakdown', () => {
})
);
const element = screen.getByTestId('token-majority-progress');
const element = await screen.findByTestId('token-majority-progress');
const style = window.getComputedStyle(element);
expect(style.width).toBe(`${yesVotes}%`);
});
it('Progress bar displays status - token participation', () => {
it('Progress bar displays status - token participation', async () => {
const yesVotes = 40;
const noVotes = 20;
const totalVotes = yesVotes + noVotes;
@@ -274,13 +278,13 @@ describe('VoteBreakdown', () => {
})
);
const element = screen.getByTestId('token-participation-progress');
const element = await screen.findByTestId('token-participation-progress');
const style = window.getComputedStyle(element);
expect(style.width).toBe(`${expectedProgress}%`);
});
it('Progress bar displays status - LP majority', () => {
it('Progress bar displays status - LP majority', async () => {
const yesVotesLP = 0.8;
const noVotesLP = 0.2;
const expectedProgress = (yesVotesLP / (yesVotesLP + noVotesLP)) * 100; // 80%
@@ -307,12 +311,12 @@ describe('VoteBreakdown', () => {
})
);
const element = screen.getByTestId('lp-majority-progress');
const element = await screen.findByTestId('lp-majority-progress');
const style = window.getComputedStyle(element);
expect(style.width).toBe(`${expectedProgress}%`);
});
it('Progress bar displays status - LP participation', () => {
it('Progress bar displays status - LP participation', async () => {
const yesVotesLP = 400;
const noVotesLP = 600;
const totalVotesLP = yesVotesLP + noVotesLP;
@@ -341,7 +345,7 @@ describe('VoteBreakdown', () => {
})
);
const element = screen.getByTestId('lp-participation-progress');
const element = await screen.findByTestId('lp-participation-progress');
const style = window.getComputedStyle(element);
expect(style.width).toBe(`${expectedProgress}%`);
});
@@ -1,13 +1,20 @@
import { type ReactNode } from 'react';
import compact from 'lodash/compact';
import countBy from 'lodash/countBy';
import { useState, type ReactNode } from 'react';
import classNames from 'classnames';
import BigNumber from 'bignumber.js';
import { useTranslation } from 'react-i18next';
import { Trans, useTranslation } from 'react-i18next';
import { useVoteInformation } from '../../hooks';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import { Tooltip, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '@vegaprotocol/utils';
import { ProposalState } from '@vegaprotocol/types';
import { CompactNumber } from '@vegaprotocol/react-helpers';
import { type Proposal } from '../../types';
import { type Proposal, type BatchProposal } from '../../types';
import {
type ProposalTermsFieldsFragment,
type VoteFieldsFragment,
} from '../../__generated__/Proposals';
import { useBatchVoteInformation } from '../../hooks/use-vote-information';
export const CompactVotes = ({ number }: { number: BigNumber }) => (
<CompactNumber
@@ -18,10 +25,6 @@ export const CompactVotes = ({ number }: { number: BigNumber }) => (
/>
);
interface VoteBreakdownProps {
proposal: Proposal;
}
interface VoteProgressProps {
percentageFor: BigNumber;
colourfulBg?: boolean;
@@ -55,7 +58,7 @@ const VoteProgress = ({
className={progressClasses}
style={{ width: `${percentageFor}%` }}
data-testid={testId}
></div>
/>
<div className={textClasses}>{children}</div>
</div>
);
@@ -75,14 +78,14 @@ const Status = ({ reached, threshold, text, testId }: StatusProps) => {
<div data-testid={testId}>
{reached ? (
<div className="flex items-center gap-2">
<Icon name="tick" size={4} />
<VegaIcon name={VegaIconNames.TICK} size={20} />
<span>
{threshold.toString()}% {text} {t('met')}
</span>
</div>
) : (
<div className="flex items-center gap-2">
<Icon name="cross" size={4} />
<VegaIcon name={VegaIconNames.CROSS} size={20} />
<span>
{threshold.toString()}% {text} {t('not met')}
</span>
@@ -92,7 +95,225 @@ const Status = ({ reached, threshold, text, testId }: StatusProps) => {
);
};
export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
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-pink"
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
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-pink"
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
key={i}
proposal={proposal}
votes={proposal.votes}
terms={p.terms}
/>
);
})}
</div>
)}
</div>
);
}
return null;
};
const VoteBreakdownBatchSubProposal = ({
proposal,
votes,
terms,
}: {
proposal: BatchProposal;
votes: VoteFieldsFragment;
terms: ProposalTermsFieldsFragment;
}) => {
const { t } = useTranslation();
const voteInfo = useVoteInformation({
votes,
terms,
});
const isProposalOpen = proposal?.state === ProposalState.STATE_OPEN;
const isUpdateMarket = terms?.change?.__typename === 'UpdateMarket';
return (
<div>
<h4>{t(terms.change.__typename)}</h4>
<VoteBreakDownUI
voteInfo={voteInfo}
isProposalOpen={isProposalOpen}
isUpdateMarket={isUpdateMarket}
/>
</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 (
<VoteBreakDownUI
voteInfo={voteInfo}
isProposalOpen={isProposalOpen}
isUpdateMarket={isUpdateMarket}
/>
);
};
const VoteBreakDownUI = ({
voteInfo,
isProposalOpen,
isUpdateMarket,
}: {
voteInfo: ReturnType<typeof useVoteInformation>;
isProposalOpen: boolean;
isUpdateMarket: boolean;
}) => {
const defaultDP = 2;
const { t } = useTranslation();
if (!voteInfo) return null;
const {
totalTokensPercentage,
participationMet,
@@ -114,12 +335,8 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
majorityLPMet,
willPassByTokenVote,
willPassByLPVote,
} = useVoteInformation({ proposal });
} = voteInfo;
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)
@@ -152,23 +369,38 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
{isProposalOpen && (
<div
data-testid="vote-status"
className="flex items-center gap-1 mb-2 text-bold"
className="flex items-center gap-2 mb-2 text-bold"
>
<span>
{willPass ? (
<Icon name="tick" size={5} className="text-vega-green" />
<VegaIcon
name={VegaIconNames.TICK}
size={20}
className="text-vega-green"
/>
) : (
<Icon name="cross" size={5} className="text-vega-pink" />
<VegaIcon
name={VegaIconNames.CROSS}
size={20}
className="text-vega-pink"
/>
)}
</span>
<span>{t('currentlySetTo')} </span>
{willPass ? (
<span>
<span className="text-vega-green">{t('pass')}</span>
<p className="m-0">
<Trans
i18nKey={'Currently expected to <0>pass</0>'}
components={[<span className="text-vega-green" />]}
/>
{isUpdateMarket && <span> {updateMarketVotePassMethod}</span>}
</span>
</p>
) : (
<span className="text-vega-pink">{t('fail')}</span>
<p className="m-0">
<Trans
i18nKey={'Currently expected to <0>fail</0>'}
components={[<span className="text-vega-pink" />]}
/>
</p>
)}
</div>
)}
@@ -8,12 +8,10 @@ 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 } from '../../types';
import { type Proposal, type BatchProposal } from '../../types';
interface UserVoteProps {
proposal: Proposal;
minVoterBalance: string | null | undefined;
spamProtectionMinTokens: string | null | undefined;
proposal: Proposal | BatchProposal;
transaction: VegaTxState | null;
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
dialog: (props: DialogProps) => JSX.Element;
@@ -23,8 +21,6 @@ interface UserVoteProps {
export const UserVote = ({
proposal,
minVoterBalance,
spamProtectionMinTokens,
submit,
transaction,
dialog,
@@ -46,12 +42,17 @@ 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,14 +19,18 @@ 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;
@@ -35,20 +39,94 @@ 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
@@ -58,8 +136,17 @@ export const VoteButtonsContainer = (props: VoteButtonsContainerProps) => {
);
};
interface VoteButtonsProps extends 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;
currentStakeAvailable: BigNumber;
minVoterBalance: string | null;
spamProtectionMinTokens: string | null;
}
export const VoteButtons = ({
@@ -1,7 +1,6 @@
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'),
@@ -29,118 +28,84 @@ 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({ proposal }));
} = renderHook(() => useProposalNetworkParams());
const expectedObj = {
requiredMajority: expect.any(BigNumber),
requiredMajorityLP: expect.any(BigNumber),
requiredParticipation: expect.any(BigNumber),
requiredParticipationLP: expect.any(BigNumber),
};
expect(current).toEqual({
requiredMajority: '0.1',
requiredMajorityLP: '0.2',
requiredParticipation: new BigNumber(0.15),
requiredParticipationLP: new BigNumber(0.25),
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,
});
});
it('returns the correct params for a market proposal', () => {
const proposal = generateProposal({
terms: {
change: {
__typename: 'NewMarket',
},
},
});
it('returns the correct values for the proposal change type', () => {
const {
result: { current },
} = renderHook(() => useProposalNetworkParams({ proposal }));
} = renderHook(() => useProposalNetworkParams());
expect(current).toEqual({
requiredMajority: '0.3',
requiredParticipation: new BigNumber(0.35),
});
});
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'
);
it('returns the correct params for an asset proposal', () => {
const proposal = generateProposal({
terms: {
change: {
__typename: 'NewAsset',
},
},
});
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');
const {
result: { current },
} = renderHook(() => useProposalNetworkParams({ proposal }));
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');
expect(current).toEqual({
requiredMajority: '0.4',
requiredParticipation: new BigNumber(0.45),
});
});
expect(current?.UpdateAsset.requiredMajority.toString()).toEqual('0.5');
expect(current?.UpdateAsset.requiredMajorityLP.toString()).toEqual('0');
expect(current?.UpdateAsset.requiredParticipation.toString()).toEqual(
'0.55'
);
it('returns the correct params for an update asset proposal', () => {
const proposal = generateProposal({
terms: {
change: {
__typename: 'UpdateAsset',
},
},
});
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');
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),
});
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'
);
});
});
@@ -3,35 +3,33 @@ import {
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { BigNumber } from '../../../lib/bignumber';
import { type Proposal } from '../types';
import { type ProposalChangeType } from '../types';
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 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);
const fallback = {
requiredMajority: new BigNumber(1),
@@ -41,86 +39,152 @@ export const useProposalNetworkParams = ({
};
if (!params) {
return fallback;
return;
}
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;
}
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;
};
@@ -79,33 +79,35 @@ describe('use-vote-information', () => {
const {
result: { current },
} = renderHook(() => useVoteInformation({ proposal }));
} = renderHook(() =>
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
);
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);
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);
});
it('correctly returns majority, participation and will-pass status for a proposal with no votes', () => {
@@ -123,11 +125,13 @@ describe('use-vote-information', () => {
const {
result: { current },
} = renderHook(() => useVoteInformation({ proposal }));
} = renderHook(() =>
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
);
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', () => {
@@ -145,9 +149,11 @@ describe('use-vote-information', () => {
const {
result: { current },
} = renderHook(() => useVoteInformation({ proposal }));
} = renderHook(() =>
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
);
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', () => {
@@ -165,11 +171,13 @@ describe('use-vote-information', () => {
const {
result: { current },
} = renderHook(() => useVoteInformation({ proposal }));
} = renderHook(() =>
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
);
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', () => {
@@ -187,11 +195,13 @@ describe('use-vote-information', () => {
const {
result: { current },
} = renderHook(() => useVoteInformation({ proposal }));
} = renderHook(() =>
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
);
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', () => {
@@ -221,10 +231,12 @@ describe('use-vote-information', () => {
const {
result: { current },
} = renderHook(() => useVoteInformation({ proposal }));
} = 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(false);
});
it('correctly shows whether an update market proposal failing token but passing LP voting', () => {
@@ -254,9 +266,11 @@ describe('use-vote-information', () => {
const {
result: { current },
} = renderHook(() => useVoteInformation({ proposal }));
} = renderHook(() =>
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
);
expect(current.willPassByTokenVote).toEqual(false);
expect(current.willPassByLPVote).toEqual(true);
expect(current?.willPassByTokenVote).toEqual(false);
expect(current?.willPassByLPVote).toEqual(true);
});
});
@@ -1,191 +1,198 @@
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 Proposal } from '../types';
import {
type ProposalTermsFieldsFragment,
type ProposalFieldsFragment,
type VoteFieldsFragment,
} from '../__generated__/Proposals';
import { type ProposalChangeType } from '../types';
export const useVoteInformation = ({ proposal }: { proposal: Proposal }) => {
export const useVoteInformation = ({
votes,
terms,
}: {
votes: VoteFieldsFragment;
terms: ProposalTermsFieldsFragment;
}) => {
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 {
requiredMajority,
requiredParticipation,
requiredMajorityLP,
requiredParticipationLP,
} = useProposalNetworkParams({
proposal,
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
);
});
};
const {
requiredMajorityPercentage,
requiredMajorityLPPercentage,
noTokens,
noEquityLikeShareWeight,
yesTokens,
yesEquityLikeShareWeight,
totalTokensVoted,
totalEquityLikeShareWeight,
yesPercentage,
yesLPPercentage,
noPercentage,
noLPPercentage,
participationMet,
participationLPMet,
majorityMet,
majorityLPMet,
totalTokensPercentage,
totalLPTokensPercentage,
willPassByTokenVote,
willPassByLPVote,
} = useMemo(() => {
const requiredMajorityPercentage = requiredMajority
? new BigNumber(requiredMajority).times(100)
: new BigNumber(100);
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 = requiredMajorityLP
? new BigNumber(requiredMajorityLP).times(100)
: new BigNumber(100);
const requiredMajorityLPPercentage = params.requiredMajorityLP
? new BigNumber(params.requiredMajorityLP).times(100)
: new BigNumber(100);
const noTokens = new BigNumber(
addDecimal(proposal?.votes.no.totalTokens ?? 0, decimals)
);
const noTokens = new BigNumber(
addDecimal(votes.no.totalTokens ?? 0, decimals)
);
const noEquityLikeShareWeight = !proposal?.votes.no
.totalEquityLikeShareWeight
? new BigNumber(0)
: new BigNumber(proposal.votes.no.totalEquityLikeShareWeight).times(100);
const noEquityLikeShareWeight = !votes.no.totalEquityLikeShareWeight
? new BigNumber(0)
: new BigNumber(votes.no.totalEquityLikeShareWeight).times(100);
const yesTokens = new BigNumber(
addDecimal(proposal?.votes.yes.totalTokens ?? 0, decimals)
);
const yesTokens = new BigNumber(
addDecimal(votes.yes.totalTokens ?? 0, decimals)
);
const yesEquityLikeShareWeight = !proposal?.votes.yes
.totalEquityLikeShareWeight
? new BigNumber(0)
: new BigNumber(proposal.votes.yes.totalEquityLikeShareWeight).times(100);
const yesEquityLikeShareWeight = !votes.yes.totalEquityLikeShareWeight
? new BigNumber(0)
: new BigNumber(votes.yes.totalEquityLikeShareWeight).times(100);
const totalTokensVoted = yesTokens.plus(noTokens);
const totalTokensVoted = yesTokens.plus(noTokens);
const totalEquityLikeShareWeight = yesEquityLikeShareWeight.plus(
noEquityLikeShareWeight
);
const totalEquityLikeShareWeight = yesEquityLikeShareWeight.plus(
noEquityLikeShareWeight
);
const yesPercentage = totalTokensVoted.isZero()
? new BigNumber(0)
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
const yesLPPercentage = yesEquityLikeShareWeight;
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 noPercentage = totalTokensVoted.isZero()
? new BigNumber(0)
: noTokens.multipliedBy(100).dividedBy(totalTokensVoted);
const noLPPercentage = totalEquityLikeShareWeight.isZero()
? new BigNumber(0)
: noEquityLikeShareWeight
.multipliedBy(100)
.dividedBy(totalEquityLikeShareWeight);
const noLPPercentage = totalEquityLikeShareWeight.isZero()
? new BigNumber(0)
: noEquityLikeShareWeight
.multipliedBy(100)
.dividedBy(totalEquityLikeShareWeight);
const participationMet = totalTokensVoted.isGreaterThan(
totalSupply.multipliedBy(requiredParticipation)
);
const participationMet = totalTokensVoted.isGreaterThan(
totalSupply.multipliedBy(params.requiredParticipation)
);
const participationLPMet = requiredParticipationLP
? totalEquityLikeShareWeight.isGreaterThan(requiredParticipationLP)
: false;
const participationLPMet = params.requiredParticipationLP
? totalEquityLikeShareWeight.isGreaterThan(params.requiredParticipationLP)
: false;
const majorityMet = yesPercentage.isGreaterThanOrEqualTo(
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 majorityLPMet = yesLPPercentage.isGreaterThanOrEqualTo(
const willPassByLPVote =
participationLPMet &&
new BigNumber(yesLPPercentage).isGreaterThanOrEqualTo(
requiredMajorityLPPercentage
);
const totalTokensPercentage = totalTokensVoted
.multipliedBy(100)
.dividedBy(totalSupply);
let willPass = false;
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,
]);
if (changeType === 'UpdateMarket' || changeType === 'UpdateMarketState') {
willPass = willPassByTokenVote && willPassByLPVote;
} else {
willPass = willPassByTokenVote;
}
return {
willPassByTokenVote,
willPassByLPVote,
totalTokensPercentage,
totalLPTokensPercentage,
participationMet,
participationLPMet,
totalTokensVoted,
totalEquityLikeShareWeight,
noPercentage,
noLPPercentage,
yesPercentage,
yesLPPercentage,
requiredMajorityPercentage,
requiredMajorityLPPercentage,
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
),
requiredMajorityPercentage,
requiredMajorityLPPercentage,
requiredParticipation: new BigNumber(requiredParticipation).times(100),
requiredParticipationLP:
requiredParticipationLP &&
new BigNumber(requiredParticipationLP).times(100),
totalTokensVoted,
totalEquityLikeShareWeight,
yesPercentage,
yesLPPercentage,
noPercentage,
noLPPercentage,
participationMet,
participationLPMet,
majorityMet,
majorityLPMet,
totalTokensPercentage,
totalLPTokensPercentage,
willPassByTokenVote,
willPassByLPVote,
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
),
requiredParticipation: new BigNumber(params.requiredParticipation).times(
100
),
requiredParticipationLP: new BigNumber(
params.requiredParticipationLP
).times(100),
willPass,
};
};
@@ -1,442 +0,0 @@
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
@@ -1,69 +0,0 @@
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,260 +1,39 @@
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__/Proposal';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
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';
import { Proposal } from '../components/proposal';
import { ProposalNotFound } from '../components/proposal-not-found';
import { useProposalQuery } from '../__generated__/Proposals';
export const ProposalContainer = () => {
const featureFlags = useFeatureFlags((state) => state.flags);
const [
mostRecentlyEnactedAssociatedMarketProposal,
setMostRecentlyEnactedAssociatedMarketProposal,
] = useState(undefined);
const params = useParams<{ proposalId: string }>();
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 {
state: { data: restData, loading: restLoading, error: restError },
} = useFetch(`${ENV.rest}governance?proposalId=${params.proposalId}`);
const { data, loading, error, refetch } = useProposalQuery({
const { data, loading, error } = 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={
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
}
loading={Boolean(loading || restLoading)}
error={error || restError}
data={{
...data,
...networkParams,
...(marketData ? { newMarketData: marketData } : {}),
...(parentMarketData ? { parentMarketData } : {}),
...(assetData ? { assetData } : {}),
...(restData ? { restData } : {}),
...(originalMarketProposalRestData
? { originalMarketProposalRestData }
: {}),
...(previouslyEnactedMarketProposalsRestData
? { previouslyEnactedMarketProposalsRestData }
: {}),
}}
>
{data?.proposal ? (
<Proposal
proposal={proposal}
networkParams={networkParams}
restData={restData}
marketData={marketData}
parentMarketData={parentMarketData}
assetData={assetData}
originalMarketProposalRestData={originalMarketProposalRestData}
mostRecentlyEnactedAssociatedMarketProposal={
mostRecentlyEnactedAssociatedMarketProposal
}
/>
<Proposal proposal={data.proposal} restData={restData} />
) : (
<ProposalNotFound />
)}
@@ -1,186 +0,0 @@
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
}
}
}
}
@@ -1,247 +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 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,31 +1,27 @@
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, removePaginationWrapper } from '@vegaprotocol/utils';
import { getNodes } from '@vegaprotocol/utils';
import {
ProposalState,
ProtocolUpgradeProposalStatus,
} from '@vegaprotocol/types';
import { type NodeConnection, type NodeEdge } from '@vegaprotocol/utils';
import {
useProposalsQuery,
type ProposalFieldsFragment,
} from './__generated__/Proposals';
import { useProposalsQuery } from '../__generated__/Proposals';
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { useProtocolUpgradeProposalsQuery } from '@vegaprotocol/proposals';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { type BatchProposal, type Proposal } from '../types';
export function getNotRejectedProposals(data?: ProposalFieldsFragment[]) {
return flow([
(data) =>
data.filter(
(p: ProposalFieldsFragment) => p?.state !== ProposalState.STATE_REJECTED
),
])(data);
export function getNotRejectedProposals(
data?: Array<Proposal | BatchProposal>
) {
if (!data) return [];
return data.filter((p) => p.state !== ProposalState.STATE_REJECTED);
}
export function getNotRejectedProtocolUpgradeProposals<
@@ -43,17 +39,11 @@ 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 {
@@ -69,7 +59,7 @@ export const ProposalsContainer = () => {
const proposals = useMemo(
() =>
getNotRejectedProposals(
removePaginationWrapper(data?.proposalsConnection?.edges)
compact(data?.proposalsConnection?.edges?.map((e) => e?.proposalNode))
),
[data]
);
@@ -1,16 +1,16 @@
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 { RejectedProposalsList } from '../components/proposals-list';
import type { ProposalFieldsFragment } from '../proposals/__generated__/Proposals';
import { useProposalsQuery } from '../proposals/__generated__/Proposals';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import { type ProposalFieldsFragment } from '../__generated__/Proposals';
import { useProposalsQuery } from '../__generated__/Proposals';
import flow from 'lodash/flow';
import orderBy from 'lodash/orderBy';
import { ProposalState } from '@vegaprotocol/types';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { type BatchProposal, type Proposal } from '../types';
const orderByDate = (arr: ProposalFieldsFragment[]) =>
orderBy(
@@ -22,7 +22,9 @@ const orderByDate = (arr: ProposalFieldsFragment[]) =>
['desc', 'desc']
);
export function getRejectedProposals(data?: ProposalFieldsFragment[] | null) {
export function getRejectedProposals(
data?: Array<Proposal | BatchProposal> | null
) {
return flow([
(data) =>
data.filter(
@@ -33,23 +35,17 @@ export function getRejectedProposals(data?: ProposalFieldsFragment[] | null) {
}
export const RejectedProposalsContainer = () => {
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 proposals = useMemo(
() =>
getRejectedProposals(
removePaginationWrapper(data?.proposalsConnection?.edges)
compact(data?.proposalsConnection?.edges?.map((e) => e?.proposalNode))
),
[data]
);
@@ -6,7 +6,7 @@ import isArray from 'lodash/isArray';
import mergeWith from 'lodash/mergeWith';
import { type PartialDeep } from 'type-fest';
import { type ProposalQuery } from '../proposal/__generated__/Proposal';
import { type ProposalQuery } from '../__generated__/Proposals';
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { type Proposal } from '../types';
+12 -4
View File
@@ -1,11 +1,19 @@
import type { ProposalQuery } from './proposal/__generated__/Proposal';
import { type ProposalNode } from '@vegaprotocol/types';
import {
type BatchProposalFieldsFragment,
type ProposalFieldsFragment,
} from './__generated__/Proposals';
/**
* The default Proposal type needs extracting from the ProposalNode union type
* as lots of fields on the original type don't exist on BatchProposal. Eventually
* we will support BatchProposal but for now we don't
*/
export type Proposal = Extract<
ProposalQuery['proposal'],
{ __typename?: 'Proposal' }
export type Proposal = ProposalFieldsFragment;
export type BatchProposal = BatchProposalFieldsFragment;
export type ProposalChangeType = NonNullable<
Proposal['terms']['change']['__typename']
>;
export type ProposalType = NonNullable<ProposalNode['__typename']>;
+1 -1
View File
@@ -22,7 +22,7 @@ NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ISOLATED_MARGIN=true
NX_ISOLATED_MARGIN=false
NX_ICEBERG_ORDERS=true
NX_METAMASK_SNAPS=true
NX_REFERRALS=true
@@ -4,7 +4,7 @@ import { CompetitionsHeader } from '../../components/competitions/competitions-h
import { Intent, Loader, TradingButton } from '@vegaprotocol/ui-toolkit';
import { useGameCards } from '../../lib/hooks/use-game-cards';
import { useCurrentEpochInfoQuery } from '../../lib/hooks/__generated__/Epoch';
import { useEpochInfoQuery } from '../../lib/hooks/__generated__/Epoch';
import { Link, useNavigate } from 'react-router-dom';
import { Links } from '../../lib/links';
import {
@@ -25,7 +25,7 @@ export const CompetitionsHome = () => {
usePageTitle(t('Competitions'));
const { data: epochData } = useCurrentEpochInfoQuery();
const { data: epochData } = useEpochInfoQuery();
const currentEpoch = Number(epochData?.epoch.id);
const { data: gamesData, loading: gamesLoading } = useGameCards({
@@ -1,12 +1,28 @@
import { useState, type ButtonHTMLAttributes } from 'react';
import { useState, type ButtonHTMLAttributes, useRef } from 'react';
import { Link, useParams } from 'react-router-dom';
import orderBy from 'lodash/orderBy';
import { Splash, truncateMiddle, Loader } from '@vegaprotocol/ui-toolkit';
import { DispatchMetricLabels, type DispatchMetric } from '@vegaprotocol/types';
import {
Splash,
truncateMiddle,
Loader,
Dialog,
Button,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import {
TransferStatus,
type Asset,
type RecurringTransfer,
} from '@vegaprotocol/types';
import classNames from 'classnames';
import { useT } from '../../lib/use-t';
import { Table } from '../../components/table';
import { formatNumber, getDateTimeFormat } from '@vegaprotocol/utils';
import {
addDecimalsFormatNumberQuantum,
formatNumber,
getDateTimeFormat,
} from '@vegaprotocol/utils';
import {
useTeam,
type TeamStats as ITeamStats,
@@ -27,6 +43,19 @@ import {
useGames,
areTeamGames,
} from '../../lib/hooks/use-games';
import { useEpochInfoQuery } from '../../lib/hooks/__generated__/Epoch';
import {
type EnrichedTransfer,
isScopedToTeams,
useGameCards,
} from '../../lib/hooks/use-game-cards';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import {
ActiveRewardCard,
DispatchMetricInfo,
} from '../../components/rewards-container/active-rewards';
import { type MarketMap, useMarketsMapProvider } from '@vegaprotocol/markets';
import format from 'date-fns/format';
export const CompetitionsTeam = () => {
const t = useT();
@@ -49,6 +78,14 @@ const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => {
const { data: games, loading: gamesLoading } = useGames(teamId);
const { data: epochData, loading: epochLoading } = useEpochInfoQuery();
const { data: transfersData, loading: transfersLoading } = useGameCards({
currentEpoch: Number(epochData?.epoch.id),
onlyActive: false,
});
const { data: markets } = useMarketsMapProvider();
// only show spinner on first load so when users join teams its smoother
if (!data && loading) {
return (
@@ -74,6 +111,9 @@ const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => {
members={members}
games={areTeamGames(games) ? games : undefined}
gamesLoading={gamesLoading}
transfers={transfersData}
transfersLoading={epochLoading || transfersLoading}
allMarkets={markets || undefined}
refetch={refetch}
/>
);
@@ -86,6 +126,9 @@ const TeamPage = ({
members,
games,
gamesLoading,
transfers,
transfersLoading,
allMarkets,
refetch,
}: {
team: TeamType;
@@ -94,6 +137,9 @@ const TeamPage = ({
members?: Member[];
games?: TeamGame[];
gamesLoading?: boolean;
transfers?: EnrichedTransfer[];
transfersLoading?: boolean;
allMarkets?: MarketMap;
refetch: () => void;
}) => {
const t = useT();
@@ -141,7 +187,13 @@ const TeamPage = ({
</ToggleButton>
</div>
{showGames ? (
<Games games={games} gamesLoading={gamesLoading} />
<Games
games={games}
gamesLoading={gamesLoading}
transfers={transfers}
transfersLoading={transfersLoading}
allMarkets={allMarkets}
/>
) : (
<Members members={members} />
)}
@@ -153,9 +205,15 @@ const TeamPage = ({
const Games = ({
games,
gamesLoading,
transfers,
transfersLoading,
allMarkets,
}: {
games?: TeamGame[];
gamesLoading?: boolean;
transfers?: EnrichedTransfer[];
transfersLoading?: boolean;
allMarkets?: MarketMap;
}) => {
const t = useT();
@@ -171,40 +229,106 @@ const Games = ({
return <p>{t('No game results available')}</p>;
}
const dependable = (value: string | JSX.Element) => {
if (transfersLoading) return <Loader size="small" />;
return value;
};
return (
<Table
columns={[
{ name: 'rank', displayName: t('Rank') },
{
name: 'epoch',
displayName: t('Epoch'),
headerClassName: 'hidden md:table-cell',
className: 'hidden md:table-cell',
},
{
name: 'endtime',
displayName: t('End time'),
},
{ name: 'type', displayName: t('Type') },
{ name: 'amount', displayName: t('Amount earned') },
{
name: 'asset',
displayName: t('Reward asset'),
},
{ name: 'daily', displayName: t('Daily reward amount') },
{ name: 'rank', displayName: t('Rank') },
{ name: 'amount', displayName: t('Amount earned this epoch') },
{ name: 'total', displayName: t('Cumulative amount earned') },
{
name: 'participatingTeams',
displayName: t('No. of participating teams'),
headerClassName: 'hidden md:table-cell',
className: 'hidden md:table-cell',
},
{
name: 'participatingMembers',
displayName: t('No. of participating members'),
headerClassName: 'hidden md:table-cell',
className: 'hidden md:table-cell',
},
]}
data={games.map((game) => ({
rank: game.team.rank,
epoch: game.epoch,
type: DispatchMetricLabels[game.team.rewardMetric as DispatchMetric],
amount: formatNumber(game.team.totalRewardsEarned),
participatingTeams: game.entities.length,
participatingMembers: game.numberOfParticipants,
}))}
noCollapse={true}
].map((c) => ({ ...c, headerClassName: 'text-left' }))}
data={games.map((game) => {
let transfer = transfers?.find((t) => {
if (!isScopedToTeams(t)) return false;
const idMatch = t.transfer.gameId === game.id;
const metricMatch =
t.transfer.kind.dispatchStrategy?.dispatchMetric ===
game.team.rewardMetric;
const start = t.transfer.kind.startEpoch <= game.epoch;
const end = t.transfer.kind.endEpoch
? t.transfer.kind.endEpoch >= game.epoch
: true;
const rejected = t.transfer.status === TransferStatus.STATUS_REJECTED;
return idMatch && metricMatch && start && end && !rejected;
});
if (!transfer || !isScopedToTeams(transfer)) transfer = undefined;
const asset = transfer?.transfer.asset;
const dailyAmount =
asset && transfer
? addDecimalsFormatNumberQuantum(
transfer.transfer.amount,
asset.decimals,
asset.quantum
)
: '-';
const earnedAmount = asset
? addDecimalsFormatNumberQuantum(
game.team.rewardEarned,
asset.decimals,
asset.quantum
)
: '-';
const totalAmount = asset
? addDecimalsFormatNumberQuantum(
game.team.totalRewardsEarned,
asset.decimals,
asset.quantum
)
: '-';
const assetSymbol = asset ? <RewardAssetCell asset={asset} /> : '-';
return {
id: game.id,
amount: dependable(earnedAmount),
asset: dependable(assetSymbol),
daily: dependable(dailyAmount),
endtime: <EndTimeCell epoch={game.epoch} />,
epoch: game.epoch,
participatingMembers: game.numberOfParticipants,
participatingTeams: game.entities.length,
rank: game.team.rank,
total: totalAmount,
// type: DispatchMetricLabels[game.team.rewardMetric as DispatchMetric],
type: dependable(
<GameTypeCell transfer={transfer} allMarkets={allMarkets} />
),
};
})}
noCollapse={false}
/>
);
};
@@ -286,3 +410,126 @@ const ToggleButton = ({
/>
);
};
const EndTimeCell = ({ epoch }: { epoch?: number }) => {
const { data, loading } = useEpochInfoQuery({
variables: {
epochId: epoch ? epoch.toString() : undefined,
},
fetchPolicy: 'cache-and-network',
});
if (loading) return <Loader size="small" />;
if (data) {
return format(
new Date(data.epoch.timestamps.expiry),
'yyyy/MM/dd hh:mm:ss'
);
}
return null;
};
const RewardAssetCell = ({ asset }: { asset: Asset }) => {
const open = useAssetDetailsDialogStore((state) => state.open);
const ref = useRef<HTMLButtonElement>(null);
return (
<button
ref={ref}
onClick={(e) => {
e.preventDefault();
open(asset.id, ref.current);
}}
className="border-b border-dashed border-vega-clight-200 dark:border-vega-cdark-200 text-left text-nowrap whitespace-nowrap"
>
{asset.symbol}
</button>
);
};
const GameTypeCell = ({
transfer,
allMarkets,
}: {
transfer?: EnrichedTransfer;
allMarkets?: MarketMap;
}) => {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLButtonElement>(null);
if (!transfer) return '-';
return (
<>
<ActiveRewardCardDialog
open={open}
onChange={(isOpen) => setOpen(isOpen)}
trigger={ref.current}
transfer={transfer}
allMarkets={allMarkets}
/>
<button
onClick={(e) => {
e.preventDefault();
setOpen(true);
}}
ref={ref}
className="border-b border-dashed border-vega-clight-200 dark:border-vega-cdark-200 text-left md:truncate md:max-w-[25vw]"
>
<DispatchMetricInfo transferNode={transfer} allMarkets={allMarkets} />
</button>
</>
);
};
const ActiveRewardCardDialog = ({
open,
onChange,
trigger,
transfer,
allMarkets,
}: {
open: boolean;
onChange: (isOpen: boolean) => void;
trigger?: HTMLElement | null;
transfer: EnrichedTransfer;
allMarkets?: MarketMap;
}) => {
const t = useT();
const { data } = useEpochInfoQuery();
return (
<Dialog
open={open}
title={t('Game details')}
onChange={(isOpen) => onChange(isOpen)}
icon={<VegaIcon name={VegaIconNames.INFO} />}
onCloseAutoFocus={(e) => {
/**
* This mimics radix's default behaviour that focuses the dialog's
* trigger after closing itself
*/
if (trigger) {
e.preventDefault();
trigger.focus();
}
}}
>
<div className="py-5 max-w-[454px]">
<ActiveRewardCard
transferNode={transfer}
currentEpoch={Number(data?.epoch.id)}
kind={transfer.transfer.kind as RecurringTransfer}
allMarkets={allMarkets}
/>
</div>
<div className="w-1/4">
<Button
data-testid="close-asset-details-dialog"
fill={true}
size="sm"
onClick={() => onChange(false)}
>
{t('Close')}
</Button>
</div>
</Dialog>
);
};
@@ -56,7 +56,6 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
<Last24hPriceChange
marketId={market.id}
decimalPlaces={market.decimalPlaces}
fallback={<span>-</span>}
/>
</HeaderStat>
<HeaderStat heading={t('Volume (24h)')} testId="market-volume">
@@ -72,7 +72,15 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
<div className="h-full flex flex-col lg:grid grid-rows-[min-content_min-content_1fr_min-content]">
<div className="flex flex-col w-full overflow-hidden">
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
{['chart', 'orderbook', 'trades', 'liquidity', 'fundingPayments']
{[
'chart',
'orderbook',
'depth',
'trades',
'liquidity',
'fundingPayments',
'funding',
]
// filter to control available views for the current market
// e.g. only perpetuals should get the funding views
.filter((_key) => {
@@ -163,9 +171,12 @@ const ViewButton = ({
onClick: () => void;
}) => {
const label = useViewLabel(view);
const className = classNames('py-2 px-4 min-w-[100px] capitalize text-sm', {
'bg-vega-clight-500 dark:bg-vega-cdark-500': isActive,
});
const className = classNames(
'py-2 px-4 capitalize text-sm whitespace-nowrap',
{
'bg-vega-clight-500 dark:bg-vega-cdark-500': isActive,
}
);
return (
<button data-testid={view} onClick={onClick} className={className}>
@@ -181,8 +192,8 @@ const useViewLabel = (view: TradingView) => {
chart: t('Chart'),
depth: t('Depth'),
liquidity: t('Liquidity'),
funding: t('Funding'),
fundingPayments: t('Funding'),
funding: t('Funding history'),
fundingPayments: t('Funding payments'),
orderbook: t('Orderbook'),
trades: t('Trades'),
positions: t('Positions'),
@@ -11,7 +11,7 @@ import { useEffect } from 'react';
import { useT } from '../../../lib/use-t';
import { matchPath, useLocation, useNavigate } from 'react-router-dom';
import { Routes } from '../../../lib/links';
import { useCurrentEpochInfoQuery } from '../../../lib/hooks/__generated__/Epoch';
import { useEpochInfoQuery } from '../../../lib/hooks/__generated__/Epoch';
const REFETCH_INTERVAL = 60 * 60 * 1000; // 1h
const NON_ELIGIBLE_REFERRAL_SET_TOAST_ID = 'non-eligible-referral-set';
@@ -23,7 +23,7 @@ const useNonEligibleReferralSet = () => {
data: epochData,
loading: epochLoading,
refetch: epochRefetch,
} = useCurrentEpochInfoQuery();
} = useEpochInfoQuery();
useEffect(() => {
const interval = setInterval(() => {
@@ -34,7 +34,7 @@ import {
} from './hooks/use-referral';
import { ApplyCodeForm, ApplyCodeFormContainer } from './apply-code-form';
import { useReferralProgram } from './hooks/use-referral-program';
import { useCurrentEpochInfoQuery } from '../../lib/hooks/__generated__/Epoch';
import { useEpochInfoQuery } from '../../lib/hooks/__generated__/Epoch';
import { QUSDTooltip } from './qusd-tooltip';
import { CodeTile, StatTile, Tile } from './tile';
import { areTeamGames, useGames } from '../../lib/hooks/use-games';
@@ -95,7 +95,7 @@ export const useStats = ({
program: ReturnType<typeof useReferralProgram>;
}) => {
const { benefitTiers } = program;
const { data: epochData } = useCurrentEpochInfoQuery({
const { data: epochData } = useEpochInfoQuery({
fetchPolicy: 'network-only',
});
const { data: statsData } = useReferralSetStatsQuery({
@@ -1,49 +1,19 @@
import { type TransferNode } from '@vegaprotocol/types';
import {
ActiveRewardCard,
isActiveReward,
} from '../rewards-container/active-rewards';
import { ActiveRewardCard } from '../rewards-container/active-rewards';
import { useT } from '../../lib/use-t';
import { useAssetsMapProvider } from '@vegaprotocol/assets';
import { type EnrichedTransfer } from '../../lib/hooks/use-game-cards';
import { useMarketsMapProvider } from '@vegaprotocol/markets';
export const GamesContainer = ({
data,
currentEpoch,
}: {
data: TransferNode[];
data: EnrichedTransfer[];
currentEpoch: number;
}) => {
const t = useT();
// Re-load markets and assets in the games container to ensure that the
// the cards are updated (not grayed out) when the user navigates to the games page
const { data: assets } = useAssetsMapProvider();
const { data: markets } = useMarketsMapProvider();
const enrichedTransfers = data
.filter((node) => isActiveReward(node, currentEpoch))
.map((node) => {
if (node.transfer.kind.__typename !== 'RecurringTransfer') {
return node;
}
const asset =
assets &&
assets[
node.transfer.kind.dispatchStrategy?.dispatchMetricAssetId || ''
];
const marketsInScope =
node.transfer.kind.dispatchStrategy?.marketIdsInScope?.map(
(id) => markets && markets[id]
);
return { ...node, asset, markets: marketsInScope };
});
if (!enrichedTransfers || !enrichedTransfers.length) return null;
if (!enrichedTransfers || enrichedTransfers.length === 0) {
if (!data || data.length === 0) {
return (
<p className="mb-6 text-muted">
{t('There are currently no games available.')}
@@ -53,7 +23,7 @@ export const GamesContainer = ({
return (
<div className="mb-12 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{enrichedTransfers.map((game, i) => {
{data.map((game, i) => {
// TODO: Remove `kind` prop from ActiveRewardCard
const { transfer } = game;
if (
@@ -29,28 +29,35 @@ export const MobileMarketHeader = () => {
if (!marketId) return null;
return (
<div className="pl-3 pr-2 flex justify-between gap-2 h-10 bg-vega-clight-700 dark:bg-vega-cdark-700">
<div className="pl-3 pr-2 grid grid-cols-2 h-10 bg-vega-clight-700 dark:bg-vega-cdark-700">
<FullScreenPopover
open={openMarket}
onOpenChange={(x) => {
setOpenMarket(x);
}}
trigger={
<h1 className="flex gap-1 sm:gap-2 md:gap-4 items-center text-base leading-3 md:text-lg whitespace-nowrap">
{data
? data.tradableInstrument.instrument.code
: t('Select market')}
<span
<button
data-testid="popover-trigger"
className="min-w-0 flex gap-1 items-center"
>
<h1 className="whitespace-nowrap overflow-hidden text-ellipsis items-center">
<span className="">
{data
? data.tradableInstrument.instrument.code
: t('Select market')}
</span>
</h1>
<VegaIcon
name={VegaIconNames.CHEVRON_DOWN}
size={16}
className={classNames(
'transition-transform ease-in-out duration-300 flex',
'origin-center transition-transform ease-in-out duration-300 flex',
{
'rotate-180': openMarket,
}
)}
>
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={16} />
</span>
</h1>
/>
</button>
}
>
<MarketSelector
@@ -64,34 +71,40 @@ export const MobileMarketHeader = () => {
setOpenPrice(x);
}}
trigger={
<span className="flex gap-2 items-end md:text-md whitespace-nowrap leading-3">
<button
data-testid="popover-trigger"
className="min-w-0 flex gap-2 items-center justify-end"
>
{data && (
<>
<span className="text-xs">
<Last24hPriceChange
marketId={data.id}
decimalPlaces={data.decimalPlaces}
/>
</span>
<span className="flex items-center gap-1">
<MarketMarkPrice
marketId={data.id}
decimalPlaces={data.decimalPlaces}
/>
<VegaIcon
name={VegaIconNames.CHEVRON_DOWN}
size={16}
className={classNames(
'transition-transform ease-in-out duration-300',
{
'rotate-180': openPrice,
}
)}
/>
<span className="min-w-0 flex flex-col items-end gap-0">
<span className="text-sm">
<MarketMarkPrice
marketId={data.id}
decimalPlaces={data.decimalPlaces}
/>
</span>
<span className="text-xs">
<Last24hPriceChange
marketId={data.id}
decimalPlaces={data.decimalPlaces}
fallback={<span />} // dont render anything so price is vertically centered
/>
</span>
</span>
<VegaIcon
name={VegaIconNames.CHEVRON_DOWN}
size={16}
className={classNames(
'min-w-0 transition-transform ease-in-out duration-300',
{
'rotate-180': openPrice,
}
)}
/>
</>
)}
</span>
</button>
}
>
{data && (
@@ -104,11 +117,11 @@ export const MobileMarketHeader = () => {
);
};
export interface PopoverProps extends PopoverPrimitive.PopoverProps {
interface PopoverProps extends PopoverPrimitive.PopoverProps {
trigger: React.ReactNode | string;
}
export const FullScreenPopover = ({
const FullScreenPopover = ({
trigger,
children,
open,
@@ -116,7 +129,7 @@ export const FullScreenPopover = ({
}: PopoverProps) => {
return (
<PopoverPrimitive.Root open={open} onOpenChange={onOpenChange}>
<PopoverPrimitive.Trigger data-testid="popover-trigger">
<PopoverPrimitive.Trigger asChild={true}>
{trigger}
</PopoverPrimitive.Trigger>
<PopoverPrimitive.Portal>
@@ -73,6 +73,7 @@ query ActiveRewards(
reference
status
timestamp
gameId
kind {
... on RecurringTransfer {
startEpoch
@@ -18,7 +18,7 @@ export type ActiveRewardsQueryVariables = Types.Exact<{
}>;
export type ActiveRewardsQuery = { __typename?: 'Query', transfersConnection?: { __typename?: 'TransferConnection', edges?: Array<{ __typename?: 'TransferEdge', node: { __typename?: 'TransferNode', transfer: { __typename?: 'Transfer', amount: string, id: string, from: string, fromAccountType: Types.AccountType, to: string, toAccountType: Types.AccountType, reference?: string | null, status: Types.TransferStatus, timestamp: any, reason?: string | null, asset?: { __typename?: 'Asset', id: string, symbol: string, decimals: number, name: string, quantum: string, status: Types.AssetStatus } | null, kind: { __typename?: 'OneOffGovernanceTransfer' } | { __typename?: 'OneOffTransfer' } | { __typename?: 'RecurringGovernanceTransfer' } | { __typename?: 'RecurringTransfer', startEpoch: number, endEpoch?: number | null, dispatchStrategy?: { __typename?: 'DispatchStrategy', dispatchMetric: Types.DispatchMetric, dispatchMetricAssetId: string, marketIdsInScope?: Array<string> | null, entityScope: Types.EntityScope, individualScope?: Types.IndividualScope | null, teamScope?: Array<string | null> | null, nTopPerformers?: string | null, stakingRequirement: string, notionalTimeWeightedAveragePositionRequirement: string, windowLength: number, lockPeriod: number, distributionStrategy: Types.DistributionStrategy, rankTable?: Array<{ __typename?: 'RankTable', startRank: number, shareRatio: number } | null> | null } | null } }, fees?: Array<{ __typename?: 'TransferFee', transferId: string, amount: string, epoch: number } | null> | null } } | null> | null } | null };
export type ActiveRewardsQuery = { __typename?: 'Query', transfersConnection?: { __typename?: 'TransferConnection', edges?: Array<{ __typename?: 'TransferEdge', node: { __typename?: 'TransferNode', transfer: { __typename?: 'Transfer', amount: string, id: string, from: string, fromAccountType: Types.AccountType, to: string, toAccountType: Types.AccountType, reference?: string | null, status: Types.TransferStatus, timestamp: any, gameId?: string | null, reason?: string | null, asset?: { __typename?: 'Asset', id: string, symbol: string, decimals: number, name: string, quantum: string, status: Types.AssetStatus } | null, kind: { __typename?: 'OneOffGovernanceTransfer' } | { __typename?: 'OneOffTransfer' } | { __typename?: 'RecurringGovernanceTransfer' } | { __typename?: 'RecurringTransfer', startEpoch: number, endEpoch?: number | null, dispatchStrategy?: { __typename?: 'DispatchStrategy', dispatchMetric: Types.DispatchMetric, dispatchMetricAssetId: string, marketIdsInScope?: Array<string> | null, entityScope: Types.EntityScope, individualScope?: Types.IndividualScope | null, teamScope?: Array<string | null> | null, nTopPerformers?: string | null, stakingRequirement: string, notionalTimeWeightedAveragePositionRequirement: string, windowLength: number, lockPeriod: number, distributionStrategy: Types.DistributionStrategy, rankTable?: Array<{ __typename?: 'RankTable', startRank: number, shareRatio: number } | null> | null } | null } }, fees?: Array<{ __typename?: 'TransferFee', transferId: string, amount: string, epoch: number } | null> | null } } | null> | null } | null };
export type RewardsHistoryQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
@@ -144,6 +144,7 @@ export const ActiveRewardsDocument = gql`
reference
status
timestamp
gameId
kind {
... on RecurringTransfer {
startEpoch
@@ -251,12 +251,7 @@ const StatusIndicator = ({
);
};
export const ActiveRewardCard = ({
transferNode,
currentEpoch,
kind,
allMarkets,
}: {
type ActiveRewardCardProps = {
transferNode: TransferNode & {
asset?: AssetFieldsFragment | null;
markets?: (MarketFieldsFragment | null)[];
@@ -264,7 +259,13 @@ export const ActiveRewardCard = ({
currentEpoch: number;
kind: RecurringTransfer;
allMarkets?: Record<string, MarketFieldsFragment | null>;
}) => {
};
export const ActiveRewardCard = ({
transferNode,
currentEpoch,
kind,
allMarkets,
}: ActiveRewardCardProps) => {
const t = useT();
const { transfer } = transferNode;
@@ -485,6 +486,62 @@ export const ActiveRewardCard = ({
);
};
export const DispatchMetricInfo = ({
transferNode,
allMarkets,
}: {
transferNode: ActiveRewardCardProps['transferNode'];
allMarkets?: ActiveRewardCardProps['allMarkets'];
}) => {
const dispatchStrategy =
transferNode.transfer.kind.__typename === 'RecurringTransfer'
? transferNode.transfer.kind.dispatchStrategy
: null;
const dispatchAsset = transferNode.transfer.asset;
const marketIdsInScope = dispatchStrategy?.marketIdsInScope;
const firstMarketData = transferNode.markets?.[0];
const specificMarkets = useMemo(() => {
if (
!firstMarketData ||
!marketIdsInScope ||
marketIdsInScope.length === 0
) {
return null;
}
if (marketIdsInScope.length > 1) {
const marketNames =
allMarkets &&
marketIdsInScope
.map((id) => allMarkets[id]?.tradableInstrument?.instrument?.name)
.join(', ');
return (
<Tooltip description={marketNames}>
<span>Specific markets</span>
</Tooltip>
);
}
const name = firstMarketData?.tradableInstrument?.instrument?.name;
if (name) {
return <span>{name}</span>;
}
return null;
}, [firstMarketData, marketIdsInScope, allMarkets]);
if (!dispatchStrategy) return null;
return (
<span data-testid="dispatch-metric-info">
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]} {' '}
<span>{specificMarkets || dispatchAsset?.name}</span>
</span>
);
};
const RewardRequirements = ({
dispatchStrategy,
assetDecimalPlaces = 0,
+3
View File
@@ -93,6 +93,9 @@ export const Table = forwardRef<
key={i}
className={classNames(dataEntry['className'] as string, {
'max-md:flex flex-col w-full': !noCollapse,
// collapsed (mobile) row divider
'first:border-t-0 max-md:border-t border-vega-clight-500 dark:border-vega-cdark-500 first:mt-0 mt-1':
!noCollapse,
})}
onClick={() => {
if (onRowClick) {
@@ -9,6 +9,16 @@ leverage_input = "#leverage-input"
tab_positions = "tab-positions"
margin_row = '[col-id="margin"]'
current_margin = "deal-ticket-fee-current-margin"
available_collateral = "deal-ticket-fee-available-collateral"
additional_margin_required = "deal-ticket-fee-additional-margin-required"
liquidation_estimate = "deal-ticket-fee-liquidation-estimate"
dialog_content = "dialog-content"
cross_margin = "cross-margin"
isolated_margin = "isolated-margin"
confirm_cross_margin_mode = "confirm-cross-margin-mode"
confirm_isolated_margin_mode = "confirm-isolated-margin-mode"
def create_position(vega: VegaServiceNull, market_id):
submit_order(vega, "Key 1", market_id, "SIDE_SELL", 100, 110)
@@ -17,6 +27,7 @@ def create_position(vega: VegaServiceNull, market_id):
vega.wait_for_total_catchup
@pytest.mark.skip("tempory disabling of isolated margin")
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_switch_cross_isolated_margin(
continuous_market, vega: VegaServiceNull, page: Page):
@@ -29,10 +40,9 @@ def test_switch_cross_isolated_margin(
expect(page.get_by_test_id(tooltip_content).nth(0)).to_have_text(
"Liquidation: 582.81328Margin: 874.21992General account: 998,084.95183"
)
page.get_by_role("button", name="Isolated 10x").click()
page.locator(leverage_input).clear()
page.locator(leverage_input).type("1")
page.get_by_role("button", name="Confirm").click()
page.get_by_test_id(isolated_margin).click()
page.locator(leverage_input).fill("1")
page.get_by_test_id(confirm_isolated_margin_mode).click()
wait_for_toast_confirmation(page)
next_epoch(vega=vega)
expect(page.get_by_test_id("toast-content")).to_have_text(
@@ -45,9 +55,53 @@ def test_switch_cross_isolated_margin(
expect(page.get_by_test_id(tooltip_content).nth(0)).to_have_text(
"Liquidation: 583.62409Margin: 11,109.99996Order: 11,000.00"
)
page.get_by_role("button", name="Cross").click()
page.get_by_role("button", name="Confirm").click()
page.get_by_test_id(cross_margin).click()
page.get_by_test_id(confirm_cross_margin_mode).click()
wait_for_toast_confirmation(page)
next_epoch(vega=vega)
expect(page.locator(margin_row).nth(1)).to_have_text(
"22,109.99996Cross1.0x")
@pytest.mark.skip("tempory disabling of isolated margin")
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_check_cross_isolated_margin_info(
continuous_market, vega: VegaServiceNull, page: Page):
create_position(vega, continuous_market)
page.goto(f"/#/markets/{continuous_market}")
expect(page.get_by_test_id(current_margin)).to_have_text(
"Current margin874.21992 tDAI")
expect(page.get_by_test_id(available_collateral)
).to_have_text("Available collateral998,084.95183 tDAI")
expect(page.get_by_test_id(additional_margin_required)
).to_have_text("Additional margin required0.00 tDAI")
expect(page.get_by_test_id(liquidation_estimate)
).to_have_text("Liquidation estimate- BTC")
page.get_by_test_id(isolated_margin).click()
page.locator(leverage_input).fill("6")
expect(page.get_by_test_id(dialog_content).get_by_test_id("notification")).to_have_text(
"You have an existing position and and open order on this market.Changing the margin mode and leverage will move 2,703.11341 tDAI from your general account to fund the position.")
expect(page.get_by_test_id(dialog_content).get_by_test_id(current_margin)).to_have_text(
"Current margin874.21992 tDAI")
expect(page.get_by_test_id(dialog_content).get_by_test_id(available_collateral)
).to_have_text("Available collateral998,084.95183 tDAI")
expect(page.get_by_test_id(dialog_content).get_by_test_id(additional_margin_required)
).to_have_text("Additional margin required2,703.11341 tDAI")
expect(page.get_by_test_id(dialog_content).get_by_test_id(liquidation_estimate)
).to_have_text("Liquidation estimate95.23553 BTC")
page.get_by_test_id(confirm_isolated_margin_mode).click()
wait_for_toast_confirmation(page)
next_epoch(vega=vega)
expect(page.get_by_test_id("toast-content")).to_have_text(
"ConfirmedYour transaction has been confirmedView in block explorerUpdate margin modeBTC:DAI_2023Isolated margin mode, leverage: 6.0x")
page.get_by_test_id(cross_margin).click()
expect(page.get_by_test_id(dialog_content).get_by_test_id(
"notification")).not_to_be_visible()
expect(page.get_by_test_id(dialog_content).get_by_test_id(current_margin)).to_have_text(
"Current margin4,223.33332 tDAI")
expect(page.get_by_test_id(dialog_content).get_by_test_id(available_collateral)
).to_have_text("Available collateral995,381.83843 tDAI")
expect(page.get_by_test_id(dialog_content).get_by_test_id(additional_margin_required)
).to_have_text("Additional margin required0.00 tDAI")
expect(page.get_by_test_id(dialog_content).get_by_test_id(liquidation_estimate)
).to_have_text("Liquidation estimate0.00 BTC")
@@ -35,9 +35,9 @@ def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page):
# 6002-MDET-003
expect(page.get_by_test_id("market-price")).to_have_text("Mark Price0.00")
# 6002-MDET-004
expect(page.get_by_test_id("market-change")).to_have_text("Change (24h)0.00%0.00")
expect(page.get_by_test_id("market-change")).to_have_text("Change (24h)-")
# 6002-MDET-005
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)- (- BTC)")
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)-")
# 6002-MDET-008
expect(page.get_by_test_id("market-settlement-asset")).to_have_text(
"Settlement assettDAI"
@@ -4,7 +4,7 @@ from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull
from vega_sim.service import MarketStateUpdateType
from datetime import datetime, timedelta
from conftest import init_vega
from conftest import init_vega, cleanup_container
from actions.utils import change_keys
from actions.vega import submit_multiple_orders
from fixtures.market import setup_perps_market
@@ -17,8 +17,9 @@ col_amount = '[col-id="amount"]'
class TestPerpetuals:
@pytest.fixture(scope="class")
def vega(self, request):
with init_vega(request) as vega:
yield vega
with init_vega(request) as vega_instance:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="class")
def perps_market(self, vega: VegaServiceNull):
+17 -17
View File
@@ -8,10 +8,13 @@ from fixtures.market import setup_continuous_market
from conftest import auth_setup, init_page, init_vega, risk_accepted_setup
from wallet_config import PARTY_A, PARTY_B, PARTY_C, PARTY_D, MM_WALLET
@pytest.fixture(scope="module")
def vega(request):
with init_vega(request) as vega_instance:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
request.addfinalizer(
lambda: cleanup_container(vega_instance)
) # Register the cleanup function
yield vega_instance
@@ -125,7 +128,7 @@ def setup_teams_and_games(vega: VegaServiceNull):
n_top_performers=1,
amount=100,
factor=1.0,
window_length=15
window_length=15,
)
vega.wait_fn(1)
vega.wait_for_total_catchup()
@@ -213,23 +216,22 @@ def create_team(vega: VegaServiceNull):
def test_team_page_games_table(team_page: Page):
team_page.pause()
team_page.get_by_test_id("games-toggle").click()
expect(team_page.get_by_test_id("games-toggle")).to_have_text("Results (10)")
expect(team_page.get_by_test_id("rank-0")).to_have_text("1")
expect(team_page.get_by_test_id("epoch-0")).to_have_text("19")
expect(team_page.get_by_test_id("type-0")
).to_have_text("Price maker fees paid")
#TODO skipped as the amount is wrong
#expect(team_page.get_by_test_id("amount-0")).to_have_text("74") # 50,000,000 on 74.1
expect(team_page.get_by_test_id("type-0")).to_have_text(
"Price maker fees paid • tDAI "
)
# TODO skipped as the amount is wrong
# expect(team_page.get_by_test_id("amount-0")).to_have_text("74") # 50,000,000 on 74.1
expect(team_page.get_by_test_id("participatingTeams-0")).to_have_text("2")
expect(team_page.get_by_test_id("participatingMembers-0")).to_have_text("3")
def test_team_page_members_table(team_page: Page):
team_page.get_by_test_id("members-toggle").click()
expect(team_page.get_by_test_id("members-toggle")
).to_have_text("Members (4)")
expect(team_page.get_by_test_id("members-toggle")).to_have_text("Members (4)")
expect(team_page.get_by_test_id("referee-0")).to_be_visible()
expect(team_page.get_by_test_id("joinedAt-0")).to_be_visible()
expect(team_page.get_by_test_id("joinedAtEpoch-0")).to_have_text("9")
@@ -267,8 +269,7 @@ def test_leaderboard(competitions_page: Page, setup_teams_and_games):
competitions_page.get_by_test_id("rank-0").locator(".text-yellow-300")
).to_have_count(1)
expect(
competitions_page.get_by_test_id(
"rank-1").locator(".text-vega-clight-500")
competitions_page.get_by_test_id("rank-1").locator(".text-vega-clight-500")
).to_have_count(1)
expect(competitions_page.get_by_test_id("team-0")).to_have_text(team_name)
expect(competitions_page.get_by_test_id("status-1")).to_have_text("Open")
@@ -282,17 +283,16 @@ def test_leaderboard(competitions_page: Page, setup_teams_and_games):
def test_game_card(competitions_page: Page):
expect(competitions_page.get_by_test_id(
"active-rewards-card")).to_have_count(2)
expect(competitions_page.get_by_test_id("active-rewards-card")).to_have_count(2)
game_1 = competitions_page.get_by_test_id("active-rewards-card").first
expect(game_1).to_be_visible()
expect(game_1.get_by_test_id("entity-scope")).to_have_text("Individual")
expect(game_1.get_by_test_id("locked-for")).to_have_text("1 epoch")
expect(game_1.get_by_test_id("reward-value")).to_have_text("100.00")
expect(game_1.get_by_test_id("distribution-strategy")
).to_have_text("Pro rata")
expect(game_1.get_by_test_id("dispatch-metric-info")
).to_have_text("Price maker fees paid • tDAI")
expect(game_1.get_by_test_id("distribution-strategy")).to_have_text("Pro rata")
expect(game_1.get_by_test_id("dispatch-metric-info")).to_have_text(
"Price maker fees paid • tDAI"
)
expect(game_1.get_by_test_id("assessed-over")).to_have_text("15 epochs")
expect(game_1.get_by_test_id("scope")).to_have_text("In team")
expect(game_1.get_by_test_id("staking-requirement")).to_have_text("0.00")
+3 -2
View File
@@ -1,9 +1,10 @@
query CurrentEpochInfo {
epoch {
query EpochInfo($epochId: ID) {
epoch(id: $epochId) {
id
timestamps {
start
end
expiry
}
}
}
+1
View File
@@ -17,6 +17,7 @@ fragment GameFields on Game {
id
epoch
numberOfParticipants
rewardAssetId
entities {
... on TeamGameEntity {
...TeamEntity
+20 -16
View File
@@ -3,47 +3,51 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type CurrentEpochInfoQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type EpochInfoQueryVariables = Types.Exact<{
epochId?: Types.InputMaybe<Types.Scalars['ID']>;
}>;
export type CurrentEpochInfoQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null } } };
export type EpochInfoQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null, expiry?: any | null } } };
export const CurrentEpochInfoDocument = gql`
query CurrentEpochInfo {
epoch {
export const EpochInfoDocument = gql`
query EpochInfo($epochId: ID) {
epoch(id: $epochId) {
id
timestamps {
start
end
expiry
}
}
}
`;
/**
* __useCurrentEpochInfoQuery__
* __useEpochInfoQuery__
*
* To run a query within a React component, call `useCurrentEpochInfoQuery` and pass it any options that fit your needs.
* When your component renders, `useCurrentEpochInfoQuery` returns an object from Apollo Client that contains loading, error, and data properties
* To run a query within a React component, call `useEpochInfoQuery` and pass it any options that fit your needs.
* When your component renders, `useEpochInfoQuery` 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 } = useCurrentEpochInfoQuery({
* const { data, loading, error } = useEpochInfoQuery({
* variables: {
* epochId: // value for 'epochId'
* },
* });
*/
export function useCurrentEpochInfoQuery(baseOptions?: Apollo.QueryHookOptions<CurrentEpochInfoQuery, CurrentEpochInfoQueryVariables>) {
export function useEpochInfoQuery(baseOptions?: Apollo.QueryHookOptions<EpochInfoQuery, EpochInfoQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<CurrentEpochInfoQuery, CurrentEpochInfoQueryVariables>(CurrentEpochInfoDocument, options);
return Apollo.useQuery<EpochInfoQuery, EpochInfoQueryVariables>(EpochInfoDocument, options);
}
export function useCurrentEpochInfoLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<CurrentEpochInfoQuery, CurrentEpochInfoQueryVariables>) {
export function useEpochInfoLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<EpochInfoQuery, EpochInfoQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<CurrentEpochInfoQuery, CurrentEpochInfoQueryVariables>(CurrentEpochInfoDocument, options);
return Apollo.useLazyQuery<EpochInfoQuery, EpochInfoQueryVariables>(EpochInfoDocument, options);
}
export type CurrentEpochInfoQueryHookResult = ReturnType<typeof useCurrentEpochInfoQuery>;
export type CurrentEpochInfoLazyQueryHookResult = ReturnType<typeof useCurrentEpochInfoLazyQuery>;
export type CurrentEpochInfoQueryResult = Apollo.QueryResult<CurrentEpochInfoQuery, CurrentEpochInfoQueryVariables>;
export type EpochInfoQueryHookResult = ReturnType<typeof useEpochInfoQuery>;
export type EpochInfoLazyQueryHookResult = ReturnType<typeof useEpochInfoLazyQuery>;
export type EpochInfoQueryResult = Apollo.QueryResult<EpochInfoQuery, EpochInfoQueryVariables>;
+3 -2
View File
@@ -5,14 +5,14 @@ import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type TeamEntityFragment = { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: Types.DispatchMetric, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string, membersParticipating: Array<{ __typename?: 'IndividualGameEntity', individual: string, rank: number }> } };
export type GameFieldsFragment = { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: Types.DispatchMetric, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string, membersParticipating: Array<{ __typename?: 'IndividualGameEntity', individual: string, rank: number }> } }> };
export type GameFieldsFragment = { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, rewardAssetId: string, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: Types.DispatchMetric, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string, membersParticipating: Array<{ __typename?: 'IndividualGameEntity', individual: string, rank: number }> } }> };
export type GamesQueryVariables = Types.Exact<{
epochFrom?: Types.InputMaybe<Types.Scalars['Int']>;
}>;
export type GamesQuery = { __typename?: 'Query', games: { __typename?: 'GamesConnection', edges?: Array<{ __typename?: 'GameEdge', node: { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: Types.DispatchMetric, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string, membersParticipating: Array<{ __typename?: 'IndividualGameEntity', individual: string, rank: number }> } }> } } | null> | null } };
export type GamesQuery = { __typename?: 'Query', games: { __typename?: 'GamesConnection', edges?: Array<{ __typename?: 'GameEdge', node: { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, rewardAssetId: string, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: Types.DispatchMetric, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string, membersParticipating: Array<{ __typename?: 'IndividualGameEntity', individual: string, rank: number }> } }> } } | null> | null } };
export const TeamEntityFragmentDoc = gql`
fragment TeamEntity on TeamGameEntity {
@@ -35,6 +35,7 @@ export const GameFieldsFragmentDoc = gql`
id
epoch
numberOfParticipants
rewardAssetId
entities {
... on TeamGameEntity {
...TeamEntity
+48 -4
View File
@@ -2,12 +2,35 @@ import compact from 'lodash/compact';
import { useActiveRewardsQuery } from '../../components/rewards-container/__generated__/Rewards';
import { isActiveReward } from '../../components/rewards-container/active-rewards';
import {
type RecurringTransfer,
type TransferNode,
EntityScope,
IndividualScope,
type TransferNode,
} from '@vegaprotocol/types';
import {
type AssetFieldsFragment,
useAssetsMapProvider,
} from '@vegaprotocol/assets';
import {
type MarketFieldsFragment,
useMarketsMapProvider,
} from '@vegaprotocol/markets';
import { type ApolloError } from '@apollo/client';
const isScopedToTeams = (node: TransferNode) =>
export type EnrichedTransfer = TransferNode & {
asset?: AssetFieldsFragment | null;
markets?: (MarketFieldsFragment | null)[];
};
type RecurringTransferKind = EnrichedTransfer & {
transfer: {
kind: RecurringTransfer;
};
};
export const isScopedToTeams = (
node: TransferNode
): node is RecurringTransferKind =>
node.transfer.kind.__typename === 'RecurringTransfer' &&
// scoped to teams
(node.transfer.kind.dispatchStrategy?.entityScope ===
@@ -25,7 +48,7 @@ export const useGameCards = ({
}: {
currentEpoch: number;
onlyActive: boolean;
}) => {
}): { data: EnrichedTransfer[]; loading: boolean; error?: ApolloError } => {
const { data, loading, error } = useActiveRewardsQuery({
variables: {
isReward: true,
@@ -33,16 +56,37 @@ export const useGameCards = ({
fetchPolicy: 'cache-and-network',
});
const { data: assets, loading: assetsLoading } = useAssetsMapProvider();
const { data: markets, loading: marketsLoading } = useMarketsMapProvider();
const games = compact(data?.transfersConnection?.edges?.map((n) => n?.node))
.map((n) => n as TransferNode)
.filter((node) => {
const active = onlyActive ? isActiveReward(node, currentEpoch) : true;
return active && isScopedToTeams(node);
})
.map((node) => {
if (node.transfer.kind.__typename !== 'RecurringTransfer') {
return node;
}
const asset =
assets &&
assets[
node.transfer.kind.dispatchStrategy?.dispatchMetricAssetId || ''
];
const marketsInScope =
node.transfer.kind.dispatchStrategy?.marketIdsInScope?.map(
(id) => markets && markets[id]
);
return { ...node, asset, markets: marketsInScope };
});
return {
data: games,
loading,
loading: loading || assetsLoading || marketsLoading,
error,
};
};
+2 -2
View File
@@ -5,7 +5,7 @@ import {
} from './__generated__/Games';
import orderBy from 'lodash/orderBy';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import { useCurrentEpochInfoQuery } from './__generated__/Epoch';
import { useEpochInfoQuery } from './__generated__/Epoch';
import { type ApolloError } from '@apollo/client';
const TAKE_EPOCHS = 30; // TODO: should this be DEFAULT_AGGREGATION_EPOCHS?
@@ -39,7 +39,7 @@ export const useGames = (teamId?: string, epochFrom?: number): GamesData => {
data: epochData,
loading: epochLoading,
error: epochError,
} = useCurrentEpochInfoQuery({
} = useEpochInfoQuery({
skip: Boolean(epochFrom),
});
+1 -1
View File
@@ -7,7 +7,7 @@ import {
type FirstDataRenderedEvent,
type SortChangedEvent,
type GridReadyEvent,
GridApi,
type GridApi,
} from 'ag-grid-community';
import { useCallback, useEffect, useRef } from 'react';
@@ -222,6 +222,7 @@ const CrossMarginModeDialog = ({
<NoWalletWarning noWalletConnected={!partyId} isReadOnly={isReadOnly} />
<Button
className="w-full"
data-testid="confirm-cross-margin-mode"
onClick={() => {
partyId &&
!isReadOnly &&
@@ -329,7 +330,11 @@ const IsolatedMarginModeDialog = ({
marginFactor={`${1 / leverage}`}
/>
<NoWalletWarning noWalletConnected={!partyId} isReadOnly={isReadOnly} />
<Button className="w-full" type="submit">
<Button
className="w-full"
type="submit"
data-testid="confirm-isolated-margin-mode"
>
{t('Confirm')}
</Button>
</form>
@@ -363,6 +368,7 @@ export const MarginModeSelector = ({ marketId }: { marketId: string }) => {
<div className="mb-4 grid h-8 leading-8 font-alpha text-xs grid-cols-2">
<button
type="button"
data-testid="cross-margin"
onClick={() => setDialog('cross')}
className={classnames('rounded', {
[enabledModeClassName]:
@@ -374,6 +380,7 @@ export const MarginModeSelector = ({ marketId }: { marketId: string }) => {
</button>
<button
type="button"
data-testid="isolated-margin"
onClick={() => setDialog('isolated')}
className={classnames('rounded', {
[enabledModeClassName]:
@@ -373,7 +373,10 @@ export const compileFeatureFlags = (refresh = false): FeatureFlags => {
windowOrDefault('NX_STOP_ORDERS', process.env['NX_STOP_ORDERS']) as string
),
ISOLATED_MARGIN: TRUTHY.includes(
windowOrDefault('NX_STOP_ORDERS', process.env['NX_STOP_ORDERS']) as string
windowOrDefault(
'NX_ISOLATED_MARGIN',
process.env['NX_ISOLATED_MARGIN']
) as string
),
SUCCESSOR_MARKETS: TRUTHY.includes(
windowOrDefault(
+12 -4
View File
@@ -55,7 +55,7 @@
"approvers": "Approvers",
"as soon as possible": "now",
"Asset change": "Asset change",
"AssetID": "Asset ID",
"Asset ID: <lozenge>{{id}}</lozenge>": "Asset ID: <lozenge>{{id}}</lozenge>",
"assets": "Assets",
"assetSpecification": "Asset specification",
"associate": "Associate",
@@ -81,6 +81,7 @@
"back": "back",
"backToStaking": "Back to Staking",
"Balance": "Balance",
"Batch proposal": "Batch proposal",
"BenefitTierMinimumActivityStreak": "Minimum activity streak",
"BenefitTierMinimumActivityStreakDescription": "The minimum number of times the party needs to have completed the activity",
"BenefitTierMinimumEpochs": "Minimum epochs",
@@ -109,7 +110,7 @@
"CancelTransfer": "Cancel transfer",
"CancelTransferProposal": "Cancel transfer proposal",
"castYourVote": "Cast your vote",
"Change": "Change",
"Change <lozenge>{{key}}</lozenge> to <lozenge>{{value}}</lozenge>": "Change <lozenge>{{key}}</lozenge> to <lozenge>{{value}}</lozenge>",
"changeVote": "Change vote",
"Check to see if you can redeem unlocked VEGA tokens": "Check to see if you can redeem unlocked $VEGA tokens",
"Check your vesting VEGA tokens": "Check your vesting $VEGA tokens",
@@ -162,7 +163,10 @@
"created": "Created",
"CreateProposalAndDownloadJSONToShare": "Create proposal and download JSON to share",
"currently": "currently",
"currentlySetTo": "Currently expected to ",
"Currently expected to <0>pass</0>": "Currently expected to <0>pass</0>",
"Currently expected to <0>fail</0>": "Currently expected to <0>fail</0>",
"Currently expected to pass: conditions met for {{count}} of {{total}} proposals": "Currently expected to pass: conditions met for {{count}} of {{total}} Proposals",
"Currently expected to fail: {{count}} of {{total}} proposals are passing": "Currently expected to fail: {{count}} of {{total}} proposals are passing",
"CurrentValue": "Current value",
"dataIsIdentical": "Data is identical",
"date": "Date",
@@ -287,7 +291,7 @@
"Keep track of locked tokens in your wallet with the VEGA (VESTING) token.": "Keep track of locked tokens in your wallet with the $VEGA (VESTING) token.",
"latestProposals": "Latest proposals",
"learnMore": "Learn more",
"left to vote": "left to vote",
"{{time}} left to vote": "{{time}} left to vote",
"Link transaction": "Link transaction",
"liquidityComingSoon": "Liquidity rewards coming soon",
"liquidityIntro": "You can read about our incentive program in this <linkToPost>blog post</linkToPost>.",
@@ -421,6 +425,7 @@
"nextEpoch": "Next epoch",
"No holders": "No holders",
"No token": "No token",
"not met": "not met",
"noClosedProposals": "There are no enacted or rejected proposals",
"Node invalid": "Node invalid",
"nodeQueryFailed": "Could not get data for validator {{node}}",
@@ -528,6 +533,8 @@
"ProductMaturityIsPassed": "Product maturity is passed",
"Proposal": "Proposal",
"proposal": "Proposal",
"Proposal passed: conditions met for {{count}} of {{total}} proposals": "Proposal passed: conditions met for {{count}} of {{total}} proposals",
"Proposal failed: {{count}} of {{total}} proposals passed": "Proposal failed: {{count}} of {{total}} proposals passed",
"Proposal rejected": "Proposal rejected",
"proposalCancelTransferDetails": "Cancel governance transfer details",
"proposalChange": "Change <code>{{key}}</code> to <code>{{value}}</code>",
@@ -713,6 +720,7 @@
"submitProposal": "Submit proposal",
"submittingProposal": "Submitting proposal",
"successfullAssociationMessage": "Vega key {{vegaKey}} can now participate in governance and nominate a validator with your associated $VEGA.",
"Successor market to": "Successor market to",
"Switch to form for immediate removal": "Switch to remove now",
"Switch to form for removal at end of epoch": "Switch to remove at end of epoch",
"Symbol": "Symbol",
+4 -1
View File
@@ -2,7 +2,7 @@
"{{liquidityPriceRange}} of mid price": "{{liquidityPriceRange}} of mid price",
"{{probability}} probability price bounds": "{{probability}} probability price bounds",
"24 hour change is unavailable at this time. The price change in the last 120 hours is:": "24 hour change is unavailable at this time. The price change in the last 120 hours is:",
"24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}}": "24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}}",
"24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}} for a total of {{candleVolumePrice}} {{quoteUnit}}": "24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}} for a total of {{candleVolumePrice}} {{quoteUnit}}",
"A concept derived from traditional markets. It is a calculated value for the current market price on a market.": "A concept derived from traditional markets. It is a calculated value for the current market price on a market.",
"A number that will be calculated by an appropriate stochastic risk model, dependent on the type of risk model used and its parameters.": "A number that will be calculated by an appropriate stochastic risk model, dependent on the type of risk model used and its parameters.",
"A sliding penalty for how much an LP bond is slashed if an LP fails to reach the minimum SLA. This is a network parameter.": "A sliding penalty for how much an LP bond is slashed if an LP fails to reach the minimum SLA. This is a network parameter.",
@@ -49,6 +49,9 @@
"Market": "Market",
"Market data": "Market data",
"Market governance": "Market governance",
"Market has not been active for 24 hours. The price change between {{start}} and {{end}} is:": "Market has not been active for 24 hours. The price change between {{start}} and {{end}} is:",
"Market has not been active for 24 hours. The volume traded between {{start}} and {{end}} is:": "Market has not been active for 24 hours. The volume traded between {{start}} and {{end}} is:",
"Market has not been active for 24 hours. The volume traded between {{start}} and {{end}} is {{candleVolumeValue}} for a total of {{candleVolumePrice}} {{quoteUnit}}": "Market has not been active for 24 hours. The volume traded between {{start}} and {{end}} is {{candleVolumeValue}} for a total of {{candleVolumePrice}} {{quoteUnit}}",
"Market ID": "Market ID",
"Market price": "Market price",
"Market specification": "Market specification",
+8 -1
View File
@@ -455,5 +455,12 @@
"Go back to the team's profile": "Go back to the team's profile",
"Go back to the competitions": "Go back to the competitions",
"Your team ID:": "Your team ID:",
"Changes successfully saved to your team.": "Changes successfully saved to your team."
"Changes successfully saved to your team.": "Changes successfully saved to your team.",
"Results {{games}}": "Results {{games}}",
"End time": "End time",
"Reward asset": "Reward asset",
"Daily reward amount": "Daily reward amount",
"Amount earned this epoch": "Amount earned this epoch",
"Cumulative amount earned": "Cumulative amount earned",
"Game details": "Game details"
}
@@ -1,7 +1,8 @@
import { type ReactNode } from 'react';
import {
addDecimalsFormatNumber,
formatNumberPercentage,
isNumeric,
getDateTimeFormat,
priceChange,
priceChangePercentage,
} from '@vegaprotocol/utils';
@@ -14,29 +15,57 @@ import { useT } from '../../use-t';
interface Props {
marketId?: string;
decimalPlaces?: number;
initialValue?: string[];
isHeader?: boolean;
noUpdate?: boolean;
// render prop for no price change
fallback?: React.ReactNode;
decimalPlaces: number;
fallback?: ReactNode;
}
export const Last24hPriceChange = ({
marketId,
decimalPlaces,
initialValue,
fallback,
}: Props) => {
const t = useT();
const { oneDayCandles, error, fiveDaysCandles } = useCandles({
const { oneDayCandles, fiveDaysCandles, error } = useCandles({
marketId,
});
if (
fiveDaysCandles &&
fiveDaysCandles.length > 0 &&
(!oneDayCandles || oneDayCandles?.length === 0)
) {
const nonIdeal = fallback || <span>{'-'}</span>;
if (error || !oneDayCandles || !fiveDaysCandles) {
return nonIdeal;
}
if (fiveDaysCandles.length < 24) {
return (
<Tooltip
description={
<span className="justify-start">
{t(
'Market has not been active for 24 hours. The price change between {{start}} and {{end}} is:',
{
start: getDateTimeFormat().format(
new Date(fiveDaysCandles[0].periodStart)
),
end: getDateTimeFormat().format(
new Date(
fiveDaysCandles[fiveDaysCandles.length - 1].periodStart
)
),
}
)}
<PriceChangeCell
candles={fiveDaysCandles.map((c) => c.close) || []}
decimalPlaces={decimalPlaces}
/>
</span>
}
>
<span>{nonIdeal}</span>
</Tooltip>
);
}
if (oneDayCandles.length < 24) {
return (
<Tooltip
description={
@@ -51,16 +80,12 @@ export const Last24hPriceChange = ({
</span>
}
>
<span>{fallback}</span>
<span>{nonIdeal}</span>
</Tooltip>
);
}
if (error || !isNumeric(decimalPlaces)) {
return <span>{fallback}</span>;
}
const candles = oneDayCandles?.map((c) => c.close) || initialValue || [];
const candles = oneDayCandles?.map((c) => c.close) || [];
const change = priceChange(candles);
const changePercentage = priceChangePercentage(candles);
@@ -2,6 +2,7 @@ import { calcCandleVolume, calcCandleVolumePrice } from '../../market-utils';
import {
addDecimalsFormatNumber,
formatNumber,
getDateTimeFormat,
isNumeric,
} from '@vegaprotocol/utils';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
@@ -26,15 +27,17 @@ export const Last24hVolume = ({
quoteUnit,
}: Props) => {
const t = useT();
const { oneDayCandles, fiveDaysCandles } = useCandles({
const { oneDayCandles, fiveDaysCandles, error } = useCandles({
marketId,
});
if (
fiveDaysCandles &&
fiveDaysCandles.length > 0 &&
(!oneDayCandles || oneDayCandles?.length === 0)
) {
const nonIdeal = <span>{'-'}</span>;
if (error || !oneDayCandles || !fiveDaysCandles) {
return nonIdeal;
}
if (fiveDaysCandles.length < 24) {
const candleVolume = calcCandleVolume(fiveDaysCandles);
const candleVolumePrice = calcCandleVolumePrice(
fiveDaysCandles,
@@ -55,14 +58,59 @@ export const Last24hVolume = ({
<div>
<span className="flex flex-col">
{t(
'24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}} ({{candleVolumePrice}} {{quoteUnit}})',
'Market has not been active for 24 hours. The volume traded between {{start}} and {{end}} is {{candleVolumeValue}} for a total of {{candleVolumePrice}} {{quoteUnit}}',
{
start: getDateTimeFormat().format(
new Date(fiveDaysCandles[0].periodStart)
),
end: getDateTimeFormat().format(
new Date(
fiveDaysCandles[fiveDaysCandles.length - 1].periodStart
)
),
candleVolumeValue,
candleVolumePrice,
quoteUnit,
}
)}
</span>
</div>
}
>
<span>{nonIdeal}</span>
</Tooltip>
);
}
if (oneDayCandles.length < 24) {
const candleVolume = calcCandleVolume(fiveDaysCandles);
const candleVolumePrice = calcCandleVolumePrice(
fiveDaysCandles,
marketDecimals,
positionDecimalPlaces
);
const candleVolumeValue =
candleVolume && isNumeric(positionDecimalPlaces)
? addDecimalsFormatNumber(
candleVolume,
positionDecimalPlaces,
formatDecimals
)
: '-';
return (
<Tooltip
description={
<div>
<span className="flex flex-col">
{t(
'24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}} for a total of ({{candleVolumePrice}} {{quoteUnit}})',
{ candleVolumeValue, candleVolumePrice, quoteUnit }
)}
</span>
</div>
}
>
<span>-</span>
<span>{nonIdeal}</span>
</Tooltip>
);
}
@@ -139,6 +139,8 @@ query MarketInfo($marketId: ID!) {
state
tradingMode
linearSlippageFactor
parentMarketID
successorMarketID
proposal {
id
rationale {
File diff suppressed because one or more lines are too long
@@ -15,7 +15,6 @@ const mockData = [
periodStart: today.toISOString(),
__typename: 'Candle',
},
null,
{
high: '6309988',
low: '6296335',
+6 -1
View File
@@ -18,7 +18,12 @@ export const useCandles = ({ marketId }: { marketId?: string }) => {
skip: !marketId,
});
const fiveDaysCandles = data?.filter(Boolean);
const fiveDaysCandles = data?.filter((c) => {
if (c.open === '' || c.close === '' || c.high === '' || c.close === '') {
return false;
}
return true;
});
const oneDayCandles = fiveDaysCandles?.filter((candle) =>
isCandleLessThan24hOld(candle, yesterday)
+3 -2
View File
@@ -47,8 +47,9 @@ export const marketsProvider = makeDataProvider<
errorPolicy: 'all',
});
export type MarketMap = Record<string, Market>;
export const marketsMapProvider = makeDerivedDataProvider<
Record<string, Market>,
MarketMap,
never,
undefined
>(
@@ -59,7 +60,7 @@ export const marketsMapProvider = makeDerivedDataProvider<
markets[market.id] = market;
return markets;
},
{} as Record<string, Market>
{} as MarketMap
);
}
);
+1 -1
View File
@@ -1,7 +1,7 @@
import { useState, useCallback } from 'react';
import {
OpenVolumeData,
openVolumeDataProvider,
type OpenVolumeData,
} from './positions-data-providers';
import { useDataProvider } from '@vegaprotocol/data-provider';
+23 -1
View File
@@ -1640,6 +1640,8 @@ export type Game = {
id: Scalars['ID'];
/** Number of participants that took part in the game during the epoch. */
numberOfParticipants: Scalars['Int'];
/** ID of asset in which the rewards were paid. */
rewardAssetId: Scalars['ID'];
};
/** Edge type containing the game metrics and cursor information returned by a GameConnection */
@@ -1704,10 +1706,14 @@ export type IndividualGameEntity = {
rank: Scalars['Int'];
/** The rewards earned by the individual during the epoch */
rewardEarned: Scalars['String'];
/** The rewards earned by the individual during the epoch in quantum value */
rewardEarnedQuantum: Scalars['String'];
/** The reward metric applied to the game */
rewardMetric: DispatchMetric;
/** Total rewards earned by the individual during the game */
totalRewardsEarned: Scalars['String'];
/** Total rewards earned by the individual during the game in quantum value */
totalRewardsEarnedQuantum: Scalars['String'];
/** The volume traded by the individual */
volume: Scalars['String'];
};
@@ -4733,10 +4739,18 @@ export type QuantumRewardsPerEpoch = {
__typename?: 'QuantumRewardsPerEpoch';
/** Epoch for which this information is valid. */
epoch: Scalars['Int'];
/** Total of rewards accumulated over the epoch period expressed in quantum value. */
/** Total of rewards accumulated over the epoch period expressed in quantum value. */
totalQuantumRewards: Scalars['String'];
};
export type QuantumVolumesPerEpoch = {
__typename?: 'QuantumVolumesPerEpoch';
/** Epoch for which this information is valid. */
epoch: Scalars['Int'];
/** Total volume across all markets, accumulated over the epoch period, expressed in quantum value. */
totalQuantumVolumes: Scalars['String'];
};
/** Queries allow a caller to read data and filter data via GraphQL. */
export type Query = {
__typename?: 'Query';
@@ -6412,12 +6426,16 @@ export type TeamGameEntity = {
rank: Scalars['Int'];
/** Total rewards earned by the team during the epoch */
rewardEarned: Scalars['String'];
/** Total rewards earned by the team during the epoch in quantum value */
rewardEarnedQuantum: Scalars['String'];
/** Reward metric applied to the game. */
rewardMetric: DispatchMetric;
/** Breakdown of the team members and their contributions to the total team metrics. */
team: TeamParticipation;
/** Total rewards earned by the team for the game */
totalRewardsEarned: Scalars['String'];
/** Total rewards earned by the team for the game in quantum value */
totalRewardsEarnedQuantum: Scalars['String'];
/** Total volume traded by the team */
volume: Scalars['String'];
};
@@ -6431,6 +6449,8 @@ export type TeamMemberStatistics = {
partyId: Scalars['String'];
/** List of rewards over the requested epoch period, expressed in quantum value for each epoch */
quantumRewards: Array<QuantumRewardsPerEpoch>;
/** List of trading volume totals per epoch, for the requested epoch period, expressed in quantum value */
quantumVolumes: Array<QuantumVolumesPerEpoch>;
/** Total number of games played. */
totalGamesPlayed: Scalars['Int'];
/** Total of rewards accumulated over the requested epoch period, expressed in quantum value. */
@@ -6533,6 +6553,8 @@ export type TeamStatistics = {
gamesPlayed: Array<Scalars['String']>;
/** List of rewards over the requested epoch period, expressed in quantum value for each epoch */
quantumRewards: Array<QuantumRewardsPerEpoch>;
/** List of trading volume totals per epoch, over the requested epoch period, expressed in quantum value */
quantumVolumes: Array<QuantumVolumesPerEpoch>;
/** Team ID the statistics are related to. */
teamId: Scalars['String'];
/** Total of games played. */
@@ -66,12 +66,12 @@ export const KeyValueTableRow = ({
id,
}: KeyValueTableRowProps) => {
const dlClassName = classNames(
'flex gap-1 flex-wrap justify-between py-1 text-sm',
'flex gap-1 flex-wrap justify-between py-1 text-sm items-start',
{
'border-b border-neutral-300 dark:border-neutral-700': !noBorder,
},
{ 'flex-col items-start': !inline },
{ 'flex-row items-center': inline },
{ 'flex-col': !inline },
{ 'flex-row': inline },
className
);
const dtClassNames = classNames(