feat(governance): vote status improvements (#4654)
This commit is contained in:
parent
104b2d145d
commit
4f610bbd1b
@ -201,6 +201,8 @@
|
|||||||
"STATE_WAITING_FOR_NODE_VOTE": "Waiting for node vote",
|
"STATE_WAITING_FOR_NODE_VOTE": "Waiting for node vote",
|
||||||
"UpdateNetworkParameter": "Network parameter",
|
"UpdateNetworkParameter": "Network parameter",
|
||||||
"NewFreeform": "Freeform",
|
"NewFreeform": "Freeform",
|
||||||
|
"setToPass": "Set to pass",
|
||||||
|
"setToFail": "Set to fail",
|
||||||
"tokenVotes": "Token votes",
|
"tokenVotes": "Token votes",
|
||||||
"liquidityVotes": "Liquidity votes",
|
"liquidityVotes": "Liquidity votes",
|
||||||
"castYourVote": "Cast your vote",
|
"castYourVote": "Cast your vote",
|
||||||
@ -209,13 +211,23 @@
|
|||||||
"against": "Against",
|
"against": "Against",
|
||||||
"majorityRequired": "Majority Required",
|
"majorityRequired": "Majority Required",
|
||||||
"participation": "Participation",
|
"participation": "Participation",
|
||||||
"met": "Met",
|
"majorityThreshold": "majority threshold",
|
||||||
"notMet": "Not Met",
|
"participationThreshold": "participation threshold",
|
||||||
|
"met": "met",
|
||||||
|
"notMet": "not met",
|
||||||
"governanceRequired": "Required",
|
"governanceRequired": "Required",
|
||||||
"daysLeft": "{{daysLeft}} left to vote.",
|
"daysLeft": "{{daysLeft}} left to vote.",
|
||||||
"toVote": "to vote",
|
"toVote": "to vote",
|
||||||
"voteFor": "Vote for",
|
"voteFor": "Vote for",
|
||||||
"voteAgainst": "Vote against",
|
"voteAgainst": "Vote against",
|
||||||
|
"tokenVote": "Token vote",
|
||||||
|
"tokenVotesFor": "Token votes for",
|
||||||
|
"tokenVotesAgainst": "Token votes against",
|
||||||
|
"totalTokensVoted": "Total tokens voted",
|
||||||
|
"liquidityProviderVote": "Liquidity provider vote",
|
||||||
|
"liquidityProviderVotesFor": "LP votes for",
|
||||||
|
"liquidityProviderVotesAgainst": "LP votes against",
|
||||||
|
"totalLiquidityProviderTokensVoted": "Total LP tokens voted",
|
||||||
"votingThresholdInfo": "If the token vote passes the participation threshold it will be the deciding vote. If not, the outcome will be determined by liquidity providers on this market.",
|
"votingThresholdInfo": "If the token vote passes the participation threshold it will be the deciding vote. If not, the outcome will be determined by liquidity providers on this market.",
|
||||||
"noGovernanceTokens": "You need some VEGA tokens to participate in governance",
|
"noGovernanceTokens": "You need some VEGA tokens to participate in governance",
|
||||||
"youVoted": "You voted",
|
"youVoted": "You voted",
|
||||||
|
@ -31,10 +31,6 @@ import {
|
|||||||
orderByDate,
|
orderByDate,
|
||||||
orderByUpgradeBlockHeight,
|
orderByUpgradeBlockHeight,
|
||||||
} from '../proposals/components/proposals-list/proposals-list';
|
} from '../proposals/components/proposals-list/proposals-list';
|
||||||
import {
|
|
||||||
NetworkParams,
|
|
||||||
useNetworkParams,
|
|
||||||
} from '@vegaprotocol/network-parameters';
|
|
||||||
import { BigNumber } from '../../lib/bignumber';
|
import { BigNumber } from '../../lib/bignumber';
|
||||||
|
|
||||||
const nodesToShow = 6;
|
const nodesToShow = 6;
|
||||||
@ -47,26 +43,8 @@ const HomeProposals = ({
|
|||||||
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
|
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
|
||||||
params: networkParams,
|
|
||||||
loading: networkParamsLoading,
|
|
||||||
error: networkParamsError,
|
|
||||||
} = useNetworkParams([
|
|
||||||
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,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer
|
|
||||||
loading={networkParamsLoading}
|
|
||||||
error={networkParamsError}
|
|
||||||
data={networkParams}
|
|
||||||
>
|
|
||||||
<section className="mb-16" data-testid="home-proposals">
|
<section className="mb-16" data-testid="home-proposals">
|
||||||
<Heading title={t('vegaGovernance')} />
|
<Heading title={t('vegaGovernance')} />
|
||||||
<h3 className="mb-6">{t('homeProposalsIntro')}</h3>
|
<h3 className="mb-6">{t('homeProposalsIntro')}</h3>
|
||||||
@ -83,11 +61,7 @@ const HomeProposals = ({
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
{proposals.map((proposal) => (
|
{proposals.map((proposal) => (
|
||||||
<ProposalsListItem
|
<ProposalsListItem key={proposal.id} proposal={proposal} />
|
||||||
key={proposal.id}
|
|
||||||
proposal={proposal}
|
|
||||||
networkParams={networkParams}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@ -97,7 +71,6 @@ const HomeProposals = ({
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</AsyncRenderer>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -16,7 +16,6 @@ import { ProposalHeader } from './proposal-header';
|
|||||||
import {
|
import {
|
||||||
lastWeek,
|
lastWeek,
|
||||||
nextWeek,
|
nextWeek,
|
||||||
mockNetworkParams,
|
|
||||||
mockWalletContext,
|
mockWalletContext,
|
||||||
createUserVoteQueryMock,
|
createUserVoteQueryMock,
|
||||||
} from '../../test-helpers/mocks';
|
} from '../../test-helpers/mocks';
|
||||||
@ -48,7 +47,6 @@ const renderComponent = (
|
|||||||
<ProposalHeader
|
<ProposalHeader
|
||||||
proposal={proposal}
|
proposal={proposal}
|
||||||
isListItem={isListItem}
|
isListItem={isListItem}
|
||||||
networkParams={mockNetworkParams}
|
|
||||||
voteState={voteState}
|
voteState={voteState}
|
||||||
/>
|
/>
|
||||||
</VegaWalletContext.Provider>
|
</VegaWalletContext.Provider>
|
||||||
|
@ -8,22 +8,19 @@ import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
|||||||
import { truncateMiddle } from '../../../../lib/truncate-middle';
|
import { truncateMiddle } from '../../../../lib/truncate-middle';
|
||||||
import { CurrentProposalState } from '../current-proposal-state';
|
import { CurrentProposalState } from '../current-proposal-state';
|
||||||
import { ProposalInfoLabel } from '../proposal-info-label';
|
import { ProposalInfoLabel } from '../proposal-info-label';
|
||||||
import { ProposalVotingStatus } from '../proposal-voting-status';
|
|
||||||
import type { NetworkParamsResult } from '@vegaprotocol/network-parameters';
|
|
||||||
import { useSuccessorMarketProposalDetails } from '@vegaprotocol/proposals';
|
import { useSuccessorMarketProposalDetails } from '@vegaprotocol/proposals';
|
||||||
import { FLAGS } from '@vegaprotocol/environment';
|
import { FLAGS } from '@vegaprotocol/environment';
|
||||||
import Routes from '../../../routes';
|
import Routes from '../../../routes';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import type { VoteState } from '../vote-details/use-user-vote';
|
import type { VoteState } from '../vote-details/use-user-vote';
|
||||||
|
import { VoteBreakdown } from '../vote-breakdown';
|
||||||
|
|
||||||
export const ProposalHeader = ({
|
export const ProposalHeader = ({
|
||||||
proposal,
|
proposal,
|
||||||
networkParams,
|
|
||||||
isListItem = true,
|
isListItem = true,
|
||||||
voteState,
|
voteState,
|
||||||
}: {
|
}: {
|
||||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||||
networkParams: Partial<NetworkParamsResult>;
|
|
||||||
isListItem?: boolean;
|
isListItem?: boolean;
|
||||||
voteState?: VoteState | null;
|
voteState?: VoteState | null;
|
||||||
}) => {
|
}) => {
|
||||||
@ -146,7 +143,7 @@ export const ProposalHeader = ({
|
|||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
data-testid={`user-voted-${voteState.toLowerCase()}`}
|
data-testid={`user-voted-${voteState.toLowerCase()}`}
|
||||||
>
|
>
|
||||||
<div className="text-vega-green" data-testid="you-voted-icon">
|
<div data-testid="you-voted-icon">
|
||||||
<VegaIcon name={VegaIconNames.VOTE} size={24} />
|
<VegaIcon name={VegaIconNames.VOTE} size={24} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -185,7 +182,7 @@ export const ProposalHeader = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ProposalVotingStatus proposal={proposal} networkParams={networkParams} />
|
<VoteBreakdown proposal={proposal} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export { ProposalVotesTable } from './proposal-votes-table';
|
|
@ -1,119 +0,0 @@
|
|||||||
import { render, screen, fireEvent } from '@testing-library/react';
|
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
|
||||||
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
|
|
||||||
import { ProposalVotesTable } from './proposal-votes-table';
|
|
||||||
import { ProposalType } from '../proposal/proposal';
|
|
||||||
import {
|
|
||||||
generateNoVotes,
|
|
||||||
generateProposal,
|
|
||||||
generateYesVotes,
|
|
||||||
} from '../../test-helpers/generate-proposals';
|
|
||||||
|
|
||||||
const defaultProposal = generateProposal();
|
|
||||||
const defaultProposalType = ProposalType.PROPOSAL_NETWORK_PARAMETER;
|
|
||||||
const updateMarketProposal = generateProposal({
|
|
||||||
terms: {
|
|
||||||
change: {
|
|
||||||
__typename: 'UpdateMarket',
|
|
||||||
marketId: '12345',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
votes: {
|
|
||||||
__typename: 'ProposalVotes',
|
|
||||||
yes: generateYesVotes(10),
|
|
||||||
no: generateNoVotes(0),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const updateMarketProposalType = ProposalType.PROPOSAL_UPDATE_MARKET;
|
|
||||||
|
|
||||||
const renderComponent = (
|
|
||||||
proposal = defaultProposal,
|
|
||||||
proposalType = defaultProposalType
|
|
||||||
) =>
|
|
||||||
render(
|
|
||||||
<MockedProvider>
|
|
||||||
<AppStateProvider>
|
|
||||||
<ProposalVotesTable proposal={proposal} proposalType={proposalType} />
|
|
||||||
</AppStateProvider>
|
|
||||||
</MockedProvider>
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('Proposal Votes Table', () => {
|
|
||||||
it('should render successfully', () => {
|
|
||||||
const { baseElement } = renderComponent();
|
|
||||||
expect(baseElement).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show vote breakdown fields, excluding custom update market fields', () => {
|
|
||||||
renderComponent();
|
|
||||||
fireEvent.click(screen.getByTestId('vote-breakdown-toggle'));
|
|
||||||
expect(screen.getByText('Expected to pass')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Token majority met')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Token participation met')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Tokens for proposal')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Total Supply')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Tokens against proposal')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Participation required')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Majority Required')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Number of voting parties')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Total tokens voted')).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.getByText('Total tokens voted percentage')
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Number of votes for')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Number of votes against')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Yes percentage')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('No percentage')).toBeInTheDocument();
|
|
||||||
expect(screen.queryByText('Liquidity majority met')).toBeNull();
|
|
||||||
expect(screen.queryByText('Liquidity participation met')).toBeNull();
|
|
||||||
expect(screen.queryByText('Liquidity shares for proposal')).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('displays different breakdown fields for update market proposal', () => {
|
|
||||||
renderComponent(updateMarketProposal, updateMarketProposalType);
|
|
||||||
fireEvent.click(screen.getByTestId('vote-breakdown-toggle'));
|
|
||||||
expect(screen.getByText('Liquidity majority met')).toBeInTheDocument();
|
|
||||||
expect(screen.getByText('Liquidity participation met')).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.getByText('Liquidity shares for proposal')
|
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(screen.queryByText('Number of voting parties')).toBeNull();
|
|
||||||
expect(screen.queryByText('Total tokens voted')).toBeNull();
|
|
||||||
expect(screen.queryByText('Total tokens voted percentage')).toBeNull();
|
|
||||||
expect(screen.queryByText('Number of votes for')).toBeNull();
|
|
||||||
expect(screen.queryByText('Number of votes against')).toBeNull();
|
|
||||||
expect(screen.queryByText('Yes percentage')).toBeNull();
|
|
||||||
expect(screen.queryByText('No percentage')).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('displays if an update market proposal will pass by token vote', () => {
|
|
||||||
renderComponent(updateMarketProposal, updateMarketProposalType);
|
|
||||||
fireEvent.click(screen.getByTestId('vote-breakdown-toggle'));
|
|
||||||
expect(screen.getByText('👍 by token vote')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('displays if an update market proposal will pass by LP vote', () => {
|
|
||||||
renderComponent(
|
|
||||||
generateProposal({
|
|
||||||
terms: {
|
|
||||||
change: {
|
|
||||||
__typename: 'UpdateMarket',
|
|
||||||
marketId: '12345',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
votes: {
|
|
||||||
__typename: 'ProposalVotes',
|
|
||||||
yes: {
|
|
||||||
...generateYesVotes(0, 1, '10'),
|
|
||||||
},
|
|
||||||
no: {
|
|
||||||
...generateNoVotes(0, 1, '0'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
updateMarketProposalType
|
|
||||||
);
|
|
||||||
fireEvent.click(screen.getByTestId('vote-breakdown-toggle'));
|
|
||||||
expect(screen.getByText('👍 by liquidity vote')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,177 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import {
|
|
||||||
KeyValueTable,
|
|
||||||
KeyValueTableRow,
|
|
||||||
Thumbs,
|
|
||||||
RoundedWrapper,
|
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { formatNumber, formatNumberPercentage } from '@vegaprotocol/utils';
|
|
||||||
import { SubHeading } from '../../../../components/heading';
|
|
||||||
import { useVoteInformation } from '../../hooks';
|
|
||||||
import { useAppState } from '../../../../contexts/app-state/app-state-context';
|
|
||||||
import { ProposalType } from '../proposal/proposal';
|
|
||||||
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
|
|
||||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
|
||||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
|
||||||
|
|
||||||
interface ProposalVotesTableProps {
|
|
||||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
|
||||||
proposalType: ProposalType | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ProposalVotesTable = ({
|
|
||||||
proposal,
|
|
||||||
proposalType,
|
|
||||||
}: ProposalVotesTableProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const {
|
|
||||||
appState: { totalSupply },
|
|
||||||
} = useAppState();
|
|
||||||
const [showDetails, setShowDetails] = useState(false);
|
|
||||||
const {
|
|
||||||
willPassByTokenVote,
|
|
||||||
willPassByLPVote,
|
|
||||||
totalTokensPercentage,
|
|
||||||
participationMet,
|
|
||||||
participationLPMet,
|
|
||||||
totalTokensVoted,
|
|
||||||
noPercentage,
|
|
||||||
yesPercentage,
|
|
||||||
noTokens,
|
|
||||||
yesTokens,
|
|
||||||
yesEquityLikeShareWeight,
|
|
||||||
yesVotes,
|
|
||||||
noVotes,
|
|
||||||
totalVotes,
|
|
||||||
requiredMajorityPercentage,
|
|
||||||
requiredParticipation,
|
|
||||||
majorityMet,
|
|
||||||
majorityLPMet,
|
|
||||||
} = useVoteInformation({ proposal });
|
|
||||||
|
|
||||||
const isUpdateMarket = proposalType === ProposalType.PROPOSAL_UPDATE_MARKET;
|
|
||||||
const updateMarketWillPass = willPassByTokenVote || willPassByLPVote;
|
|
||||||
const updateMarketVotePassMethod = willPassByTokenVote
|
|
||||||
? t('byTokenVote')
|
|
||||||
: t('byLiquidityVote');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<CollapsibleToggle
|
|
||||||
toggleState={showDetails}
|
|
||||||
setToggleState={setShowDetails}
|
|
||||||
dataTestId="vote-breakdown-toggle"
|
|
||||||
>
|
|
||||||
<SubHeading title={t('voteBreakdown')} />
|
|
||||||
</CollapsibleToggle>
|
|
||||||
|
|
||||||
{showDetails && (
|
|
||||||
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
|
|
||||||
<KeyValueTable
|
|
||||||
data-testid="proposal-votes-table"
|
|
||||||
numerical={true}
|
|
||||||
headingLevel={4}
|
|
||||||
>
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('expectedToPass')}
|
|
||||||
{isUpdateMarket ? (
|
|
||||||
updateMarketWillPass ? (
|
|
||||||
<Thumbs up={true} text={updateMarketVotePassMethod} />
|
|
||||||
) : (
|
|
||||||
<Thumbs up={false} />
|
|
||||||
)
|
|
||||||
) : willPassByTokenVote ? (
|
|
||||||
<Thumbs up={true} />
|
|
||||||
) : (
|
|
||||||
<Thumbs up={false} />
|
|
||||||
)}
|
|
||||||
</KeyValueTableRow>
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('majorityMet')}
|
|
||||||
{majorityMet ? <Thumbs up={true} /> : <Thumbs up={false} />}
|
|
||||||
</KeyValueTableRow>
|
|
||||||
{isUpdateMarket && (
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('majorityLPMet')}
|
|
||||||
{majorityLPMet ? <Thumbs up={true} /> : <Thumbs up={false} />}
|
|
||||||
</KeyValueTableRow>
|
|
||||||
)}
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('participationMet')}
|
|
||||||
{participationMet ? <Thumbs up={true} /> : <Thumbs up={false} />}
|
|
||||||
</KeyValueTableRow>
|
|
||||||
{isUpdateMarket && (
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('participationLPMet')}
|
|
||||||
{participationLPMet ? (
|
|
||||||
<Thumbs up={true} />
|
|
||||||
) : (
|
|
||||||
<Thumbs up={false} />
|
|
||||||
)}
|
|
||||||
</KeyValueTableRow>
|
|
||||||
)}
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('tokenForProposal')}
|
|
||||||
{formatNumber(yesTokens, 2)}
|
|
||||||
</KeyValueTableRow>
|
|
||||||
{isUpdateMarket && (
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('tokenLPForProposal')}
|
|
||||||
{formatNumber(yesEquityLikeShareWeight, 2)}
|
|
||||||
</KeyValueTableRow>
|
|
||||||
)}
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('totalSupply')}
|
|
||||||
{formatNumber(totalSupply, 2)}
|
|
||||||
</KeyValueTableRow>
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('tokensAgainstProposal')}
|
|
||||||
{formatNumber(noTokens, 2)}
|
|
||||||
</KeyValueTableRow>
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('participationRequired')}
|
|
||||||
{formatNumberPercentage(requiredParticipation)}
|
|
||||||
</KeyValueTableRow>
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('majorityRequired')}
|
|
||||||
{formatNumberPercentage(requiredMajorityPercentage)}
|
|
||||||
</KeyValueTableRow>
|
|
||||||
{!isUpdateMarket && (
|
|
||||||
<>
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('numberOfVotingParties')}
|
|
||||||
{formatNumber(totalVotes, 0)}
|
|
||||||
</KeyValueTableRow>
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('totalTokensVotes')}
|
|
||||||
{formatNumber(totalTokensVoted, 2)}
|
|
||||||
</KeyValueTableRow>
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('totalTokenVotedPercentage')}
|
|
||||||
{formatNumberPercentage(totalTokensPercentage, 2)}
|
|
||||||
</KeyValueTableRow>
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('numberOfForVotes')}
|
|
||||||
{formatNumber(yesVotes, 0)}
|
|
||||||
</KeyValueTableRow>
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('numberOfAgainstVotes')}
|
|
||||||
{formatNumber(noVotes, 0)}
|
|
||||||
</KeyValueTableRow>
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('yesPercentage')}
|
|
||||||
{formatNumberPercentage(yesPercentage, 2)}
|
|
||||||
</KeyValueTableRow>
|
|
||||||
<KeyValueTableRow noBorder={true}>
|
|
||||||
{t('noPercentage')}
|
|
||||||
{formatNumberPercentage(noPercentage, 2)}
|
|
||||||
</KeyValueTableRow>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</KeyValueTable>
|
|
||||||
</RoundedWrapper>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1 +0,0 @@
|
|||||||
export * from './proposal-voting-status';
|
|
@ -1,153 +0,0 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import { BrowserRouter as Router } from 'react-router-dom';
|
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
|
||||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
|
||||||
import {
|
|
||||||
lastWeek,
|
|
||||||
mockWalletContext,
|
|
||||||
networkParamsQueryMock,
|
|
||||||
nextWeek,
|
|
||||||
mockNetworkParams,
|
|
||||||
} from '../../test-helpers/mocks';
|
|
||||||
import { ProposalVotingStatus } from './proposal-voting-status';
|
|
||||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
|
||||||
import type { MockedResponse } from '@apollo/client/testing';
|
|
||||||
import {
|
|
||||||
generateNoVotes,
|
|
||||||
generateProposal,
|
|
||||||
generateYesVotes,
|
|
||||||
} from '../../test-helpers/generate-proposals';
|
|
||||||
import { ProposalState } from '@vegaprotocol/types';
|
|
||||||
import { BigNumber } from '../../../../lib/bignumber';
|
|
||||||
import type { AppState } from '../../../../contexts/app-state/app-state-context';
|
|
||||||
|
|
||||||
const mockTotalSupply = new BigNumber(100);
|
|
||||||
// Note - giving a fixedTokenValue of 1 means a ratio of 1:1 votes to tokens, making sums easier :)
|
|
||||||
const fixedTokenValue = 1000000000000000000;
|
|
||||||
|
|
||||||
const mockAppState: AppState = {
|
|
||||||
totalAssociated: new BigNumber('50063005'),
|
|
||||||
decimals: 18,
|
|
||||||
totalSupply: mockTotalSupply,
|
|
||||||
vegaWalletManageOverlay: false,
|
|
||||||
transactionOverlay: false,
|
|
||||||
bannerMessage: '',
|
|
||||||
disconnectNotice: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
|
|
||||||
useAppState: () => ({
|
|
||||||
appState: mockAppState,
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const renderComponent = (
|
|
||||||
proposal: ProposalQuery['proposal'],
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
mocks: MockedResponse<any>[] = [networkParamsQueryMock]
|
|
||||||
) =>
|
|
||||||
render(
|
|
||||||
<Router>
|
|
||||||
<MockedProvider mocks={mocks}>
|
|
||||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
|
||||||
<ProposalVotingStatus
|
|
||||||
proposal={proposal}
|
|
||||||
networkParams={mockNetworkParams}
|
|
||||||
/>
|
|
||||||
</VegaWalletContext.Provider>
|
|
||||||
</MockedProvider>
|
|
||||||
</Router>
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('ProposalVotingStatus', () => {
|
|
||||||
beforeAll(() => {
|
|
||||||
jest.useFakeTimers();
|
|
||||||
jest.setSystemTime(0);
|
|
||||||
});
|
|
||||||
afterAll(() => {
|
|
||||||
jest.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Renders majority reached', () => {
|
|
||||||
const yesVotes = 100;
|
|
||||||
const noVotes = 0;
|
|
||||||
|
|
||||||
renderComponent(
|
|
||||||
generateProposal({
|
|
||||||
state: ProposalState.STATE_PASSED,
|
|
||||||
terms: {
|
|
||||||
closingDatetime: lastWeek.toString(),
|
|
||||||
enactmentDatetime: nextWeek.toString(),
|
|
||||||
},
|
|
||||||
votes: {
|
|
||||||
__typename: 'ProposalVotes',
|
|
||||||
yes: generateYesVotes(yesVotes, fixedTokenValue),
|
|
||||||
no: generateNoVotes(noVotes, fixedTokenValue),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(screen.getByTestId('majority-reached')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Renders majority not reached', () => {
|
|
||||||
const yesVotes = 20;
|
|
||||||
const noVotes = 80;
|
|
||||||
|
|
||||||
renderComponent(
|
|
||||||
generateProposal({
|
|
||||||
state: ProposalState.STATE_PASSED,
|
|
||||||
terms: {
|
|
||||||
closingDatetime: lastWeek.toString(),
|
|
||||||
enactmentDatetime: nextWeek.toString(),
|
|
||||||
},
|
|
||||||
votes: {
|
|
||||||
__typename: 'ProposalVotes',
|
|
||||||
yes: generateYesVotes(yesVotes, fixedTokenValue),
|
|
||||||
no: generateNoVotes(noVotes, fixedTokenValue),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(screen.getByTestId('majority-not-reached')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Renders participation reached', () => {
|
|
||||||
const yesVotes = 1000;
|
|
||||||
const noVotes = 0;
|
|
||||||
|
|
||||||
renderComponent(
|
|
||||||
generateProposal({
|
|
||||||
state: ProposalState.STATE_PASSED,
|
|
||||||
terms: {
|
|
||||||
closingDatetime: lastWeek.toString(),
|
|
||||||
enactmentDatetime: nextWeek.toString(),
|
|
||||||
},
|
|
||||||
votes: {
|
|
||||||
__typename: 'ProposalVotes',
|
|
||||||
yes: generateYesVotes(yesVotes, fixedTokenValue),
|
|
||||||
no: generateNoVotes(noVotes, fixedTokenValue),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(screen.getByTestId('participation-reached')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Renders participation not reached', () => {
|
|
||||||
const yesVotes = 0;
|
|
||||||
const noVotes = 0;
|
|
||||||
|
|
||||||
renderComponent(
|
|
||||||
generateProposal({
|
|
||||||
terms: {
|
|
||||||
closingDatetime: lastWeek.toString(),
|
|
||||||
enactmentDatetime: nextWeek.toString(),
|
|
||||||
},
|
|
||||||
votes: {
|
|
||||||
__typename: 'ProposalVotes',
|
|
||||||
yes: generateYesVotes(yesVotes, fixedTokenValue),
|
|
||||||
no: generateNoVotes(noVotes, fixedTokenValue),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(screen.getByTestId('participation-not-reached')).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,174 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { Icon } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { useVoteInformation } from '../../hooks';
|
|
||||||
import { BigNumber } from '../../../../lib/bignumber';
|
|
||||||
import type { NetworkParamsResult } from '@vegaprotocol/network-parameters';
|
|
||||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
|
||||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
|
||||||
|
|
||||||
const statusClasses = (reached: boolean) =>
|
|
||||||
classNames('flex items-center gap-2 px-4 py-2 rounded-md', {
|
|
||||||
'bg-vega-green-700': reached,
|
|
||||||
'bg-vega-red-700': !reached,
|
|
||||||
});
|
|
||||||
|
|
||||||
const MajorityStatus = ({
|
|
||||||
reached,
|
|
||||||
requiredMajority,
|
|
||||||
}: {
|
|
||||||
reached: boolean;
|
|
||||||
requiredMajority: string | null | undefined;
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={statusClasses(reached)}
|
|
||||||
data-testid="proposal-majority-status"
|
|
||||||
>
|
|
||||||
{reached ? <Icon name="tick" /> : <Icon name="cross" />}
|
|
||||||
{reached ? (
|
|
||||||
<div data-testid="majority-reached">
|
|
||||||
{requiredMajority ? (
|
|
||||||
<>
|
|
||||||
{new BigNumber(requiredMajority).times(100).toString()}%{' '}
|
|
||||||
{t('majorityVotedForProposal')}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
t('requiredMajorityVotedForProposal')
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div data-testid="majority-not-reached">
|
|
||||||
{requiredMajority ? (
|
|
||||||
<>
|
|
||||||
{new BigNumber(requiredMajority).times(100).toString()}%{' '}
|
|
||||||
{t('majorityNotVotedForProposal')}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
t('requiredMajorityNotVotedForProposal')
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ParticipationStatus = ({ reached }: { reached: boolean }) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={statusClasses(reached)}>
|
|
||||||
{reached ? (
|
|
||||||
<>
|
|
||||||
<Icon name="tick" />
|
|
||||||
<div data-testid="participation-reached">
|
|
||||||
{t('minParticipationReached')}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Icon name="cross" />
|
|
||||||
<div data-testid="participation-not-reached">
|
|
||||||
{t('minParticipationNotReached')}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ProposalVotingStatus = ({
|
|
||||||
proposal,
|
|
||||||
networkParams,
|
|
||||||
}: {
|
|
||||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
|
||||||
networkParams: Partial<NetworkParamsResult>;
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { majorityMet, majorityLPMet, participationMet, participationLPMet } =
|
|
||||||
useVoteInformation({
|
|
||||||
proposal,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!proposal) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUpdateMarket = proposal?.terms.change.__typename === 'UpdateMarket';
|
|
||||||
|
|
||||||
let requiredVotingMajority = null;
|
|
||||||
let requiredVotingMajorityLP = null;
|
|
||||||
|
|
||||||
if (networkParams) {
|
|
||||||
switch (proposal.terms.change.__typename) {
|
|
||||||
case 'NewMarket':
|
|
||||||
requiredVotingMajority =
|
|
||||||
networkParams.governance_proposal_market_requiredMajority;
|
|
||||||
break;
|
|
||||||
case 'UpdateMarket':
|
|
||||||
requiredVotingMajority =
|
|
||||||
networkParams.governance_proposal_updateMarket_requiredMajority;
|
|
||||||
requiredVotingMajorityLP =
|
|
||||||
networkParams.governance_proposal_updateMarket_requiredMajorityLP;
|
|
||||||
break;
|
|
||||||
case 'NewAsset':
|
|
||||||
requiredVotingMajority =
|
|
||||||
networkParams.governance_proposal_asset_requiredMajority;
|
|
||||||
break;
|
|
||||||
case 'UpdateAsset':
|
|
||||||
requiredVotingMajority =
|
|
||||||
networkParams.governance_proposal_updateAsset_requiredMajority;
|
|
||||||
break;
|
|
||||||
case 'UpdateNetworkParameter':
|
|
||||||
requiredVotingMajority =
|
|
||||||
networkParams.governance_proposal_updateNetParam_requiredMajority;
|
|
||||||
break;
|
|
||||||
case 'NewFreeform':
|
|
||||||
requiredVotingMajority =
|
|
||||||
networkParams.governance_proposal_freeform_requiredMajority;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isUpdateMarket) {
|
|
||||||
return (
|
|
||||||
<div className="mb-6">
|
|
||||||
<p>{t('Token vote')}</p>
|
|
||||||
<div
|
|
||||||
className="grid grid-cols-2 gap-4 items-center"
|
|
||||||
data-testid="token-vote-statuses"
|
|
||||||
>
|
|
||||||
<MajorityStatus
|
|
||||||
reached={majorityMet}
|
|
||||||
requiredMajority={requiredVotingMajority}
|
|
||||||
/>{' '}
|
|
||||||
<ParticipationStatus reached={participationMet} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="mt-4">{t('Liquidity provider vote')}</p>
|
|
||||||
<div
|
|
||||||
className="grid grid-cols-2 gap-4 items-center"
|
|
||||||
data-testid="lp-vote-statuses"
|
|
||||||
>
|
|
||||||
<MajorityStatus
|
|
||||||
reached={majorityLPMet}
|
|
||||||
requiredMajority={requiredVotingMajorityLP}
|
|
||||||
/>{' '}
|
|
||||||
<ParticipationStatus reached={participationLPMet} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="grid grid-cols-2 gap-4 items-center mb-6">
|
|
||||||
<MajorityStatus
|
|
||||||
reached={majorityMet}
|
|
||||||
requiredMajority={requiredVotingMajority}
|
|
||||||
/>{' '}
|
|
||||||
<ParticipationStatus reached={participationMet} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -34,12 +34,6 @@ jest.mock('../proposal-change-table', () => ({
|
|||||||
jest.mock('../proposal-json', () => ({
|
jest.mock('../proposal-json', () => ({
|
||||||
ProposalJson: () => <div data-testid="proposal-json"></div>,
|
ProposalJson: () => <div data-testid="proposal-json"></div>,
|
||||||
}));
|
}));
|
||||||
jest.mock('../proposal-votes-table', () => ({
|
|
||||||
ProposalVotesTable: () => <div data-testid="proposal-votes-table"></div>,
|
|
||||||
}));
|
|
||||||
jest.mock('../vote-details', () => ({
|
|
||||||
VoteDetails: () => <div data-testid="proposal-vote-details"></div>,
|
|
||||||
}));
|
|
||||||
jest.mock('../list-asset', () => ({
|
jest.mock('../list-asset', () => ({
|
||||||
ListAsset: () => <div data-testid="proposal-list-asset"></div>,
|
ListAsset: () => <div data-testid="proposal-list-asset"></div>,
|
||||||
}));
|
}));
|
||||||
@ -104,8 +98,6 @@ it('renders each section', async () => {
|
|||||||
expect(await screen.findByTestId('proposal-header')).toBeInTheDocument();
|
expect(await screen.findByTestId('proposal-header')).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('proposal-change-table')).toBeInTheDocument();
|
expect(screen.getByTestId('proposal-change-table')).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('proposal-json')).toBeInTheDocument();
|
expect(screen.getByTestId('proposal-json')).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('proposal-votes-table')).toBeInTheDocument();
|
|
||||||
expect(screen.getByTestId('proposal-vote-details')).toBeInTheDocument();
|
|
||||||
expect(screen.queryByTestId('proposal-list-asset')).not.toBeInTheDocument();
|
expect(screen.queryByTestId('proposal-list-asset')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -5,9 +5,8 @@ import { ProposalHeader } from '../proposal-detail-header/proposal-header';
|
|||||||
import { ProposalDescription } from '../proposal-description';
|
import { ProposalDescription } from '../proposal-description';
|
||||||
import { ProposalChangeTable } from '../proposal-change-table';
|
import { ProposalChangeTable } from '../proposal-change-table';
|
||||||
import { ProposalJson } from '../proposal-json';
|
import { ProposalJson } from '../proposal-json';
|
||||||
import { ProposalVotesTable } from '../proposal-votes-table';
|
|
||||||
import { ProposalAssetDetails } from '../proposal-asset-details';
|
import { ProposalAssetDetails } from '../proposal-asset-details';
|
||||||
import { VoteDetails } from '../vote-details';
|
import { UserVote } from '../vote-details';
|
||||||
import { ListAsset } from '../list-asset';
|
import { ListAsset } from '../list-asset';
|
||||||
import Routes from '../../../routes';
|
import Routes from '../../../routes';
|
||||||
import { ProposalMarketData } from '../proposal-market-data';
|
import { ProposalMarketData } from '../proposal-market-data';
|
||||||
@ -22,14 +21,6 @@ import type { NetworkParamsResult } from '@vegaprotocol/network-parameters';
|
|||||||
import { useVoteSubmit } from '@vegaprotocol/proposals';
|
import { useVoteSubmit } from '@vegaprotocol/proposals';
|
||||||
import { useUserVote } from '../vote-details/use-user-vote';
|
import { useUserVote } from '../vote-details/use-user-vote';
|
||||||
|
|
||||||
export enum ProposalType {
|
|
||||||
PROPOSAL_NEW_MARKET = 'PROPOSAL_NEW_MARKET',
|
|
||||||
PROPOSAL_UPDATE_MARKET = 'PROPOSAL_UPDATE_MARKET',
|
|
||||||
PROPOSAL_NEW_ASSET = 'PROPOSAL_NEW_ASSET',
|
|
||||||
PROPOSAL_UPDATE_ASSET = 'PROPOSAL_UPDATE_ASSET',
|
|
||||||
PROPOSAL_NETWORK_PARAMETER = 'PROPOSAL_NETWORK_PARAMETER',
|
|
||||||
PROPOSAL_FREEFORM = 'PROPOSAL_FREEFORM',
|
|
||||||
}
|
|
||||||
export interface ProposalProps {
|
export interface ProposalProps {
|
||||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||||
networkParams: Partial<NetworkParamsResult>;
|
networkParams: Partial<NetworkParamsResult>;
|
||||||
@ -80,39 +71,32 @@ export const Proposal = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
let minVoterBalance = null;
|
let minVoterBalance = null;
|
||||||
let proposalType = null;
|
|
||||||
|
|
||||||
if (networkParams) {
|
if (networkParams) {
|
||||||
switch (proposal.terms.change.__typename) {
|
switch (proposal.terms.change.__typename) {
|
||||||
case 'NewMarket':
|
case 'NewMarket':
|
||||||
minVoterBalance =
|
minVoterBalance =
|
||||||
networkParams.governance_proposal_market_minVoterBalance;
|
networkParams.governance_proposal_market_minVoterBalance;
|
||||||
proposalType = ProposalType.PROPOSAL_NEW_MARKET;
|
|
||||||
break;
|
break;
|
||||||
case 'UpdateMarket':
|
case 'UpdateMarket':
|
||||||
minVoterBalance =
|
minVoterBalance =
|
||||||
networkParams.governance_proposal_updateMarket_minVoterBalance;
|
networkParams.governance_proposal_updateMarket_minVoterBalance;
|
||||||
proposalType = ProposalType.PROPOSAL_UPDATE_MARKET;
|
|
||||||
break;
|
break;
|
||||||
case 'NewAsset':
|
case 'NewAsset':
|
||||||
minVoterBalance =
|
minVoterBalance =
|
||||||
networkParams.governance_proposal_asset_minVoterBalance;
|
networkParams.governance_proposal_asset_minVoterBalance;
|
||||||
proposalType = ProposalType.PROPOSAL_NEW_ASSET;
|
|
||||||
break;
|
break;
|
||||||
case 'UpdateAsset':
|
case 'UpdateAsset':
|
||||||
minVoterBalance =
|
minVoterBalance =
|
||||||
networkParams.governance_proposal_updateAsset_minVoterBalance;
|
networkParams.governance_proposal_updateAsset_minVoterBalance;
|
||||||
proposalType = ProposalType.PROPOSAL_UPDATE_ASSET;
|
|
||||||
break;
|
break;
|
||||||
case 'UpdateNetworkParameter':
|
case 'UpdateNetworkParameter':
|
||||||
minVoterBalance =
|
minVoterBalance =
|
||||||
networkParams.governance_proposal_updateNetParam_minVoterBalance;
|
networkParams.governance_proposal_updateNetParam_minVoterBalance;
|
||||||
proposalType = ProposalType.PROPOSAL_NETWORK_PARAMETER;
|
|
||||||
break;
|
break;
|
||||||
case 'NewFreeform':
|
case 'NewFreeform':
|
||||||
minVoterBalance =
|
minVoterBalance =
|
||||||
networkParams.governance_proposal_freeform_minVoterBalance;
|
networkParams.governance_proposal_freeform_minVoterBalance;
|
||||||
proposalType = ProposalType.PROPOSAL_FREEFORM;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -140,11 +124,9 @@ export const Proposal = ({
|
|||||||
<ProposalHeader
|
<ProposalHeader
|
||||||
proposal={proposal}
|
proposal={proposal}
|
||||||
isListItem={false}
|
isListItem={false}
|
||||||
networkParams={networkParams}
|
|
||||||
voteState={voteState}
|
voteState={voteState}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div id="details">
|
|
||||||
<div className="my-10">
|
<div className="my-10">
|
||||||
<ProposalChangeTable proposal={proposal} />
|
<ProposalChangeTable proposal={proposal} />
|
||||||
</div>
|
</div>
|
||||||
@ -180,8 +162,8 @@ export const Proposal = ({
|
|||||||
?.changes || {}
|
?.changes || {}
|
||||||
}
|
}
|
||||||
latestEnactedProposal={
|
latestEnactedProposal={
|
||||||
mostRecentlyEnactedAssociatedMarketProposal?.node?.proposal
|
mostRecentlyEnactedAssociatedMarketProposal?.node?.proposal?.terms
|
||||||
?.terms?.updateMarket?.changes || {}
|
?.updateMarket?.changes || {}
|
||||||
}
|
}
|
||||||
updatedProposal={
|
updatedProposal={
|
||||||
restData?.data?.proposal?.terms?.updateMarket?.changes || {}
|
restData?.data?.proposal?.terms?.updateMarket?.changes || {}
|
||||||
@ -198,17 +180,10 @@ export const Proposal = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mb-6">
|
|
||||||
<ProposalJson proposal={restData?.data?.proposal} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="voting">
|
|
||||||
<div className="mb-10">
|
<div className="mb-10">
|
||||||
<RoundedWrapper paddingBottom={true}>
|
<RoundedWrapper paddingBottom={true}>
|
||||||
<VoteDetails
|
<UserVote
|
||||||
proposal={proposal}
|
proposal={proposal}
|
||||||
proposalType={proposalType}
|
|
||||||
minVoterBalance={minVoterBalance}
|
minVoterBalance={minVoterBalance}
|
||||||
spamProtectionMinTokens={
|
spamProtectionMinTokens={
|
||||||
networkParams?.spam_protection_voting_min_tokens
|
networkParams?.spam_protection_voting_min_tokens
|
||||||
@ -222,9 +197,8 @@ export const Proposal = ({
|
|||||||
</RoundedWrapper>
|
</RoundedWrapper>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-4">
|
<div className="mb-6">
|
||||||
<ProposalVotesTable proposal={proposal} proposalType={proposalType} />
|
<ProposalJson proposal={restData?.data?.proposal} />
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
@ -6,11 +6,7 @@ import { MockedProvider } from '@apollo/client/testing';
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { ProposalRejectionReason, ProposalState } from '@vegaprotocol/types';
|
import { ProposalRejectionReason, ProposalState } from '@vegaprotocol/types';
|
||||||
import {
|
import { generateProposal } from '../../test-helpers/generate-proposals';
|
||||||
generateNoVotes,
|
|
||||||
generateProposal,
|
|
||||||
generateYesVotes,
|
|
||||||
} from '../../test-helpers/generate-proposals';
|
|
||||||
import { ProposalsListItemDetails } from './proposals-list-item-details';
|
import { ProposalsListItemDetails } from './proposals-list-item-details';
|
||||||
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
|
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
|
||||||
import {
|
import {
|
||||||
@ -93,84 +89,6 @@ describe('Proposals list item details', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Renders proposal state: Update market proposal - Currently expected to pass by LP vote', () => {
|
|
||||||
renderComponent(
|
|
||||||
generateProposal({
|
|
||||||
state: ProposalState.STATE_OPEN,
|
|
||||||
terms: {
|
|
||||||
change: {
|
|
||||||
__typename: 'UpdateMarket',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
votes: {
|
|
||||||
yes: {
|
|
||||||
...generateYesVotes(0),
|
|
||||||
totalEquityLikeShareWeight: '1000',
|
|
||||||
},
|
|
||||||
no: {
|
|
||||||
...generateNoVotes(0),
|
|
||||||
totalEquityLikeShareWeight: '0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
|
||||||
'Currently expected to pass by LP vote'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Renders proposal state: Update market proposal - Currently expected to pass by token vote', () => {
|
|
||||||
renderComponent(
|
|
||||||
generateProposal({
|
|
||||||
state: ProposalState.STATE_OPEN,
|
|
||||||
terms: {
|
|
||||||
change: {
|
|
||||||
__typename: 'UpdateMarket',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
votes: {
|
|
||||||
yes: {
|
|
||||||
...generateYesVotes(1000, 1000),
|
|
||||||
totalEquityLikeShareWeight: '0',
|
|
||||||
},
|
|
||||||
no: {
|
|
||||||
...generateNoVotes(0),
|
|
||||||
totalEquityLikeShareWeight: '0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
|
||||||
'Currently expected to pass by token vote'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Renders proposal state: Update market proposal - Currently expected to fail', () => {
|
|
||||||
renderComponent(
|
|
||||||
generateProposal({
|
|
||||||
state: ProposalState.STATE_OPEN,
|
|
||||||
terms: {
|
|
||||||
change: {
|
|
||||||
__typename: 'UpdateMarket',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
votes: {
|
|
||||||
yes: {
|
|
||||||
...generateYesVotes(0),
|
|
||||||
totalEquityLikeShareWeight: '0',
|
|
||||||
},
|
|
||||||
no: {
|
|
||||||
...generateNoVotes(0),
|
|
||||||
totalEquityLikeShareWeight: '0',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
|
||||||
'Currently expected to fail'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Renders proposal state: Open - 5 minutes left to vote', () => {
|
it('Renders proposal state: Open - 5 minutes left to vote', () => {
|
||||||
renderComponent(
|
renderComponent(
|
||||||
generateProposal({
|
generateProposal({
|
||||||
@ -213,43 +131,6 @@ describe('Proposals list item details', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Renders proposal state: Open - majority not reached', () => {
|
|
||||||
renderComponent(
|
|
||||||
generateProposal({
|
|
||||||
state: ProposalState.STATE_OPEN,
|
|
||||||
terms: {
|
|
||||||
enactmentDatetime: nextWeek.toString(),
|
|
||||||
},
|
|
||||||
votes: {
|
|
||||||
no: generateNoVotes(1, 1000000000000000000),
|
|
||||||
yes: generateYesVotes(1, 1000000000000000000),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
|
||||||
'Currently expected to fail'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Renders proposal state: Open - will pass', () => {
|
|
||||||
renderComponent(
|
|
||||||
generateProposal({
|
|
||||||
state: ProposalState.STATE_OPEN,
|
|
||||||
votes: {
|
|
||||||
__typename: 'ProposalVotes',
|
|
||||||
yes: generateYesVotes(3000, 1000000000000000000),
|
|
||||||
no: generateNoVotes(0),
|
|
||||||
},
|
|
||||||
terms: {
|
|
||||||
closingDatetime: nextWeek.toString(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
|
||||||
'Currently expected to pass'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Renders proposal state: Rejected', () => {
|
it('Renders proposal state: Rejected', () => {
|
||||||
renderComponent(
|
renderComponent(
|
||||||
generateProposal({
|
generateProposal({
|
||||||
|
@ -11,7 +11,6 @@ import {
|
|||||||
import Routes from '../../../routes';
|
import Routes from '../../../routes';
|
||||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||||
import { useVoteInformation } from '../../hooks';
|
|
||||||
|
|
||||||
export const ProposalsListItemDetails = ({
|
export const ProposalsListItemDetails = ({
|
||||||
proposal,
|
proposal,
|
||||||
@ -20,18 +19,10 @@ export const ProposalsListItemDetails = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const state = proposal?.state;
|
const state = proposal?.state;
|
||||||
const { willPassByTokenVote, willPassByLPVote } = useVoteInformation({
|
|
||||||
proposal,
|
|
||||||
});
|
|
||||||
const updateMarketWillPass = willPassByTokenVote || willPassByLPVote;
|
|
||||||
const updateMarketVotePassMethod = willPassByTokenVote
|
|
||||||
? t('byTokenVote')
|
|
||||||
: t('byLPVote');
|
|
||||||
const nowToEnactmentInHours = differenceInHours(
|
const nowToEnactmentInHours = differenceInHours(
|
||||||
new Date(proposal?.terms.closingDatetime),
|
new Date(proposal?.terms.closingDatetime),
|
||||||
new Date()
|
new Date()
|
||||||
);
|
);
|
||||||
const isUpdateMarket = proposal?.terms.change.__typename === 'UpdateMarket';
|
|
||||||
|
|
||||||
let voteDetails: ReactNode;
|
let voteDetails: ReactNode;
|
||||||
let voteStatus: ReactNode;
|
let voteStatus: ReactNode;
|
||||||
@ -78,31 +69,11 @@ export const ProposalsListItemDetails = ({
|
|||||||
}
|
}
|
||||||
case ProposalState.STATE_OPEN: {
|
case ProposalState.STATE_OPEN: {
|
||||||
voteDetails = (
|
voteDetails = (
|
||||||
<span className={nowToEnactmentInHours < 6 ? 'text-vega-pink' : ''}>
|
<span className={nowToEnactmentInHours < 6 ? 'text-vega-orange' : ''}>
|
||||||
{formatDistanceToNowStrict(new Date(proposal?.terms.closingDatetime))}{' '}
|
{formatDistanceToNowStrict(new Date(proposal?.terms.closingDatetime))}{' '}
|
||||||
{t('left to vote')}
|
{t('left to vote')}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
voteStatus =
|
|
||||||
(isUpdateMarket &&
|
|
||||||
(updateMarketWillPass ? (
|
|
||||||
<>
|
|
||||||
{t('currentlySetTo')} {t('pass')} {updateMarketVotePassMethod}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{t('currentlySetTo')} {t('fail')}
|
|
||||||
</>
|
|
||||||
))) ||
|
|
||||||
(willPassByTokenVote ? (
|
|
||||||
<>
|
|
||||||
{t('currentlySetTo')} {t('pass')}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{t('currentlySetTo')} {t('fail')}
|
|
||||||
</>
|
|
||||||
));
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ProposalState.STATE_REJECTED: {
|
case ProposalState.STATE_REJECTED: {
|
||||||
|
@ -4,28 +4,19 @@ import { ProposalsListItemDetails } from './proposals-list-item-details';
|
|||||||
import { useUserVote } from '../vote-details/use-user-vote';
|
import { useUserVote } from '../vote-details/use-user-vote';
|
||||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||||
import type { NetworkParamsResult } from '@vegaprotocol/network-parameters';
|
|
||||||
|
|
||||||
interface ProposalsListItemProps {
|
interface ProposalsListItemProps {
|
||||||
proposal?: ProposalFieldsFragment | ProposalQuery['proposal'] | null;
|
proposal?: ProposalFieldsFragment | ProposalQuery['proposal'] | null;
|
||||||
networkParams: Partial<NetworkParamsResult> | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProposalsListItem = ({
|
export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => {
|
||||||
proposal,
|
|
||||||
networkParams,
|
|
||||||
}: ProposalsListItemProps) => {
|
|
||||||
const { voteState } = useUserVote(proposal?.id);
|
const { voteState } = useUserVote(proposal?.id);
|
||||||
if (!proposal || !proposal.id || !networkParams) return null;
|
if (!proposal || !proposal.id) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li id={proposal.id} data-testid="proposals-list-item">
|
<li id={proposal.id} data-testid="proposals-list-item">
|
||||||
<RoundedWrapper paddingBottom={true} heightFull={true}>
|
<RoundedWrapper paddingBottom={true} heightFull={true}>
|
||||||
<ProposalHeader
|
<ProposalHeader proposal={proposal} voteState={voteState} />
|
||||||
proposal={proposal}
|
|
||||||
networkParams={networkParams}
|
|
||||||
voteState={voteState}
|
|
||||||
/>
|
|
||||||
<ProposalsListItemDetails proposal={proposal} />
|
<ProposalsListItemDetails proposal={proposal} />
|
||||||
</RoundedWrapper>
|
</RoundedWrapper>
|
||||||
</li>
|
</li>
|
||||||
|
@ -8,7 +8,6 @@ import { ProtocolUpgradeProposalsListItem } from '../protocol-upgrade-proposals-
|
|||||||
import { ProposalsListFilter } from '../proposals-list-filter';
|
import { ProposalsListFilter } from '../proposals-list-filter';
|
||||||
import Routes from '../../../routes';
|
import Routes from '../../../routes';
|
||||||
import {
|
import {
|
||||||
AsyncRenderer,
|
|
||||||
Button,
|
Button,
|
||||||
Toggle,
|
Toggle,
|
||||||
VegaIcon,
|
VegaIcon,
|
||||||
@ -20,10 +19,6 @@ import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
|||||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||||
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
|
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||||
import { DocsLinks, ExternalLinks } from '@vegaprotocol/environment';
|
import { DocsLinks, ExternalLinks } from '@vegaprotocol/environment';
|
||||||
import {
|
|
||||||
NetworkParams,
|
|
||||||
useNetworkParams,
|
|
||||||
} from '@vegaprotocol/network-parameters';
|
|
||||||
|
|
||||||
interface ProposalsListProps {
|
interface ProposalsListProps {
|
||||||
proposals: Array<ProposalFieldsFragment | ProposalQuery['proposal']>;
|
proposals: Array<ProposalFieldsFragment | ProposalQuery['proposal']>;
|
||||||
@ -75,20 +70,6 @@ export const ProposalsList = ({
|
|||||||
lastBlockHeight,
|
lastBlockHeight,
|
||||||
}: ProposalsListProps) => {
|
}: ProposalsListProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
|
||||||
params: networkParams,
|
|
||||||
loading: networkParamsLoading,
|
|
||||||
error: networkParamsError,
|
|
||||||
} = useNetworkParams([
|
|
||||||
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,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const [filterString, setFilterString] = useState('');
|
const [filterString, setFilterString] = useState('');
|
||||||
const [closedProposalsView, setClosedProposalsView] =
|
const [closedProposalsView, setClosedProposalsView] =
|
||||||
useState<ClosedProposalsViewOptions>(
|
useState<ClosedProposalsViewOptions>(
|
||||||
@ -153,11 +134,6 @@ export const ProposalsList = ({
|
|||||||
p?.party?.id?.toString().includes(filterString);
|
p?.party?.id?.toString().includes(filterString);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer
|
|
||||||
loading={networkParamsLoading}
|
|
||||||
error={networkParamsError}
|
|
||||||
data={networkParams}
|
|
||||||
>
|
|
||||||
<div data-testid="proposals-list">
|
<div data-testid="proposals-list">
|
||||||
<div className="grid xs:grid-cols-2 items-center">
|
<div className="grid xs:grid-cols-2 items-center">
|
||||||
<Heading
|
<Heading
|
||||||
@ -167,10 +143,7 @@ export const ProposalsList = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{DocsLinks && (
|
{DocsLinks && (
|
||||||
<div
|
<div className="xs:justify-self-end" data-testid="new-proposal-link">
|
||||||
className="xs:justify-self-end"
|
|
||||||
data-testid="new-proposal-link"
|
|
||||||
>
|
|
||||||
<ExternalLink href={DocsLinks.PROPOSALS_GUIDE}>
|
<ExternalLink href={DocsLinks.PROPOSALS_GUIDE}>
|
||||||
<Button variant="primary" size="sm">
|
<Button variant="primary" size="sm">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
@ -227,11 +200,7 @@ export const ProposalsList = ({
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
{sortedProposals.open.filter(filterPredicate).map((proposal) => (
|
{sortedProposals.open.filter(filterPredicate).map((proposal) => (
|
||||||
<ProposalsListItem
|
<ProposalsListItem key={proposal?.id} proposal={proposal} />
|
||||||
key={proposal?.id}
|
|
||||||
proposal={proposal}
|
|
||||||
networkParams={networkParams}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
@ -266,8 +235,7 @@ export const ProposalsList = ({
|
|||||||
label: t(
|
label: t(
|
||||||
ClosedProposalsViewOptions.NetworkGovernance
|
ClosedProposalsViewOptions.NetworkGovernance
|
||||||
),
|
),
|
||||||
value:
|
value: ClosedProposalsViewOptions.NetworkGovernance,
|
||||||
ClosedProposalsViewOptions.NetworkGovernance,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t(
|
label: t(
|
||||||
@ -310,7 +278,6 @@ export const ProposalsList = ({
|
|||||||
<ProposalsListItem
|
<ProposalsListItem
|
||||||
key={proposal?.id}
|
key={proposal?.id}
|
||||||
proposal={proposal}
|
proposal={proposal}
|
||||||
networkParams={networkParams}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -328,6 +295,5 @@ export const ProposalsList = ({
|
|||||||
{t('seeRejectedProposals')}
|
{t('seeRejectedProposals')}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</AsyncRenderer>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,15 +1,10 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { Heading } from '../../../../components/heading';
|
import { Heading } from '../../../../components/heading';
|
||||||
import { ProposalsListItem } from '../proposals-list-item';
|
import { ProposalsListItem } from '../proposals-list-item';
|
||||||
import { ProposalsListFilter } from '../proposals-list-filter';
|
import { ProposalsListFilter } from '../proposals-list-filter';
|
||||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||||
import {
|
|
||||||
NetworkParams,
|
|
||||||
useNetworkParams,
|
|
||||||
} from '@vegaprotocol/network-parameters';
|
|
||||||
|
|
||||||
interface ProposalsListProps {
|
interface ProposalsListProps {
|
||||||
proposals: Array<ProposalQuery['proposal'] | ProposalFieldsFragment>;
|
proposals: Array<ProposalQuery['proposal'] | ProposalFieldsFragment>;
|
||||||
@ -17,19 +12,6 @@ interface ProposalsListProps {
|
|||||||
|
|
||||||
export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
|
export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
|
||||||
params: networkParams,
|
|
||||||
loading: networkParamsLoading,
|
|
||||||
error: networkParamsError,
|
|
||||||
} = useNetworkParams([
|
|
||||||
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,
|
|
||||||
]);
|
|
||||||
const [filterString, setFilterString] = useState('');
|
const [filterString, setFilterString] = useState('');
|
||||||
|
|
||||||
const filterPredicate = (
|
const filterPredicate = (
|
||||||
@ -39,11 +21,7 @@ export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
|
|||||||
p?.party?.id?.toString().includes(filterString);
|
p?.party?.id?.toString().includes(filterString);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer
|
<>
|
||||||
loading={networkParamsLoading}
|
|
||||||
error={networkParamsError}
|
|
||||||
data={networkParams}
|
|
||||||
>
|
|
||||||
<Heading title={t('pageTitleRejectedProposals')} />
|
<Heading title={t('pageTitleRejectedProposals')} />
|
||||||
<ProposalsListFilter
|
<ProposalsListFilter
|
||||||
filterString={filterString}
|
filterString={filterString}
|
||||||
@ -53,11 +31,7 @@ export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
|
|||||||
{proposals.length > 0 ? (
|
{proposals.length > 0 ? (
|
||||||
<ul data-testid="rejected-proposals">
|
<ul data-testid="rejected-proposals">
|
||||||
{proposals.filter(filterPredicate).map((proposal) => (
|
{proposals.filter(filterPredicate).map((proposal) => (
|
||||||
<ProposalsListItem
|
<ProposalsListItem key={proposal?.id} proposal={proposal} />
|
||||||
key={proposal?.id}
|
|
||||||
proposal={proposal}
|
|
||||||
networkParams={networkParams}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
@ -66,6 +40,6 @@ export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
</AsyncRenderer>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
export * from './vote-breakdown';
|
@ -0,0 +1,348 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { BrowserRouter as Router } from 'react-router-dom';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||||
|
import {
|
||||||
|
lastWeek,
|
||||||
|
mockWalletContext,
|
||||||
|
networkParamsQueryMock,
|
||||||
|
nextWeek,
|
||||||
|
} from '../../test-helpers/mocks';
|
||||||
|
import { VoteBreakdown } from './vote-breakdown';
|
||||||
|
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||||
|
import type { MockedResponse } from '@apollo/client/testing';
|
||||||
|
import {
|
||||||
|
generateNoVotes,
|
||||||
|
generateProposal,
|
||||||
|
generateYesVotes,
|
||||||
|
} from '../../test-helpers/generate-proposals';
|
||||||
|
import { ProposalState } from '@vegaprotocol/types';
|
||||||
|
import { BigNumber } from '../../../../lib/bignumber';
|
||||||
|
import type { AppState } from '../../../../contexts/app-state/app-state-context';
|
||||||
|
|
||||||
|
const mockTotalSupply = new BigNumber(100);
|
||||||
|
// Note - giving a fixedTokenValue of 1 means a ratio of 1:1 votes to tokens, making sums easier :)
|
||||||
|
const fixedTokenValue = 1000000000000000000;
|
||||||
|
|
||||||
|
const mockAppState: AppState = {
|
||||||
|
totalAssociated: new BigNumber('50063005'),
|
||||||
|
decimals: 18,
|
||||||
|
totalSupply: mockTotalSupply,
|
||||||
|
vegaWalletManageOverlay: false,
|
||||||
|
transactionOverlay: false,
|
||||||
|
bannerMessage: '',
|
||||||
|
disconnectNotice: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
|
||||||
|
useAppState: () => ({
|
||||||
|
appState: mockAppState,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const renderComponent = (
|
||||||
|
proposal: ProposalQuery['proposal'],
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
mocks: MockedResponse<any>[] = [networkParamsQueryMock]
|
||||||
|
) =>
|
||||||
|
render(
|
||||||
|
<Router>
|
||||||
|
<MockedProvider mocks={mocks}>
|
||||||
|
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||||
|
<VoteBreakdown proposal={proposal} />
|
||||||
|
</VegaWalletContext.Provider>
|
||||||
|
</MockedProvider>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('VoteBreakdown', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
jest.setSystemTime(0);
|
||||||
|
});
|
||||||
|
afterAll(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders majority reached', () => {
|
||||||
|
const yesVotes = 100;
|
||||||
|
const noVotes = 0;
|
||||||
|
|
||||||
|
renderComponent(
|
||||||
|
generateProposal({
|
||||||
|
state: ProposalState.STATE_PASSED,
|
||||||
|
terms: {
|
||||||
|
closingDatetime: lastWeek.toString(),
|
||||||
|
enactmentDatetime: nextWeek.toString(),
|
||||||
|
},
|
||||||
|
votes: {
|
||||||
|
__typename: 'ProposalVotes',
|
||||||
|
yes: generateYesVotes(yesVotes, fixedTokenValue),
|
||||||
|
no: generateNoVotes(noVotes, fixedTokenValue),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('token-majority-met')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders majority not reached', () => {
|
||||||
|
const yesVotes = 20;
|
||||||
|
const noVotes = 80;
|
||||||
|
|
||||||
|
renderComponent(
|
||||||
|
generateProposal({
|
||||||
|
state: ProposalState.STATE_PASSED,
|
||||||
|
terms: {
|
||||||
|
closingDatetime: lastWeek.toString(),
|
||||||
|
enactmentDatetime: nextWeek.toString(),
|
||||||
|
},
|
||||||
|
votes: {
|
||||||
|
__typename: 'ProposalVotes',
|
||||||
|
yes: generateYesVotes(yesVotes, fixedTokenValue),
|
||||||
|
no: generateNoVotes(noVotes, fixedTokenValue),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('token-majority-not-met')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders participation reached', () => {
|
||||||
|
const yesVotes = 1000;
|
||||||
|
const noVotes = 0;
|
||||||
|
|
||||||
|
renderComponent(
|
||||||
|
generateProposal({
|
||||||
|
state: ProposalState.STATE_PASSED,
|
||||||
|
terms: {
|
||||||
|
closingDatetime: lastWeek.toString(),
|
||||||
|
enactmentDatetime: nextWeek.toString(),
|
||||||
|
},
|
||||||
|
votes: {
|
||||||
|
__typename: 'ProposalVotes',
|
||||||
|
yes: generateYesVotes(yesVotes, fixedTokenValue),
|
||||||
|
no: generateNoVotes(noVotes, fixedTokenValue),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('token-participation-met')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders participation not reached', () => {
|
||||||
|
const yesVotes = 0;
|
||||||
|
const noVotes = 0;
|
||||||
|
|
||||||
|
renderComponent(
|
||||||
|
generateProposal({
|
||||||
|
terms: {
|
||||||
|
closingDatetime: lastWeek.toString(),
|
||||||
|
enactmentDatetime: nextWeek.toString(),
|
||||||
|
},
|
||||||
|
votes: {
|
||||||
|
__typename: 'ProposalVotes',
|
||||||
|
yes: generateYesVotes(yesVotes, fixedTokenValue),
|
||||||
|
no: generateNoVotes(noVotes, fixedTokenValue),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByTestId('token-participation-not-met')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders proposal state: Update market proposal - Currently expected to pass by LP vote', () => {
|
||||||
|
renderComponent(
|
||||||
|
generateProposal({
|
||||||
|
state: ProposalState.STATE_OPEN,
|
||||||
|
terms: {
|
||||||
|
change: {
|
||||||
|
__typename: 'UpdateMarket',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
votes: {
|
||||||
|
yes: {
|
||||||
|
...generateYesVotes(0),
|
||||||
|
totalEquityLikeShareWeight: '1000',
|
||||||
|
},
|
||||||
|
no: {
|
||||||
|
...generateNoVotes(0),
|
||||||
|
totalEquityLikeShareWeight: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||||
|
'Currently expected to pass by liquidity vote'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders proposal state: Update market proposal - Currently expected to pass by token vote', () => {
|
||||||
|
renderComponent(
|
||||||
|
generateProposal({
|
||||||
|
state: ProposalState.STATE_OPEN,
|
||||||
|
terms: {
|
||||||
|
change: {
|
||||||
|
__typename: 'UpdateMarket',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
votes: {
|
||||||
|
yes: {
|
||||||
|
...generateYesVotes(1000, fixedTokenValue),
|
||||||
|
totalEquityLikeShareWeight: '0',
|
||||||
|
},
|
||||||
|
no: {
|
||||||
|
...generateNoVotes(0, fixedTokenValue),
|
||||||
|
totalEquityLikeShareWeight: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||||
|
'Currently expected to pass by token vote'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders proposal state: Update market proposal - Currently expected to fail', () => {
|
||||||
|
renderComponent(
|
||||||
|
generateProposal({
|
||||||
|
state: ProposalState.STATE_OPEN,
|
||||||
|
terms: {
|
||||||
|
change: {
|
||||||
|
__typename: 'UpdateMarket',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
votes: {
|
||||||
|
yes: {
|
||||||
|
...generateYesVotes(0),
|
||||||
|
totalEquityLikeShareWeight: '0',
|
||||||
|
},
|
||||||
|
no: {
|
||||||
|
...generateNoVotes(0),
|
||||||
|
totalEquityLikeShareWeight: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||||
|
'Currently expected to fail'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Progress bar displays status - token majority', () => {
|
||||||
|
const yesVotes = 80;
|
||||||
|
const noVotes = 20;
|
||||||
|
|
||||||
|
renderComponent(
|
||||||
|
generateProposal({
|
||||||
|
state: ProposalState.STATE_PASSED,
|
||||||
|
terms: {
|
||||||
|
closingDatetime: lastWeek.toString(),
|
||||||
|
enactmentDatetime: nextWeek.toString(),
|
||||||
|
},
|
||||||
|
votes: {
|
||||||
|
__typename: 'ProposalVotes',
|
||||||
|
yes: generateYesVotes(yesVotes, fixedTokenValue),
|
||||||
|
no: generateNoVotes(noVotes, fixedTokenValue),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const element = screen.getByTestId('token-majority-progress');
|
||||||
|
const style = window.getComputedStyle(element);
|
||||||
|
|
||||||
|
expect(style.width).toBe(`${yesVotes}%`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Progress bar displays status - token participation', () => {
|
||||||
|
const yesVotes = 40;
|
||||||
|
const noVotes = 20;
|
||||||
|
const totalVotes = yesVotes + noVotes;
|
||||||
|
const totalSupplyValue = mockTotalSupply.toNumber();
|
||||||
|
const expectedProgress = (totalVotes / totalSupplyValue) * 100; // Here it should be 60%
|
||||||
|
|
||||||
|
renderComponent(
|
||||||
|
generateProposal({
|
||||||
|
state: ProposalState.STATE_PASSED,
|
||||||
|
terms: {
|
||||||
|
closingDatetime: lastWeek.toString(),
|
||||||
|
enactmentDatetime: nextWeek.toString(),
|
||||||
|
},
|
||||||
|
votes: {
|
||||||
|
__typename: 'ProposalVotes',
|
||||||
|
yes: generateYesVotes(yesVotes, fixedTokenValue),
|
||||||
|
no: generateNoVotes(noVotes, fixedTokenValue),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const element = screen.getByTestId('token-participation-progress');
|
||||||
|
const style = window.getComputedStyle(element);
|
||||||
|
|
||||||
|
expect(style.width).toBe(`${expectedProgress}%`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Progress bar displays status - LP majority', () => {
|
||||||
|
const yesVotesLP = 800;
|
||||||
|
const noVotesLP = 200;
|
||||||
|
const expectedProgress = (yesVotesLP / (yesVotesLP + noVotesLP)) * 100; // 80%
|
||||||
|
|
||||||
|
renderComponent(
|
||||||
|
generateProposal({
|
||||||
|
state: ProposalState.STATE_PASSED,
|
||||||
|
terms: {
|
||||||
|
change: {
|
||||||
|
__typename: 'UpdateMarket',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
votes: {
|
||||||
|
__typename: 'ProposalVotes',
|
||||||
|
yes: {
|
||||||
|
...generateYesVotes(0),
|
||||||
|
totalEquityLikeShareWeight: `${yesVotesLP}`,
|
||||||
|
},
|
||||||
|
no: {
|
||||||
|
...generateNoVotes(0),
|
||||||
|
totalEquityLikeShareWeight: `${noVotesLP}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const element = screen.getByTestId('lp-majority-progress');
|
||||||
|
const style = window.getComputedStyle(element);
|
||||||
|
expect(style.width).toBe(`${expectedProgress}%`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Progress bar displays status - LP participation', () => {
|
||||||
|
const yesVotesLP = 400;
|
||||||
|
const noVotesLP = 600;
|
||||||
|
const totalVotesLP = yesVotesLP + noVotesLP;
|
||||||
|
const totalLPSupply = 1000;
|
||||||
|
const expectedProgress = (totalVotesLP / totalLPSupply) * 100; // 100%
|
||||||
|
|
||||||
|
renderComponent(
|
||||||
|
generateProposal({
|
||||||
|
state: ProposalState.STATE_PASSED,
|
||||||
|
terms: {
|
||||||
|
change: {
|
||||||
|
__typename: 'UpdateMarket',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
votes: {
|
||||||
|
__typename: 'ProposalVotes',
|
||||||
|
yes: {
|
||||||
|
...generateYesVotes(0),
|
||||||
|
totalEquityLikeShareWeight: `${yesVotesLP}`,
|
||||||
|
},
|
||||||
|
no: {
|
||||||
|
...generateNoVotes(0),
|
||||||
|
totalEquityLikeShareWeight: `${noVotesLP}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const element = screen.getByTestId('lp-participation-progress');
|
||||||
|
const style = window.getComputedStyle(element);
|
||||||
|
expect(style.width).toBe(`${expectedProgress}%`);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,378 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useVoteInformation } from '../../hooks';
|
||||||
|
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { formatNumber, toBigNum } from '@vegaprotocol/utils';
|
||||||
|
import { ProposalState } from '@vegaprotocol/types';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||||
|
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||||
|
|
||||||
|
interface VoteBreakdownProps {
|
||||||
|
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VoteProgressProps {
|
||||||
|
percentageFor: BigNumber;
|
||||||
|
colourfulBg?: boolean;
|
||||||
|
testId?: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VoteProgress = ({
|
||||||
|
percentageFor,
|
||||||
|
colourfulBg,
|
||||||
|
testId,
|
||||||
|
children,
|
||||||
|
}: VoteProgressProps) => {
|
||||||
|
const containerClasses = classNames(
|
||||||
|
'relative h-10 rounded-md border border-vega-dark-300 overflow-hidden',
|
||||||
|
colourfulBg ? 'bg-vega-pink' : 'bg-vega-dark-400'
|
||||||
|
);
|
||||||
|
|
||||||
|
const progressClasses = classNames(
|
||||||
|
'absolute h-full top-0 left-0',
|
||||||
|
colourfulBg ? 'bg-vega-green' : 'bg-white'
|
||||||
|
);
|
||||||
|
|
||||||
|
const textClasses = classNames(
|
||||||
|
'absolute top-0 left-0 w-full h-full flex items-center justify-start px-3 text-black'
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={containerClasses}>
|
||||||
|
<div
|
||||||
|
className={progressClasses}
|
||||||
|
style={{ width: `${percentageFor}%` }}
|
||||||
|
data-testid={testId}
|
||||||
|
></div>
|
||||||
|
<div className={textClasses}>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface StatusProps {
|
||||||
|
reached: boolean;
|
||||||
|
threshold: BigNumber;
|
||||||
|
text: string;
|
||||||
|
testId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Status = ({ reached, threshold, text, testId }: StatusProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-testid={testId}>
|
||||||
|
{reached ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Icon name="tick" size={4} />
|
||||||
|
<span>
|
||||||
|
{threshold.toString()}% {text} {t('met')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Icon name="cross" size={4} />
|
||||||
|
<span>
|
||||||
|
{threshold.toString()}% {text} {t('not met')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
|
||||||
|
const {
|
||||||
|
totalTokensPercentage,
|
||||||
|
participationMet,
|
||||||
|
totalTokensVoted,
|
||||||
|
totalLPTokensPercentage,
|
||||||
|
noPercentage,
|
||||||
|
noLPPercentage,
|
||||||
|
yesPercentage,
|
||||||
|
yesLPPercentage,
|
||||||
|
yesTokens,
|
||||||
|
noTokens,
|
||||||
|
yesEquityLikeShareWeight,
|
||||||
|
noEquityLikeShareWeight,
|
||||||
|
totalEquityLikeShareWeight,
|
||||||
|
requiredMajorityPercentage,
|
||||||
|
requiredMajorityLPPercentage,
|
||||||
|
requiredParticipation,
|
||||||
|
requiredParticipationLP,
|
||||||
|
participationLPMet,
|
||||||
|
majorityMet,
|
||||||
|
majorityLPMet,
|
||||||
|
willPassByTokenVote,
|
||||||
|
willPassByLPVote,
|
||||||
|
} = useVoteInformation({ proposal });
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const defaultDP = 2;
|
||||||
|
const isProposalOpen = proposal?.state === ProposalState.STATE_OPEN;
|
||||||
|
const isUpdateMarket = proposal?.terms?.change?.__typename === 'UpdateMarket';
|
||||||
|
const participationThresholdProgress = BigNumber.min(
|
||||||
|
totalTokensPercentage.dividedBy(requiredParticipation).multipliedBy(100),
|
||||||
|
new BigNumber(100)
|
||||||
|
);
|
||||||
|
const lpParticipationThresholdProgress =
|
||||||
|
requiredParticipationLP &&
|
||||||
|
BigNumber.min(
|
||||||
|
totalLPTokensPercentage
|
||||||
|
.dividedBy(requiredParticipationLP)
|
||||||
|
.multipliedBy(100),
|
||||||
|
new BigNumber(100)
|
||||||
|
);
|
||||||
|
const willPass = willPassByTokenVote || willPassByLPVote;
|
||||||
|
const updateMarketVotePassMethod = willPassByTokenVote
|
||||||
|
? t('byTokenVote')
|
||||||
|
: t('byLiquidityVote');
|
||||||
|
|
||||||
|
const sectionWrapperClasses = classNames('grid sm:grid-cols-2 gap-6');
|
||||||
|
const headingClasses = classNames('mb-2 text-vega-dark-400');
|
||||||
|
const progressDetailsClasses = classNames(
|
||||||
|
'flex justify-between flex-wrap mt-2 text-sm'
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-6">
|
||||||
|
{isProposalOpen && (
|
||||||
|
<div
|
||||||
|
data-testid="vote-status"
|
||||||
|
className="flex items-center gap-1 mb-2 text-bold"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{willPass ? (
|
||||||
|
<Icon name="tick" size={5} className="text-vega-green" />
|
||||||
|
) : (
|
||||||
|
<Icon name="cross" size={5} className="text-vega-pink" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span>{t('currentlySetTo')} </span>
|
||||||
|
{willPass ? (
|
||||||
|
<span>
|
||||||
|
<span className="text-vega-green">{t('pass')}</span>
|
||||||
|
{isUpdateMarket && <span> {updateMarketVotePassMethod}</span>}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-vega-pink">{t('fail')}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isUpdateMarket && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className={headingClasses}>{t('liquidityProviderVote')}</h3>
|
||||||
|
<div className={sectionWrapperClasses}>
|
||||||
|
<section data-testid="lp-majority-breakdown">
|
||||||
|
<VoteProgress
|
||||||
|
percentageFor={yesLPPercentage}
|
||||||
|
colourfulBg={true}
|
||||||
|
testId="lp-majority-progress"
|
||||||
|
>
|
||||||
|
<Status
|
||||||
|
reached={majorityLPMet}
|
||||||
|
threshold={requiredMajorityLPPercentage}
|
||||||
|
text={t('majorityThreshold')}
|
||||||
|
testId={
|
||||||
|
majorityLPMet ? 'lp-majority-met' : 'lp-majority-not-met'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</VoteProgress>
|
||||||
|
|
||||||
|
<div className={progressDetailsClasses}>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span>{t('liquidityProviderVotesFor')}:</span>
|
||||||
|
<Tooltip
|
||||||
|
description={formatNumber(
|
||||||
|
yesEquityLikeShareWeight,
|
||||||
|
defaultDP
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<button>
|
||||||
|
{yesEquityLikeShareWeight
|
||||||
|
.dividedBy(toBigNum(10 ** 6, 0))
|
||||||
|
.toFixed(1)}
|
||||||
|
M
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<span>
|
||||||
|
(
|
||||||
|
<Tooltip
|
||||||
|
description={
|
||||||
|
<span>{yesLPPercentage.toFixed(defaultDP)}%</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<button>{yesLPPercentage.toFixed(0)}%</button>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span>{t('liquidityProviderVotesAgainst')}:</span>
|
||||||
|
<Tooltip
|
||||||
|
description={formatNumber(
|
||||||
|
noEquityLikeShareWeight,
|
||||||
|
defaultDP
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<button>
|
||||||
|
{noEquityLikeShareWeight
|
||||||
|
.dividedBy(toBigNum(10 ** 6, 0))
|
||||||
|
.toFixed(1)}
|
||||||
|
M
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<span>
|
||||||
|
(
|
||||||
|
<Tooltip
|
||||||
|
description={
|
||||||
|
<span>{noLPPercentage.toFixed(defaultDP)}%</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<button>{noLPPercentage.toFixed(0)}%</button>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section data-testid="lp-participation-breakdown">
|
||||||
|
<VoteProgress
|
||||||
|
percentageFor={
|
||||||
|
lpParticipationThresholdProgress || new BigNumber(0)
|
||||||
|
}
|
||||||
|
testId="lp-participation-progress"
|
||||||
|
>
|
||||||
|
<Status
|
||||||
|
reached={participationLPMet}
|
||||||
|
threshold={requiredParticipationLP || new BigNumber(1)}
|
||||||
|
text={t('participationThreshold')}
|
||||||
|
testId={
|
||||||
|
participationLPMet
|
||||||
|
? 'lp-participation-met'
|
||||||
|
: 'lp-participation-not-met'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</VoteProgress>
|
||||||
|
|
||||||
|
<div className="flex mt-2 text-sm">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span>{t('totalLiquidityProviderTokensVoted')}:</span>
|
||||||
|
<Tooltip
|
||||||
|
description={formatNumber(
|
||||||
|
totalEquityLikeShareWeight,
|
||||||
|
defaultDP
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<button>
|
||||||
|
{totalEquityLikeShareWeight
|
||||||
|
.dividedBy(toBigNum(10 ** 6, 0))
|
||||||
|
.toFixed(1)}
|
||||||
|
M
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<span>
|
||||||
|
({totalEquityLikeShareWeight.toFixed(defaultDP)}%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isUpdateMarket && <h3 className={headingClasses}>{t('tokenVote')}</h3>}
|
||||||
|
<div className={sectionWrapperClasses}>
|
||||||
|
<section data-testid="token-majority-breakdown">
|
||||||
|
<VoteProgress
|
||||||
|
percentageFor={yesPercentage}
|
||||||
|
colourfulBg={true}
|
||||||
|
testId="token-majority-progress"
|
||||||
|
>
|
||||||
|
<Status
|
||||||
|
reached={majorityMet}
|
||||||
|
threshold={requiredMajorityPercentage}
|
||||||
|
text={t('majorityThreshold')}
|
||||||
|
testId={
|
||||||
|
majorityMet ? 'token-majority-met' : 'token-majority-not-met'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</VoteProgress>
|
||||||
|
|
||||||
|
<div className={progressDetailsClasses}>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span>{t('tokenVotesFor')}:</span>
|
||||||
|
<Tooltip description={formatNumber(yesTokens, defaultDP)}>
|
||||||
|
<button>
|
||||||
|
{yesTokens.dividedBy(toBigNum(10 ** 6, 0)).toFixed(1)}M
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<span>
|
||||||
|
(
|
||||||
|
<Tooltip
|
||||||
|
description={<span>{yesPercentage.toFixed(defaultDP)}%</span>}
|
||||||
|
>
|
||||||
|
<button>{yesPercentage.toFixed(0)}%</button>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span>{t('tokenVotesAgainst')}:</span>
|
||||||
|
<Tooltip description={formatNumber(noTokens, defaultDP)}>
|
||||||
|
<button>
|
||||||
|
{noTokens.dividedBy(toBigNum(10 ** 6, 0)).toFixed(1)}M
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<span>
|
||||||
|
(
|
||||||
|
<Tooltip
|
||||||
|
description={<span>{noPercentage.toFixed(defaultDP)}%</span>}
|
||||||
|
>
|
||||||
|
<button>{noPercentage.toFixed(0)}%</button>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section data-testid="token-participation-breakdown">
|
||||||
|
<VoteProgress
|
||||||
|
percentageFor={participationThresholdProgress}
|
||||||
|
testId="token-participation-progress"
|
||||||
|
>
|
||||||
|
<Status
|
||||||
|
reached={participationMet}
|
||||||
|
threshold={requiredParticipation}
|
||||||
|
text={t('participationThreshold')}
|
||||||
|
testId={
|
||||||
|
participationMet
|
||||||
|
? 'token-participation-met'
|
||||||
|
: 'token-participation-not-met'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</VoteProgress>
|
||||||
|
|
||||||
|
<div className="flex mt-2 text-sm">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span>{t('totalTokensVoted')}:</span>
|
||||||
|
<Tooltip description={formatNumber(totalTokensVoted, defaultDP)}>
|
||||||
|
<button>
|
||||||
|
{totalTokensVoted.dividedBy(toBigNum(10 ** 6, 0)).toFixed(1)}M
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<span>({totalTokensPercentage.toFixed(defaultDP)}%)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1 +1 @@
|
|||||||
export { VoteDetails } from './vote-details';
|
export { UserVote } from './user-vote';
|
||||||
|
@ -0,0 +1,78 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Icon, ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
|
import { ProposalState } from '@vegaprotocol/types';
|
||||||
|
import { ConnectToVega } from '../../../../components/connect-to-vega';
|
||||||
|
import { VoteButtonsContainer } from './vote-buttons';
|
||||||
|
import { SubHeading } from '../../../../components/heading';
|
||||||
|
import type { VoteValue } from '@vegaprotocol/types';
|
||||||
|
import type { DialogProps, VegaTxState } from '@vegaprotocol/wallet';
|
||||||
|
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||||
|
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||||
|
import type { VoteState } from './use-user-vote';
|
||||||
|
|
||||||
|
interface UserVoteProps {
|
||||||
|
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||||
|
minVoterBalance: string | null | undefined;
|
||||||
|
spamProtectionMinTokens: string | null | undefined;
|
||||||
|
transaction: VegaTxState | null;
|
||||||
|
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
|
||||||
|
dialog: (props: DialogProps) => JSX.Element;
|
||||||
|
voteState: VoteState | null;
|
||||||
|
voteDatetime: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserVote = ({
|
||||||
|
proposal,
|
||||||
|
minVoterBalance,
|
||||||
|
spamProtectionMinTokens,
|
||||||
|
submit,
|
||||||
|
transaction,
|
||||||
|
dialog,
|
||||||
|
voteState,
|
||||||
|
voteDatetime,
|
||||||
|
}: UserVoteProps) => {
|
||||||
|
const { pubKey } = useVegaWallet();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section data-testid="user-vote">
|
||||||
|
{proposal?.state === ProposalState.STATE_OPEN ? (
|
||||||
|
<SubHeading title={t('castYourVote')} />
|
||||||
|
) : (
|
||||||
|
<SubHeading title={t('yourVote')} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{pubKey ? (
|
||||||
|
proposal && (
|
||||||
|
<VoteButtonsContainer
|
||||||
|
voteState={voteState}
|
||||||
|
voteDatetime={voteDatetime}
|
||||||
|
proposalState={proposal.state}
|
||||||
|
proposalId={proposal.id ?? ''}
|
||||||
|
minVoterBalance={minVoterBalance}
|
||||||
|
spamProtectionMinTokens={spamProtectionMinTokens}
|
||||||
|
className="flex"
|
||||||
|
submit={submit}
|
||||||
|
transaction={transaction}
|
||||||
|
dialog={dialog}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div className="pb-2">
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<Icon name={'info-sign'} />
|
||||||
|
<div>{t('connectAVegaWalletToVote')}</div>
|
||||||
|
</div>
|
||||||
|
<ExternalLink href="https://blog.vega.xyz/how-to-vote-on-vega-2195d1e52ec5">
|
||||||
|
{t('findOutMoreAboutHowToVote')}
|
||||||
|
</ExternalLink>
|
||||||
|
</div>
|
||||||
|
<ConnectToVega />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
@ -190,14 +190,15 @@ export const VoteButtons = ({
|
|||||||
(voteState === VoteState.Yes || voteState === VoteState.No) && (
|
(voteState === VoteState.Yes || voteState === VoteState.No) && (
|
||||||
<p data-testid="you-voted">
|
<p data-testid="you-voted">
|
||||||
<span>{t('youVoted')}:</span>{' '}
|
<span>{t('youVoted')}:</span>{' '}
|
||||||
<span className="text-white font-bold">
|
<span className="mx-1 text-white font-bold uppercase">
|
||||||
{t(`voteState_${voteState}`)}
|
{t(`voteState_${voteState}`)}
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
{voteDatetime ? (
|
{voteDatetime ? (
|
||||||
<span>{format(voteDatetime, DATE_FORMAT_LONG)}. </span>
|
<span>on {format(voteDatetime, DATE_FORMAT_LONG)}. </span>
|
||||||
) : null}
|
) : null}
|
||||||
{proposalVotable ? (
|
{proposalVotable ? (
|
||||||
<ButtonLink
|
<ButtonLink
|
||||||
|
className="text-white"
|
||||||
data-testid="change-vote-button"
|
data-testid="change-vote-button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setChangeVote(true);
|
setChangeVote(true);
|
||||||
|
@ -1,255 +0,0 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { formatDistanceToNow } from 'date-fns';
|
|
||||||
import { RoundedWrapper, Icon, ExternalLink } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
|
||||||
import { ProposalState } from '@vegaprotocol/types';
|
|
||||||
import { VoteProgress } from '@vegaprotocol/proposals';
|
|
||||||
import { formatNumber } from '../../../../lib/format-number';
|
|
||||||
import { ConnectToVega } from '../../../../components/connect-to-vega';
|
|
||||||
import { useVoteInformation } from '../../hooks';
|
|
||||||
import { CurrentProposalStatus } from '../current-proposal-status';
|
|
||||||
import { VoteButtonsContainer } from './vote-buttons';
|
|
||||||
import { SubHeading } from '../../../../components/heading';
|
|
||||||
import { ProposalType } from '../proposal/proposal';
|
|
||||||
import type { VoteValue } from '@vegaprotocol/types';
|
|
||||||
import type { DialogProps, VegaTxState } from '@vegaprotocol/wallet';
|
|
||||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
|
||||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
|
||||||
import type { VoteState } from './use-user-vote';
|
|
||||||
|
|
||||||
interface VoteDetailsProps {
|
|
||||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
|
||||||
minVoterBalance: string | null | undefined;
|
|
||||||
spamProtectionMinTokens: string | null | undefined;
|
|
||||||
proposalType: ProposalType | null;
|
|
||||||
transaction: VegaTxState | null;
|
|
||||||
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
|
|
||||||
dialog: (props: DialogProps) => JSX.Element;
|
|
||||||
voteState: VoteState | null;
|
|
||||||
voteDatetime: Date | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const VoteDetails = ({
|
|
||||||
proposal,
|
|
||||||
minVoterBalance,
|
|
||||||
spamProtectionMinTokens,
|
|
||||||
proposalType,
|
|
||||||
submit,
|
|
||||||
transaction,
|
|
||||||
dialog,
|
|
||||||
voteState,
|
|
||||||
voteDatetime,
|
|
||||||
}: VoteDetailsProps) => {
|
|
||||||
const { pubKey } = useVegaWallet();
|
|
||||||
const {
|
|
||||||
totalTokensPercentage,
|
|
||||||
participationMet,
|
|
||||||
totalTokensVoted,
|
|
||||||
totalLPTokensPercentage,
|
|
||||||
noPercentage,
|
|
||||||
noLPPercentage,
|
|
||||||
yesPercentage,
|
|
||||||
yesLPPercentage,
|
|
||||||
yesTokens,
|
|
||||||
noTokens,
|
|
||||||
requiredMajorityPercentage,
|
|
||||||
requiredMajorityLPPercentage,
|
|
||||||
requiredParticipation,
|
|
||||||
requiredParticipationLP,
|
|
||||||
participationLPMet,
|
|
||||||
} = useVoteInformation({ proposal });
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const defaultDecimals = 2;
|
|
||||||
const daysLeft = t('daysLeft', {
|
|
||||||
daysLeft: formatDistanceToNow(new Date(proposal?.terms.closingDatetime)),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{proposalType === ProposalType.PROPOSAL_UPDATE_MARKET && (
|
|
||||||
<section>
|
|
||||||
<SubHeading title={t('liquidityVotes')} />
|
|
||||||
<p data-testid="liquidity-votes-status">
|
|
||||||
<span>
|
|
||||||
<CurrentProposalStatus proposal={proposal} />
|
|
||||||
</span>
|
|
||||||
{'. '}
|
|
||||||
{proposal?.state === ProposalState.STATE_OPEN ? daysLeft : null}
|
|
||||||
</p>
|
|
||||||
<table className="w-full mb-8">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className="text-vega-green w-[18%] text-left">
|
|
||||||
{t('for')}
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
<VoteProgress
|
|
||||||
threshold={requiredMajorityLPPercentage}
|
|
||||||
progress={yesLPPercentage}
|
|
||||||
/>
|
|
||||||
</th>
|
|
||||||
<th className="text-danger w-[18%] text-right">
|
|
||||||
{t('against')}
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
className="text-left"
|
|
||||||
data-testid="vote-progress-indicator-percentage-for"
|
|
||||||
>
|
|
||||||
{yesLPPercentage.toFixed(defaultDecimals)}%
|
|
||||||
</td>
|
|
||||||
<td className="text-center text-white">
|
|
||||||
{t('majorityRequired')}{' '}
|
|
||||||
{requiredMajorityLPPercentage.toFixed(defaultDecimals)}%
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className="text-right"
|
|
||||||
data-testid="vote-progress-indicator-percentage-against"
|
|
||||||
>
|
|
||||||
{noLPPercentage.toFixed(defaultDecimals)}%
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<p className="mb-6">
|
|
||||||
{t('participation')}
|
|
||||||
{': '}
|
|
||||||
{participationLPMet ? (
|
|
||||||
<span className="text-vega-green mx-4">{t('met')}</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-danger mx-4">{t('notMet')}</span>
|
|
||||||
)}{' '}
|
|
||||||
{formatNumber(totalLPTokensPercentage, defaultDecimals)}%
|
|
||||||
<span className="ml-4">
|
|
||||||
{requiredParticipationLP && (
|
|
||||||
<>
|
|
||||||
({formatNumber(requiredParticipationLP, defaultDecimals)}%{' '}
|
|
||||||
{t('governanceRequired')})
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
<section data-testid="votes-table">
|
|
||||||
<SubHeading title={t('tokenVotes')} />
|
|
||||||
<p data-testid="token-votes-status">
|
|
||||||
<span>
|
|
||||||
<CurrentProposalStatus proposal={proposal} />
|
|
||||||
</span>
|
|
||||||
{'. '}
|
|
||||||
{proposal?.state === ProposalState.STATE_OPEN ? daysLeft : null}
|
|
||||||
</p>
|
|
||||||
<table className="w-full mb-4">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className="text-vega-green w-[18%] text-left">{t('for')}</th>
|
|
||||||
<th>
|
|
||||||
<VoteProgress
|
|
||||||
threshold={requiredMajorityPercentage}
|
|
||||||
progress={yesPercentage}
|
|
||||||
/>
|
|
||||||
</th>
|
|
||||||
<th className="text-danger w-[18%] text-right">{t('against')}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
className="text-left"
|
|
||||||
data-testid="vote-progress-indicator-percentage-for"
|
|
||||||
>
|
|
||||||
{yesPercentage.toFixed(defaultDecimals)}%
|
|
||||||
</td>
|
|
||||||
<td className="text-center text-white">
|
|
||||||
{t('majorityRequired')}{' '}
|
|
||||||
{requiredMajorityPercentage.toFixed(defaultDecimals)}%
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
className="text-right"
|
|
||||||
data-testid="vote-progress-indicator-percentage-against"
|
|
||||||
>
|
|
||||||
{noPercentage.toFixed(defaultDecimals)}%
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td data-testid="vote-progress-indicator-tokens-for">
|
|
||||||
{' '}
|
|
||||||
{formatNumber(yesTokens, defaultDecimals)}{' '}
|
|
||||||
</td>
|
|
||||||
<td></td>
|
|
||||||
<td
|
|
||||||
data-testid="vote-progress-indicator-tokens-against"
|
|
||||||
className="text-right"
|
|
||||||
>
|
|
||||||
{formatNumber(noTokens, defaultDecimals)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<p className="mb-6">
|
|
||||||
{t('participation')}
|
|
||||||
{': '}
|
|
||||||
{participationMet ? (
|
|
||||||
<span className="text-vega-green mx-4">{t('met')}</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-danger mx-4">{t('notMet')}</span>
|
|
||||||
)}{' '}
|
|
||||||
{formatNumber(totalTokensVoted, defaultDecimals)}{' '}
|
|
||||||
{formatNumber(totalTokensPercentage, defaultDecimals)}%
|
|
||||||
<span className="ml-4">
|
|
||||||
({formatNumber(requiredParticipation, defaultDecimals)}%{' '}
|
|
||||||
{t('governanceRequired')})
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
{proposalType === ProposalType.PROPOSAL_UPDATE_MARKET && (
|
|
||||||
<p>{t('votingThresholdInfo')}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<section className="mt-10">
|
|
||||||
{proposal?.state === ProposalState.STATE_OPEN ? (
|
|
||||||
<SubHeading title={t('castYourVote')} />
|
|
||||||
) : (
|
|
||||||
<SubHeading title={t('yourVote')} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{pubKey ? (
|
|
||||||
proposal && (
|
|
||||||
<VoteButtonsContainer
|
|
||||||
voteState={voteState}
|
|
||||||
voteDatetime={voteDatetime}
|
|
||||||
proposalState={proposal.state}
|
|
||||||
proposalId={proposal.id ?? ''}
|
|
||||||
minVoterBalance={minVoterBalance}
|
|
||||||
spamProtectionMinTokens={spamProtectionMinTokens}
|
|
||||||
className="flex"
|
|
||||||
submit={submit}
|
|
||||||
transaction={transaction}
|
|
||||||
dialog={dialog}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<RoundedWrapper paddingBottom={true}>
|
|
||||||
<div className="mb-4">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<Icon name={'info-sign'} />
|
|
||||||
<div>{t('connectAVegaWalletToVote')}</div>
|
|
||||||
</div>
|
|
||||||
<ExternalLink href="https://blog.vega.xyz/how-to-vote-on-vega-2195d1e52ec5">
|
|
||||||
{t('findOutMoreAboutHowToVote')}
|
|
||||||
</ExternalLink>
|
|
||||||
</div>
|
|
||||||
<ConnectToVega />
|
|
||||||
</RoundedWrapper>
|
|
||||||
)}
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -7,7 +7,7 @@ export type { IconName } from '@blueprintjs/icons';
|
|||||||
export interface IconProps {
|
export interface IconProps {
|
||||||
name: IconName;
|
name: IconName;
|
||||||
className?: string;
|
className?: string;
|
||||||
size?: 2 | 3 | 4 | 6 | 8 | 10 | 12 | 14 | 16;
|
size?: 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 | 14 | 16;
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,6 +23,7 @@ export const Icon = ({ size = 4, name, className, ariaLabel }: IconProps) => {
|
|||||||
'w-2 h-2': size === 2,
|
'w-2 h-2': size === 2,
|
||||||
'w-3 h-3': size === 3,
|
'w-3 h-3': size === 3,
|
||||||
'w-4 h-4': size === 4,
|
'w-4 h-4': size === 4,
|
||||||
|
'w-5 h-5': size === 5,
|
||||||
'w-6 h-6': size === 6,
|
'w-6 h-6': size === 6,
|
||||||
'w-8 h-8': size === 8,
|
'w-8 h-8': size === 8,
|
||||||
'w-10 h-10': size === 10,
|
'w-10 h-10': size === 10,
|
||||||
|
Loading…
Reference in New Issue
Block a user