Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc101a74a9 | ||
|
|
1f08e8225a | ||
|
|
5fff6ba3f7 | ||
|
|
6cacc46a74 | ||
|
|
98ff4f3c04 | ||
|
|
8268acac6d | ||
|
|
4ad1a47ded | ||
|
|
fea97aac19 | ||
|
|
f652292e02 |
@@ -2,7 +2,7 @@ export const proposalsData = {
|
||||
proposalsConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
proposalNode: {
|
||||
id: 'e8ba9d268e12514644fd1fc7ff289292f4ce6489cc32cc73133aea52c04aef89',
|
||||
rationale: {
|
||||
title: 'Add asset Wrapped Ether',
|
||||
@@ -56,7 +56,7 @@ export const proposalsData = {
|
||||
__typename: 'ProposalEdge',
|
||||
},
|
||||
{
|
||||
node: {
|
||||
proposalNode: {
|
||||
id: 'd848fc7881f13d366df5f61ab139d5fcfa72bf838151bb51b54381870e357931',
|
||||
rationale: {
|
||||
title: 'Add asset Dai Stablecoin',
|
||||
@@ -110,60 +110,7 @@ export const proposalsData = {
|
||||
__typename: 'ProposalEdge',
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: 'ccbd651b4a1167fd73c4a0340ac759fa0a31ca487ad46a13254b741ad71947ed',
|
||||
rationale: {
|
||||
title: 'New DAI market',
|
||||
description: 'New DAI market',
|
||||
__typename: 'ProposalRationale',
|
||||
},
|
||||
reference: '0VFQusmmESdrP5GuL8naB6lxfoE3RPGaEeo7abdN',
|
||||
state: 'STATE_ENACTED',
|
||||
datetime: '2022-11-26T19:36:19.26034Z',
|
||||
rejectionReason: null,
|
||||
party: {
|
||||
id: '69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f',
|
||||
__typename: 'Party',
|
||||
},
|
||||
errorDetails: null,
|
||||
terms: {
|
||||
closingDatetime: '2022-11-26T19:36:42Z',
|
||||
enactmentDatetime: '2023-03-22T13:57:37Z',
|
||||
change: {
|
||||
instrument: {
|
||||
name: 'UNIDAI Monthly (Dec 2022)',
|
||||
code: 'UNIDAI.MF21',
|
||||
product: {
|
||||
settlementAsset: { symbol: 'tDAI', __typename: 'Asset' },
|
||||
__typename: 'FutureProduct',
|
||||
},
|
||||
__typename: 'InstrumentConfiguration',
|
||||
},
|
||||
__typename: 'NewMarket',
|
||||
},
|
||||
__typename: 'ProposalTerms',
|
||||
},
|
||||
votes: {
|
||||
yes: {
|
||||
totalTokens: '0',
|
||||
totalNumber: '0',
|
||||
totalEquityLikeShareWeight: '0',
|
||||
__typename: 'ProposalVoteSide',
|
||||
},
|
||||
no: {
|
||||
totalTokens: '0',
|
||||
totalNumber: '0',
|
||||
totalEquityLikeShareWeight: '0',
|
||||
__typename: 'ProposalVoteSide',
|
||||
},
|
||||
__typename: 'ProposalVotes',
|
||||
},
|
||||
__typename: 'Proposal',
|
||||
},
|
||||
__typename: 'ProposalEdge',
|
||||
},
|
||||
{
|
||||
node: {
|
||||
proposalNode: {
|
||||
id: 'bc70383f0e9515b15542cf4c63590cd2ca46b3363ba7c4a72af0e62112b3951b',
|
||||
rationale: {
|
||||
title: 'USDC-III',
|
||||
@@ -217,60 +164,7 @@ export const proposalsData = {
|
||||
__typename: 'ProposalEdge',
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: '9d9b2a9d0179d0e4ccb317f6c4a5db0b905d893190bfb5e5499985ef313281c8',
|
||||
rationale: {
|
||||
title: 'New BTC market',
|
||||
description: 'New BTC market',
|
||||
__typename: 'ProposalRationale',
|
||||
},
|
||||
reference: 'AXeRWS3TvLBFDgWOSHQpKFJf3NTbnWK6310q02fZ',
|
||||
state: 'STATE_ENACTED',
|
||||
datetime: '2022-11-26T19:36:19.26034Z',
|
||||
rejectionReason: null,
|
||||
party: {
|
||||
id: '69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f',
|
||||
__typename: 'Party',
|
||||
},
|
||||
errorDetails: null,
|
||||
terms: {
|
||||
closingDatetime: '2022-11-26T19:36:42Z',
|
||||
enactmentDatetime: '2023-03-22T13:57:37Z',
|
||||
change: {
|
||||
instrument: {
|
||||
name: 'ETHBTC Quarterly (Feb 2023)',
|
||||
code: 'ETHBTC.QM21',
|
||||
product: {
|
||||
settlementAsset: { symbol: 'tBTC', __typename: 'Asset' },
|
||||
__typename: 'FutureProduct',
|
||||
},
|
||||
__typename: 'InstrumentConfiguration',
|
||||
},
|
||||
__typename: 'NewMarket',
|
||||
},
|
||||
__typename: 'ProposalTerms',
|
||||
},
|
||||
votes: {
|
||||
yes: {
|
||||
totalTokens: '0',
|
||||
totalNumber: '0',
|
||||
totalEquityLikeShareWeight: '0',
|
||||
__typename: 'ProposalVoteSide',
|
||||
},
|
||||
no: {
|
||||
totalTokens: '0',
|
||||
totalNumber: '0',
|
||||
totalEquityLikeShareWeight: '0',
|
||||
__typename: 'ProposalVoteSide',
|
||||
},
|
||||
__typename: 'ProposalVotes',
|
||||
},
|
||||
__typename: 'Proposal',
|
||||
},
|
||||
__typename: 'ProposalEdge',
|
||||
},
|
||||
{
|
||||
node: {
|
||||
proposalNode: {
|
||||
id: '9c48796e7988769ededc2b2b02220b00e93f65f23e8141bf1fd23a6983d95943',
|
||||
rationale: {
|
||||
title: 'Update governance.proposal.asset.requiredMajority',
|
||||
|
||||
@@ -236,7 +236,7 @@ context(
|
||||
});
|
||||
|
||||
// 1002-STKE-041 1002-STKE-053
|
||||
it(
|
||||
it.skip(
|
||||
'Able to remove part of a stake against a validator',
|
||||
// @ts-ignore clash between jest and cypress
|
||||
{ tags: '@smoke' },
|
||||
|
||||
@@ -7,7 +7,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
||||
});
|
||||
|
||||
describe('Links and buttons', function () {
|
||||
it('should have link for proposal page', function () {
|
||||
it.skip('should have link for proposal page', function () {
|
||||
cy.getByTestId('home-proposals').within(() => {
|
||||
cy.get('[href="/proposals"]')
|
||||
.should('exist')
|
||||
@@ -27,7 +27,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
||||
cy.getByTestId('app-announcement').should('not.exist');
|
||||
});
|
||||
|
||||
it('should show open or enacted proposals without proposal summary', function () {
|
||||
it.skip('should show open or enacted proposals without proposal summary', function () {
|
||||
cy.get('body').then(($body) => {
|
||||
if (!$body.find('[data-testid="proposals-list-item"]').length) {
|
||||
cy.createMarket();
|
||||
@@ -51,7 +51,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('should have external link for governance', function () {
|
||||
it.skip('should have external link for governance', function () {
|
||||
cy.getByTestId('home-proposals').within(() => {
|
||||
cy.getByTestId('external-link')
|
||||
.should('have.attr', 'href')
|
||||
@@ -59,7 +59,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('should have link for validator page', function () {
|
||||
it.skip('should have link for validator page', function () {
|
||||
cy.getByTestId('home-validators').within(() => {
|
||||
cy.get('[href="/validators"]')
|
||||
.first()
|
||||
@@ -68,7 +68,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('should have external link for validators', function () {
|
||||
it.skip('should have external link for validators', function () {
|
||||
cy.getByTestId('home-validators').within(() => {
|
||||
cy.getByTestId('external-link')
|
||||
.should('have.attr', 'href')
|
||||
@@ -79,21 +79,21 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('should have information on active nodes', function () {
|
||||
it.skip('should have information on active nodes', function () {
|
||||
cy.getByTestId('node-information')
|
||||
.first()
|
||||
.should('contain.text', '2')
|
||||
.and('contain.text', 'active nodes');
|
||||
});
|
||||
|
||||
it('should have information on consensus nodes', function () {
|
||||
it.skip('should have information on consensus nodes', function () {
|
||||
cy.getByTestId('node-information')
|
||||
.last()
|
||||
.should('contain.text', '2')
|
||||
.and('contain.text', 'consensus nodes');
|
||||
});
|
||||
|
||||
it('should contain link to specific validators', function () {
|
||||
it.skip('should contain link to specific validators', function () {
|
||||
cy.getByTestId('validators')
|
||||
.should('have.length', '2')
|
||||
.each(($validator) => {
|
||||
@@ -101,7 +101,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('should have link for rewards page', function () {
|
||||
it.skip('should have link for rewards page', function () {
|
||||
cy.getByTestId('home-rewards').within(() => {
|
||||
cy.get('[href="/rewards"]')
|
||||
.first()
|
||||
@@ -110,7 +110,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('should have link for withdrawal page', function () {
|
||||
it.skip('should have link for withdrawal page', function () {
|
||||
cy.getByTestId('home-vega-token').within(() => {
|
||||
cy.get('[href="/token/withdraw"]')
|
||||
.first()
|
||||
@@ -132,7 +132,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
||||
});
|
||||
|
||||
// 0006-NETW-003 0006-NETW-008 0006-NETW-009 0006-NETW-010 0006-NETW-012 0006-NETW-013 0006-NETW-017 0006-NETW-018 0006-NETW-019 0006-NETW-020
|
||||
it('should have option to switch to different network node', function () {
|
||||
it.skip('should have option to switch to different network node', function () {
|
||||
cy.getByTestId('git-network-data').within(() => {
|
||||
cy.getByTestId('link').click();
|
||||
});
|
||||
@@ -189,7 +189,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
||||
cy.viewport('iphone-xr');
|
||||
});
|
||||
|
||||
it('should have burger button', () => {
|
||||
it.skip('should have burger button', () => {
|
||||
cy.getByTestId('button-menu-drawer').should('be.visible').click();
|
||||
cy.getByTestId('menu-drawer').should('be.visible');
|
||||
});
|
||||
|
||||
@@ -33,12 +33,12 @@ context(
|
||||
verifyTabHighlighted(navigation.proposals);
|
||||
});
|
||||
|
||||
it('should have GOVERNANCE header visible', function () {
|
||||
it.skip('should have GOVERNANCE header visible', function () {
|
||||
verifyPageHeader('Proposals');
|
||||
});
|
||||
|
||||
// 3002-PROP-023 3004-PMAC-002 3005-PASN-002 3006-PASC-002 3007-PNEC-002 3008-PFRO-003
|
||||
it('new proposal page should have button for link to more information on proposals', function () {
|
||||
it.skip('new proposal page should have button for link to more information on proposals', function () {
|
||||
cy.getByTestId('new-proposal-link').click();
|
||||
cy.url().should('include', '/proposals/propose/raw');
|
||||
cy.contains('To see Explorer data on proposals visit').within(() => {
|
||||
@@ -73,7 +73,7 @@ context(
|
||||
navigateTo(navigation.proposals);
|
||||
});
|
||||
|
||||
it('should be able to see a working link for - find out more about Vega governance', function () {
|
||||
it.skip('should be able to see a working link for - find out more about Vega governance', function () {
|
||||
// 3001-VOTE-001 // 3002-PROP-001
|
||||
cy.getByTestId(proposalDocumentationLink)
|
||||
.should('be.visible')
|
||||
|
||||
@@ -46,11 +46,11 @@ context('Validators Page - verify elements on page', function () {
|
||||
|
||||
// @ts-ignore clash between jest and cypress
|
||||
describe('with wallets disconnected', { tags: '@smoke' }, function () {
|
||||
it('Should have validators tab highlighted', function () {
|
||||
it.skip('Should have validators tab highlighted', function () {
|
||||
verifyTabHighlighted(navigation.validators);
|
||||
});
|
||||
|
||||
it('Should have validators ON VEGA header visible', function () {
|
||||
it.skip('Should have validators ON VEGA header visible', function () {
|
||||
verifyPageHeader('Validators');
|
||||
});
|
||||
|
||||
@@ -192,7 +192,7 @@ context('Validators Page - verify elements on page', function () {
|
||||
});
|
||||
|
||||
// 1002-STKE-006
|
||||
it('Should be able to see validator name', function () {
|
||||
it.skip('Should be able to see validator name', function () {
|
||||
cy.getByTestId(validatorTitle).should('not.be.empty');
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import classnames from 'classnames';
|
||||
import { Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
||||
import type { Dispatch, SetStateAction, ReactNode } from 'react';
|
||||
|
||||
interface CollapsibleToggleProps {
|
||||
@@ -30,7 +30,7 @@ export const CollapsibleToggle = ({
|
||||
<div className="flex items-center gap-3">
|
||||
{children}
|
||||
<div className={classes} data-testid="toggle-icon-wrapper">
|
||||
<Icon name="chevron-down" size={8} />
|
||||
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={20} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import compact from 'lodash/compact';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
@@ -12,10 +13,10 @@ import { useRefreshAfterEpoch } from '../../hooks/use-refresh-after-epoch';
|
||||
import { ProposalsListItem } from '../proposals/components/proposals-list-item';
|
||||
import { ProtocolUpgradeProposalsListItem } from '../proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item';
|
||||
import Routes from '../routes';
|
||||
import { ExternalLinks, useFeatureFlags } from '@vegaprotocol/environment';
|
||||
import { ExternalLinks } from '@vegaprotocol/environment';
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import { useNodesQuery } from '../staking/home/__generated__/Nodes';
|
||||
import { useProposalsQuery } from '../proposals/proposals/__generated__/Proposals';
|
||||
import { useProposalsQuery } from '../proposals/__generated__/Proposals';
|
||||
import {
|
||||
getNotRejectedProposals,
|
||||
getNotRejectedProtocolUpgradeProposals,
|
||||
@@ -31,7 +32,7 @@ import {
|
||||
orderByUpgradeBlockHeight,
|
||||
} from '../proposals/components/proposals-list/proposals-list';
|
||||
import { BigNumber } from '../../lib/bignumber';
|
||||
import { type Proposal } from '../proposals/types';
|
||||
import { type Proposal, type BatchProposal } from '../proposals/types';
|
||||
|
||||
const nodesToShow = 6;
|
||||
|
||||
@@ -39,7 +40,7 @@ const HomeProposals = ({
|
||||
proposals,
|
||||
protocolUpgradeProposals,
|
||||
}: {
|
||||
proposals: Proposal[];
|
||||
proposals: Array<Proposal | BatchProposal>;
|
||||
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
@@ -60,12 +61,9 @@ const HomeProposals = ({
|
||||
<ProtocolUpgradeProposalsListItem key={index} proposal={proposal} />
|
||||
))}
|
||||
|
||||
{proposals.map(
|
||||
(proposal) =>
|
||||
proposal?.id && (
|
||||
<ProposalsListItem key={proposal.id} proposal={proposal} />
|
||||
)
|
||||
)}
|
||||
{compact(proposals).map((proposal) => {
|
||||
return <ProposalsListItem key={proposal.id} proposal={proposal} />;
|
||||
})}
|
||||
</ul>
|
||||
|
||||
<div className="mt-6">
|
||||
@@ -175,7 +173,6 @@ export const ValidatorDetailsLink = ({
|
||||
};
|
||||
|
||||
const GovernanceHome = ({ name }: RouteChildProps) => {
|
||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||
useDocumentTitle(name);
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
@@ -186,11 +183,6 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
|
||||
pollInterval: 5000,
|
||||
fetchPolicy: 'network-only',
|
||||
errorPolicy: 'ignore',
|
||||
variables: {
|
||||
includeNewMarketProductFields: !!featureFlags.PRODUCT_PERPETUALS,
|
||||
includeUpdateMarketStates: !!featureFlags.UPDATE_MARKET_STATE,
|
||||
includeUpdateReferralPrograms: !!featureFlags.REFERRALS,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -212,15 +204,18 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
|
||||
|
||||
useRefreshAfterEpoch(validatorsData?.epoch.timestamps.expiry, refetch);
|
||||
|
||||
const proposals = useMemo(
|
||||
() =>
|
||||
proposalsData
|
||||
? getNotRejectedProposals(
|
||||
removePaginationWrapper(proposalsData.proposalsConnection?.edges)
|
||||
const proposals = useMemo(() => {
|
||||
if (!proposalsData?.proposalsConnection?.edges?.length) return [];
|
||||
return proposalsData
|
||||
? getNotRejectedProposals(
|
||||
compact(
|
||||
proposalsData.proposalsConnection.edges.map(
|
||||
(edge) => edge?.proposalNode
|
||||
)
|
||||
)
|
||||
: [],
|
||||
[proposalsData]
|
||||
);
|
||||
)
|
||||
: [];
|
||||
}, [proposalsData]);
|
||||
|
||||
const sortedProposals = useMemo(
|
||||
() => orderByDate(proposals).reverse(),
|
||||
|
||||
@@ -0,0 +1,493 @@
|
||||
fragment UpdateMarketStates on UpdateMarketState {
|
||||
__typename
|
||||
updateType
|
||||
market {
|
||||
decimalPlaces
|
||||
id
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
product {
|
||||
__typename
|
||||
... on Future {
|
||||
quoteName
|
||||
}
|
||||
... on Perpetual {
|
||||
quoteName
|
||||
}
|
||||
}
|
||||
name
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
updateType
|
||||
price
|
||||
}
|
||||
|
||||
fragment UpdateReferralPrograms on UpdateReferralProgram {
|
||||
__typename
|
||||
benefitTiers {
|
||||
minimumEpochs
|
||||
minimumRunningNotionalTakerVolume
|
||||
referralDiscountFactor
|
||||
referralRewardFactor
|
||||
}
|
||||
endOfProgram: endOfProgramTimestamp
|
||||
windowLength
|
||||
stakingTiers {
|
||||
minimumStakedTokens
|
||||
referralRewardMultiplier
|
||||
}
|
||||
}
|
||||
|
||||
fragment UpdateVolumeDiscountPrograms on UpdateVolumeDiscountProgram {
|
||||
__typename
|
||||
benefitTiers {
|
||||
minimumRunningNotionalTakerVolume
|
||||
volumeDiscountFactor
|
||||
}
|
||||
endOfProgramTimestamp
|
||||
windowLength
|
||||
}
|
||||
|
||||
# I prefix due to clash in libs/proposals
|
||||
fragment IUpdateMarketFields on UpdateMarket {
|
||||
__typename
|
||||
marketId
|
||||
updateMarketConfiguration {
|
||||
instrument {
|
||||
code
|
||||
product {
|
||||
... on UpdateFutureProduct {
|
||||
quoteName
|
||||
dataSourceSpecForSettlementData {
|
||||
sourceType {
|
||||
... on DataSourceDefinitionInternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfigurationTime {
|
||||
conditions {
|
||||
operator
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on DataSourceDefinitionExternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfiguration {
|
||||
signers {
|
||||
signer {
|
||||
... on PubKey {
|
||||
key
|
||||
}
|
||||
... on ETHAddress {
|
||||
address
|
||||
}
|
||||
}
|
||||
}
|
||||
filters {
|
||||
key {
|
||||
name
|
||||
type
|
||||
}
|
||||
conditions {
|
||||
operator
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
# dataSourceSpecForTradingTermination {
|
||||
# sourceType {
|
||||
# ... on DataSourceDefinitionInternal {
|
||||
# sourceType {
|
||||
# ... on DataSourceSpecConfigurationTime {
|
||||
# conditions {
|
||||
# operator
|
||||
# value
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# ... on DataSourceDefinitionExternal {
|
||||
# sourceType {
|
||||
# ... on DataSourceSpecConfiguration {
|
||||
# signers {
|
||||
# signer {
|
||||
# ... on PubKey {
|
||||
# key
|
||||
# }
|
||||
# ... on ETHAddress {
|
||||
# address
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# filters {
|
||||
# key {
|
||||
# name
|
||||
# type
|
||||
# }
|
||||
# conditions {
|
||||
# operator
|
||||
# value
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
dataSourceSpecBinding {
|
||||
settlementDataProperty
|
||||
tradingTerminationProperty
|
||||
}
|
||||
}
|
||||
... on UpdatePerpetualProduct {
|
||||
quoteName
|
||||
dataSourceSpecForSettlementData {
|
||||
sourceType {
|
||||
... on DataSourceDefinitionInternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfigurationTime {
|
||||
conditions {
|
||||
operator
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on DataSourceDefinitionExternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfiguration {
|
||||
signers {
|
||||
signer {
|
||||
... on PubKey {
|
||||
key
|
||||
}
|
||||
... on ETHAddress {
|
||||
address
|
||||
}
|
||||
}
|
||||
}
|
||||
filters {
|
||||
key {
|
||||
name
|
||||
type
|
||||
}
|
||||
conditions {
|
||||
operator
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dataSourceSpecBinding {
|
||||
settlementDataProperty
|
||||
settlementScheduleProperty
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
metadata
|
||||
priceMonitoringParameters {
|
||||
triggers {
|
||||
horizonSecs
|
||||
probability
|
||||
auctionExtensionSecs
|
||||
}
|
||||
}
|
||||
liquidityMonitoringParameters {
|
||||
targetStakeParameters {
|
||||
timeWindow
|
||||
scalingFactor
|
||||
}
|
||||
}
|
||||
riskParameters {
|
||||
... on UpdateMarketSimpleRiskModel {
|
||||
simple {
|
||||
factorLong
|
||||
factorShort
|
||||
}
|
||||
}
|
||||
... on UpdateMarketLogNormalRiskModel {
|
||||
logNormal {
|
||||
riskAversionParameter
|
||||
tau
|
||||
params {
|
||||
r
|
||||
sigma
|
||||
mu
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# I prefix due to clash in libs/proposals
|
||||
fragment INewMarketFields on NewMarket {
|
||||
__typename
|
||||
decimalPlaces
|
||||
metadata
|
||||
riskParameters {
|
||||
... on LogNormalRiskModel {
|
||||
riskAversionParameter
|
||||
tau
|
||||
params {
|
||||
mu
|
||||
r
|
||||
sigma
|
||||
}
|
||||
}
|
||||
... on SimpleRiskModel {
|
||||
params {
|
||||
factorLong
|
||||
factorShort
|
||||
}
|
||||
}
|
||||
}
|
||||
successorConfiguration {
|
||||
parentMarketId
|
||||
}
|
||||
instrument {
|
||||
name
|
||||
code
|
||||
product {
|
||||
... on FutureProduct {
|
||||
settlementAsset {
|
||||
id
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
}
|
||||
quoteName
|
||||
dataSourceSpecBinding {
|
||||
settlementDataProperty
|
||||
tradingTerminationProperty
|
||||
}
|
||||
dataSourceSpecForSettlementData {
|
||||
sourceType {
|
||||
... on DataSourceDefinitionInternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfigurationTime {
|
||||
conditions {
|
||||
operator
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on DataSourceDefinitionExternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfiguration {
|
||||
signers {
|
||||
signer {
|
||||
... on PubKey {
|
||||
key
|
||||
}
|
||||
... on ETHAddress {
|
||||
address
|
||||
}
|
||||
}
|
||||
}
|
||||
filters {
|
||||
key {
|
||||
name
|
||||
type
|
||||
}
|
||||
conditions {
|
||||
operator
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on PerpetualProduct {
|
||||
settlementAsset {
|
||||
id
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
}
|
||||
quoteName
|
||||
}
|
||||
}
|
||||
}
|
||||
priceMonitoringParameters {
|
||||
triggers {
|
||||
horizonSecs
|
||||
probability
|
||||
auctionExtensionSecs
|
||||
}
|
||||
}
|
||||
liquidityMonitoringParameters {
|
||||
targetStakeParameters {
|
||||
timeWindow
|
||||
scalingFactor
|
||||
}
|
||||
}
|
||||
positionDecimalPlaces
|
||||
linearSlippageFactor
|
||||
}
|
||||
|
||||
# I prefix due to clash in lib/proposals
|
||||
fragment INewAssetFields on NewAsset {
|
||||
__typename
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
source {
|
||||
... on BuiltinAsset {
|
||||
maxFaucetAmountMint
|
||||
}
|
||||
... on ERC20 {
|
||||
contractAddress
|
||||
withdrawThreshold
|
||||
lifetimeLimit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# I prefix due to clash in libs/proposals
|
||||
fragment IUpdateAssetFields on UpdateAsset {
|
||||
__typename
|
||||
assetId
|
||||
quantum
|
||||
source {
|
||||
... on UpdateERC20 {
|
||||
lifetimeLimit
|
||||
withdrawThreshold
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# I prefix due to clash in libs/proposals
|
||||
fragment IUpdateNetworkParameterFields on UpdateNetworkParameter {
|
||||
__typename
|
||||
networkParameter {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
fragment VoteFields on ProposalVotes {
|
||||
yes {
|
||||
totalTokens
|
||||
totalNumber
|
||||
totalEquityLikeShareWeight
|
||||
}
|
||||
no {
|
||||
totalTokens
|
||||
totalNumber
|
||||
totalEquityLikeShareWeight
|
||||
}
|
||||
}
|
||||
|
||||
fragment ProposalTermsFields on ProposalTerms {
|
||||
closingDatetime
|
||||
enactmentDatetime
|
||||
change {
|
||||
__typename
|
||||
...UpdateMarketStates
|
||||
...UpdateReferralPrograms
|
||||
...UpdateVolumeDiscountPrograms
|
||||
...INewMarketFields
|
||||
...IUpdateMarketFields
|
||||
...INewAssetFields
|
||||
...IUpdateNetworkParameterFields
|
||||
...IUpdateAssetFields
|
||||
}
|
||||
}
|
||||
|
||||
fragment ProposalFields on Proposal {
|
||||
id
|
||||
rationale {
|
||||
title
|
||||
description
|
||||
}
|
||||
reference
|
||||
state
|
||||
datetime
|
||||
rejectionReason
|
||||
party {
|
||||
id
|
||||
}
|
||||
errorDetails
|
||||
terms {
|
||||
...ProposalTermsFields
|
||||
}
|
||||
votes {
|
||||
...VoteFields
|
||||
}
|
||||
}
|
||||
|
||||
fragment BatchProposalFields on BatchProposal {
|
||||
id
|
||||
rationale {
|
||||
title
|
||||
description
|
||||
}
|
||||
reference
|
||||
state
|
||||
datetime
|
||||
rejectionReason
|
||||
party {
|
||||
id
|
||||
}
|
||||
errorDetails
|
||||
batchTerms {
|
||||
closingDatetime
|
||||
changes {
|
||||
enactmentDatetime
|
||||
}
|
||||
}
|
||||
subProposals {
|
||||
datetime
|
||||
terms {
|
||||
...ProposalTermsFields
|
||||
}
|
||||
}
|
||||
votes {
|
||||
...VoteFields
|
||||
}
|
||||
}
|
||||
|
||||
query Proposals {
|
||||
proposalsConnection {
|
||||
edges {
|
||||
proposalNode {
|
||||
__typename
|
||||
... on Proposal {
|
||||
...ProposalFields
|
||||
}
|
||||
... on BatchProposal {
|
||||
...BatchProposalFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query Proposal($proposalId: ID!) {
|
||||
proposal(id: $proposalId) {
|
||||
... on Proposal {
|
||||
...ProposalFields
|
||||
}
|
||||
... on BatchProposal {
|
||||
...BatchProposalFields
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
+6
-2
@@ -3,9 +3,13 @@ import { ProposalState } from '@vegaprotocol/types';
|
||||
import { ProposalInfoLabel } from '../proposal-info-label';
|
||||
import { type ReactNode } from 'react';
|
||||
import { type ProposalInfoLabelVariant } from '../proposal-info-label';
|
||||
import { type Proposal } from '../../types';
|
||||
import { type Proposal, type BatchProposal } from '../../types';
|
||||
|
||||
export const CurrentProposalState = ({ proposal }: { proposal: Proposal }) => {
|
||||
export const CurrentProposalState = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: Proposal | BatchProposal;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
let proposalStatus: ReactNode;
|
||||
let variant = 'tertiary' as ProposalInfoLabelVariant;
|
||||
|
||||
@@ -79,13 +79,22 @@ export const ListAsset = ({
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (data.asset.source.__typename !== 'ERC20') return null;
|
||||
|
||||
if (data.asset.source.__typename !== 'ERC20') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (data.asset.status !== Schema.AssetStatus.STATUS_PENDING_LISTING) {
|
||||
return null;
|
||||
}
|
||||
if (errorAsset || errorBundle) return null;
|
||||
|
||||
if (errorAsset || errorBundle) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { assetSource, signatures, vegaAssetId, nonce } =
|
||||
assetData.erc20ListAssetBundle;
|
||||
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<h3 className="mb-2 text-xl">{t('ListAsset')}</h3>
|
||||
|
||||
+40
-6
@@ -2,19 +2,53 @@ import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
|
||||
import { AssetDetail, AssetDetailsTable } from '@vegaprotocol/assets';
|
||||
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
import {
|
||||
AssetDetail,
|
||||
AssetDetailsTable,
|
||||
useAssetQuery,
|
||||
} from '@vegaprotocol/assets';
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import {
|
||||
type INewAssetFieldsFragment,
|
||||
type IUpdateAssetFieldsFragment,
|
||||
} from '../../__generated__/Proposals';
|
||||
|
||||
export const ProposalAssetDetails = ({
|
||||
asset,
|
||||
originalAsset,
|
||||
change,
|
||||
assetId,
|
||||
}: {
|
||||
asset: AssetFieldsFragment;
|
||||
originalAsset?: AssetFieldsFragment;
|
||||
change: IUpdateAssetFieldsFragment | INewAssetFieldsFragment;
|
||||
assetId: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [showAssetDetails, setShowAssetDetails] = useState(false);
|
||||
|
||||
const { data } = useAssetQuery({
|
||||
fetchPolicy: 'network-only',
|
||||
variables: {
|
||||
assetId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
let asset = removePaginationWrapper(data?.assetsConnection?.edges)[0];
|
||||
|
||||
const originalAsset = asset;
|
||||
|
||||
if (change.__typename === 'UpdateAsset') {
|
||||
asset = {
|
||||
...asset,
|
||||
quantum: change.quantum,
|
||||
source: { ...asset.source },
|
||||
};
|
||||
|
||||
if (asset.source.__typename === 'ERC20') {
|
||||
asset.source.lifetimeLimit = change.source.lifetimeLimit;
|
||||
asset.source.withdrawThreshold = change.source.withdrawThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section data-testid="proposal-asset-details">
|
||||
<CollapsibleToggle
|
||||
|
||||
+35
-19
@@ -6,16 +6,46 @@ import {
|
||||
KeyValueTableRow,
|
||||
RoundedWrapper,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { type Proposal } from '../../types';
|
||||
import { type Proposal, type BatchProposal } from '../../types';
|
||||
|
||||
interface ProposalChangeTableProps {
|
||||
proposal: Proposal;
|
||||
proposal: Proposal | BatchProposal;
|
||||
}
|
||||
|
||||
export const ProposalChangeTable = ({ proposal }: ProposalChangeTableProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const terms = proposal?.terms;
|
||||
const closingTimeRow =
|
||||
proposal.__typename === 'Proposal' ? (
|
||||
<KeyValueTableRow>
|
||||
{isFuture(new Date(proposal.terms?.closingDatetime))
|
||||
? t('closesOn')
|
||||
: t('closedOn')}
|
||||
{formatDateWithLocalTimezone(new Date(proposal.terms?.closingDatetime))}
|
||||
</KeyValueTableRow>
|
||||
) : proposal.__typename === 'BatchProposal' ? (
|
||||
<KeyValueTableRow>
|
||||
{isFuture(new Date(proposal.batchTerms?.closingDatetime))
|
||||
? t('closesOn')
|
||||
: t('closedOn')}
|
||||
{formatDateWithLocalTimezone(
|
||||
new Date(proposal.batchTerms?.closingDatetime)
|
||||
)}
|
||||
</KeyValueTableRow>
|
||||
) : null;
|
||||
|
||||
const enactmentRow =
|
||||
proposal.__typename === 'Proposal' &&
|
||||
proposal.terms.change.__typename !== 'NewFreeform' ? (
|
||||
<KeyValueTableRow>
|
||||
{isFuture(new Date(proposal.terms?.enactmentDatetime || 0))
|
||||
? t('proposedEnactment')
|
||||
: t('enactedOn')}
|
||||
{formatDateWithLocalTimezone(
|
||||
new Date(proposal.terms?.enactmentDatetime || 0)
|
||||
)}
|
||||
</KeyValueTableRow>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<RoundedWrapper paddingBottom={true}>
|
||||
@@ -24,22 +54,8 @@ export const ProposalChangeTable = ({ proposal }: ProposalChangeTableProps) => {
|
||||
{t('id')}
|
||||
{proposal?.id}
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{isFuture(new Date(terms?.closingDatetime))
|
||||
? t('closesOn')
|
||||
: t('closedOn')}
|
||||
{formatDateWithLocalTimezone(new Date(terms?.closingDatetime))}
|
||||
</KeyValueTableRow>
|
||||
{terms?.change.__typename !== 'NewFreeform' ? (
|
||||
<KeyValueTableRow>
|
||||
{isFuture(new Date(terms?.enactmentDatetime || 0))
|
||||
? t('proposedEnactment')
|
||||
: t('enactedOn')}
|
||||
{formatDateWithLocalTimezone(
|
||||
new Date(terms?.enactmentDatetime || 0)
|
||||
)}
|
||||
</KeyValueTableRow>
|
||||
) : null}
|
||||
{closingTimeRow}
|
||||
{enactmentRow}
|
||||
<KeyValueTableRow>
|
||||
{t('proposedBy')}
|
||||
<span style={{ wordBreak: 'break-word' }}>{proposal?.party.id}</span>
|
||||
|
||||
+53
-30
@@ -18,20 +18,21 @@ import {
|
||||
nextWeek,
|
||||
mockWalletContext,
|
||||
createUserVoteQueryMock,
|
||||
networkParamsQueryMock,
|
||||
} from '../../test-helpers/mocks';
|
||||
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { VoteState } from '../vote-details/use-user-vote';
|
||||
import { useNewTransferProposalDetails } from '@vegaprotocol/proposals';
|
||||
import {
|
||||
InstrumentDetailsDocument,
|
||||
useNewTransferProposalDetails,
|
||||
type InstrumentDetailsQuery,
|
||||
type InstrumentDetailsQueryVariables,
|
||||
} from '@vegaprotocol/proposals';
|
||||
import { type MockedResponse } from '@apollo/client/testing';
|
||||
import { type Proposal } from '../../types';
|
||||
|
||||
jest.mock('@vegaprotocol/proposals', () => ({
|
||||
...jest.requireActual('@vegaprotocol/proposals'),
|
||||
useSuccessorMarketProposalDetails: () => ({
|
||||
code: 'PARENT_CODE',
|
||||
parentMarketId: 'PARENT_ID',
|
||||
}),
|
||||
useNewTransferProposalDetails: jest.fn(),
|
||||
}));
|
||||
|
||||
@@ -44,7 +45,7 @@ const renderComponent = (
|
||||
render(
|
||||
<AppStateProvider>
|
||||
<BrowserRouter>
|
||||
<MockedProvider mocks={mocks}>
|
||||
<MockedProvider mocks={[networkParamsQueryMock, ...mocks]}>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<ProposalHeader
|
||||
proposal={proposal}
|
||||
@@ -61,10 +62,39 @@ describe('Proposal header', () => {
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('Renders New market proposal', () => {
|
||||
useFeatureFlags.setState({ flags: { SUCCESSOR_MARKETS: true } });
|
||||
|
||||
it('Renders New market proposal', async () => {
|
||||
const parentMarketId = 'parent-id';
|
||||
const parentCode = 'parent-code';
|
||||
const parentName = 'parent-name';
|
||||
const mock: MockedResponse<
|
||||
InstrumentDetailsQuery,
|
||||
InstrumentDetailsQueryVariables
|
||||
> = {
|
||||
request: {
|
||||
query: InstrumentDetailsDocument,
|
||||
variables: {
|
||||
marketId: parentMarketId,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
code: parentCode,
|
||||
name: parentName,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
rationale: {
|
||||
title: 'New some market',
|
||||
@@ -73,6 +103,9 @@ describe('Proposal header', () => {
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'NewMarket',
|
||||
successorConfiguration: {
|
||||
parentMarketId,
|
||||
},
|
||||
instrument: {
|
||||
__typename: 'InstrumentConfiguration',
|
||||
name: 'Some market',
|
||||
@@ -87,7 +120,9 @@ describe('Proposal header', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}),
|
||||
undefined,
|
||||
[mock]
|
||||
);
|
||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
|
||||
'New some market'
|
||||
@@ -96,14 +131,13 @@ describe('Proposal header', () => {
|
||||
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
|
||||
'tGBP settled future.'
|
||||
);
|
||||
expect(screen.getByTestId('proposal-successor-info')).toHaveTextContent(
|
||||
'PARENT_CODE'
|
||||
);
|
||||
expect(
|
||||
await screen.findByTestId('proposal-successor-info')
|
||||
).toHaveTextContent(parentCode);
|
||||
});
|
||||
|
||||
it('Renders Update market proposal', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
rationale: {
|
||||
title: 'New market id',
|
||||
@@ -132,7 +166,6 @@ describe('Proposal header', () => {
|
||||
|
||||
it('Renders New asset proposal - ERC20', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
rationale: {
|
||||
title: 'New asset: Fake currency',
|
||||
@@ -162,8 +195,10 @@ describe('Proposal header', () => {
|
||||
|
||||
it('Renders New asset proposal - BuiltInAsset', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
rationale: {
|
||||
title: 'New asset',
|
||||
},
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'NewAsset',
|
||||
@@ -177,9 +212,7 @@ describe('Proposal header', () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent(
|
||||
'New asset proposal'
|
||||
);
|
||||
expect(screen.getByTestId('proposal-title')).toHaveTextContent('New asset');
|
||||
expect(screen.getByTestId('proposal-type')).toHaveTextContent('New asset');
|
||||
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
|
||||
'Symbol: BIA. Max faucet amount mint: 300'
|
||||
@@ -188,7 +221,6 @@ describe('Proposal header', () => {
|
||||
|
||||
it('Renders Update network', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
rationale: {
|
||||
title: 'Network parameter',
|
||||
@@ -218,7 +250,6 @@ describe('Proposal header', () => {
|
||||
|
||||
it('Renders Freeform proposal - short rationale', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
id: 'short',
|
||||
rationale: {
|
||||
@@ -240,7 +271,6 @@ describe('Proposal header', () => {
|
||||
|
||||
it('Renders Freeform proposal - long rationale (105 chars) - listing', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
id: 'long',
|
||||
rationale: {
|
||||
@@ -266,7 +296,6 @@ describe('Proposal header', () => {
|
||||
// Remove once proposals have rationale and re-enable above tests
|
||||
it('Renders Freeform proposal - id for title', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
id: 'freeform id',
|
||||
rationale: {
|
||||
@@ -323,7 +352,6 @@ describe('Proposal header', () => {
|
||||
|
||||
it('Renders proposal state: Enacted', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_ENACTED,
|
||||
terms: {
|
||||
@@ -336,7 +364,6 @@ describe('Proposal header', () => {
|
||||
|
||||
it('Renders proposal state: Passed', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_PASSED,
|
||||
terms: {
|
||||
@@ -350,7 +377,6 @@ describe('Proposal header', () => {
|
||||
|
||||
it('Renders proposal state: Waiting for node vote', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_WAITING_FOR_NODE_VOTE,
|
||||
terms: {
|
||||
@@ -365,7 +391,6 @@ describe('Proposal header', () => {
|
||||
|
||||
it('Renders proposal state: Open', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
votes: {
|
||||
@@ -434,8 +459,6 @@ describe('Proposal header', () => {
|
||||
});
|
||||
});
|
||||
|
||||
jest.mock('@vegaprotocol/proposals');
|
||||
|
||||
describe('<NewTransferSummary />', () => {
|
||||
it('renders null if no details are provided', () => {
|
||||
(useNewTransferProposalDetails as jest.Mock).mockReturnValue(null);
|
||||
|
||||
+505
-220
@@ -1,4 +1,4 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import {
|
||||
CopyWithTooltip,
|
||||
Lozenge,
|
||||
@@ -8,14 +8,13 @@ import {
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { shorten } from '@vegaprotocol/utils';
|
||||
import { Heading, SubHeading } from '../../../../components/heading';
|
||||
import { type ReactNode } from 'react';
|
||||
import { truncateMiddle } from '../../../../lib/truncate-middle';
|
||||
import { CurrentProposalState } from '../current-proposal-state';
|
||||
import { ProposalInfoLabel } from '../proposal-info-label';
|
||||
import {
|
||||
useCancelTransferProposalDetails,
|
||||
useInstrumentDetailsQuery,
|
||||
useNewTransferProposalDetails,
|
||||
useSuccessorMarketProposalDetails,
|
||||
} from '@vegaprotocol/proposals';
|
||||
import {
|
||||
CONSOLE_MARKET_PAGE,
|
||||
@@ -27,217 +26,510 @@ import Routes from '../../../routes';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { type VoteState } from '../vote-details/use-user-vote';
|
||||
import { VoteBreakdown } from '../vote-breakdown';
|
||||
import { GovernanceTransferKindMapping } from '@vegaprotocol/types';
|
||||
import { type Proposal } from '../../types';
|
||||
import {
|
||||
GovernanceTransferKindMapping,
|
||||
type ProposalRejectionReason,
|
||||
ProposalRejectionReasonMapping,
|
||||
ProposalState,
|
||||
} from '@vegaprotocol/types';
|
||||
import { type Proposal, type BatchProposal } from '../../types';
|
||||
import { type ProposalTermsFieldsFragment } from '../../__generated__/Proposals';
|
||||
import { differenceInHours, format, formatDistanceToNowStrict } from 'date-fns';
|
||||
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
|
||||
|
||||
const ProposalTypeTags = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: Proposal | BatchProposal;
|
||||
}) => {
|
||||
if (proposal.__typename === 'Proposal') {
|
||||
return (
|
||||
<div data-testid="proposal-type">
|
||||
<ProposalTypeTag terms={proposal.terms} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (proposal.__typename === 'BatchProposal') {
|
||||
return (
|
||||
<div data-testid="proposal-type" className="flex gap-1">
|
||||
{proposal.subProposals?.map((subProposal, i) => {
|
||||
if (!subProposal?.terms) return null;
|
||||
return <ProposalTypeTag key={i} terms={subProposal.terms} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const ProposalTypeTag = ({ terms }: { terms: ProposalTermsFieldsFragment }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
switch (terms.change.__typename) {
|
||||
// Speical case for markets where we want to show the product type in the tag
|
||||
case 'NewMarket': {
|
||||
return (
|
||||
<ProposalInfoLabel variant="secondary">
|
||||
{t(
|
||||
terms.change?.instrument?.product?.__typename
|
||||
? `NewMarket${terms.change.instrument.product.__typename}`
|
||||
: 'NewMarket'
|
||||
)}
|
||||
</ProposalInfoLabel>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return (
|
||||
<ProposalInfoLabel variant="secondary">
|
||||
{t(terms.change.__typename)}
|
||||
</ProposalInfoLabel>
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const ProposalDetails = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: Proposal | BatchProposal;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const featureFlags = useFeatureFlags((store) => store.flags);
|
||||
const consoleLink = useLinks(DApp.Console);
|
||||
|
||||
const renderDetails = (terms: ProposalTermsFieldsFragment) => {
|
||||
switch (terms.change?.__typename) {
|
||||
case 'NewMarket': {
|
||||
const getAsset = (terms: ProposalTermsFieldsFragment) => {
|
||||
if (
|
||||
terms?.change.__typename === 'NewMarket' &&
|
||||
(terms.change.instrument.product?.__typename === 'FutureProduct' ||
|
||||
terms.change.instrument.product?.__typename ===
|
||||
'PerpetualProduct')
|
||||
) {
|
||||
return terms.change.instrument.product.settlementAsset;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{terms.change.successorConfiguration && (
|
||||
<ParentMarketCode
|
||||
parentMarketId={
|
||||
terms.change.successorConfiguration.parentMarketId
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<span>
|
||||
{t('Code')}: {terms.change.instrument.code}.
|
||||
</span>{' '}
|
||||
{terms && getAsset(terms)?.symbol ? (
|
||||
<>
|
||||
<span className="font-semibold">{getAsset(terms)?.symbol}</span>{' '}
|
||||
{t('settled future')}.
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'UpdateMarketState': {
|
||||
return (
|
||||
<span>
|
||||
{featureFlags.UPDATE_MARKET_STATE &&
|
||||
terms.change?.market?.id &&
|
||||
terms.change.updateType ? (
|
||||
<>
|
||||
{t(terms.change.updateType)}:{' '}
|
||||
{truncateMiddle(terms.change.market.id)}
|
||||
</>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
case 'UpdateMarket': {
|
||||
return (
|
||||
<>
|
||||
<span>{t('UpdateToMarket')}:</span>{' '}
|
||||
<span className="inline-flex items-start gap-2">
|
||||
<span className="break-all">{terms.change.marketId} </span>
|
||||
<span className="inline-flex items-end gap-0">
|
||||
<CopyWithTooltip
|
||||
text={terms.change.marketId}
|
||||
description={t('copyToClipboard')}
|
||||
>
|
||||
<button className="inline-block px-1">
|
||||
<VegaIcon size={20} name={VegaIconNames.COPY} />
|
||||
</button>
|
||||
</CopyWithTooltip>
|
||||
<Tooltip description={t('OpenInConsole')} align="center">
|
||||
<button
|
||||
className="inline-block px-1"
|
||||
onClick={() => {
|
||||
const marketPageLink = consoleLink(
|
||||
CONSOLE_MARKET_PAGE.replace(
|
||||
':marketId',
|
||||
// @ts-ignore ts doesn't like this field even though its already a string above???
|
||||
terms.change.marketId
|
||||
)
|
||||
);
|
||||
window.open(marketPageLink, '_blank');
|
||||
}}
|
||||
>
|
||||
<VegaIcon size={20} name={VegaIconNames.OPEN_EXTERNAL} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'UpdateReferralProgram': {
|
||||
return null;
|
||||
}
|
||||
case 'UpdateVolumeDiscountProgram': {
|
||||
return null;
|
||||
}
|
||||
case 'NewAsset': {
|
||||
return (
|
||||
<>
|
||||
<span>{t('Symbol')}:</span>{' '}
|
||||
<Lozenge>{terms.change.symbol}.</Lozenge>{' '}
|
||||
{terms.change.source.__typename === 'ERC20' && (
|
||||
<>
|
||||
<span>{t('ERC20ContractAddress')}:</span>{' '}
|
||||
<Lozenge>{terms.change.source.contractAddress}</Lozenge>
|
||||
</>
|
||||
)}{' '}
|
||||
{terms.change.source.__typename === 'BuiltinAsset' && (
|
||||
<>
|
||||
<span>{t('MaxFaucetAmountMint')}:</span>{' '}
|
||||
<Lozenge>{terms.change.source.maxFaucetAmountMint}</Lozenge>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'UpdateNetworkParameter': {
|
||||
return (
|
||||
<Trans
|
||||
i18nKey="Change <lozenge>{{key}}</lozenge> to <lozenge>{{value}}</lozenge>"
|
||||
values={{
|
||||
key: terms.change.networkParameter.key,
|
||||
value: terms.change.networkParameter.value,
|
||||
}}
|
||||
components={{
|
||||
// @ts-ignore children passed by i18next
|
||||
lozenge: <Lozenge />,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'NewFreeform': {
|
||||
return <span />;
|
||||
}
|
||||
case 'UpdateAsset': {
|
||||
return (
|
||||
<Trans
|
||||
i18nKey="Asset ID: <lozenge>{{id}}</lozenge>"
|
||||
values={{
|
||||
id: truncateMiddle(terms.change.assetId),
|
||||
}}
|
||||
components={{
|
||||
// @ts-ignore children passed by i18next
|
||||
lozenge: <Lozenge />,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'NewTransfer':
|
||||
return featureFlags.GOVERNANCE_TRANSFERS ? (
|
||||
<NewTransferSummary proposalId={proposal?.id} />
|
||||
) : null;
|
||||
case 'CancelTransfer':
|
||||
return featureFlags.GOVERNANCE_TRANSFERS ? (
|
||||
<CancelTransferSummary proposalId={proposal?.id} />
|
||||
) : null;
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let details = null;
|
||||
|
||||
if (proposal.__typename === 'Proposal') {
|
||||
details = (
|
||||
<div>
|
||||
<div>{renderDetails(proposal.terms)}</div>
|
||||
<VoteStateText
|
||||
state={proposal.state}
|
||||
closingDatetime={proposal.terms.closingDatetime}
|
||||
enactmentDatetime={proposal.terms.enactmentDatetime}
|
||||
rejectionReason={proposal.rejectionReason}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (proposal.__typename === 'BatchProposal' && proposal.subProposals) {
|
||||
details = (
|
||||
<div>
|
||||
<h3 className="text-xl border-b border-default pb-3 mb-3">
|
||||
Proposals in batch
|
||||
</h3>
|
||||
<ul className="flex flex-col gap-2 border-b border-default pb-3 mb-3">
|
||||
{proposal.subProposals.map((p, i) => {
|
||||
if (!p?.terms) return null;
|
||||
return (
|
||||
<li key={i}>
|
||||
<div>{renderDetails(p.terms)}</div>
|
||||
<SubProposalStateText
|
||||
state={proposal.state}
|
||||
enactmentDatetime={p.terms.enactmentDatetime}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<BatchProposalStateText
|
||||
state={proposal.state}
|
||||
closingDatetime={proposal.batchTerms?.closingDatetime}
|
||||
rejectionReason={proposal.rejectionReason}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="proposal-details"
|
||||
className="break-words mb-6 text-vega-light-200"
|
||||
>
|
||||
{details}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const VoteStateText = ({
|
||||
state,
|
||||
closingDatetime,
|
||||
enactmentDatetime,
|
||||
rejectionReason,
|
||||
}: {
|
||||
state: ProposalState;
|
||||
closingDatetime: string;
|
||||
enactmentDatetime: string;
|
||||
rejectionReason: ProposalRejectionReason | null | undefined;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const nowToCloseInHours = differenceInHours(
|
||||
new Date(closingDatetime),
|
||||
new Date()
|
||||
);
|
||||
|
||||
const props = {
|
||||
'data-testid': 'vote-details',
|
||||
};
|
||||
|
||||
switch (state) {
|
||||
case ProposalState.STATE_ENACTED: {
|
||||
return (
|
||||
<p {...props}>
|
||||
{t('enactedOn{{date}}', {
|
||||
enactmentDate:
|
||||
enactmentDatetime &&
|
||||
format(new Date(enactmentDatetime), DATE_FORMAT_DETAILED),
|
||||
})}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
case ProposalState.STATE_PASSED:
|
||||
case ProposalState.STATE_WAITING_FOR_NODE_VOTE: {
|
||||
return (
|
||||
<p {...props}>
|
||||
{t('enactsOn{{date}}', {
|
||||
enactmentDate:
|
||||
enactmentDatetime &&
|
||||
format(new Date(enactmentDatetime), DATE_FORMAT_DETAILED),
|
||||
})}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
case ProposalState.STATE_OPEN: {
|
||||
return (
|
||||
<p {...props}>
|
||||
<span className={nowToCloseInHours < 6 ? 'text-vega-orange' : ''}>
|
||||
{t('{{time}} left to vote', {
|
||||
time: formatDistanceToNowStrict(new Date(closingDatetime)),
|
||||
})}
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
case ProposalState.STATE_DECLINED: {
|
||||
return <p {...props}>{t(state)}</p>;
|
||||
}
|
||||
case ProposalState.STATE_REJECTED: {
|
||||
const props = { 'data-testid': 'vote-status' };
|
||||
|
||||
if (rejectionReason) {
|
||||
return (
|
||||
<p {...props}>{t(ProposalRejectionReasonMapping[rejectionReason])}</p>
|
||||
);
|
||||
}
|
||||
|
||||
return <p {...props}>{t('Proposal rejected')}</p>;
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders state details relevant to the sub proposal, namely the enactment
|
||||
* date and time
|
||||
*/
|
||||
const SubProposalStateText = ({
|
||||
state,
|
||||
enactmentDatetime,
|
||||
}: {
|
||||
state: ProposalState;
|
||||
enactmentDatetime: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const props = {
|
||||
'data-testid': 'vote-details',
|
||||
className: 'm-0',
|
||||
};
|
||||
|
||||
switch (state) {
|
||||
case ProposalState.STATE_ENACTED: {
|
||||
return (
|
||||
<p {...props}>
|
||||
{t('enactedOn{{date}}', {
|
||||
enactmentDate:
|
||||
enactmentDatetime &&
|
||||
format(new Date(enactmentDatetime), DATE_FORMAT_DETAILED),
|
||||
})}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
case ProposalState.STATE_OPEN:
|
||||
case ProposalState.STATE_PASSED:
|
||||
case ProposalState.STATE_WAITING_FOR_NODE_VOTE: {
|
||||
return (
|
||||
<p {...props}>
|
||||
{t('enactsOn{{date}}', {
|
||||
enactmentDate:
|
||||
enactmentDatetime &&
|
||||
format(new Date(enactmentDatetime), DATE_FORMAT_DETAILED),
|
||||
})}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
case ProposalState.STATE_REJECTED:
|
||||
case ProposalState.STATE_DECLINED: {
|
||||
// If voting is still open we render a single clost time for all sub proposals
|
||||
return null;
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders state details relevant for the entire batch. IE. if the proposal was
|
||||
* rejected or declined, or the vote close time. Does not render enactment times as
|
||||
* those are relevant to the sub proposal
|
||||
*/
|
||||
const BatchProposalStateText = ({
|
||||
state,
|
||||
closingDatetime,
|
||||
rejectionReason,
|
||||
}: {
|
||||
state: ProposalState;
|
||||
closingDatetime: string;
|
||||
rejectionReason: ProposalRejectionReason | null | undefined;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const nowToCloseInHours = differenceInHours(
|
||||
new Date(closingDatetime),
|
||||
new Date()
|
||||
);
|
||||
|
||||
const props = {
|
||||
'data-testid': 'vote-details',
|
||||
};
|
||||
|
||||
switch (state) {
|
||||
case ProposalState.STATE_ENACTED:
|
||||
case ProposalState.STATE_PASSED:
|
||||
case ProposalState.STATE_WAITING_FOR_NODE_VOTE: {
|
||||
return null;
|
||||
}
|
||||
case ProposalState.STATE_OPEN: {
|
||||
return (
|
||||
<p {...props}>
|
||||
<span className={nowToCloseInHours < 6 ? 'text-vega-orange' : ''}>
|
||||
{t('{{time}} left to vote', {
|
||||
time: formatDistanceToNowStrict(new Date(closingDatetime)),
|
||||
})}
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
case ProposalState.STATE_DECLINED: {
|
||||
return <p {...props}>{t(state)}</p>;
|
||||
}
|
||||
case ProposalState.STATE_REJECTED: {
|
||||
const props = { 'data-testid': 'vote-status' };
|
||||
|
||||
if (rejectionReason) {
|
||||
return (
|
||||
<p {...props}>{t(ProposalRejectionReasonMapping[rejectionReason])}</p>
|
||||
);
|
||||
}
|
||||
|
||||
return <p {...props}>{t('Proposal rejected')}</p>;
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const ProposalHeader = ({
|
||||
proposal,
|
||||
isListItem = true,
|
||||
voteState,
|
||||
}: {
|
||||
proposal: Proposal;
|
||||
proposal: Proposal | BatchProposal;
|
||||
isListItem?: boolean;
|
||||
voteState?: VoteState | null;
|
||||
}) => {
|
||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||
const { t } = useTranslation();
|
||||
const change = proposal?.terms.change;
|
||||
|
||||
const consoleLink = useLinks(DApp.Console);
|
||||
|
||||
let details: ReactNode;
|
||||
let proposalType = '';
|
||||
let fallbackTitle = '';
|
||||
|
||||
const title = proposal?.rationale.title.trim();
|
||||
|
||||
const fallbackTitle = t(
|
||||
proposal.__typename === 'Proposal'
|
||||
? 'Unknown proposal'
|
||||
: 'Unknown batch proposal'
|
||||
);
|
||||
const titleContent = shorten(title ?? '', 100);
|
||||
|
||||
const getAsset = (proposal: Proposal) => {
|
||||
const terms = proposal?.terms;
|
||||
if (
|
||||
terms?.change.__typename === 'NewMarket' &&
|
||||
(terms.change.instrument.product?.__typename === 'FutureProduct' ||
|
||||
terms.change.instrument.product?.__typename === 'PerpetualProduct')
|
||||
) {
|
||||
return terms.change.instrument.product.settlementAsset;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
switch (change?.__typename) {
|
||||
case 'NewMarket': {
|
||||
proposalType =
|
||||
featureFlags.PRODUCT_PERPETUALS &&
|
||||
change?.instrument?.product?.__typename
|
||||
? `NewMarket${change?.instrument?.product?.__typename}`
|
||||
: 'NewMarket';
|
||||
fallbackTitle = t('NewMarketProposal');
|
||||
details = (
|
||||
<>
|
||||
{featureFlags.SUCCESSOR_MARKETS && (
|
||||
<SuccessorCode proposalId={proposal?.id} />
|
||||
)}
|
||||
<span>
|
||||
{t('Code')}: {change.instrument.code}.
|
||||
</span>{' '}
|
||||
{proposal?.terms && getAsset(proposal)?.symbol ? (
|
||||
<>
|
||||
<span className="font-semibold">
|
||||
{getAsset(proposal)?.symbol}
|
||||
</span>{' '}
|
||||
{t('settled future')}.
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'UpdateMarketState': {
|
||||
proposalType =
|
||||
featureFlags.UPDATE_MARKET_STATE && change?.updateType
|
||||
? t(change.updateType)
|
||||
: 'UpdateMarketState';
|
||||
fallbackTitle = t('UpdateMarketStateProposal');
|
||||
details = (
|
||||
<span>
|
||||
{featureFlags.UPDATE_MARKET_STATE &&
|
||||
change?.market?.id &&
|
||||
change.updateType ? (
|
||||
<>
|
||||
{t(change.updateType)}: {truncateMiddle(change.market.id)}
|
||||
</>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'UpdateMarket': {
|
||||
proposalType = 'UpdateMarket';
|
||||
fallbackTitle = t('UpdateMarketProposal');
|
||||
details = (
|
||||
<>
|
||||
<span>{t('UpdateToMarket')}:</span>{' '}
|
||||
<span className="inline-flex items-start gap-2">
|
||||
<span className="break-all">{change.marketId} </span>
|
||||
<span className="inline-flex items-end gap-0">
|
||||
<CopyWithTooltip
|
||||
text={change.marketId}
|
||||
description={t('copyToClipboard')}
|
||||
>
|
||||
<button className="inline-block px-1">
|
||||
<VegaIcon size={20} name={VegaIconNames.COPY} />
|
||||
</button>
|
||||
</CopyWithTooltip>
|
||||
<Tooltip description={t('OpenInConsole')} align="center">
|
||||
<button
|
||||
className="inline-block px-1"
|
||||
onClick={() => {
|
||||
const marketPageLink = consoleLink(
|
||||
CONSOLE_MARKET_PAGE.replace(':marketId', change.marketId)
|
||||
);
|
||||
window.open(marketPageLink, '_blank');
|
||||
}}
|
||||
>
|
||||
<VegaIcon size={20} name={VegaIconNames.OPEN_EXTERNAL} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'UpdateReferralProgram': {
|
||||
proposalType = 'UpdateReferralProgram';
|
||||
fallbackTitle = t('UpdateReferralProgramProposal');
|
||||
break;
|
||||
}
|
||||
case 'UpdateVolumeDiscountProgram': {
|
||||
proposalType = 'UpdateVolumeDiscountProgram';
|
||||
fallbackTitle = t('UpdateVolumeDiscountProgramProposal');
|
||||
break;
|
||||
}
|
||||
case 'NewAsset': {
|
||||
proposalType = 'NewAsset';
|
||||
fallbackTitle = t('NewAssetProposal');
|
||||
details = (
|
||||
<>
|
||||
<span>{t('Symbol')}:</span> <Lozenge>{change.symbol}.</Lozenge>{' '}
|
||||
{change.source.__typename === 'ERC20' && (
|
||||
<>
|
||||
<span>{t('ERC20ContractAddress')}:</span>{' '}
|
||||
<Lozenge>{change.source.contractAddress}</Lozenge>
|
||||
</>
|
||||
)}{' '}
|
||||
{change.source.__typename === 'BuiltinAsset' && (
|
||||
<>
|
||||
<span>{t('MaxFaucetAmountMint')}:</span>{' '}
|
||||
<Lozenge>{change.source.maxFaucetAmountMint}</Lozenge>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'UpdateNetworkParameter': {
|
||||
proposalType = 'NetworkParameter';
|
||||
fallbackTitle = t('NetworkParameterProposal');
|
||||
details = (
|
||||
<>
|
||||
<span>{t('Change')}:</span>{' '}
|
||||
<Lozenge>{change.networkParameter.key}</Lozenge>{' '}
|
||||
<span>{t('to')}</span>{' '}
|
||||
<span className="whitespace-nowrap">
|
||||
<Lozenge>{change.networkParameter.value}</Lozenge>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'NewFreeform': {
|
||||
proposalType = 'Freeform';
|
||||
fallbackTitle = t('FreeformProposal');
|
||||
details = <span />;
|
||||
break;
|
||||
}
|
||||
case 'UpdateAsset': {
|
||||
proposalType = 'UpdateAsset';
|
||||
fallbackTitle = t('UpdateAssetProposal');
|
||||
details = (
|
||||
<>
|
||||
<span>{t('AssetID')}:</span>{' '}
|
||||
<Lozenge>{truncateMiddle(change.assetId)}</Lozenge>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'NewTransfer':
|
||||
proposalType = 'NewTransfer';
|
||||
fallbackTitle = t('NewTransferProposal');
|
||||
details = featureFlags.GOVERNANCE_TRANSFERS ? (
|
||||
<NewTransferSummary proposalId={proposal?.id} />
|
||||
) : null;
|
||||
break;
|
||||
case 'CancelTransfer':
|
||||
proposalType = 'CancelTransfer';
|
||||
fallbackTitle = t('CancelTransferProposal');
|
||||
details = featureFlags.GOVERNANCE_TRANSFERS ? (
|
||||
<CancelTransferSummary proposalId={proposal?.id} />
|
||||
) : null;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-between gap-4 mb-6 text-sm">
|
||||
<div data-testid="proposal-type">
|
||||
<ProposalInfoLabel variant="secondary">
|
||||
{t(`${proposalType}`)}
|
||||
</ProposalInfoLabel>
|
||||
</div>
|
||||
<ProposalTypeTags proposal={proposal} />
|
||||
|
||||
<div className="flex items-center gap-6">
|
||||
{(voteState === 'Yes' || voteState === 'No') && (
|
||||
@@ -264,50 +556,43 @@ export const ProposalHeader = ({
|
||||
<div data-testid="proposal-title" className="break-all">
|
||||
{isListItem ? (
|
||||
<header>
|
||||
<SubHeading
|
||||
title={titleContent || fallbackTitle || t('Unknown proposal')}
|
||||
/>
|
||||
<SubHeading title={titleContent || fallbackTitle} />
|
||||
</header>
|
||||
) : (
|
||||
<Heading
|
||||
title={titleContent || fallbackTitle || t('Unknown proposal')}
|
||||
/>
|
||||
<Heading title={titleContent || fallbackTitle} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{details && (
|
||||
<div
|
||||
data-testid="proposal-details"
|
||||
className="break-words mb-6 text-vega-light-200"
|
||||
>
|
||||
{details}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ProposalDetails proposal={proposal} />
|
||||
<VoteBreakdown proposal={proposal} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const SuccessorCode = ({
|
||||
proposalId,
|
||||
export const ParentMarketCode = ({
|
||||
parentMarketId,
|
||||
}: {
|
||||
proposalId?: string | null;
|
||||
parentMarketId: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const successor = useSuccessorMarketProposalDetails(proposalId);
|
||||
const { data } = useInstrumentDetailsQuery({
|
||||
variables: {
|
||||
marketId: parentMarketId,
|
||||
},
|
||||
});
|
||||
|
||||
return successor.parentMarketId || successor.code ? (
|
||||
if (!data?.market?.tradableInstrument.instrument.code) return null;
|
||||
|
||||
return (
|
||||
<span className="block" data-testid="proposal-successor-info">
|
||||
{t('Successor market to')}:{' '}
|
||||
<Link
|
||||
to={`${Routes.PROPOSALS}/${successor.parentMarketId}`}
|
||||
to={`${Routes.PROPOSALS}/${parentMarketId}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{successor.code || successor.parentMarketId}
|
||||
{data.market.tradableInstrument.instrument.code}
|
||||
</Link>
|
||||
</span>
|
||||
) : null;
|
||||
);
|
||||
};
|
||||
|
||||
export const NewTransferSummary = ({
|
||||
|
||||
@@ -3,13 +3,15 @@ import { useTranslation } from 'react-i18next';
|
||||
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import {
|
||||
type BatchProposalFieldsFragment,
|
||||
type ProposalFieldsFragment,
|
||||
} from '../../__generated__/Proposals';
|
||||
|
||||
export const ProposalJson = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
proposal: ProposalFieldsFragment | BatchProposalFieldsFragment;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
|
||||
+3
-15
@@ -57,33 +57,21 @@ describe('applyImmutableKeysFromEarlierVersion', () => {
|
||||
describe('ProposalMarketChanges', () => {
|
||||
it('renders correctly', () => {
|
||||
const { getByTestId } = render(
|
||||
<ProposalMarketChanges
|
||||
originalProposal={{}}
|
||||
latestEnactedProposal={{}}
|
||||
updatedProposal={{}}
|
||||
/>
|
||||
<ProposalMarketChanges marketId="market-id" updatedProposal={{}} />
|
||||
);
|
||||
expect(getByTestId('proposal-market-changes')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('JsonDiff is not visible when showChanges is false', () => {
|
||||
const { queryByTestId } = render(
|
||||
<ProposalMarketChanges
|
||||
originalProposal={{}}
|
||||
latestEnactedProposal={{}}
|
||||
updatedProposal={{}}
|
||||
/>
|
||||
<ProposalMarketChanges marketId="market-id" updatedProposal={{}} />
|
||||
);
|
||||
expect(queryByTestId('json-diff')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('JsonDiff is visible when showChanges is true', async () => {
|
||||
const { getByTestId } = render(
|
||||
<ProposalMarketChanges
|
||||
originalProposal={{}}
|
||||
latestEnactedProposal={{}}
|
||||
updatedProposal={{}}
|
||||
/>
|
||||
<ProposalMarketChanges marketId="market-id" updatedProposal={{}} />
|
||||
);
|
||||
fireEvent.click(getByTestId('proposal-market-changes-toggle'));
|
||||
expect(getByTestId('json-diff')).toBeInTheDocument();
|
||||
|
||||
+38
-4
@@ -7,6 +7,8 @@ import { useState } from 'react';
|
||||
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import type { JsonValue } from '../../../../components/json-diff';
|
||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||
import { ENV } from '../../../../config';
|
||||
|
||||
const immutableKeys = [
|
||||
'decimalPlaces',
|
||||
@@ -40,19 +42,51 @@ export const applyImmutableKeysFromEarlierVersion = (
|
||||
};
|
||||
|
||||
interface ProposalMarketChangesProps {
|
||||
originalProposal: JsonValue;
|
||||
latestEnactedProposal: JsonValue | undefined;
|
||||
marketId: string;
|
||||
updatedProposal: JsonValue;
|
||||
}
|
||||
|
||||
export const ProposalMarketChanges = ({
|
||||
originalProposal,
|
||||
latestEnactedProposal,
|
||||
marketId,
|
||||
updatedProposal,
|
||||
}: ProposalMarketChangesProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [showChanges, setShowChanges] = useState(false);
|
||||
|
||||
const {
|
||||
state: { data },
|
||||
} = useFetch(`${ENV.rest}governance?proposalId=${marketId}`, undefined, true);
|
||||
|
||||
const {
|
||||
state: { data: enactedProposalData },
|
||||
} = useFetch(
|
||||
`${ENV.rest}governances?proposalState=STATE_ENACTED&proposalType=TYPE_UPDATE_MARKET`,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
|
||||
// @ts-ignore no types here :-/
|
||||
const enacted = enactedProposalData?.connection?.edges
|
||||
.filter(
|
||||
// @ts-ignore no type here
|
||||
({ node }) => node?.proposal?.terms?.updateMarket?.marketId === marketId
|
||||
)
|
||||
// @ts-ignore no type here
|
||||
.sort((a, b) => {
|
||||
return (
|
||||
new Date(a?.node?.terms?.enactmentTimestamp).getTime() -
|
||||
new Date(b?.node?.terms?.enactmentTimestamp).getTime()
|
||||
);
|
||||
});
|
||||
|
||||
const latestEnactedProposal = enacted?.length
|
||||
? enacted[enacted.length - 1]
|
||||
: undefined;
|
||||
|
||||
const originalProposal =
|
||||
// @ts-ignore no types with useFetch TODO: check this is good
|
||||
data?.data?.proposal?.terms?.newMarket?.changes;
|
||||
|
||||
return (
|
||||
<section data-testid="proposal-market-changes">
|
||||
<CollapsibleToggle
|
||||
|
||||
+21
-9
@@ -18,6 +18,7 @@ import {
|
||||
getDataSourceSpecForTradingTermination,
|
||||
getSigners,
|
||||
MarginScalingFactorsPanel,
|
||||
marketInfoProvider,
|
||||
} from '@vegaprotocol/markets';
|
||||
import {
|
||||
Button,
|
||||
@@ -28,8 +29,8 @@ import {
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
|
||||
import type { MarketInfo } from '@vegaprotocol/markets';
|
||||
import { create } from 'zustand';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
|
||||
type MarketDataDialogState = {
|
||||
isOpen: boolean;
|
||||
@@ -48,18 +49,29 @@ export const useMarketDataDialogStore = create<MarketDataDialogState>(
|
||||
const marketDataHeaderStyles =
|
||||
'font-alpha calt text-base border-b border-vega-dark-200 mt-2 py-2';
|
||||
|
||||
export const ProposalMarketData = ({
|
||||
marketData,
|
||||
parentMarketData,
|
||||
}: {
|
||||
marketData: MarketInfo;
|
||||
parentMarketData?: MarketInfo;
|
||||
}) => {
|
||||
export const ProposalMarketData = ({ proposalId }: { proposalId: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, open, close } = useMarketDataDialogStore();
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
|
||||
if (!marketData) {
|
||||
const { data: marketData } = useDataProvider({
|
||||
dataProvider: marketInfoProvider,
|
||||
skipUpdates: true,
|
||||
variables: {
|
||||
marketId: proposalId,
|
||||
},
|
||||
});
|
||||
|
||||
const { data: parentMarketData } = useDataProvider({
|
||||
dataProvider: marketInfoProvider,
|
||||
skipUpdates: true,
|
||||
skip: !marketData?.parentMarketID,
|
||||
variables: {
|
||||
marketId: marketData?.parentMarketID || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!marketData || !parentMarketData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+43
-66
@@ -7,7 +7,6 @@ import {
|
||||
formatReferralRewardMultiplier,
|
||||
ProposalReferralProgramDetails,
|
||||
} from './proposal-referral-program-details';
|
||||
import { generateProposal } from '../../test-helpers/generate-proposals';
|
||||
|
||||
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
|
||||
useAppState: () => ({
|
||||
@@ -59,87 +58,65 @@ describe('ProposalReferralProgramDetails helper functions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const mockReferralProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateReferralProgram',
|
||||
benefitTiers: [
|
||||
{
|
||||
minimumEpochs: 6,
|
||||
minimumRunningNotionalTakerVolume: '10000',
|
||||
referralDiscountFactor: '0.001',
|
||||
referralRewardFactor: '0.001',
|
||||
},
|
||||
{
|
||||
minimumEpochs: 24,
|
||||
minimumRunningNotionalTakerVolume: '500000',
|
||||
referralDiscountFactor: '0.005',
|
||||
referralRewardFactor: '0.005',
|
||||
},
|
||||
{
|
||||
minimumEpochs: 48,
|
||||
minimumRunningNotionalTakerVolume: '1000000',
|
||||
referralDiscountFactor: '0.01',
|
||||
referralRewardFactor: '0.01',
|
||||
},
|
||||
],
|
||||
endOfProgram: '2026-10-03T10:34:34Z',
|
||||
windowLength: 3,
|
||||
stakingTiers: [
|
||||
{
|
||||
minimumStakedTokens: '1',
|
||||
referralRewardMultiplier: '1',
|
||||
},
|
||||
{
|
||||
minimumStakedTokens: '2',
|
||||
referralRewardMultiplier: '2',
|
||||
},
|
||||
{
|
||||
minimumStakedTokens: '5',
|
||||
referralRewardMultiplier: '3',
|
||||
},
|
||||
],
|
||||
const mockChange = {
|
||||
__typename: 'UpdateReferralProgram' as const,
|
||||
benefitTiers: [
|
||||
{
|
||||
minimumEpochs: 6,
|
||||
minimumRunningNotionalTakerVolume: '10000',
|
||||
referralDiscountFactor: '0.001',
|
||||
referralRewardFactor: '0.001',
|
||||
},
|
||||
},
|
||||
});
|
||||
{
|
||||
minimumEpochs: 24,
|
||||
minimumRunningNotionalTakerVolume: '500000',
|
||||
referralDiscountFactor: '0.005',
|
||||
referralRewardFactor: '0.005',
|
||||
},
|
||||
{
|
||||
minimumEpochs: 48,
|
||||
minimumRunningNotionalTakerVolume: '1000000',
|
||||
referralDiscountFactor: '0.01',
|
||||
referralRewardFactor: '0.01',
|
||||
},
|
||||
],
|
||||
endOfProgram: '2026-10-03T10:34:34Z',
|
||||
windowLength: 3,
|
||||
stakingTiers: [
|
||||
{
|
||||
minimumStakedTokens: '1',
|
||||
referralRewardMultiplier: '1',
|
||||
},
|
||||
{
|
||||
minimumStakedTokens: '2',
|
||||
referralRewardMultiplier: '2',
|
||||
},
|
||||
{
|
||||
minimumStakedTokens: '5',
|
||||
referralRewardMultiplier: '3',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('<ProposalReferralProgramDetails />', () => {
|
||||
it('should not render if proposal is null', () => {
|
||||
render(<ProposalReferralProgramDetails proposal={null} />);
|
||||
expect(
|
||||
screen.queryByTestId('proposal-referral-program-details')
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should not render if __typename is not UpdateReferralProgram', () => {
|
||||
const updateMarketProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateMarket',
|
||||
},
|
||||
},
|
||||
});
|
||||
render(<ProposalReferralProgramDetails proposal={updateMarketProposal} />);
|
||||
render(<ProposalReferralProgramDetails change={null} />);
|
||||
expect(
|
||||
screen.queryByTestId('proposal-referral-program-details')
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should not render if there are no relevant fields', () => {
|
||||
const incompleteProposal = generateProposal({
|
||||
terms: {
|
||||
change: {},
|
||||
},
|
||||
});
|
||||
|
||||
render(<ProposalReferralProgramDetails proposal={incompleteProposal} />);
|
||||
const emptyChange = {};
|
||||
// @ts-ignore change deliberately empty
|
||||
render(<ProposalReferralProgramDetails change={emptyChange} />);
|
||||
expect(
|
||||
screen.queryByTestId('proposal-referral-program-details')
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should render relevant fields if present', () => {
|
||||
render(<ProposalReferralProgramDetails proposal={mockReferralProposal} />);
|
||||
render(<ProposalReferralProgramDetails change={mockChange} />);
|
||||
expect(
|
||||
screen.getByTestId('proposal-referral-program-window-length')
|
||||
).toBeInTheDocument();
|
||||
|
||||
+9
-10
@@ -13,10 +13,10 @@ import {
|
||||
} from '@vegaprotocol/utils';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { useAppState } from '../../../../contexts/app-state/app-state-context';
|
||||
import { type Proposal } from '../../types';
|
||||
import { type UpdateReferralProgramsFragment } from '../../__generated__/Proposals';
|
||||
|
||||
interface ProposalReferralProgramDetailsProps {
|
||||
proposal: Proposal | null;
|
||||
change: UpdateReferralProgramsFragment | null;
|
||||
}
|
||||
|
||||
export const formatEndOfProgramTimestamp = (value: string) => {
|
||||
@@ -44,20 +44,19 @@ export const formatReferralRewardMultiplier = (value: string) => {
|
||||
};
|
||||
|
||||
export const ProposalReferralProgramDetails = ({
|
||||
proposal,
|
||||
change,
|
||||
}: ProposalReferralProgramDetailsProps) => {
|
||||
const {
|
||||
appState: { decimals },
|
||||
} = useAppState();
|
||||
const { t } = useTranslation();
|
||||
if (proposal?.terms?.change?.__typename !== 'UpdateReferralProgram') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const benefitTiers = proposal?.terms?.change?.benefitTiers.slice();
|
||||
const stakingTiers = proposal?.terms?.change?.stakingTiers.slice();
|
||||
const windowLength = proposal?.terms?.change?.windowLength;
|
||||
const endOfProgramTimestamp = proposal?.terms?.change?.endOfProgram;
|
||||
if (change?.__typename !== 'UpdateReferralProgram') return null;
|
||||
|
||||
const benefitTiers = change?.benefitTiers.slice();
|
||||
const stakingTiers = change?.stakingTiers.slice();
|
||||
const windowLength = change?.windowLength;
|
||||
const endOfProgramTimestamp = change?.endOfProgram;
|
||||
|
||||
if (
|
||||
!benefitTiers &&
|
||||
|
||||
+3
-4
@@ -6,15 +6,14 @@ import {
|
||||
RoundedWrapper,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import { type Proposal } from '../../types';
|
||||
|
||||
export const ProposalCancelTransferDetails = ({
|
||||
proposal,
|
||||
proposalId,
|
||||
}: {
|
||||
proposal: Proposal;
|
||||
proposalId: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const details = useCancelTransferProposalDetails(proposal?.id);
|
||||
const details = useCancelTransferProposalDetails(proposalId);
|
||||
|
||||
if (!details) {
|
||||
return null;
|
||||
|
||||
+3
-4
@@ -19,17 +19,16 @@ import {
|
||||
addDecimalsFormatNumberQuantum,
|
||||
formatDateWithLocalTimezone,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { type Proposal } from '../../types';
|
||||
|
||||
export const ProposalTransferDetails = ({
|
||||
proposal,
|
||||
proposalId,
|
||||
}: {
|
||||
proposal: Proposal;
|
||||
proposalId: string;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
const details = useNewTransferProposalDetails(proposal?.id);
|
||||
const details = useNewTransferProposalDetails(proposalId);
|
||||
if (!details) {
|
||||
return null;
|
||||
}
|
||||
|
||||
+51
-94
@@ -1,6 +1,5 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ProposalUpdateBenefitTiers } from './proposal-update-benefit-tiers-details';
|
||||
import { generateProposal } from '../../test-helpers/generate-proposals';
|
||||
|
||||
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
|
||||
useAppState: () => ({
|
||||
@@ -10,109 +9,71 @@ jest.mock('../../../../contexts/app-state/app-state-context', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockVestingBenefitTierProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
networkParameter: {
|
||||
key: 'blah.blah.benefitTiers',
|
||||
value: JSON.stringify({
|
||||
tiers: [
|
||||
{
|
||||
minimum_quantum_balance: '10000',
|
||||
reward_multiplier: '0.05',
|
||||
},
|
||||
{
|
||||
minimum_quantum_balance: '500000000000',
|
||||
reward_multiplier: '0.1',
|
||||
},
|
||||
{
|
||||
minimum_quantum_balance: '10000000000000',
|
||||
reward_multiplier: '10',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
const mockChange1 = {
|
||||
__typename: 'UpdateNetworkParameter' as const,
|
||||
networkParameter: {
|
||||
key: 'blah.blah.benefitTiers',
|
||||
value: JSON.stringify({
|
||||
tiers: [
|
||||
{
|
||||
minimum_quantum_balance: '10000',
|
||||
reward_multiplier: '0.05',
|
||||
},
|
||||
{
|
||||
minimum_quantum_balance: '500000000000',
|
||||
reward_multiplier: '0.1',
|
||||
},
|
||||
{
|
||||
minimum_quantum_balance: '10000000000000',
|
||||
reward_multiplier: '10',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const mockActivityStreakBenefitTierProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
networkParameter: {
|
||||
key: 'blah.blah.benefitTiers',
|
||||
value: JSON.stringify({
|
||||
tiers: [
|
||||
{
|
||||
minimum_activity_streak: '10000',
|
||||
vesting_multiplier: '5',
|
||||
reward_multiplier: '0.1',
|
||||
},
|
||||
{
|
||||
minimum_activity_streak: '10000000000000',
|
||||
vesting_multiplier: '100',
|
||||
reward_multiplier: '10',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
const mockChange2 = {
|
||||
__typename: 'UpdateNetworkParameter' as const,
|
||||
networkParameter: {
|
||||
key: 'blah.blah.benefitTiers',
|
||||
value: JSON.stringify({
|
||||
tiers: [
|
||||
{
|
||||
minimum_activity_streak: '10000',
|
||||
vesting_multiplier: '5',
|
||||
reward_multiplier: '0.1',
|
||||
},
|
||||
{
|
||||
minimum_activity_streak: '10000000000000',
|
||||
vesting_multiplier: '100',
|
||||
reward_multiplier: '10',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe('ProposalUpdateBenefitTiers', () => {
|
||||
it('should not render if proposal is null', () => {
|
||||
render(<ProposalUpdateBenefitTiers proposal={null} />);
|
||||
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not render if __typename is not UpdateNetworkParameter', () => {
|
||||
const updateMarketProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateMarket',
|
||||
},
|
||||
},
|
||||
});
|
||||
render(<ProposalUpdateBenefitTiers proposal={updateMarketProposal} />);
|
||||
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not render if there are no relevant fields', () => {
|
||||
const incompleteProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<ProposalUpdateBenefitTiers proposal={incompleteProposal} />);
|
||||
render(<ProposalUpdateBenefitTiers change={null} />);
|
||||
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
|
||||
});
|
||||
|
||||
it('should not render if there are relevant fields that are empty', () => {
|
||||
const incompleteProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
networkParameter: {
|
||||
key: 'blah.blah.benefitTiers',
|
||||
value: JSON.stringify({}),
|
||||
},
|
||||
},
|
||||
const incompleteProposal = {
|
||||
__typename: 'UpdateNetworkParameter' as const,
|
||||
networkParameter: {
|
||||
key: 'blah.blah.benefitTiers',
|
||||
value: JSON.stringify({}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
render(<ProposalUpdateBenefitTiers proposal={incompleteProposal} />);
|
||||
render(<ProposalUpdateBenefitTiers change={incompleteProposal} />);
|
||||
expect(screen.queryByTestId('proposal-update-benefit-tiers')).toBeNull();
|
||||
});
|
||||
|
||||
it('should render a valid vesting benefit tier proposal', () => {
|
||||
render(
|
||||
<ProposalUpdateBenefitTiers proposal={mockVestingBenefitTierProposal} />
|
||||
);
|
||||
render(<ProposalUpdateBenefitTiers change={mockChange1} />);
|
||||
|
||||
// 3 tiers in the sample data
|
||||
expect(screen.getByText('Tier 1')).toBeInTheDocument();
|
||||
@@ -133,11 +94,7 @@ describe('ProposalUpdateBenefitTiers', () => {
|
||||
});
|
||||
|
||||
it('should render a valid activity streak benefit tier proposal', () => {
|
||||
render(
|
||||
<ProposalUpdateBenefitTiers
|
||||
proposal={mockActivityStreakBenefitTierProposal}
|
||||
/>
|
||||
);
|
||||
render(<ProposalUpdateBenefitTiers change={mockChange2} />);
|
||||
|
||||
// 3 tiers in the sample data
|
||||
expect(screen.getByText('Tier 1')).toBeInTheDocument();
|
||||
|
||||
+6
-6
@@ -11,7 +11,7 @@ import {
|
||||
} from '../proposal-referral-program-details';
|
||||
import { formatNumberPercentage } from '@vegaprotocol/utils';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { type Proposal } from '../../types';
|
||||
import { type IUpdateNetworkParameterFieldsFragment } from '../../__generated__/Proposals';
|
||||
|
||||
// These types are not generated as it's not known how dynamic these are
|
||||
type VestingBenefitTier = {
|
||||
@@ -43,7 +43,7 @@ export const formatVolumeDiscountFactor = (value: string) => {
|
||||
};
|
||||
|
||||
interface ProposalReferralProgramDetailsProps {
|
||||
proposal: Proposal | null;
|
||||
change: IUpdateNetworkParameterFieldsFragment | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,17 +55,17 @@ interface ProposalReferralProgramDetailsProps {
|
||||
* It only renders known fields so that they can be formatted correctly.
|
||||
*/
|
||||
export const ProposalUpdateBenefitTiers = ({
|
||||
proposal,
|
||||
change,
|
||||
}: ProposalReferralProgramDetailsProps) => {
|
||||
const { t } = useTranslation();
|
||||
if (
|
||||
proposal?.terms?.change?.__typename !== 'UpdateNetworkParameter' ||
|
||||
proposal?.terms?.change?.networkParameter.key.slice(-13) !== '.benefitTiers'
|
||||
change?.__typename !== 'UpdateNetworkParameter' ||
|
||||
change?.networkParameter.key.slice(-13) !== '.benefitTiers'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const benefitTiersString = proposal?.terms?.change?.networkParameter.value;
|
||||
const benefitTiersString = change?.networkParameter.value;
|
||||
const benefitTiers = getBenefitTiers(benefitTiersString);
|
||||
|
||||
if (!benefitTiers) {
|
||||
|
||||
+48
-61
@@ -1,86 +1,73 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { ProposalUpdateMarketState } from './proposal-update-market-state';
|
||||
import { generateProposal } from '../../test-helpers/generate-proposals';
|
||||
import { MarketUpdateType } from '@vegaprotocol/types';
|
||||
|
||||
describe('<ProposalUpdateMarketState />', () => {
|
||||
const suspendProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateMarketState',
|
||||
market: {
|
||||
id: '1',
|
||||
decimalPlaces: 0,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'suspendProposal Name',
|
||||
code: 'suspendProposal Code',
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
quoteName: 'USD',
|
||||
},
|
||||
},
|
||||
const suspendProposal = {
|
||||
__typename: 'UpdateMarketState' as const,
|
||||
market: {
|
||||
id: '1',
|
||||
decimalPlaces: 0,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'suspendProposal Name',
|
||||
code: 'suspendProposal Code',
|
||||
product: {
|
||||
__typename: 'Future' as const,
|
||||
quoteName: 'USD',
|
||||
},
|
||||
},
|
||||
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_SUSPEND,
|
||||
},
|
||||
},
|
||||
});
|
||||
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_SUSPEND,
|
||||
};
|
||||
|
||||
const resumeProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateMarketState',
|
||||
market: {
|
||||
id: '1',
|
||||
decimalPlaces: 0,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'resumeProposal Name',
|
||||
code: 'resumeProposal Code',
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
quoteName: 'USD',
|
||||
},
|
||||
},
|
||||
const resumeProposal = {
|
||||
__typename: 'UpdateMarketState' as const,
|
||||
market: {
|
||||
id: '1',
|
||||
decimalPlaces: 0,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'resumeProposal Name',
|
||||
code: 'resumeProposal Code',
|
||||
product: {
|
||||
__typename: 'Future' as const,
|
||||
quoteName: 'USD',
|
||||
},
|
||||
},
|
||||
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_RESUME,
|
||||
},
|
||||
},
|
||||
});
|
||||
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_RESUME,
|
||||
};
|
||||
|
||||
const terminateProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateMarketState',
|
||||
market: {
|
||||
id: '1',
|
||||
decimalPlaces: 0,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'terminateProposal Name',
|
||||
code: 'terminateProposal Code',
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
quoteName: 'USD',
|
||||
},
|
||||
},
|
||||
const terminateProposal = {
|
||||
__typename: 'UpdateMarketState' as const,
|
||||
market: {
|
||||
id: '1',
|
||||
decimalPlaces: 0,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'terminateProposal Name',
|
||||
code: 'terminateProposal Code',
|
||||
product: {
|
||||
__typename: 'Future' as const,
|
||||
quoteName: 'USD',
|
||||
},
|
||||
},
|
||||
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
|
||||
price: '123',
|
||||
},
|
||||
},
|
||||
});
|
||||
updateType: MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
|
||||
price: '123',
|
||||
};
|
||||
|
||||
it('should render nothing if proposal is null', () => {
|
||||
render(<ProposalUpdateMarketState proposal={null} />);
|
||||
render(<ProposalUpdateMarketState change={null} />);
|
||||
expect(screen.queryByTestId('proposal-update-market-state')).toBeNull();
|
||||
});
|
||||
|
||||
it('should toggle details when CollapsibleToggle is clicked', () => {
|
||||
render(<ProposalUpdateMarketState proposal={suspendProposal} />);
|
||||
render(<ProposalUpdateMarketState change={suspendProposal} />);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('proposal-update-market-state-table')
|
||||
@@ -94,7 +81,7 @@ describe('<ProposalUpdateMarketState />', () => {
|
||||
});
|
||||
|
||||
it('should display suspend market information when showDetails is true', () => {
|
||||
render(<ProposalUpdateMarketState proposal={suspendProposal} />);
|
||||
render(<ProposalUpdateMarketState change={suspendProposal} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId('proposal-market-data-toggle'));
|
||||
|
||||
@@ -103,7 +90,7 @@ describe('<ProposalUpdateMarketState />', () => {
|
||||
});
|
||||
|
||||
it('should display resume market information when showDetails is true', () => {
|
||||
render(<ProposalUpdateMarketState proposal={resumeProposal} />);
|
||||
render(<ProposalUpdateMarketState change={resumeProposal} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId('proposal-market-data-toggle'));
|
||||
|
||||
@@ -112,7 +99,7 @@ describe('<ProposalUpdateMarketState />', () => {
|
||||
});
|
||||
|
||||
it('should display terminate market information when showDetails is true', () => {
|
||||
render(<ProposalUpdateMarketState proposal={terminateProposal} />);
|
||||
render(<ProposalUpdateMarketState change={terminateProposal} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId('proposal-market-data-toggle'));
|
||||
|
||||
|
||||
+10
-12
@@ -8,29 +8,27 @@ import { Row } from '@vegaprotocol/markets';
|
||||
import { useState } from 'react';
|
||||
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import { type Proposal } from '../../types';
|
||||
import { type UpdateMarketStatesFragment } from '../../__generated__/Proposals';
|
||||
|
||||
interface ProposalUpdateMarketStateProps {
|
||||
proposal: Proposal | null;
|
||||
change: UpdateMarketStatesFragment | null;
|
||||
}
|
||||
|
||||
export const ProposalUpdateMarketState = ({
|
||||
proposal,
|
||||
change,
|
||||
}: ProposalUpdateMarketStateProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [showDetails, setShowDetails] = useState(false);
|
||||
let market;
|
||||
let isTerminate = false;
|
||||
|
||||
if (!proposal) {
|
||||
if (!change) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (proposal?.terms.change.__typename === 'UpdateMarketState') {
|
||||
market = proposal?.terms?.change?.market;
|
||||
isTerminate =
|
||||
proposal?.terms?.change?.updateType ===
|
||||
'MARKET_STATE_UPDATE_TYPE_TERMINATE';
|
||||
if (change.__typename === 'UpdateMarketState') {
|
||||
market = change?.market;
|
||||
isTerminate = change?.updateType === 'MARKET_STATE_UPDATE_TYPE_TERMINATE';
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -45,7 +43,7 @@ export const ProposalUpdateMarketState = ({
|
||||
|
||||
{showDetails && (
|
||||
<RoundedWrapper paddingBottom={true} marginBottomLarge={true}>
|
||||
{proposal?.terms.change.__typename === 'UpdateMarketState' && (
|
||||
{change.__typename === 'UpdateMarketState' && (
|
||||
<KeyValueTable data-testid="proposal-update-market-state-table">
|
||||
<KeyValueTableRow>
|
||||
{t('marketId')}
|
||||
@@ -59,10 +57,10 @@ export const ProposalUpdateMarketState = ({
|
||||
{t('marketCode')}
|
||||
{market?.tradableInstrument?.instrument?.code}
|
||||
</KeyValueTableRow>
|
||||
{isTerminate && (
|
||||
{isTerminate && market && (
|
||||
<Row
|
||||
field="termination-price"
|
||||
value={proposal?.terms?.change?.price}
|
||||
value={change?.price}
|
||||
assetSymbol={
|
||||
market?.tradableInstrument?.instrument?.product
|
||||
?.__typename === 'Future' ||
|
||||
|
||||
+40
-80
@@ -1,6 +1,5 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ProposalVolumeDiscountProgramDetails } from './proposal-volume-discount-program-details';
|
||||
import { generateProposal } from '../../test-helpers/generate-proposals';
|
||||
|
||||
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
|
||||
useAppState: () => ({
|
||||
@@ -10,95 +9,56 @@ jest.mock('../../../../contexts/app-state/app-state-context', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockReferralProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateVolumeDiscountProgram',
|
||||
benefitTiers: [
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '10000',
|
||||
volumeDiscountFactor: '0.05',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '50000',
|
||||
volumeDiscountFactor: '0.1',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '100000',
|
||||
volumeDiscountFactor: '0.15',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '250000',
|
||||
volumeDiscountFactor: '0.2',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '500000',
|
||||
volumeDiscountFactor: '0.25',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '1000000',
|
||||
volumeDiscountFactor: '0.3',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '1500000',
|
||||
volumeDiscountFactor: '0.35',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '2000000',
|
||||
volumeDiscountFactor: '0.4',
|
||||
},
|
||||
],
|
||||
endOfProgramTimestamp: '1970-01-01T00:00:01.791568493Z',
|
||||
windowLength: 7,
|
||||
const mockChange = {
|
||||
__typename: 'UpdateVolumeDiscountProgram' as const,
|
||||
benefitTiers: [
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '10000',
|
||||
volumeDiscountFactor: '0.05',
|
||||
},
|
||||
},
|
||||
});
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '50000',
|
||||
volumeDiscountFactor: '0.1',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '100000',
|
||||
volumeDiscountFactor: '0.15',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '250000',
|
||||
volumeDiscountFactor: '0.2',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '500000',
|
||||
volumeDiscountFactor: '0.25',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '1000000',
|
||||
volumeDiscountFactor: '0.3',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '1500000',
|
||||
volumeDiscountFactor: '0.35',
|
||||
},
|
||||
{
|
||||
minimumRunningNotionalTakerVolume: '2000000',
|
||||
volumeDiscountFactor: '0.4',
|
||||
},
|
||||
],
|
||||
endOfProgramTimestamp: '1970-01-01T00:00:01.791568493Z',
|
||||
windowLength: 7,
|
||||
};
|
||||
|
||||
describe('ProposalVolumeDiscountProgramDetails', () => {
|
||||
it('should not render if proposal is null', () => {
|
||||
render(<ProposalVolumeDiscountProgramDetails proposal={null} />);
|
||||
expect(
|
||||
screen.queryByTestId('proposal-volume-discount-program-details')
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should not render if __typename is not UpdateVolumeDiscountProgram', () => {
|
||||
const updateMarketProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateMarket',
|
||||
},
|
||||
},
|
||||
});
|
||||
render(
|
||||
<ProposalVolumeDiscountProgramDetails proposal={updateMarketProposal} />
|
||||
);
|
||||
expect(
|
||||
screen.queryByTestId('proposal-volume-discount-program-details')
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should not render if there are no relevant fields', () => {
|
||||
const incompleteProposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateVolumeDiscountProgram',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<ProposalVolumeDiscountProgramDetails proposal={incompleteProposal} />
|
||||
);
|
||||
render(<ProposalVolumeDiscountProgramDetails change={null} />);
|
||||
expect(
|
||||
screen.queryByTestId('proposal-volume-discount-program-details')
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should render relevant fields if present', () => {
|
||||
render(
|
||||
<ProposalVolumeDiscountProgramDetails proposal={mockReferralProposal} />
|
||||
);
|
||||
render(<ProposalVolumeDiscountProgramDetails change={mockChange} />);
|
||||
expect(
|
||||
screen.getByTestId('proposal-volume-discount-program-window-length')
|
||||
).toBeInTheDocument();
|
||||
|
||||
+7
-7
@@ -11,10 +11,10 @@ import {
|
||||
} from '../proposal-referral-program-details';
|
||||
import { formatNumberPercentage } from '@vegaprotocol/utils';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { type Proposal } from '../../types';
|
||||
import { type UpdateVolumeDiscountProgramsFragment } from '../../__generated__/Proposals';
|
||||
|
||||
interface ProposalReferralProgramDetailsProps {
|
||||
proposal: Proposal | null;
|
||||
change: UpdateVolumeDiscountProgramsFragment | null;
|
||||
}
|
||||
|
||||
export const formatVolumeDiscountFactor = (value: string) => {
|
||||
@@ -22,16 +22,16 @@ export const formatVolumeDiscountFactor = (value: string) => {
|
||||
};
|
||||
|
||||
export const ProposalVolumeDiscountProgramDetails = ({
|
||||
proposal,
|
||||
change,
|
||||
}: ProposalReferralProgramDetailsProps) => {
|
||||
const { t } = useTranslation();
|
||||
if (proposal?.terms?.change?.__typename !== 'UpdateVolumeDiscountProgram') {
|
||||
if (change?.__typename !== 'UpdateVolumeDiscountProgram') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const benefitTiers = proposal?.terms?.change?.benefitTiers;
|
||||
const windowLength = proposal?.terms?.change?.windowLength;
|
||||
const endOfProgramTimestamp = proposal?.terms?.change?.endOfProgramTimestamp;
|
||||
const benefitTiers = change.benefitTiers;
|
||||
const windowLength = change?.windowLength;
|
||||
const endOfProgramTimestamp = change?.endOfProgramTimestamp;
|
||||
|
||||
if (!benefitTiers && !windowLength && !endOfProgramTimestamp) {
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { type ProposalTermsFieldsFragment } from '../../__generated__/Proposals';
|
||||
import { type Proposal, type BatchProposal } from '../../types';
|
||||
import { ListAsset } from '../list-asset';
|
||||
import { ProposalAssetDetails } from '../proposal-asset-details';
|
||||
import { ProposalMarketChanges } from '../proposal-market-changes';
|
||||
import { ProposalMarketData } from '../proposal-market-data';
|
||||
import { ProposalReferralProgramDetails } from '../proposal-referral-program-details';
|
||||
import {
|
||||
ProposalCancelTransferDetails,
|
||||
ProposalTransferDetails,
|
||||
} from '../proposal-transfer';
|
||||
import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers';
|
||||
import { ProposalUpdateMarketState } from '../proposal-update-market-state';
|
||||
import { ProposalVolumeDiscountProgramDetails } from '../proposal-volume-discount-program-details';
|
||||
|
||||
export const ProposalChangeDetails = ({
|
||||
proposal,
|
||||
terms,
|
||||
restData,
|
||||
}: {
|
||||
proposal: Proposal | BatchProposal;
|
||||
terms: ProposalTermsFieldsFragment;
|
||||
// eslint-disable-next-line
|
||||
restData: any;
|
||||
}) => {
|
||||
switch (terms.change.__typename) {
|
||||
case 'NewAsset': {
|
||||
if (proposal.id && terms.change.source.__typename === 'ERC20') {
|
||||
return (
|
||||
<div>
|
||||
<ListAsset
|
||||
assetId={proposal.id}
|
||||
withdrawalThreshold={terms.change.source.withdrawThreshold}
|
||||
lifetimeLimit={terms.change.source.lifetimeLimit}
|
||||
/>
|
||||
<ProposalAssetDetails change={terms.change} assetId={proposal.id} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case 'UpdateAsset': {
|
||||
if (proposal.id) {
|
||||
return (
|
||||
<ProposalAssetDetails
|
||||
change={terms.change}
|
||||
assetId={terms.change.assetId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case 'NewMarket': {
|
||||
if (proposal.id) {
|
||||
return <ProposalMarketData proposalId={proposal.id} />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case 'UpdateMarket': {
|
||||
if (proposal.id) {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<ProposalMarketData proposalId={proposal.id} />
|
||||
<ProposalMarketChanges
|
||||
marketId={terms.change.marketId}
|
||||
updatedProposal={
|
||||
restData?.data?.proposal?.terms?.updateMarket?.changes
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
case 'NewTransfer': {
|
||||
if (proposal.id) {
|
||||
return <ProposalTransferDetails proposalId={proposal.id} />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case 'CancelTransfer': {
|
||||
if (proposal.id) {
|
||||
return <ProposalCancelTransferDetails proposalId={proposal.id} />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
case 'UpdateMarketState': {
|
||||
return <ProposalUpdateMarketState change={terms.change} />;
|
||||
}
|
||||
case 'UpdateReferralProgram': {
|
||||
return <ProposalReferralProgramDetails change={terms.change} />;
|
||||
}
|
||||
case 'UpdateVolumeDiscountProgram': {
|
||||
return <ProposalVolumeDiscountProgramDetails change={terms.change} />;
|
||||
}
|
||||
case 'UpdateNetworkParameter': {
|
||||
if (
|
||||
terms.change.networkParameter.key === 'rewards.vesting.benefitTiers' ||
|
||||
terms.change.networkParameter.key ===
|
||||
'rewards.activityStreak.benefitTiers'
|
||||
) {
|
||||
return <ProposalUpdateBenefitTiers change={terms.change} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
case 'NewFreeform':
|
||||
case 'NewSpotMarket':
|
||||
case 'UpdateSpotMarket': {
|
||||
return null;
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -6,7 +6,6 @@ import { render, screen } from '@testing-library/react';
|
||||
import { generateProposal } from '../../test-helpers/generate-proposals';
|
||||
import { Proposal } from './proposal';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { mockNetworkParams } from '../../test-helpers/mocks';
|
||||
import { type Proposal as IProposal } from '../../types';
|
||||
|
||||
jest.mock('@vegaprotocol/network-parameters', () => ({
|
||||
@@ -38,6 +37,12 @@ jest.mock('../list-asset', () => ({
|
||||
ListAsset: () => <div data-testid="proposal-list-asset"></div>,
|
||||
}));
|
||||
|
||||
jest.mock('./proposal-change-details', () => ({
|
||||
ProposalChangeDetails: () => (
|
||||
<div data-testid="proposal-change-details"></div>
|
||||
),
|
||||
}));
|
||||
|
||||
const vegaWalletConfig: VegaWalletConfig = {
|
||||
network: 'TESTNET',
|
||||
vegaUrl: 'https://vega.xyz',
|
||||
@@ -56,11 +61,7 @@ const renderComponent = (proposal: IProposal) => {
|
||||
<MemoryRouter>
|
||||
<MockedProvider>
|
||||
<VegaWalletProvider config={vegaWalletConfig}>
|
||||
<Proposal
|
||||
restData={{}}
|
||||
proposal={proposal}
|
||||
networkParams={mockNetworkParams}
|
||||
/>
|
||||
<Proposal restData={{}} proposal={proposal} />
|
||||
</VegaWalletProvider>
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
@@ -99,27 +100,6 @@ it('renders each section', async () => {
|
||||
expect(await screen.findByTestId('proposal-header')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('proposal-change-table')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('proposal-json')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('proposal-change-details')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('proposal-list-asset')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders whitelist section if proposal is new asset and source is erc20', async () => {
|
||||
const proposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'NewAsset',
|
||||
name: 'foo',
|
||||
symbol: 'FOO',
|
||||
decimals: 18,
|
||||
quantum: '1',
|
||||
source: {
|
||||
__typename: 'ERC20',
|
||||
lifetimeLimit: '1',
|
||||
withdrawThreshold: '100',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
renderComponent(proposal);
|
||||
|
||||
expect(screen.getByTestId('proposal-list-asset')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -5,152 +5,25 @@ import { ProposalHeader } from '../proposal-detail-header/proposal-header';
|
||||
import { ProposalDescription } from '../proposal-description';
|
||||
import { ProposalChangeTable } from '../proposal-change-table';
|
||||
import { ProposalJson } from '../proposal-json';
|
||||
import { ProposalAssetDetails } from '../proposal-asset-details';
|
||||
import { ProposalReferralProgramDetails } from '../proposal-referral-program-details';
|
||||
import { ProposalVolumeDiscountProgramDetails } from '../proposal-volume-discount-program-details';
|
||||
import { UserVote } from '../vote-details';
|
||||
import { ListAsset } from '../list-asset';
|
||||
import Routes from '../../../routes';
|
||||
import { ProposalMarketData } from '../proposal-market-data';
|
||||
import { type MarketInfo } from '@vegaprotocol/markets';
|
||||
import { type AssetQuery } from '@vegaprotocol/assets';
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { ProposalMarketChanges } from '../proposal-market-changes';
|
||||
import { ProposalUpdateMarketState } from '../proposal-update-market-state';
|
||||
import { type NetworkParamsResult } from '@vegaprotocol/network-parameters';
|
||||
import { useVoteSubmit } from '@vegaprotocol/proposals';
|
||||
import { useUserVote } from '../vote-details/use-user-vote';
|
||||
import {
|
||||
ProposalCancelTransferDetails,
|
||||
ProposalTransferDetails,
|
||||
} from '../proposal-transfer';
|
||||
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||
import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers';
|
||||
import { type Proposal as IProposal } from '../../types';
|
||||
import { type Proposal as IProposal, type BatchProposal } from '../../types';
|
||||
import { ProposalChangeDetails } from './proposal-change-details';
|
||||
|
||||
export interface ProposalProps {
|
||||
proposal: IProposal;
|
||||
networkParams: Partial<NetworkParamsResult>;
|
||||
marketData?: MarketInfo | null;
|
||||
parentMarketData?: MarketInfo | null;
|
||||
assetData?: AssetQuery | null;
|
||||
proposal: IProposal | BatchProposal;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
restData: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
originalMarketProposalRestData?: any;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
mostRecentlyEnactedAssociatedMarketProposal?: any;
|
||||
}
|
||||
|
||||
export const Proposal = ({
|
||||
proposal,
|
||||
networkParams,
|
||||
restData,
|
||||
marketData,
|
||||
parentMarketData,
|
||||
assetData,
|
||||
originalMarketProposalRestData,
|
||||
mostRecentlyEnactedAssociatedMarketProposal,
|
||||
}: ProposalProps) => {
|
||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||
export const Proposal = ({ proposal, restData }: ProposalProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { submit, Dialog, finalizedVote, transaction } = useVoteSubmit();
|
||||
const { voteState, voteDatetime } = useUserVote(proposal?.id, finalizedVote);
|
||||
|
||||
if (!proposal) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let asset = assetData
|
||||
? removePaginationWrapper(assetData.assetsConnection?.edges)[0]
|
||||
: undefined;
|
||||
|
||||
const originalAsset = asset;
|
||||
|
||||
if (proposal.terms.change.__typename === 'UpdateAsset' && asset) {
|
||||
asset = {
|
||||
...asset,
|
||||
quantum: proposal.terms.change.quantum,
|
||||
source: { ...asset.source },
|
||||
};
|
||||
|
||||
if (asset.source.__typename === 'ERC20') {
|
||||
asset.source.lifetimeLimit = proposal.terms.change.source.lifetimeLimit;
|
||||
asset.source.withdrawThreshold =
|
||||
proposal.terms.change.source.withdrawThreshold;
|
||||
}
|
||||
}
|
||||
|
||||
let minVoterBalance = null;
|
||||
|
||||
if (networkParams) {
|
||||
switch (proposal.terms.change.__typename) {
|
||||
case 'UpdateMarket':
|
||||
case 'UpdateMarketState':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_updateMarket_minVoterBalance;
|
||||
break;
|
||||
case 'NewMarket':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_market_minVoterBalance;
|
||||
break;
|
||||
case 'NewAsset':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_asset_minVoterBalance;
|
||||
break;
|
||||
case 'UpdateAsset':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_updateAsset_minVoterBalance;
|
||||
break;
|
||||
case 'UpdateNetworkParameter':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_updateNetParam_minVoterBalance;
|
||||
break;
|
||||
case 'NewFreeform':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_freeform_minVoterBalance;
|
||||
break;
|
||||
case 'NewTransfer':
|
||||
// TODO: check minVoterBalance for 'NewTransfer'
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_freeform_minVoterBalance;
|
||||
break;
|
||||
case 'CancelTransfer':
|
||||
// TODO: check minVoterBalance for 'CancelTransfer'
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_freeform_minVoterBalance;
|
||||
break;
|
||||
case 'UpdateReferralProgram':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_referralProgram_minVoterBalance;
|
||||
break;
|
||||
case 'UpdateVolumeDiscountProgram':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_VolumeDiscountProgram_minVoterBalance;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Show governance transfer details only if the GOVERNANCE_TRANSFERS flag is on.
|
||||
const governanceTransferDetails = featureFlags.GOVERNANCE_TRANSFERS && (
|
||||
<>
|
||||
{proposal.terms.change.__typename === 'NewTransfer' && (
|
||||
/** Governance New Transfer Details */
|
||||
<div className="mb-4">
|
||||
<ProposalTransferDetails proposal={proposal} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{proposal.terms.change.__typename === 'CancelTransfer' && (
|
||||
/** Governance Cancel Transfer Details */
|
||||
<div className="mb-4">
|
||||
<ProposalCancelTransferDetails proposal={proposal} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<section data-testid="proposal">
|
||||
<div className="flex items-center gap-1 mb-6">
|
||||
@@ -181,91 +54,36 @@ export const Proposal = ({
|
||||
<ProposalChangeTable proposal={proposal} />
|
||||
</div>
|
||||
|
||||
{proposal.terms.change.__typename === 'NewAsset' &&
|
||||
proposal.terms.change.source.__typename === 'ERC20' &&
|
||||
proposal.id ? (
|
||||
<ListAsset
|
||||
assetId={proposal.id}
|
||||
withdrawalThreshold={proposal.terms.change.source.withdrawThreshold}
|
||||
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div className="mb-4">
|
||||
<ProposalDescription description={proposal.rationale.description} />
|
||||
</div>
|
||||
|
||||
{marketData && (
|
||||
<div className="mb-4">
|
||||
<ProposalMarketData
|
||||
marketData={marketData}
|
||||
parentMarketData={parentMarketData ? parentMarketData : undefined}
|
||||
<div className="mb-4">
|
||||
{proposal.__typename === 'Proposal' ? (
|
||||
<ProposalChangeDetails
|
||||
proposal={proposal}
|
||||
terms={proposal.terms}
|
||||
restData={restData}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{proposal.terms.change.__typename === 'UpdateMarketState' && (
|
||||
<div className="mb-4">
|
||||
<ProposalUpdateMarketState proposal={proposal} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{proposal.terms.change.__typename === 'UpdateMarket' && (
|
||||
<div className="mb-4">
|
||||
<ProposalMarketChanges
|
||||
originalProposal={
|
||||
originalMarketProposalRestData?.data?.proposal?.terms?.newMarket
|
||||
?.changes || {}
|
||||
}
|
||||
latestEnactedProposal={
|
||||
mostRecentlyEnactedAssociatedMarketProposal?.node?.proposal?.terms
|
||||
?.updateMarket?.changes || {}
|
||||
}
|
||||
updatedProposal={
|
||||
restData?.data?.proposal?.terms?.updateMarket?.changes || {}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(proposal.terms.change.__typename === 'NewAsset' ||
|
||||
proposal.terms.change.__typename === 'UpdateAsset') &&
|
||||
asset && (
|
||||
<div className="mb-4">
|
||||
<ProposalAssetDetails asset={asset} originalAsset={originalAsset} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{proposal.terms.change.__typename === 'UpdateReferralProgram' && (
|
||||
<div className="mb-4">
|
||||
<ProposalReferralProgramDetails proposal={proposal} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{proposal.terms.change.__typename === 'UpdateVolumeDiscountProgram' && (
|
||||
<div className="mb-4">
|
||||
<ProposalVolumeDiscountProgramDetails proposal={proposal} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{proposal.terms.change.__typename === 'UpdateNetworkParameter' &&
|
||||
proposal.terms.change.networkParameter.key.slice(-13) ===
|
||||
'.benefitTiers' && (
|
||||
<div className="mb-4">
|
||||
<ProposalUpdateBenefitTiers proposal={proposal} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{governanceTransferDetails}
|
||||
) : proposal.__typename === 'BatchProposal' ? (
|
||||
proposal.subProposals?.map((p, i) => {
|
||||
if (!p?.terms) return null;
|
||||
return (
|
||||
<ProposalChangeDetails
|
||||
key={i}
|
||||
proposal={proposal}
|
||||
terms={p.terms}
|
||||
restData={restData}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mb-10">
|
||||
<RoundedWrapper paddingBottom={true}>
|
||||
<UserVote
|
||||
proposal={proposal}
|
||||
minVoterBalance={minVoterBalance}
|
||||
spamProtectionMinTokens={
|
||||
networkParams?.spam_protection_voting_min_tokens
|
||||
}
|
||||
submit={submit}
|
||||
dialog={Dialog}
|
||||
transaction={transaction}
|
||||
|
||||
+11
-135
@@ -1,149 +1,25 @@
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { type MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { format } from 'date-fns';
|
||||
import { ProposalRejectionReason, ProposalState } from '@vegaprotocol/types';
|
||||
import { generateProposal } from '../../test-helpers/generate-proposals';
|
||||
import { ProposalsListItemDetails } from './proposals-list-item-details';
|
||||
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
|
||||
import {
|
||||
mockWalletContext,
|
||||
networkParamsQueryMock,
|
||||
fiveMinutes,
|
||||
fiveHours,
|
||||
fiveDays,
|
||||
lastWeek,
|
||||
nextWeek,
|
||||
} from '../../test-helpers/mocks';
|
||||
import { type Proposal } from '../../types';
|
||||
import { mockWalletContext } from '../../test-helpers/mocks';
|
||||
|
||||
const renderComponent = (
|
||||
proposal: Proposal,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
mocks: MockedResponse<any>[] = [networkParamsQueryMock]
|
||||
) =>
|
||||
const renderComponent = (id: string) =>
|
||||
render(
|
||||
<Router>
|
||||
<MockedProvider mocks={mocks}>
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<ProposalsListItemDetails proposal={proposal} />
|
||||
</VegaWalletContext.Provider>
|
||||
</AppStateProvider>
|
||||
</MockedProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<ProposalsListItemDetails id={id} />
|
||||
</VegaWalletContext.Provider>
|
||||
</Router>
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(0);
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe('Proposals list item details', () => {
|
||||
it('Renders proposal state: Enacted', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_ENACTED,
|
||||
terms: {
|
||||
enactmentDatetime: lastWeek.toString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
format(lastWeek, DATE_FORMAT_DETAILED)
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Passed', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_PASSED,
|
||||
terms: {
|
||||
closingDatetime: lastWeek.toString(),
|
||||
enactmentDatetime: nextWeek.toString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
`Enacts on ${format(nextWeek, DATE_FORMAT_DETAILED)}`
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Waiting for node vote', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_WAITING_FOR_NODE_VOTE,
|
||||
terms: {
|
||||
enactmentDatetime: nextWeek.toString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
`Enacts on ${format(nextWeek, DATE_FORMAT_DETAILED)}`
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - 5 minutes left to vote', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
closingDatetime: fiveMinutes.toString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
'5 minutes left to vote'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - 5 hours left to vote', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
closingDatetime: fiveHours.toString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
'5 hours left to vote'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Open - 5 days left to vote', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
closingDatetime: fiveDays.toString(),
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-details')).toHaveTextContent(
|
||||
'5 days left to vote'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Rejected', () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_REJECTED,
|
||||
terms: {
|
||||
enactmentDatetime: lastWeek.toString(),
|
||||
},
|
||||
rejectionReason:
|
||||
ProposalRejectionReason.PROPOSAL_ERROR_INVALID_FUTURE_PRODUCT,
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
'Invalid future product'
|
||||
it('links to single proposal page', () => {
|
||||
const proposalId = 'proposal-id';
|
||||
renderComponent(proposalId);
|
||||
expect(screen.getByRole('link')).toHaveAttribute(
|
||||
'href',
|
||||
expect.stringContaining(proposalId)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+4
-89
@@ -1,101 +1,16 @@
|
||||
import { type ReactNode } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { differenceInHours, format, formatDistanceToNowStrict } from 'date-fns';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
|
||||
import {
|
||||
ProposalRejectionReasonMapping,
|
||||
ProposalState,
|
||||
} from '@vegaprotocol/types';
|
||||
import Routes from '../../../routes';
|
||||
import { type Proposal } from '../../types';
|
||||
|
||||
export const ProposalsListItemDetails = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: Proposal;
|
||||
}) => {
|
||||
export const ProposalsListItemDetails = ({ id }: { id: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const state = proposal?.state;
|
||||
const nowToEnactmentInHours = differenceInHours(
|
||||
new Date(proposal?.terms.closingDatetime),
|
||||
new Date()
|
||||
);
|
||||
|
||||
let voteDetails: ReactNode;
|
||||
let voteStatus: ReactNode;
|
||||
|
||||
switch (state) {
|
||||
case ProposalState.STATE_ENACTED: {
|
||||
voteDetails =
|
||||
proposal?.terms.enactmentDatetime &&
|
||||
t('enactedOn{{date}}', {
|
||||
enactmentDate:
|
||||
proposal?.terms.enactmentDatetime &&
|
||||
format(
|
||||
new Date(proposal?.terms.enactmentDatetime),
|
||||
DATE_FORMAT_DETAILED
|
||||
),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_PASSED: {
|
||||
voteDetails =
|
||||
proposal?.terms.change.__typename !== 'NewFreeform' &&
|
||||
t('enactsOn{{date}}', {
|
||||
enactmentDate:
|
||||
proposal?.terms.enactmentDatetime &&
|
||||
format(
|
||||
new Date(proposal.terms.enactmentDatetime),
|
||||
DATE_FORMAT_DETAILED
|
||||
),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_WAITING_FOR_NODE_VOTE: {
|
||||
voteDetails =
|
||||
proposal?.terms.change.__typename !== 'NewFreeform' &&
|
||||
t('enactsOn{{date}}', {
|
||||
enactmentDate:
|
||||
proposal?.terms.enactmentDatetime &&
|
||||
format(
|
||||
new Date(proposal.terms.enactmentDatetime),
|
||||
DATE_FORMAT_DETAILED
|
||||
),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_OPEN: {
|
||||
voteDetails = (
|
||||
<span className={nowToEnactmentInHours < 6 ? 'text-vega-orange' : ''}>
|
||||
{formatDistanceToNowStrict(new Date(proposal?.terms.closingDatetime))}{' '}
|
||||
{t('left to vote')}
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case ProposalState.STATE_REJECTED: {
|
||||
voteStatus = proposal?.rejectionReason && (
|
||||
<>{t(ProposalRejectionReasonMapping[proposal.rejectionReason])}</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-4 items-start text-sm">
|
||||
<div className="flex items-center gap-2 text-vega-light-300 mb-2">
|
||||
{voteDetails && <span data-testid="vote-details">{voteDetails}</span>}
|
||||
{voteDetails && voteStatus && <span>·</span>}
|
||||
{voteStatus && <span data-testid="vote-status">{voteStatus}</span>}
|
||||
</div>
|
||||
|
||||
{proposal?.id && (
|
||||
<Link to={`${Routes.PROPOSALS}/${proposal.id}`}>
|
||||
<Button data-testid="view-proposal-btn">{t('viewDetails')}</Button>
|
||||
</Link>
|
||||
)}
|
||||
<Link to={`${Routes.PROPOSALS}/${id}`}>
|
||||
<Button data-testid="view-proposal-btn">{t('viewDetails')}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+3
-3
@@ -2,10 +2,10 @@ import { RoundedWrapper } from '@vegaprotocol/ui-toolkit';
|
||||
import { ProposalHeader } from '../proposal-detail-header/proposal-header';
|
||||
import { ProposalsListItemDetails } from './proposals-list-item-details';
|
||||
import { useUserVote } from '../vote-details/use-user-vote';
|
||||
import { type Proposal } from '../../types';
|
||||
import { type Proposal, type BatchProposal } from '../../types';
|
||||
|
||||
interface ProposalsListItemProps {
|
||||
proposal?: Proposal | null;
|
||||
proposal?: Proposal | BatchProposal;
|
||||
}
|
||||
|
||||
export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => {
|
||||
@@ -16,7 +16,7 @@ export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => {
|
||||
<li id={proposal.id} data-testid="proposals-list-item">
|
||||
<RoundedWrapper paddingBottom={true} heightFull={true}>
|
||||
<ProposalHeader proposal={proposal} voteState={voteState} />
|
||||
<ProposalsListItemDetails proposal={proposal} />
|
||||
<ProposalsListItemDetails id={proposal.id} />
|
||||
</RoundedWrapper>
|
||||
</li>
|
||||
);
|
||||
|
||||
+14
-12
@@ -3,14 +3,12 @@ import {
|
||||
generateProtocolUpgradeProposal,
|
||||
} from '../../test-helpers/generate-proposals';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
|
||||
import { ProposalsList } from './proposals-list';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import {
|
||||
mockWalletContext,
|
||||
networkParamsQueryMock,
|
||||
lastWeek,
|
||||
nextWeek,
|
||||
@@ -20,6 +18,16 @@ import {
|
||||
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { type Proposal } from '../../types';
|
||||
|
||||
jest.mock('../vote-details/use-user-vote', () => ({
|
||||
useUserVote: jest.fn().mockImplementation(() => ({ voteState: 'NotCast' })),
|
||||
}));
|
||||
|
||||
jest.mock('../proposals-list-item', () => ({
|
||||
ProposalsListItem: ({ proposal }: { proposal: { id: string } }) => (
|
||||
<div data-testid="proposals-list-item" id={proposal.id} />
|
||||
),
|
||||
}));
|
||||
|
||||
const openProposalClosesNextMonth = generateProposal({
|
||||
id: 'proposal1',
|
||||
state: ProposalState.STATE_OPEN,
|
||||
@@ -69,12 +77,10 @@ const renderComponent = (
|
||||
<Router>
|
||||
<MockedProvider mocks={[networkParamsQueryMock]}>
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<ProposalsList
|
||||
proposals={proposals}
|
||||
protocolUpgradeProposals={protocolUpgradeProposals || []}
|
||||
/>
|
||||
</VegaWalletContext.Provider>
|
||||
<ProposalsList
|
||||
proposals={proposals}
|
||||
protocolUpgradeProposals={protocolUpgradeProposals || []}
|
||||
/>
|
||||
</AppStateProvider>
|
||||
</MockedProvider>
|
||||
</Router>
|
||||
@@ -88,10 +94,6 @@ afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
jest.mock('../vote-details/use-user-vote', () => ({
|
||||
useUserVote: jest.fn().mockImplementation(() => ({ voteState: 'NotCast' })),
|
||||
}));
|
||||
|
||||
describe('Proposals list', () => {
|
||||
it('Render a page title and link to the make proposal form', async () => {
|
||||
render(renderComponent([]));
|
||||
|
||||
@@ -11,19 +11,20 @@ import { Button, Toggle } from '@vegaprotocol/ui-toolkit';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { ExternalLinks } from '@vegaprotocol/environment';
|
||||
import { type ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { type Proposal } from '../../types';
|
||||
import { type BatchProposal, type Proposal } from '../../types';
|
||||
|
||||
type Proposals = Array<Proposal | BatchProposal>;
|
||||
|
||||
interface ProposalsListProps {
|
||||
proposals: Proposal[];
|
||||
proposals: Proposals;
|
||||
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
|
||||
lastBlockHeight?: string;
|
||||
}
|
||||
|
||||
interface SortedProposalsProps {
|
||||
open: Proposal[];
|
||||
closed: Proposal[];
|
||||
open: Proposals;
|
||||
closed: Proposals;
|
||||
}
|
||||
|
||||
interface SortedProtocolUpgradeProposalsProps {
|
||||
@@ -31,15 +32,26 @@ interface SortedProtocolUpgradeProposalsProps {
|
||||
closed: ProtocolUpgradeProposalFieldsFragment[];
|
||||
}
|
||||
|
||||
export const orderByDate = (arr: Proposal[]) =>
|
||||
export const orderByDate = (arr: Proposals) =>
|
||||
orderBy(
|
||||
arr,
|
||||
[
|
||||
(p) =>
|
||||
p?.terms?.enactmentDatetime
|
||||
? new Date(p?.terms?.enactmentDatetime).getTime()
|
||||
: // has to be defaulted to 0 because new Date(null).getTime() -> NaN which is first when ordered
|
||||
new Date(p?.terms?.closingDatetime || 0).getTime(),
|
||||
(p) => {
|
||||
if (p.__typename === 'BatchProposal') {
|
||||
// Batch proposals can have different enactment dates, this could be improved by ordering
|
||||
// by soonest enactment date in the batch
|
||||
return new Date(p.batchTerms?.closingDatetime || p.datetime);
|
||||
}
|
||||
|
||||
if (p.__typename === 'Proposal') {
|
||||
return p?.terms?.enactmentDatetime
|
||||
? new Date(p?.terms?.enactmentDatetime).getTime()
|
||||
: // has to be defaulted to 0 because new Date(null).getTime() -> NaN which is first when ordered
|
||||
new Date(p?.terms?.closingDatetime || 0).getTime();
|
||||
}
|
||||
|
||||
throw new Error('invalid proposal');
|
||||
},
|
||||
(p) => new Date(p?.datetime).getTime(),
|
||||
],
|
||||
['asc', 'asc']
|
||||
@@ -76,18 +88,40 @@ export const ProposalsList = ({
|
||||
|
||||
const sortedProposals: SortedProposalsProps = useMemo(() => {
|
||||
const initialSorting = proposals.reduce(
|
||||
(acc: SortedProposalsProps, proposal) => {
|
||||
if (isFuture(new Date(proposal?.terms.closingDatetime))) {
|
||||
acc.open.push(proposal);
|
||||
} else {
|
||||
acc.closed.push(proposal);
|
||||
(acc, proposal) => {
|
||||
if (proposal.__typename === 'Proposal') {
|
||||
if (isFuture(new Date(proposal?.terms.closingDatetime))) {
|
||||
acc.open.push(proposal);
|
||||
} else {
|
||||
acc.closed.push(proposal);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (proposal.__typename === 'BatchProposal') {
|
||||
if (
|
||||
// this could be improved by sorting by soonest enactment date of all the
|
||||
// sub proposals
|
||||
isFuture(
|
||||
new Date(
|
||||
proposal.batchTerms?.closingDatetime || proposal.datetime
|
||||
)
|
||||
)
|
||||
) {
|
||||
acc.open.push(proposal);
|
||||
} else {
|
||||
acc.closed.push(proposal);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
open: [],
|
||||
closed: [],
|
||||
}
|
||||
} as SortedProposalsProps
|
||||
);
|
||||
return {
|
||||
open:
|
||||
@@ -121,7 +155,7 @@ export const ProposalsList = ({
|
||||
};
|
||||
}, [protocolUpgradeProposals, lastBlockHeight]);
|
||||
|
||||
const filterPredicate = (p: ProposalFieldsFragment | Proposal) =>
|
||||
const filterPredicate = (p: Proposal | BatchProposal) =>
|
||||
p?.id?.includes(filterString) ||
|
||||
p?.party?.id?.toString().includes(filterString);
|
||||
|
||||
|
||||
+9
-46
@@ -1,17 +1,8 @@
|
||||
import { generateProposal } from '../../test-helpers/generate-proposals';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
|
||||
import { RejectedProposalsList } from './rejected-proposals-list';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||
import {
|
||||
mockWalletContext,
|
||||
networkParamsQueryMock,
|
||||
nextWeek,
|
||||
lastMonth,
|
||||
} from '../../test-helpers/mocks';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { nextWeek, lastMonth } from '../../test-helpers/mocks';
|
||||
import { type Proposal } from '../../types';
|
||||
|
||||
const rejectedProposalClosesNextWeek = generateProposal({
|
||||
@@ -36,31 +27,15 @@ const rejectedProposalClosedLastMonth = generateProposal({
|
||||
});
|
||||
|
||||
const renderComponent = (proposals: Proposal[]) => (
|
||||
<Router>
|
||||
<MockedProvider mocks={[networkParamsQueryMock]}>
|
||||
<AppStateProvider>
|
||||
<VegaWalletContext.Provider value={mockWalletContext}>
|
||||
<RejectedProposalsList proposals={proposals} />
|
||||
</VegaWalletContext.Provider>
|
||||
</AppStateProvider>
|
||||
</MockedProvider>
|
||||
</Router>
|
||||
<RejectedProposalsList proposals={proposals} />
|
||||
);
|
||||
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(0);
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
jest.mock('../vote-details/use-user-vote', () => ({
|
||||
useUserVote: jest.fn().mockImplementation(() => ({ voteState: 'NotCast' })),
|
||||
jest.mock('../proposals-list-item', () => ({
|
||||
ProposalsListItem: () => <div data-testid="proposals-list-item" />,
|
||||
}));
|
||||
|
||||
describe('Rejected proposals list', () => {
|
||||
it('Renders a list of proposals', async () => {
|
||||
it('Renders a list of proposals', () => {
|
||||
render(
|
||||
renderComponent([
|
||||
rejectedProposalClosedLastMonth,
|
||||
@@ -68,25 +43,13 @@ describe('Rejected proposals list', () => {
|
||||
])
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const rejectedProposals = within(
|
||||
screen.getByTestId('rejected-proposals')
|
||||
);
|
||||
const rejectedProposalsItems = rejectedProposals.getAllByTestId(
|
||||
'proposals-list-item'
|
||||
);
|
||||
expect(rejectedProposalsItems).toHaveLength(2);
|
||||
});
|
||||
expect(screen.getAllByTestId('proposals-list-item')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('Displays text when there are no proposals', async () => {
|
||||
render(renderComponent([]));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('no-rejected-proposals')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('rejected-proposals')
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByTestId('no-rejected-proposals')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('rejected-proposals')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
+3
-3
@@ -3,17 +3,17 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Heading } from '../../../../components/heading';
|
||||
import { ProposalsListItem } from '../proposals-list-item';
|
||||
import { ProposalsListFilter } from '../proposals-list-filter';
|
||||
import { type Proposal } from '../../types';
|
||||
import { type BatchProposal, type Proposal } from '../../types';
|
||||
|
||||
interface ProposalsListProps {
|
||||
proposals: Proposal[];
|
||||
proposals: Array<Proposal | BatchProposal>;
|
||||
}
|
||||
|
||||
export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [filterString, setFilterString] = useState('');
|
||||
|
||||
const filterPredicate = (p: Proposal) =>
|
||||
const filterPredicate = (p: Proposal | BatchProposal) =>
|
||||
p?.id?.includes(filterString) ||
|
||||
p?.party?.id?.toString().includes(filterString);
|
||||
|
||||
|
||||
+26
-22
@@ -64,7 +64,7 @@ describe('VoteBreakdown', () => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('Renders majority reached', () => {
|
||||
it('Renders majority reached', async () => {
|
||||
const yesVotes = 100;
|
||||
const noVotes = 0;
|
||||
|
||||
@@ -82,10 +82,10 @@ describe('VoteBreakdown', () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('token-majority-met')).toBeInTheDocument();
|
||||
expect(await screen.findByTestId('token-majority-met')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders majority not reached', () => {
|
||||
it('Renders majority not reached', async () => {
|
||||
const yesVotes = 20;
|
||||
const noVotes = 80;
|
||||
|
||||
@@ -103,10 +103,12 @@ describe('VoteBreakdown', () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('token-majority-not-met')).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByTestId('token-majority-not-met')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders participation reached', () => {
|
||||
it('Renders participation reached', async () => {
|
||||
const yesVotes = 1000;
|
||||
const noVotes = 0;
|
||||
|
||||
@@ -124,10 +126,12 @@ describe('VoteBreakdown', () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('token-participation-met')).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByTestId('token-participation-met')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders participation not reached', () => {
|
||||
it('Renders participation not reached', async () => {
|
||||
const yesVotes = 0;
|
||||
const noVotes = 0;
|
||||
|
||||
@@ -145,11 +149,11 @@ describe('VoteBreakdown', () => {
|
||||
})
|
||||
);
|
||||
expect(
|
||||
screen.getByTestId('token-participation-not-met')
|
||||
await screen.findByTestId('token-participation-not-met')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders proposal state: Update market proposal - Currently expected to pass by LP vote', () => {
|
||||
it('Renders proposal state: Update market proposal - Currently expected to pass by LP vote', async () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
@@ -170,12 +174,12 @@ describe('VoteBreakdown', () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
expect(await screen.findByTestId('vote-status')).toHaveTextContent(
|
||||
'Currently expected to pass by liquidity vote'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Update market proposal - Currently expected to pass by token vote', () => {
|
||||
it('Renders proposal state: Update market proposal - Currently expected to pass by token vote', async () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
@@ -196,12 +200,12 @@ describe('VoteBreakdown', () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
expect(await screen.findByTestId('vote-status')).toHaveTextContent(
|
||||
'Currently expected to pass by token vote'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders proposal state: Update market proposal - Currently expected to fail', () => {
|
||||
it('Renders proposal state: Update market proposal - Currently expected to fail', async () => {
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
@@ -222,12 +226,12 @@ describe('VoteBreakdown', () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(screen.getByTestId('vote-status')).toHaveTextContent(
|
||||
expect(await screen.findByTestId('vote-status')).toHaveTextContent(
|
||||
'Currently expected to fail'
|
||||
);
|
||||
});
|
||||
|
||||
it('Progress bar displays status - token majority', () => {
|
||||
it('Progress bar displays status - token majority', async () => {
|
||||
const yesVotes = 80;
|
||||
const noVotes = 20;
|
||||
|
||||
@@ -246,13 +250,13 @@ describe('VoteBreakdown', () => {
|
||||
})
|
||||
);
|
||||
|
||||
const element = screen.getByTestId('token-majority-progress');
|
||||
const element = await screen.findByTestId('token-majority-progress');
|
||||
const style = window.getComputedStyle(element);
|
||||
|
||||
expect(style.width).toBe(`${yesVotes}%`);
|
||||
});
|
||||
|
||||
it('Progress bar displays status - token participation', () => {
|
||||
it('Progress bar displays status - token participation', async () => {
|
||||
const yesVotes = 40;
|
||||
const noVotes = 20;
|
||||
const totalVotes = yesVotes + noVotes;
|
||||
@@ -274,13 +278,13 @@ describe('VoteBreakdown', () => {
|
||||
})
|
||||
);
|
||||
|
||||
const element = screen.getByTestId('token-participation-progress');
|
||||
const element = await screen.findByTestId('token-participation-progress');
|
||||
const style = window.getComputedStyle(element);
|
||||
|
||||
expect(style.width).toBe(`${expectedProgress}%`);
|
||||
});
|
||||
|
||||
it('Progress bar displays status - LP majority', () => {
|
||||
it('Progress bar displays status - LP majority', async () => {
|
||||
const yesVotesLP = 0.8;
|
||||
const noVotesLP = 0.2;
|
||||
const expectedProgress = (yesVotesLP / (yesVotesLP + noVotesLP)) * 100; // 80%
|
||||
@@ -307,12 +311,12 @@ describe('VoteBreakdown', () => {
|
||||
})
|
||||
);
|
||||
|
||||
const element = screen.getByTestId('lp-majority-progress');
|
||||
const element = await screen.findByTestId('lp-majority-progress');
|
||||
const style = window.getComputedStyle(element);
|
||||
expect(style.width).toBe(`${expectedProgress}%`);
|
||||
});
|
||||
|
||||
it('Progress bar displays status - LP participation', () => {
|
||||
it('Progress bar displays status - LP participation', async () => {
|
||||
const yesVotesLP = 400;
|
||||
const noVotesLP = 600;
|
||||
const totalVotesLP = yesVotesLP + noVotesLP;
|
||||
@@ -341,7 +345,7 @@ describe('VoteBreakdown', () => {
|
||||
})
|
||||
);
|
||||
|
||||
const element = screen.getByTestId('lp-participation-progress');
|
||||
const element = await screen.findByTestId('lp-participation-progress');
|
||||
const style = window.getComputedStyle(element);
|
||||
expect(style.width).toBe(`${expectedProgress}%`);
|
||||
});
|
||||
|
||||
+257
-25
@@ -1,13 +1,20 @@
|
||||
import { type ReactNode } from 'react';
|
||||
import compact from 'lodash/compact';
|
||||
import countBy from 'lodash/countBy';
|
||||
import { useState, type ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useVoteInformation } from '../../hooks';
|
||||
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import { Tooltip, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
||||
import { formatNumber } from '@vegaprotocol/utils';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { CompactNumber } from '@vegaprotocol/react-helpers';
|
||||
import { type Proposal } from '../../types';
|
||||
import { type Proposal, type BatchProposal } from '../../types';
|
||||
import {
|
||||
type ProposalTermsFieldsFragment,
|
||||
type VoteFieldsFragment,
|
||||
} from '../../__generated__/Proposals';
|
||||
import { useBatchVoteInformation } from '../../hooks/use-vote-information';
|
||||
|
||||
export const CompactVotes = ({ number }: { number: BigNumber }) => (
|
||||
<CompactNumber
|
||||
@@ -18,10 +25,6 @@ export const CompactVotes = ({ number }: { number: BigNumber }) => (
|
||||
/>
|
||||
);
|
||||
|
||||
interface VoteBreakdownProps {
|
||||
proposal: Proposal;
|
||||
}
|
||||
|
||||
interface VoteProgressProps {
|
||||
percentageFor: BigNumber;
|
||||
colourfulBg?: boolean;
|
||||
@@ -55,7 +58,7 @@ const VoteProgress = ({
|
||||
className={progressClasses}
|
||||
style={{ width: `${percentageFor}%` }}
|
||||
data-testid={testId}
|
||||
></div>
|
||||
/>
|
||||
<div className={textClasses}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
@@ -75,14 +78,14 @@ const Status = ({ reached, threshold, text, testId }: StatusProps) => {
|
||||
<div data-testid={testId}>
|
||||
{reached ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon name="tick" size={4} />
|
||||
<VegaIcon name={VegaIconNames.TICK} size={20} />
|
||||
<span>
|
||||
{threshold.toString()}% {text} {t('met')}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon name="cross" size={4} />
|
||||
<VegaIcon name={VegaIconNames.CROSS} size={20} />
|
||||
<span>
|
||||
{threshold.toString()}% {text} {t('not met')}
|
||||
</span>
|
||||
@@ -92,7 +95,225 @@ const Status = ({ reached, threshold, text, testId }: StatusProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
|
||||
export const VoteBreakdown = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: Proposal | BatchProposal;
|
||||
}) => {
|
||||
if (proposal.__typename === 'Proposal') {
|
||||
return <VoteBreakdownNormal proposal={proposal} />;
|
||||
}
|
||||
|
||||
if (proposal.__typename === 'BatchProposal') {
|
||||
return <VoteBreakdownBatch proposal={proposal} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const VoteBreakdownBatch = ({ proposal }: { proposal: BatchProposal }) => {
|
||||
const [fullBreakdown, setFullBreakdown] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const voteInfo = useBatchVoteInformation({
|
||||
terms: compact(
|
||||
proposal.subProposals ? proposal.subProposals.map((p) => p?.terms) : []
|
||||
),
|
||||
votes: proposal.votes,
|
||||
});
|
||||
|
||||
if (!voteInfo) return null;
|
||||
|
||||
const batchWillPass = voteInfo.every((i) => i.willPass);
|
||||
|
||||
const passingCount = countBy(voteInfo, (v) => v.willPass);
|
||||
|
||||
if (proposal.state === ProposalState.STATE_OPEN) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{batchWillPass ? (
|
||||
<p className="flex gap-2 m-0 items-center">
|
||||
<VegaIcon
|
||||
name={VegaIconNames.TICK}
|
||||
className="text-vega-green"
|
||||
size={20}
|
||||
/>
|
||||
{t(
|
||||
'Currently expected to pass: conditions met for {{count}} of {{total}} proposals',
|
||||
{
|
||||
count: passingCount['true'] || 0,
|
||||
total: voteInfo.length,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
<p className="flex gap-2 m-0 items-center">
|
||||
<VegaIcon
|
||||
name={VegaIconNames.CROSS}
|
||||
className="text-vega-pink"
|
||||
size={20}
|
||||
/>
|
||||
{t(
|
||||
'Currently expected to fail: {{count}} of {{total}} proposals are passing',
|
||||
{
|
||||
count: passingCount['true'] || 0,
|
||||
total: voteInfo.length,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
className="underline"
|
||||
onClick={() => setFullBreakdown((x) => !x)}
|
||||
>
|
||||
{fullBreakdown ? 'Hide vote breakdown' : 'Show vote breakdown'}
|
||||
</button>
|
||||
</div>
|
||||
{fullBreakdown && (
|
||||
<div>
|
||||
{proposal.subProposals?.map((p, i) => {
|
||||
if (!p?.terms) return null;
|
||||
return (
|
||||
<VoteBreakdownBatchSubProposal
|
||||
key={i}
|
||||
proposal={proposal}
|
||||
votes={proposal.votes}
|
||||
terms={p.terms}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} else if (
|
||||
proposal.state === ProposalState.STATE_DECLINED ||
|
||||
proposal.state === ProposalState.STATE_PASSED
|
||||
) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{batchWillPass ? (
|
||||
<p className="flex gap-2 m-0 items-center">
|
||||
<VegaIcon
|
||||
name={VegaIconNames.TICK}
|
||||
className="text-vega-green"
|
||||
size={20}
|
||||
/>
|
||||
{t(
|
||||
'Proposal passed: conditions met for {{count}} of {{total}} proposals',
|
||||
{
|
||||
count: passingCount['true'] || 0,
|
||||
total: voteInfo.length,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
<p className="flex gap-2 m-0 items-center">
|
||||
<VegaIcon
|
||||
name={VegaIconNames.CROSS}
|
||||
className="text-vega-pink"
|
||||
size={20}
|
||||
/>
|
||||
{t('Proposal failed: {{count}} of {{total}} proposals passed', {
|
||||
count: passingCount['true'] || 0,
|
||||
total: voteInfo.length,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
className="underline"
|
||||
onClick={() => setFullBreakdown((x) => !x)}
|
||||
>
|
||||
{fullBreakdown ? 'Hide vote breakdown' : 'Show vote breakdown'}
|
||||
</button>
|
||||
</div>
|
||||
{fullBreakdown && (
|
||||
<div>
|
||||
{proposal.subProposals?.map((p, i) => {
|
||||
if (!p?.terms) return null;
|
||||
return (
|
||||
<VoteBreakdownBatchSubProposal
|
||||
key={i}
|
||||
proposal={proposal}
|
||||
votes={proposal.votes}
|
||||
terms={p.terms}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const VoteBreakdownBatchSubProposal = ({
|
||||
proposal,
|
||||
votes,
|
||||
terms,
|
||||
}: {
|
||||
proposal: BatchProposal;
|
||||
votes: VoteFieldsFragment;
|
||||
terms: ProposalTermsFieldsFragment;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const voteInfo = useVoteInformation({
|
||||
votes,
|
||||
terms,
|
||||
});
|
||||
|
||||
const isProposalOpen = proposal?.state === ProposalState.STATE_OPEN;
|
||||
const isUpdateMarket = terms?.change?.__typename === 'UpdateMarket';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4>{t(terms.change.__typename)}</h4>
|
||||
<VoteBreakDownUI
|
||||
voteInfo={voteInfo}
|
||||
isProposalOpen={isProposalOpen}
|
||||
isUpdateMarket={isUpdateMarket}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const VoteBreakdownNormal = ({ proposal }: { proposal: Proposal }) => {
|
||||
const voteInfo = useVoteInformation({
|
||||
votes: proposal.votes,
|
||||
terms: proposal.terms,
|
||||
});
|
||||
|
||||
const isProposalOpen = proposal?.state === ProposalState.STATE_OPEN;
|
||||
const isUpdateMarket = proposal?.terms?.change?.__typename === 'UpdateMarket';
|
||||
|
||||
return (
|
||||
<VoteBreakDownUI
|
||||
voteInfo={voteInfo}
|
||||
isProposalOpen={isProposalOpen}
|
||||
isUpdateMarket={isUpdateMarket}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const VoteBreakDownUI = ({
|
||||
voteInfo,
|
||||
isProposalOpen,
|
||||
isUpdateMarket,
|
||||
}: {
|
||||
voteInfo: ReturnType<typeof useVoteInformation>;
|
||||
isProposalOpen: boolean;
|
||||
isUpdateMarket: boolean;
|
||||
}) => {
|
||||
const defaultDP = 2;
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!voteInfo) return null;
|
||||
|
||||
const {
|
||||
totalTokensPercentage,
|
||||
participationMet,
|
||||
@@ -114,12 +335,8 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
|
||||
majorityLPMet,
|
||||
willPassByTokenVote,
|
||||
willPassByLPVote,
|
||||
} = useVoteInformation({ proposal });
|
||||
} = voteInfo;
|
||||
|
||||
const { t } = useTranslation();
|
||||
const defaultDP = 2;
|
||||
const isProposalOpen = proposal?.state === ProposalState.STATE_OPEN;
|
||||
const isUpdateMarket = proposal?.terms?.change?.__typename === 'UpdateMarket';
|
||||
const participationThresholdProgress = BigNumber.min(
|
||||
totalTokensPercentage.dividedBy(requiredParticipation).multipliedBy(100),
|
||||
new BigNumber(100)
|
||||
@@ -152,23 +369,38 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
|
||||
{isProposalOpen && (
|
||||
<div
|
||||
data-testid="vote-status"
|
||||
className="flex items-center gap-1 mb-2 text-bold"
|
||||
className="flex items-center gap-2 mb-2 text-bold"
|
||||
>
|
||||
<span>
|
||||
{willPass ? (
|
||||
<Icon name="tick" size={5} className="text-vega-green" />
|
||||
<VegaIcon
|
||||
name={VegaIconNames.TICK}
|
||||
size={20}
|
||||
className="text-vega-green"
|
||||
/>
|
||||
) : (
|
||||
<Icon name="cross" size={5} className="text-vega-pink" />
|
||||
<VegaIcon
|
||||
name={VegaIconNames.CROSS}
|
||||
size={20}
|
||||
className="text-vega-pink"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
<span>{t('currentlySetTo')} </span>
|
||||
{willPass ? (
|
||||
<span>
|
||||
<span className="text-vega-green">{t('pass')}</span>
|
||||
<p className="m-0">
|
||||
<Trans
|
||||
i18nKey={'Currently expected to <0>pass</0>'}
|
||||
components={[<span className="text-vega-green" />]}
|
||||
/>
|
||||
{isUpdateMarket && <span> {updateMarketVotePassMethod}</span>}
|
||||
</span>
|
||||
</p>
|
||||
) : (
|
||||
<span className="text-vega-pink">{t('fail')}</span>
|
||||
<p className="m-0">
|
||||
<Trans
|
||||
i18nKey={'Currently expected to <0>fail</0>'}
|
||||
components={[<span className="text-vega-pink" />]}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -8,12 +8,10 @@ import { SubHeading } from '../../../../components/heading';
|
||||
import { type VoteValue } from '@vegaprotocol/types';
|
||||
import { type DialogProps, type VegaTxState } from '@vegaprotocol/proposals';
|
||||
import { type VoteState } from './use-user-vote';
|
||||
import { type Proposal } from '../../types';
|
||||
import { type Proposal, type BatchProposal } from '../../types';
|
||||
|
||||
interface UserVoteProps {
|
||||
proposal: Proposal;
|
||||
minVoterBalance: string | null | undefined;
|
||||
spamProtectionMinTokens: string | null | undefined;
|
||||
proposal: Proposal | BatchProposal;
|
||||
transaction: VegaTxState | null;
|
||||
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
|
||||
dialog: (props: DialogProps) => JSX.Element;
|
||||
@@ -23,8 +21,6 @@ interface UserVoteProps {
|
||||
|
||||
export const UserVote = ({
|
||||
proposal,
|
||||
minVoterBalance,
|
||||
spamProtectionMinTokens,
|
||||
submit,
|
||||
transaction,
|
||||
dialog,
|
||||
@@ -46,12 +42,17 @@ export const UserVote = ({
|
||||
{pubKey ? (
|
||||
proposal && (
|
||||
<VoteButtonsContainer
|
||||
changeType={
|
||||
proposal.__typename === 'BatchProposal'
|
||||
? // @ts-ignore should not be null/undefined
|
||||
proposal.subProposals[0]?.terms?.change.__typename
|
||||
: // @ts-ignore should not be null/undefined
|
||||
proposal.terms?.change.__typename
|
||||
}
|
||||
voteState={voteState}
|
||||
voteDatetime={voteDatetime}
|
||||
proposalState={proposal.state}
|
||||
proposalId={proposal.id ?? ''}
|
||||
minVoterBalance={minVoterBalance}
|
||||
spamProtectionMinTokens={spamProtectionMinTokens}
|
||||
className="flex"
|
||||
submit={submit}
|
||||
transaction={transaction}
|
||||
|
||||
@@ -19,14 +19,18 @@ import { VoteTransactionDialog } from './vote-transaction-dialog';
|
||||
import { useVoteButtonsQuery } from './__generated__/Stake';
|
||||
import type { DialogProps, VegaTxState } from '@vegaprotocol/proposals';
|
||||
import { filterAcceptableGraphqlErrors } from '../../../../lib/party';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
import { type ProposalChangeType } from '../../types';
|
||||
|
||||
interface VoteButtonsContainerProps {
|
||||
changeType: ProposalChangeType;
|
||||
voteState: VoteState | null;
|
||||
voteDatetime: Date | null;
|
||||
proposalId: string | null;
|
||||
proposalState: ProposalState;
|
||||
minVoterBalance: string | null | undefined;
|
||||
spamProtectionMinTokens: string | null | undefined;
|
||||
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
|
||||
transaction: VegaTxState | null;
|
||||
dialog: (props: DialogProps) => JSX.Element;
|
||||
@@ -35,20 +39,94 @@ interface VoteButtonsContainerProps {
|
||||
|
||||
export const VoteButtonsContainer = (props: VoteButtonsContainerProps) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const {
|
||||
appState: { decimals },
|
||||
} = useAppState();
|
||||
|
||||
const { data, loading, error } = useVoteButtonsQuery({
|
||||
variables: { partyId: pubKey || '' },
|
||||
skip: !pubKey,
|
||||
});
|
||||
|
||||
const { params: networkParams } = useNetworkParams([
|
||||
NetworkParams.governance_proposal_market_minVoterBalance,
|
||||
NetworkParams.governance_proposal_updateMarket_minVoterBalance,
|
||||
NetworkParams.governance_proposal_asset_minVoterBalance,
|
||||
NetworkParams.governance_proposal_updateAsset_minVoterBalance,
|
||||
NetworkParams.governance_proposal_updateNetParam_minVoterBalance,
|
||||
NetworkParams.governance_proposal_freeform_minVoterBalance,
|
||||
NetworkParams.governance_proposal_referralProgram_minVoterBalance,
|
||||
NetworkParams.governance_proposal_VolumeDiscountProgram_minVoterBalance,
|
||||
NetworkParams.governance_proposal_transfer_minVoterBalance,
|
||||
NetworkParams.spam_protection_voting_min_tokens,
|
||||
NetworkParams.governance_proposal_market_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateMarket_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateMarket_requiredMajorityLP,
|
||||
NetworkParams.governance_proposal_asset_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateAsset_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateNetParam_requiredMajority,
|
||||
NetworkParams.governance_proposal_freeform_requiredMajority,
|
||||
NetworkParams.governance_proposal_referralProgram_requiredMajority,
|
||||
NetworkParams.governance_proposal_VolumeDiscountProgram_requiredMajority,
|
||||
NetworkParams.governance_proposal_transfer_requiredMajority,
|
||||
]);
|
||||
|
||||
let minVoterBalance = null;
|
||||
|
||||
if (networkParams) {
|
||||
switch (props.changeType) {
|
||||
case 'UpdateMarket':
|
||||
case 'UpdateMarketState':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_updateMarket_minVoterBalance;
|
||||
break;
|
||||
case 'NewMarket':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_market_minVoterBalance;
|
||||
break;
|
||||
case 'NewAsset':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_asset_minVoterBalance;
|
||||
break;
|
||||
case 'UpdateAsset':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_updateAsset_minVoterBalance;
|
||||
break;
|
||||
case 'UpdateNetworkParameter':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_updateNetParam_minVoterBalance;
|
||||
break;
|
||||
case 'NewFreeform':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_freeform_minVoterBalance;
|
||||
break;
|
||||
case 'CancelTransfer':
|
||||
case 'NewTransfer':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_transfer_requiredMajority;
|
||||
break;
|
||||
case 'UpdateReferralProgram':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_referralProgram_minVoterBalance;
|
||||
break;
|
||||
case 'UpdateVolumeDiscountProgram':
|
||||
minVoterBalance =
|
||||
networkParams.governance_proposal_VolumeDiscountProgram_minVoterBalance;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const filteredErrors = filterAcceptableGraphqlErrors(error);
|
||||
|
||||
return (
|
||||
<AsyncRenderer loading={loading} error={filteredErrors} data={data}>
|
||||
<VoteButtons
|
||||
{...props}
|
||||
minVoterBalance={minVoterBalance}
|
||||
spamProtectionMinTokens={
|
||||
networkParams.spam_protection_voting_min_tokens
|
||||
}
|
||||
currentStakeAvailable={toBigNum(
|
||||
data?.party?.stakingSummary.currentStakeAvailable || 0,
|
||||
decimals
|
||||
@@ -58,8 +136,17 @@ export const VoteButtonsContainer = (props: VoteButtonsContainerProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
interface VoteButtonsProps extends VoteButtonsContainerProps {
|
||||
interface VoteButtonsProps {
|
||||
voteState: VoteState | null;
|
||||
voteDatetime: Date | null;
|
||||
proposalId: string | null;
|
||||
proposalState: ProposalState;
|
||||
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
|
||||
transaction: VegaTxState | null;
|
||||
dialog: (props: DialogProps) => JSX.Element;
|
||||
currentStakeAvailable: BigNumber;
|
||||
minVoterBalance: string | null;
|
||||
spamProtectionMinTokens: string | null;
|
||||
}
|
||||
|
||||
export const VoteButtons = ({
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { BigNumber } from '../../../lib/bignumber';
|
||||
import { useProposalNetworkParams } from './use-proposal-network-params';
|
||||
import { generateProposal } from '../test-helpers/generate-proposals';
|
||||
|
||||
jest.mock('@vegaprotocol/network-parameters', () => ({
|
||||
...jest.requireActual('@vegaprotocol/network-parameters'),
|
||||
@@ -29,118 +28,84 @@ jest.mock('@vegaprotocol/network-parameters', () => ({
|
||||
|
||||
describe('use-proposal-network-params', () => {
|
||||
it('returns the correct params for an update market proposal', () => {
|
||||
const proposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateMarket',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useProposalNetworkParams({ proposal }));
|
||||
} = renderHook(() => useProposalNetworkParams());
|
||||
|
||||
const expectedObj = {
|
||||
requiredMajority: expect.any(BigNumber),
|
||||
requiredMajorityLP: expect.any(BigNumber),
|
||||
requiredParticipation: expect.any(BigNumber),
|
||||
requiredParticipationLP: expect.any(BigNumber),
|
||||
};
|
||||
|
||||
expect(current).toEqual({
|
||||
requiredMajority: '0.1',
|
||||
requiredMajorityLP: '0.2',
|
||||
requiredParticipation: new BigNumber(0.15),
|
||||
requiredParticipationLP: new BigNumber(0.25),
|
||||
NewMarket: expectedObj,
|
||||
NewSpotMarket: expectedObj,
|
||||
UpdateMarket: expectedObj,
|
||||
UpdateMarketState: expectedObj,
|
||||
UpdateSpotMarket: expectedObj,
|
||||
UpdateNetworkParameter: expectedObj,
|
||||
NewAsset: expectedObj,
|
||||
UpdateAsset: expectedObj,
|
||||
NewFreeform: expectedObj,
|
||||
UpdateReferralProgram: expectedObj,
|
||||
UpdateVolumeDiscountProgram: expectedObj,
|
||||
NewTransfer: expectedObj,
|
||||
CancelTransfer: expectedObj,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the correct params for a market proposal', () => {
|
||||
const proposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'NewMarket',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it('returns the correct values for the proposal change type', () => {
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useProposalNetworkParams({ proposal }));
|
||||
} = renderHook(() => useProposalNetworkParams());
|
||||
|
||||
expect(current).toEqual({
|
||||
requiredMajority: '0.3',
|
||||
requiredParticipation: new BigNumber(0.35),
|
||||
});
|
||||
});
|
||||
expect(current?.UpdateMarket.requiredMajority.toString()).toEqual('0.1');
|
||||
expect(current?.UpdateMarket.requiredMajorityLP.toString()).toEqual('0.2');
|
||||
expect(current?.UpdateMarket.requiredParticipation.toString()).toEqual(
|
||||
'0.15'
|
||||
);
|
||||
expect(current?.UpdateMarket.requiredParticipationLP.toString()).toEqual(
|
||||
'0.25'
|
||||
);
|
||||
|
||||
it('returns the correct params for an asset proposal', () => {
|
||||
const proposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'NewAsset',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(current?.NewMarket.requiredMajority.toString()).toEqual('0.3');
|
||||
expect(current?.NewMarket.requiredMajorityLP.toString()).toEqual('0');
|
||||
expect(current?.NewMarket.requiredParticipation.toString()).toEqual('0.35');
|
||||
expect(current?.NewMarket.requiredParticipationLP.toString()).toEqual('0');
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useProposalNetworkParams({ proposal }));
|
||||
expect(current?.NewAsset.requiredMajority.toString()).toEqual('0.4');
|
||||
expect(current?.NewAsset.requiredMajorityLP.toString()).toEqual('0');
|
||||
expect(current?.NewAsset.requiredParticipation.toString()).toEqual('0.45');
|
||||
expect(current?.NewAsset.requiredParticipationLP.toString()).toEqual('0');
|
||||
|
||||
expect(current).toEqual({
|
||||
requiredMajority: '0.4',
|
||||
requiredParticipation: new BigNumber(0.45),
|
||||
});
|
||||
});
|
||||
expect(current?.UpdateAsset.requiredMajority.toString()).toEqual('0.5');
|
||||
expect(current?.UpdateAsset.requiredMajorityLP.toString()).toEqual('0');
|
||||
expect(current?.UpdateAsset.requiredParticipation.toString()).toEqual(
|
||||
'0.55'
|
||||
);
|
||||
|
||||
it('returns the correct params for an update asset proposal', () => {
|
||||
const proposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateAsset',
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(current?.UpdateNetworkParameter.requiredMajority.toString()).toEqual(
|
||||
'0.6'
|
||||
);
|
||||
expect(
|
||||
current?.UpdateNetworkParameter.requiredMajorityLP.toString()
|
||||
).toEqual('0');
|
||||
expect(
|
||||
current?.UpdateNetworkParameter.requiredParticipation.toString()
|
||||
).toEqual('0.65');
|
||||
expect(
|
||||
current?.UpdateNetworkParameter.requiredParticipationLP.toString()
|
||||
).toEqual('0');
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useProposalNetworkParams({ proposal }));
|
||||
|
||||
expect(current).toEqual({
|
||||
requiredMajority: '0.5',
|
||||
requiredParticipation: new BigNumber(0.55),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the correct params for a network params proposal', () => {
|
||||
const proposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useProposalNetworkParams({ proposal }));
|
||||
|
||||
expect(current).toEqual({
|
||||
requiredMajority: '0.6',
|
||||
requiredParticipation: new BigNumber(0.65),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the correct params for a freeform proposal', () => {
|
||||
const proposal = generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'NewFreeform',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useProposalNetworkParams({ proposal }));
|
||||
|
||||
expect(current).toEqual({
|
||||
requiredMajority: '0.7',
|
||||
requiredParticipation: new BigNumber(0.75),
|
||||
});
|
||||
expect(current?.NewFreeform.requiredMajority.toString()).toEqual('0.7');
|
||||
expect(current?.NewFreeform.requiredMajorityLP.toString()).toEqual('0');
|
||||
expect(current?.NewFreeform.requiredParticipation.toString()).toEqual(
|
||||
'0.75'
|
||||
);
|
||||
expect(current?.NewFreeform.requiredParticipationLP.toString()).toEqual(
|
||||
'0'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,35 +3,33 @@ import {
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
import { BigNumber } from '../../../lib/bignumber';
|
||||
import { type Proposal } from '../types';
|
||||
import { type ProposalChangeType } from '../types';
|
||||
|
||||
export const useProposalNetworkParams = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: Proposal;
|
||||
}) => {
|
||||
const { params } = useNetworkParams([
|
||||
NetworkParams.governance_proposal_updateMarket_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateMarket_requiredMajorityLP,
|
||||
NetworkParams.governance_proposal_updateMarket_requiredParticipation,
|
||||
NetworkParams.governance_proposal_updateMarket_requiredParticipationLP,
|
||||
NetworkParams.governance_proposal_market_requiredMajority,
|
||||
NetworkParams.governance_proposal_market_requiredParticipation,
|
||||
NetworkParams.governance_proposal_updateAsset_requiredMajority,
|
||||
NetworkParams.governance_proposal_referralProgram_requiredMajority,
|
||||
NetworkParams.governance_proposal_referralProgram_requiredParticipation,
|
||||
NetworkParams.governance_proposal_updateAsset_requiredParticipation,
|
||||
NetworkParams.governance_proposal_asset_requiredMajority,
|
||||
NetworkParams.governance_proposal_asset_requiredParticipation,
|
||||
NetworkParams.governance_proposal_updateNetParam_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateNetParam_requiredParticipation,
|
||||
NetworkParams.governance_proposal_freeform_requiredMajority,
|
||||
NetworkParams.governance_proposal_freeform_requiredParticipation,
|
||||
NetworkParams.governance_proposal_VolumeDiscountProgram_requiredMajority,
|
||||
NetworkParams.governance_proposal_VolumeDiscountProgram_requiredParticipation,
|
||||
NetworkParams.governance_proposal_transfer_requiredParticipation,
|
||||
NetworkParams.governance_proposal_transfer_requiredMajority,
|
||||
]);
|
||||
const REQUIRED_PARAMS = [
|
||||
NetworkParams.governance_proposal_updateMarket_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateMarket_requiredMajorityLP,
|
||||
NetworkParams.governance_proposal_updateMarket_requiredParticipation,
|
||||
NetworkParams.governance_proposal_updateMarket_requiredParticipationLP,
|
||||
NetworkParams.governance_proposal_market_requiredMajority,
|
||||
NetworkParams.governance_proposal_market_requiredParticipation,
|
||||
NetworkParams.governance_proposal_updateAsset_requiredMajority,
|
||||
NetworkParams.governance_proposal_referralProgram_requiredMajority,
|
||||
NetworkParams.governance_proposal_referralProgram_requiredParticipation,
|
||||
NetworkParams.governance_proposal_updateAsset_requiredParticipation,
|
||||
NetworkParams.governance_proposal_asset_requiredMajority,
|
||||
NetworkParams.governance_proposal_asset_requiredParticipation,
|
||||
NetworkParams.governance_proposal_updateNetParam_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateNetParam_requiredParticipation,
|
||||
NetworkParams.governance_proposal_freeform_requiredMajority,
|
||||
NetworkParams.governance_proposal_freeform_requiredParticipation,
|
||||
NetworkParams.governance_proposal_VolumeDiscountProgram_requiredMajority,
|
||||
NetworkParams.governance_proposal_VolumeDiscountProgram_requiredParticipation,
|
||||
NetworkParams.governance_proposal_transfer_requiredParticipation,
|
||||
NetworkParams.governance_proposal_transfer_requiredMajority,
|
||||
];
|
||||
|
||||
export const useProposalNetworkParams = () => {
|
||||
const { params } = useNetworkParams(REQUIRED_PARAMS);
|
||||
|
||||
const fallback = {
|
||||
requiredMajority: new BigNumber(1),
|
||||
@@ -41,86 +39,152 @@ export const useProposalNetworkParams = ({
|
||||
};
|
||||
|
||||
if (!params) {
|
||||
return fallback;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (proposal?.terms.change.__typename) {
|
||||
case 'UpdateMarket':
|
||||
case 'UpdateMarketState':
|
||||
return {
|
||||
requiredMajority:
|
||||
params.governance_proposal_updateMarket_requiredMajority,
|
||||
requiredMajorityLP:
|
||||
params.governance_proposal_updateMarket_requiredMajorityLP,
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredParticipation
|
||||
),
|
||||
requiredParticipationLP: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredParticipationLP
|
||||
),
|
||||
};
|
||||
case 'UpdateNetworkParameter':
|
||||
return {
|
||||
requiredMajority:
|
||||
params.governance_proposal_updateNetParam_requiredMajority,
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_updateNetParam_requiredParticipation
|
||||
),
|
||||
};
|
||||
case 'NewAsset':
|
||||
return {
|
||||
requiredMajority: params.governance_proposal_asset_requiredMajority,
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_asset_requiredParticipation
|
||||
),
|
||||
};
|
||||
case 'UpdateAsset':
|
||||
return {
|
||||
requiredMajority:
|
||||
params.governance_proposal_updateAsset_requiredMajority,
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_updateAsset_requiredParticipation
|
||||
),
|
||||
};
|
||||
case 'NewMarket':
|
||||
return {
|
||||
requiredMajority: params.governance_proposal_market_requiredMajority,
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_market_requiredParticipation
|
||||
),
|
||||
};
|
||||
case 'NewFreeform':
|
||||
return {
|
||||
requiredMajority: params.governance_proposal_freeform_requiredMajority,
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_freeform_requiredParticipation
|
||||
),
|
||||
};
|
||||
case 'UpdateReferralProgram':
|
||||
return {
|
||||
requiredMajority:
|
||||
params.governance_proposal_referralProgram_requiredMajority,
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_referralProgram_requiredParticipation
|
||||
),
|
||||
};
|
||||
case 'UpdateVolumeDiscountProgram':
|
||||
return {
|
||||
requiredMajority:
|
||||
params.governance_proposal_VolumeDiscountProgram_requiredMajority,
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_VolumeDiscountProgram_requiredParticipation
|
||||
),
|
||||
};
|
||||
case 'NewTransfer':
|
||||
case 'CancelTransfer':
|
||||
return {
|
||||
requiredMajority: params.governance_proposal_transfer_requiredMajority,
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_transfer_requiredParticipation
|
||||
),
|
||||
};
|
||||
default:
|
||||
return fallback;
|
||||
}
|
||||
const result: Record<
|
||||
ProposalChangeType,
|
||||
{
|
||||
requiredMajority: BigNumber;
|
||||
requiredParticipation: BigNumber;
|
||||
requiredMajorityLP: BigNumber;
|
||||
requiredParticipationLP: BigNumber;
|
||||
}
|
||||
> = {
|
||||
NewMarket: {
|
||||
...fallback,
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_market_requiredMajority || 1
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_market_requiredParticipation || 1
|
||||
),
|
||||
},
|
||||
NewSpotMarket: {
|
||||
...fallback,
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_market_requiredMajority || 1
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_market_requiredParticipation || 1
|
||||
),
|
||||
},
|
||||
UpdateMarket: {
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredMajority || 1
|
||||
),
|
||||
requiredMajorityLP: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredMajorityLP || 0
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredParticipation || 1
|
||||
),
|
||||
requiredParticipationLP: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredParticipationLP || 0
|
||||
),
|
||||
},
|
||||
UpdateMarketState: {
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredMajority || 1
|
||||
),
|
||||
requiredMajorityLP: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredMajorityLP || 0
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredParticipation || 1
|
||||
),
|
||||
requiredParticipationLP: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredParticipationLP || 0
|
||||
),
|
||||
},
|
||||
UpdateSpotMarket: {
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredMajority || 1
|
||||
),
|
||||
requiredMajorityLP: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredMajorityLP || 0
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredParticipation || 1
|
||||
),
|
||||
requiredParticipationLP: new BigNumber(
|
||||
params.governance_proposal_updateMarket_requiredParticipationLP || 0
|
||||
),
|
||||
},
|
||||
UpdateNetworkParameter: {
|
||||
...fallback,
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_updateNetParam_requiredMajority || 1
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_updateNetParam_requiredParticipation || 1
|
||||
),
|
||||
},
|
||||
NewAsset: {
|
||||
...fallback,
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_asset_requiredMajority || 1
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_asset_requiredParticipation || 1
|
||||
),
|
||||
},
|
||||
UpdateAsset: {
|
||||
...fallback,
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_updateAsset_requiredMajority || 1
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_updateAsset_requiredParticipation || 1
|
||||
),
|
||||
},
|
||||
NewFreeform: {
|
||||
...fallback,
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_freeform_requiredMajority || 1
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_freeform_requiredParticipation || 1
|
||||
),
|
||||
},
|
||||
UpdateReferralProgram: {
|
||||
...fallback,
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_referralProgram_requiredMajority || 1
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_referralProgram_requiredParticipation || 1
|
||||
),
|
||||
},
|
||||
UpdateVolumeDiscountProgram: {
|
||||
...fallback,
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_VolumeDiscountProgram_requiredMajority || 1
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_VolumeDiscountProgram_requiredParticipation ||
|
||||
1
|
||||
),
|
||||
},
|
||||
NewTransfer: {
|
||||
...fallback,
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_transfer_requiredMajority || 1
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_transfer_requiredParticipation || 1
|
||||
),
|
||||
},
|
||||
CancelTransfer: {
|
||||
...fallback,
|
||||
requiredMajority: new BigNumber(
|
||||
params.governance_proposal_transfer_requiredMajority || 1
|
||||
),
|
||||
requiredParticipation: new BigNumber(
|
||||
params.governance_proposal_transfer_requiredParticipation || 1
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -79,33 +79,35 @@ describe('use-vote-information', () => {
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useVoteInformation({ proposal }));
|
||||
} = renderHook(() =>
|
||||
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
|
||||
);
|
||||
|
||||
expect(current.requiredMajorityPercentage).toEqual(new BigNumber(50));
|
||||
expect(current.requiredMajorityLPPercentage).toEqual(new BigNumber(50));
|
||||
expect(current.noTokens).toEqual(new BigNumber(60));
|
||||
expect(current.noVotes).toEqual(new BigNumber(60));
|
||||
expect(current.noEquityLikeShareWeight).toEqual(new BigNumber(70));
|
||||
expect(current.yesTokens).toEqual(new BigNumber(40));
|
||||
expect(current.yesVotes).toEqual(new BigNumber(40));
|
||||
expect(current.yesEquityLikeShareWeight).toEqual(new BigNumber(30));
|
||||
expect(current.totalTokensVoted).toEqual(new BigNumber(100));
|
||||
expect(current.totalVotes).toEqual(new BigNumber(100));
|
||||
expect(current.totalEquityLikeShareWeight).toEqual(new BigNumber(100));
|
||||
expect(current.yesPercentage).toEqual(new BigNumber(40));
|
||||
expect(current.yesLPPercentage).toEqual(new BigNumber(30));
|
||||
expect(current.noPercentage).toEqual(new BigNumber(60));
|
||||
expect(current.noLPPercentage).toEqual(new BigNumber(70));
|
||||
expect(current.requiredParticipation).toEqual(new BigNumber(50));
|
||||
expect(current.participationMet).toEqual(true);
|
||||
expect(current.requiredParticipationLP).toEqual(new BigNumber(50));
|
||||
expect(current.participationLPMet).toEqual(true);
|
||||
expect(current.majorityMet).toEqual(false);
|
||||
expect(current.majorityLPMet).toEqual(false);
|
||||
expect(current.totalTokensPercentage).toEqual(new BigNumber(100));
|
||||
expect(current.totalLPTokensPercentage).toEqual(new BigNumber(100));
|
||||
expect(current.willPassByTokenVote).toEqual(false);
|
||||
expect(current.willPassByLPVote).toEqual(false);
|
||||
expect(current?.requiredMajorityLPPercentage).toEqual(new BigNumber(50));
|
||||
expect(current?.requiredMajorityPercentage).toEqual(new BigNumber(50));
|
||||
expect(current?.noTokens).toEqual(new BigNumber(60));
|
||||
expect(current?.noVotes).toEqual(new BigNumber(60));
|
||||
expect(current?.noEquityLikeShareWeight).toEqual(new BigNumber(70));
|
||||
expect(current?.yesTokens).toEqual(new BigNumber(40));
|
||||
expect(current?.yesVotes).toEqual(new BigNumber(40));
|
||||
expect(current?.yesEquityLikeShareWeight).toEqual(new BigNumber(30));
|
||||
expect(current?.totalTokensVoted).toEqual(new BigNumber(100));
|
||||
expect(current?.totalVotes).toEqual(new BigNumber(100));
|
||||
expect(current?.totalEquityLikeShareWeight).toEqual(new BigNumber(100));
|
||||
expect(current?.yesPercentage).toEqual(new BigNumber(40));
|
||||
expect(current?.yesLPPercentage).toEqual(new BigNumber(30));
|
||||
expect(current?.noPercentage).toEqual(new BigNumber(60));
|
||||
expect(current?.noLPPercentage).toEqual(new BigNumber(70));
|
||||
expect(current?.requiredParticipation).toEqual(new BigNumber(50));
|
||||
expect(current?.participationMet).toEqual(true);
|
||||
expect(current?.requiredParticipationLP).toEqual(new BigNumber(50));
|
||||
expect(current?.participationLPMet).toEqual(true);
|
||||
expect(current?.majorityMet).toEqual(false);
|
||||
expect(current?.majorityLPMet).toEqual(false);
|
||||
expect(current?.totalTokensPercentage).toEqual(new BigNumber(100));
|
||||
expect(current?.totalLPTokensPercentage).toEqual(new BigNumber(100));
|
||||
expect(current?.willPassByTokenVote).toEqual(false);
|
||||
expect(current?.willPassByLPVote).toEqual(false);
|
||||
});
|
||||
|
||||
it('correctly returns majority, participation and will-pass status for a proposal with no votes', () => {
|
||||
@@ -123,11 +125,13 @@ describe('use-vote-information', () => {
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useVoteInformation({ proposal }));
|
||||
} = renderHook(() =>
|
||||
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
|
||||
);
|
||||
|
||||
expect(current.participationMet).toEqual(false);
|
||||
expect(current.majorityMet).toEqual(false);
|
||||
expect(current.willPassByTokenVote).toEqual(false);
|
||||
expect(current?.participationMet).toEqual(false);
|
||||
expect(current?.majorityMet).toEqual(false);
|
||||
expect(current?.willPassByTokenVote).toEqual(false);
|
||||
});
|
||||
|
||||
it('correctly shows lack of participation for a failing proposal lacking votes', () => {
|
||||
@@ -145,9 +149,11 @@ describe('use-vote-information', () => {
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useVoteInformation({ proposal }));
|
||||
} = renderHook(() =>
|
||||
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
|
||||
);
|
||||
|
||||
expect(current.participationMet).toEqual(false);
|
||||
expect(current?.participationMet).toEqual(false);
|
||||
});
|
||||
|
||||
it('correctly shows participation but lack of majority for a failing proposal with enough votes but not enough majority', () => {
|
||||
@@ -165,11 +171,13 @@ describe('use-vote-information', () => {
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useVoteInformation({ proposal }));
|
||||
} = renderHook(() =>
|
||||
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
|
||||
);
|
||||
|
||||
expect(current.participationMet).toEqual(true);
|
||||
expect(current.majorityMet).toEqual(false);
|
||||
expect(current.willPassByTokenVote).toEqual(false);
|
||||
expect(current?.participationMet).toEqual(true);
|
||||
expect(current?.majorityMet).toEqual(false);
|
||||
expect(current?.willPassByTokenVote).toEqual(false);
|
||||
});
|
||||
|
||||
it('correctly shows participation, majority and will-pass data for successful proposal', () => {
|
||||
@@ -187,11 +195,13 @@ describe('use-vote-information', () => {
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useVoteInformation({ proposal }));
|
||||
} = renderHook(() =>
|
||||
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
|
||||
);
|
||||
|
||||
expect(current.participationMet).toEqual(true);
|
||||
expect(current.majorityMet).toEqual(true);
|
||||
expect(current.willPassByTokenVote).toEqual(true);
|
||||
expect(current?.participationMet).toEqual(true);
|
||||
expect(current?.majorityMet).toEqual(true);
|
||||
expect(current?.willPassByTokenVote).toEqual(true);
|
||||
});
|
||||
|
||||
it('correctly shows whether an update market proposal will pass by token or LP vote - both failing', () => {
|
||||
@@ -221,10 +231,12 @@ describe('use-vote-information', () => {
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useVoteInformation({ proposal }));
|
||||
} = renderHook(() =>
|
||||
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
|
||||
);
|
||||
|
||||
expect(current.willPassByTokenVote).toEqual(false);
|
||||
expect(current.willPassByLPVote).toEqual(false);
|
||||
expect(current?.willPassByTokenVote).toEqual(false);
|
||||
expect(current?.willPassByLPVote).toEqual(false);
|
||||
});
|
||||
|
||||
it('correctly shows whether an update market proposal failing token but passing LP voting', () => {
|
||||
@@ -254,9 +266,11 @@ describe('use-vote-information', () => {
|
||||
|
||||
const {
|
||||
result: { current },
|
||||
} = renderHook(() => useVoteInformation({ proposal }));
|
||||
} = renderHook(() =>
|
||||
useVoteInformation({ terms: proposal.terms, votes: proposal.votes })
|
||||
);
|
||||
|
||||
expect(current.willPassByTokenVote).toEqual(false);
|
||||
expect(current.willPassByLPVote).toEqual(true);
|
||||
expect(current?.willPassByTokenVote).toEqual(false);
|
||||
expect(current?.willPassByLPVote).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,191 +1,198 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useAppState } from '../../../contexts/app-state/app-state-context';
|
||||
import { BigNumber } from '../../../lib/bignumber';
|
||||
import { useProposalNetworkParams } from './use-proposal-network-params';
|
||||
import { addDecimal } from '@vegaprotocol/utils';
|
||||
import { type Proposal } from '../types';
|
||||
import {
|
||||
type ProposalTermsFieldsFragment,
|
||||
type ProposalFieldsFragment,
|
||||
type VoteFieldsFragment,
|
||||
} from '../__generated__/Proposals';
|
||||
import { type ProposalChangeType } from '../types';
|
||||
|
||||
export const useVoteInformation = ({ proposal }: { proposal: Proposal }) => {
|
||||
export const useVoteInformation = ({
|
||||
votes,
|
||||
terms,
|
||||
}: {
|
||||
votes: VoteFieldsFragment;
|
||||
terms: ProposalTermsFieldsFragment;
|
||||
}) => {
|
||||
const {
|
||||
appState: { totalSupply, decimals },
|
||||
} = useAppState();
|
||||
|
||||
const params = useProposalNetworkParams();
|
||||
|
||||
if (!params) return;
|
||||
|
||||
const paramsForChange = params[terms.change.__typename];
|
||||
|
||||
return getVoteData(
|
||||
terms.change.__typename,
|
||||
paramsForChange,
|
||||
votes,
|
||||
totalSupply,
|
||||
decimals
|
||||
);
|
||||
};
|
||||
|
||||
export const useBatchVoteInformation = ({
|
||||
votes,
|
||||
terms,
|
||||
}: {
|
||||
votes: VoteFieldsFragment;
|
||||
terms: ProposalTermsFieldsFragment[];
|
||||
}) => {
|
||||
const {
|
||||
requiredMajority,
|
||||
requiredParticipation,
|
||||
requiredMajorityLP,
|
||||
requiredParticipationLP,
|
||||
} = useProposalNetworkParams({
|
||||
proposal,
|
||||
appState: { totalSupply, decimals },
|
||||
} = useAppState();
|
||||
|
||||
const params = useProposalNetworkParams();
|
||||
|
||||
if (!params) return;
|
||||
|
||||
return terms.map((t) => {
|
||||
const paramsForChange = params[t.change.__typename];
|
||||
return getVoteData(
|
||||
t.change.__typename,
|
||||
paramsForChange,
|
||||
votes,
|
||||
totalSupply,
|
||||
decimals
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
requiredMajorityPercentage,
|
||||
requiredMajorityLPPercentage,
|
||||
noTokens,
|
||||
noEquityLikeShareWeight,
|
||||
yesTokens,
|
||||
yesEquityLikeShareWeight,
|
||||
totalTokensVoted,
|
||||
totalEquityLikeShareWeight,
|
||||
yesPercentage,
|
||||
yesLPPercentage,
|
||||
noPercentage,
|
||||
noLPPercentage,
|
||||
participationMet,
|
||||
participationLPMet,
|
||||
majorityMet,
|
||||
majorityLPMet,
|
||||
totalTokensPercentage,
|
||||
totalLPTokensPercentage,
|
||||
willPassByTokenVote,
|
||||
willPassByLPVote,
|
||||
} = useMemo(() => {
|
||||
const requiredMajorityPercentage = requiredMajority
|
||||
? new BigNumber(requiredMajority).times(100)
|
||||
: new BigNumber(100);
|
||||
const getVoteData = (
|
||||
changeType: ProposalChangeType,
|
||||
params: {
|
||||
requiredMajority: BigNumber;
|
||||
requiredMajorityLP: BigNumber;
|
||||
requiredParticipation: BigNumber;
|
||||
requiredParticipationLP: BigNumber;
|
||||
},
|
||||
votes: ProposalFieldsFragment['votes'],
|
||||
totalSupply: BigNumber,
|
||||
decimals: number
|
||||
) => {
|
||||
const requiredMajorityPercentage = params.requiredMajority
|
||||
? new BigNumber(params.requiredMajority).times(100)
|
||||
: new BigNumber(100);
|
||||
|
||||
const requiredMajorityLPPercentage = requiredMajorityLP
|
||||
? new BigNumber(requiredMajorityLP).times(100)
|
||||
: new BigNumber(100);
|
||||
const requiredMajorityLPPercentage = params.requiredMajorityLP
|
||||
? new BigNumber(params.requiredMajorityLP).times(100)
|
||||
: new BigNumber(100);
|
||||
|
||||
const noTokens = new BigNumber(
|
||||
addDecimal(proposal?.votes.no.totalTokens ?? 0, decimals)
|
||||
);
|
||||
const noTokens = new BigNumber(
|
||||
addDecimal(votes.no.totalTokens ?? 0, decimals)
|
||||
);
|
||||
|
||||
const noEquityLikeShareWeight = !proposal?.votes.no
|
||||
.totalEquityLikeShareWeight
|
||||
? new BigNumber(0)
|
||||
: new BigNumber(proposal.votes.no.totalEquityLikeShareWeight).times(100);
|
||||
const noEquityLikeShareWeight = !votes.no.totalEquityLikeShareWeight
|
||||
? new BigNumber(0)
|
||||
: new BigNumber(votes.no.totalEquityLikeShareWeight).times(100);
|
||||
|
||||
const yesTokens = new BigNumber(
|
||||
addDecimal(proposal?.votes.yes.totalTokens ?? 0, decimals)
|
||||
);
|
||||
const yesTokens = new BigNumber(
|
||||
addDecimal(votes.yes.totalTokens ?? 0, decimals)
|
||||
);
|
||||
|
||||
const yesEquityLikeShareWeight = !proposal?.votes.yes
|
||||
.totalEquityLikeShareWeight
|
||||
? new BigNumber(0)
|
||||
: new BigNumber(proposal.votes.yes.totalEquityLikeShareWeight).times(100);
|
||||
const yesEquityLikeShareWeight = !votes.yes.totalEquityLikeShareWeight
|
||||
? new BigNumber(0)
|
||||
: new BigNumber(votes.yes.totalEquityLikeShareWeight).times(100);
|
||||
|
||||
const totalTokensVoted = yesTokens.plus(noTokens);
|
||||
const totalTokensVoted = yesTokens.plus(noTokens);
|
||||
|
||||
const totalEquityLikeShareWeight = yesEquityLikeShareWeight.plus(
|
||||
noEquityLikeShareWeight
|
||||
);
|
||||
const totalEquityLikeShareWeight = yesEquityLikeShareWeight.plus(
|
||||
noEquityLikeShareWeight
|
||||
);
|
||||
|
||||
const yesPercentage = totalTokensVoted.isZero()
|
||||
? new BigNumber(0)
|
||||
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
|
||||
const yesLPPercentage = yesEquityLikeShareWeight;
|
||||
const yesPercentage = totalTokensVoted.isZero()
|
||||
? new BigNumber(0)
|
||||
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
|
||||
const yesLPPercentage = yesEquityLikeShareWeight;
|
||||
|
||||
const noPercentage = totalTokensVoted.isZero()
|
||||
? new BigNumber(0)
|
||||
: noTokens.multipliedBy(100).dividedBy(totalTokensVoted);
|
||||
const noPercentage = totalTokensVoted.isZero()
|
||||
? new BigNumber(0)
|
||||
: noTokens.multipliedBy(100).dividedBy(totalTokensVoted);
|
||||
|
||||
const noLPPercentage = totalEquityLikeShareWeight.isZero()
|
||||
? new BigNumber(0)
|
||||
: noEquityLikeShareWeight
|
||||
.multipliedBy(100)
|
||||
.dividedBy(totalEquityLikeShareWeight);
|
||||
const noLPPercentage = totalEquityLikeShareWeight.isZero()
|
||||
? new BigNumber(0)
|
||||
: noEquityLikeShareWeight
|
||||
.multipliedBy(100)
|
||||
.dividedBy(totalEquityLikeShareWeight);
|
||||
|
||||
const participationMet = totalTokensVoted.isGreaterThan(
|
||||
totalSupply.multipliedBy(requiredParticipation)
|
||||
);
|
||||
const participationMet = totalTokensVoted.isGreaterThan(
|
||||
totalSupply.multipliedBy(params.requiredParticipation)
|
||||
);
|
||||
|
||||
const participationLPMet = requiredParticipationLP
|
||||
? totalEquityLikeShareWeight.isGreaterThan(requiredParticipationLP)
|
||||
: false;
|
||||
const participationLPMet = params.requiredParticipationLP
|
||||
? totalEquityLikeShareWeight.isGreaterThan(params.requiredParticipationLP)
|
||||
: false;
|
||||
|
||||
const majorityMet = yesPercentage.isGreaterThanOrEqualTo(
|
||||
const majorityMet = yesPercentage.isGreaterThanOrEqualTo(
|
||||
requiredMajorityPercentage
|
||||
);
|
||||
|
||||
const majorityLPMet = yesLPPercentage.isGreaterThanOrEqualTo(
|
||||
requiredMajorityLPPercentage
|
||||
);
|
||||
|
||||
const totalTokensPercentage = totalTokensVoted
|
||||
.multipliedBy(100)
|
||||
.dividedBy(totalSupply);
|
||||
|
||||
const totalLPTokensPercentage = totalEquityLikeShareWeight;
|
||||
|
||||
const willPassByTokenVote =
|
||||
participationMet &&
|
||||
new BigNumber(yesPercentage).isGreaterThanOrEqualTo(
|
||||
requiredMajorityPercentage
|
||||
);
|
||||
|
||||
const majorityLPMet = yesLPPercentage.isGreaterThanOrEqualTo(
|
||||
const willPassByLPVote =
|
||||
participationLPMet &&
|
||||
new BigNumber(yesLPPercentage).isGreaterThanOrEqualTo(
|
||||
requiredMajorityLPPercentage
|
||||
);
|
||||
|
||||
const totalTokensPercentage = totalTokensVoted
|
||||
.multipliedBy(100)
|
||||
.dividedBy(totalSupply);
|
||||
let willPass = false;
|
||||
|
||||
const totalLPTokensPercentage = totalEquityLikeShareWeight;
|
||||
|
||||
const willPassByTokenVote =
|
||||
participationMet &&
|
||||
new BigNumber(yesPercentage).isGreaterThanOrEqualTo(
|
||||
requiredMajorityPercentage
|
||||
);
|
||||
|
||||
const willPassByLPVote =
|
||||
participationLPMet &&
|
||||
new BigNumber(yesLPPercentage).isGreaterThanOrEqualTo(
|
||||
requiredMajorityLPPercentage
|
||||
);
|
||||
|
||||
return {
|
||||
requiredMajorityPercentage,
|
||||
requiredMajorityLPPercentage,
|
||||
noTokens,
|
||||
noEquityLikeShareWeight,
|
||||
yesTokens,
|
||||
yesEquityLikeShareWeight,
|
||||
totalTokensVoted,
|
||||
totalEquityLikeShareWeight,
|
||||
yesPercentage,
|
||||
yesLPPercentage,
|
||||
noPercentage,
|
||||
noLPPercentage,
|
||||
participationMet,
|
||||
participationLPMet,
|
||||
majorityMet,
|
||||
majorityLPMet,
|
||||
totalTokensPercentage,
|
||||
totalLPTokensPercentage,
|
||||
willPassByTokenVote,
|
||||
willPassByLPVote,
|
||||
};
|
||||
}, [
|
||||
decimals,
|
||||
proposal?.votes.no.totalEquityLikeShareWeight,
|
||||
proposal?.votes.no.totalTokens,
|
||||
proposal?.votes.yes.totalEquityLikeShareWeight,
|
||||
proposal?.votes.yes.totalTokens,
|
||||
requiredMajority,
|
||||
requiredMajorityLP,
|
||||
requiredParticipation,
|
||||
requiredParticipationLP,
|
||||
totalSupply,
|
||||
]);
|
||||
if (changeType === 'UpdateMarket' || changeType === 'UpdateMarketState') {
|
||||
willPass = willPassByTokenVote && willPassByLPVote;
|
||||
} else {
|
||||
willPass = willPassByTokenVote;
|
||||
}
|
||||
|
||||
return {
|
||||
willPassByTokenVote,
|
||||
willPassByLPVote,
|
||||
totalTokensPercentage,
|
||||
totalLPTokensPercentage,
|
||||
participationMet,
|
||||
participationLPMet,
|
||||
totalTokensVoted,
|
||||
totalEquityLikeShareWeight,
|
||||
noPercentage,
|
||||
noLPPercentage,
|
||||
yesPercentage,
|
||||
yesLPPercentage,
|
||||
requiredMajorityPercentage,
|
||||
requiredMajorityLPPercentage,
|
||||
noTokens,
|
||||
noEquityLikeShareWeight,
|
||||
yesTokens,
|
||||
yesEquityLikeShareWeight,
|
||||
yesVotes: new BigNumber(proposal?.votes.yes.totalNumber ?? 0),
|
||||
noVotes: new BigNumber(proposal?.votes.no.totalNumber ?? 0),
|
||||
totalVotes: new BigNumber(proposal?.votes.yes.totalNumber ?? 0).plus(
|
||||
proposal?.votes.no.totalNumber ?? 0
|
||||
),
|
||||
requiredMajorityPercentage,
|
||||
requiredMajorityLPPercentage,
|
||||
requiredParticipation: new BigNumber(requiredParticipation).times(100),
|
||||
requiredParticipationLP:
|
||||
requiredParticipationLP &&
|
||||
new BigNumber(requiredParticipationLP).times(100),
|
||||
totalTokensVoted,
|
||||
totalEquityLikeShareWeight,
|
||||
yesPercentage,
|
||||
yesLPPercentage,
|
||||
noPercentage,
|
||||
noLPPercentage,
|
||||
participationMet,
|
||||
participationLPMet,
|
||||
majorityMet,
|
||||
majorityLPMet,
|
||||
totalTokensPercentage,
|
||||
totalLPTokensPercentage,
|
||||
willPassByTokenVote,
|
||||
willPassByLPVote,
|
||||
yesVotes: new BigNumber(votes.yes.totalNumber ?? 0),
|
||||
noVotes: new BigNumber(votes.no.totalNumber ?? 0),
|
||||
totalVotes: new BigNumber(votes.yes.totalNumber ?? 0).plus(
|
||||
votes.no.totalNumber ?? 0
|
||||
),
|
||||
requiredParticipation: new BigNumber(params.requiredParticipation).times(
|
||||
100
|
||||
),
|
||||
requiredParticipationLP: new BigNumber(
|
||||
params.requiredParticipationLP
|
||||
).times(100),
|
||||
willPass,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,442 +0,0 @@
|
||||
fragment NewMarketProductField on Proposal {
|
||||
terms {
|
||||
change {
|
||||
... on NewMarket {
|
||||
instrument {
|
||||
product {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment UpdateMarketState on Proposal {
|
||||
terms {
|
||||
change {
|
||||
... on UpdateMarketState {
|
||||
updateType
|
||||
market {
|
||||
decimalPlaces
|
||||
id
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
product {
|
||||
__typename
|
||||
... on Future {
|
||||
quoteName
|
||||
}
|
||||
... on Perpetual {
|
||||
quoteName
|
||||
}
|
||||
}
|
||||
name
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
updateType
|
||||
price
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment UpdateReferralProgram on Proposal {
|
||||
terms {
|
||||
change {
|
||||
... on UpdateReferralProgram {
|
||||
benefitTiers {
|
||||
minimumEpochs
|
||||
minimumRunningNotionalTakerVolume
|
||||
referralDiscountFactor
|
||||
referralRewardFactor
|
||||
}
|
||||
endOfProgram: endOfProgramTimestamp
|
||||
windowLength
|
||||
stakingTiers {
|
||||
minimumStakedTokens
|
||||
referralRewardMultiplier
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment UpdateVolumeDiscountProgram on Proposal {
|
||||
terms {
|
||||
change {
|
||||
... on UpdateVolumeDiscountProgram {
|
||||
benefitTiers {
|
||||
minimumRunningNotionalTakerVolume
|
||||
volumeDiscountFactor
|
||||
}
|
||||
endOfProgramTimestamp
|
||||
windowLength
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query Proposal(
|
||||
$proposalId: ID!
|
||||
$includeNewMarketProductField: Boolean!
|
||||
$includeUpdateMarketState: Boolean!
|
||||
$includeUpdateReferralProgram: Boolean!
|
||||
) {
|
||||
proposal(id: $proposalId) {
|
||||
... on Proposal {
|
||||
id
|
||||
rationale {
|
||||
title
|
||||
description
|
||||
}
|
||||
reference
|
||||
state
|
||||
datetime
|
||||
rejectionReason
|
||||
party {
|
||||
id
|
||||
}
|
||||
errorDetails
|
||||
...NewMarketProductField @include(if: $includeNewMarketProductField)
|
||||
...UpdateMarketState @include(if: $includeUpdateMarketState)
|
||||
...UpdateReferralProgram @include(if: $includeUpdateReferralProgram)
|
||||
...UpdateVolumeDiscountProgram
|
||||
terms {
|
||||
closingDatetime
|
||||
enactmentDatetime
|
||||
change {
|
||||
... on NewMarket {
|
||||
decimalPlaces
|
||||
metadata
|
||||
riskParameters {
|
||||
... on LogNormalRiskModel {
|
||||
riskAversionParameter
|
||||
tau
|
||||
params {
|
||||
mu
|
||||
r
|
||||
sigma
|
||||
}
|
||||
}
|
||||
... on SimpleRiskModel {
|
||||
params {
|
||||
factorLong
|
||||
factorShort
|
||||
}
|
||||
}
|
||||
}
|
||||
instrument {
|
||||
name
|
||||
code
|
||||
product {
|
||||
... on FutureProduct {
|
||||
settlementAsset {
|
||||
id
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
}
|
||||
quoteName
|
||||
dataSourceSpecBinding {
|
||||
settlementDataProperty
|
||||
tradingTerminationProperty
|
||||
}
|
||||
dataSourceSpecForSettlementData {
|
||||
sourceType {
|
||||
... on DataSourceDefinitionInternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfigurationTime {
|
||||
conditions {
|
||||
operator
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on DataSourceDefinitionExternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfiguration {
|
||||
signers {
|
||||
signer {
|
||||
... on PubKey {
|
||||
key
|
||||
}
|
||||
... on ETHAddress {
|
||||
address
|
||||
}
|
||||
}
|
||||
}
|
||||
filters {
|
||||
key {
|
||||
name
|
||||
type
|
||||
}
|
||||
conditions {
|
||||
operator
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on PerpetualProduct {
|
||||
settlementAsset {
|
||||
id
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
}
|
||||
quoteName
|
||||
}
|
||||
}
|
||||
}
|
||||
priceMonitoringParameters {
|
||||
triggers {
|
||||
horizonSecs
|
||||
probability
|
||||
auctionExtensionSecs
|
||||
}
|
||||
}
|
||||
liquidityMonitoringParameters {
|
||||
targetStakeParameters {
|
||||
timeWindow
|
||||
scalingFactor
|
||||
}
|
||||
}
|
||||
positionDecimalPlaces
|
||||
linearSlippageFactor
|
||||
}
|
||||
... on UpdateMarket {
|
||||
marketId
|
||||
updateMarketConfiguration {
|
||||
instrument {
|
||||
code
|
||||
product {
|
||||
... on UpdateFutureProduct {
|
||||
quoteName
|
||||
dataSourceSpecForSettlementData {
|
||||
sourceType {
|
||||
... on DataSourceDefinitionInternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfigurationTime {
|
||||
conditions {
|
||||
operator
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on DataSourceDefinitionExternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfiguration {
|
||||
signers {
|
||||
signer {
|
||||
... on PubKey {
|
||||
key
|
||||
}
|
||||
... on ETHAddress {
|
||||
address
|
||||
}
|
||||
}
|
||||
}
|
||||
filters {
|
||||
key {
|
||||
name
|
||||
type
|
||||
}
|
||||
conditions {
|
||||
operator
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
# dataSourceSpecForTradingTermination {
|
||||
# sourceType {
|
||||
# ... on DataSourceDefinitionInternal {
|
||||
# sourceType {
|
||||
# ... on DataSourceSpecConfigurationTime {
|
||||
# conditions {
|
||||
# operator
|
||||
# value
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# ... on DataSourceDefinitionExternal {
|
||||
# sourceType {
|
||||
# ... on DataSourceSpecConfiguration {
|
||||
# signers {
|
||||
# signer {
|
||||
# ... on PubKey {
|
||||
# key
|
||||
# }
|
||||
# ... on ETHAddress {
|
||||
# address
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# filters {
|
||||
# key {
|
||||
# name
|
||||
# type
|
||||
# }
|
||||
# conditions {
|
||||
# operator
|
||||
# value
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
dataSourceSpecBinding {
|
||||
settlementDataProperty
|
||||
tradingTerminationProperty
|
||||
}
|
||||
}
|
||||
... on UpdatePerpetualProduct {
|
||||
quoteName
|
||||
dataSourceSpecForSettlementData {
|
||||
sourceType {
|
||||
... on DataSourceDefinitionInternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfigurationTime {
|
||||
conditions {
|
||||
operator
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on DataSourceDefinitionExternal {
|
||||
sourceType {
|
||||
... on DataSourceSpecConfiguration {
|
||||
signers {
|
||||
signer {
|
||||
... on PubKey {
|
||||
key
|
||||
}
|
||||
... on ETHAddress {
|
||||
address
|
||||
}
|
||||
}
|
||||
}
|
||||
filters {
|
||||
key {
|
||||
name
|
||||
type
|
||||
}
|
||||
conditions {
|
||||
operator
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dataSourceSpecBinding {
|
||||
settlementDataProperty
|
||||
settlementScheduleProperty
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
metadata
|
||||
priceMonitoringParameters {
|
||||
triggers {
|
||||
horizonSecs
|
||||
probability
|
||||
auctionExtensionSecs
|
||||
}
|
||||
}
|
||||
liquidityMonitoringParameters {
|
||||
targetStakeParameters {
|
||||
timeWindow
|
||||
scalingFactor
|
||||
}
|
||||
}
|
||||
riskParameters {
|
||||
... on UpdateMarketSimpleRiskModel {
|
||||
simple {
|
||||
factorLong
|
||||
factorShort
|
||||
}
|
||||
}
|
||||
... on UpdateMarketLogNormalRiskModel {
|
||||
logNormal {
|
||||
riskAversionParameter
|
||||
tau
|
||||
params {
|
||||
r
|
||||
sigma
|
||||
mu
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on NewAsset {
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
source {
|
||||
... on BuiltinAsset {
|
||||
maxFaucetAmountMint
|
||||
}
|
||||
... on ERC20 {
|
||||
contractAddress
|
||||
lifetimeLimit
|
||||
withdrawThreshold
|
||||
}
|
||||
}
|
||||
}
|
||||
... on UpdateNetworkParameter {
|
||||
networkParameter {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
... on UpdateAsset {
|
||||
quantum
|
||||
assetId
|
||||
source {
|
||||
... on UpdateERC20 {
|
||||
lifetimeLimit
|
||||
withdrawThreshold
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
votes {
|
||||
yes {
|
||||
totalTokens
|
||||
totalNumber
|
||||
totalEquityLikeShareWeight
|
||||
}
|
||||
no {
|
||||
totalTokens
|
||||
totalNumber
|
||||
totalEquityLikeShareWeight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -1,69 +0,0 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { generateProposal } from '../test-helpers/generate-proposals';
|
||||
import type { ProposalQuery } from './__generated__/Proposal';
|
||||
import { ProposalContainer } from './proposal-container';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
import { ProposalDocument } from './__generated__/Proposal';
|
||||
|
||||
jest.mock('@vegaprotocol/data-provider', () => ({
|
||||
...jest.requireActual('@vegaprotocol/data-provider'),
|
||||
useDataProvider: jest.fn(() => ({ data: [], loading: false })),
|
||||
}));
|
||||
|
||||
jest.mock('../components/proposal', () => ({
|
||||
Proposal: () => <div data-testid="proposal" />,
|
||||
}));
|
||||
|
||||
jest.mock('../components/proposal-not-found', () => ({
|
||||
ProposalNotFound: () => <div data-testid="proposal-not-found" />,
|
||||
}));
|
||||
|
||||
const renderComponent = (
|
||||
proposal: ProposalQuery['proposal'] | null,
|
||||
id: string
|
||||
) => {
|
||||
return (
|
||||
<MemoryRouter initialEntries={[`/governance/${id}`]}>
|
||||
<MockedProvider
|
||||
mocks={[
|
||||
{
|
||||
request: {
|
||||
query: ProposalDocument,
|
||||
variables: {
|
||||
proposalId: id,
|
||||
},
|
||||
},
|
||||
result: { data: { proposal } },
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Routes>
|
||||
<Route
|
||||
path={`/governance/:proposalId`}
|
||||
element={<ProposalContainer />}
|
||||
/>
|
||||
</Routes>
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
// These tests are broken due to schema changes. NewMarket.futureProduct -> NewMarket.product union
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
describe.skip('Proposal container', () => {
|
||||
it('Renders not found if the proposal is not found', async () => {
|
||||
render(renderComponent(null, 'foo'));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('proposal-not-found')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Renders proposal details if proposal is found', async () => {
|
||||
const proposal = generateProposal({ id: 'foo' });
|
||||
render(renderComponent(proposal as ProposalQuery['proposal'], 'foo'));
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('proposal')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,260 +1,39 @@
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { Proposal } from '../components/proposal';
|
||||
import { ProposalNotFound } from '../components/proposal-not-found';
|
||||
import { useProposalQuery } from './__generated__/Proposal';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||
import { ENV } from '../../../config';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { marketInfoProvider } from '@vegaprotocol/markets';
|
||||
import { useAssetQuery } from '@vegaprotocol/assets';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
import { useParentMarketIdQuery } from '@vegaprotocol/markets';
|
||||
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||
import { useSuccessorMarketProposalDetails } from '@vegaprotocol/proposals';
|
||||
import { type Proposal as IProposal } from '../types';
|
||||
import { Proposal } from '../components/proposal';
|
||||
import { ProposalNotFound } from '../components/proposal-not-found';
|
||||
import { useProposalQuery } from '../__generated__/Proposals';
|
||||
|
||||
export const ProposalContainer = () => {
|
||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||
const [
|
||||
mostRecentlyEnactedAssociatedMarketProposal,
|
||||
setMostRecentlyEnactedAssociatedMarketProposal,
|
||||
] = useState(undefined);
|
||||
const params = useParams<{ proposalId: string }>();
|
||||
|
||||
const {
|
||||
params: networkParams,
|
||||
loading: networkParamsLoading,
|
||||
error: networkParamsError,
|
||||
} = useNetworkParams([
|
||||
NetworkParams.governance_proposal_market_minVoterBalance,
|
||||
NetworkParams.governance_proposal_updateMarket_minVoterBalance,
|
||||
NetworkParams.governance_proposal_asset_minVoterBalance,
|
||||
NetworkParams.governance_proposal_updateAsset_minVoterBalance,
|
||||
NetworkParams.governance_proposal_updateNetParam_minVoterBalance,
|
||||
NetworkParams.governance_proposal_freeform_minVoterBalance,
|
||||
NetworkParams.governance_proposal_referralProgram_minVoterBalance,
|
||||
NetworkParams.governance_proposal_VolumeDiscountProgram_minVoterBalance,
|
||||
NetworkParams.spam_protection_voting_min_tokens,
|
||||
NetworkParams.governance_proposal_market_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateMarket_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateMarket_requiredMajorityLP,
|
||||
NetworkParams.governance_proposal_asset_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateAsset_requiredMajority,
|
||||
NetworkParams.governance_proposal_updateNetParam_requiredMajority,
|
||||
NetworkParams.governance_proposal_freeform_requiredMajority,
|
||||
NetworkParams.governance_proposal_referralProgram_requiredMajority,
|
||||
NetworkParams.governance_proposal_VolumeDiscountProgram_requiredMajority,
|
||||
]);
|
||||
|
||||
const {
|
||||
state: { data: restData, loading: restLoading, error: restError },
|
||||
} = useFetch(`${ENV.rest}governance?proposalId=${params.proposalId}`);
|
||||
|
||||
const { data, loading, error, refetch } = useProposalQuery({
|
||||
const { data, loading, error } = useProposalQuery({
|
||||
fetchPolicy: 'network-only',
|
||||
errorPolicy: 'ignore',
|
||||
variables: {
|
||||
proposalId: params.proposalId || '',
|
||||
includeNewMarketProductField: !!featureFlags.PRODUCT_PERPETUALS,
|
||||
includeUpdateMarketState: !!featureFlags.UPDATE_MARKET_STATE,
|
||||
includeUpdateReferralProgram: !!featureFlags.REFERRALS,
|
||||
},
|
||||
skip: !params.proposalId,
|
||||
pollInterval: 2000,
|
||||
});
|
||||
|
||||
const proposal = data?.proposal as IProposal;
|
||||
|
||||
const successor = useSuccessorMarketProposalDetails(params.proposalId);
|
||||
|
||||
const isSuccessor = !!successor?.parentMarketId || !!successor.code;
|
||||
|
||||
const {
|
||||
state: {
|
||||
data: originalMarketProposalRestData,
|
||||
loading: originalMarketProposalRestLoading,
|
||||
error: originalMarketProposalRestError,
|
||||
},
|
||||
} = useFetch(
|
||||
`${ENV.rest}governance?proposalId=${
|
||||
proposal?.terms.change.__typename === 'UpdateMarket' &&
|
||||
proposal.terms.change.marketId
|
||||
}`,
|
||||
undefined,
|
||||
true,
|
||||
proposal?.terms.change.__typename !== 'UpdateMarket'
|
||||
);
|
||||
|
||||
const {
|
||||
state: {
|
||||
data: previouslyEnactedMarketProposalsRestData,
|
||||
loading: previouslyEnactedMarketProposalsRestLoading,
|
||||
error: previouslyEnactedMarketProposalsRestError,
|
||||
},
|
||||
} = useFetch(
|
||||
`${ENV.rest}governances?proposalState=STATE_ENACTED&proposalType=TYPE_UPDATE_MARKET`,
|
||||
undefined,
|
||||
true,
|
||||
proposal?.terms.change.__typename !== 'UpdateMarket'
|
||||
);
|
||||
|
||||
const {
|
||||
data: marketData,
|
||||
loading: marketLoading,
|
||||
error: marketError,
|
||||
} = useDataProvider({
|
||||
dataProvider: marketInfoProvider,
|
||||
skipUpdates: true,
|
||||
variables: {
|
||||
marketId: proposal?.id || '',
|
||||
skip: !proposal?.id,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
data: parentMarketId,
|
||||
loading: parentMarketIdLoading,
|
||||
error: parentMarketIdError,
|
||||
} = useParentMarketIdQuery({
|
||||
variables: {
|
||||
marketId: marketData?.id || '',
|
||||
},
|
||||
skip: !featureFlags.SUCCESSOR_MARKETS || !isSuccessor || !marketData?.id,
|
||||
});
|
||||
|
||||
const {
|
||||
data: parentMarketData,
|
||||
loading: parentMarketLoading,
|
||||
error: parentMarketError,
|
||||
} = useDataProvider({
|
||||
dataProvider: marketInfoProvider,
|
||||
skipUpdates: true,
|
||||
variables: {
|
||||
marketId: parentMarketId?.market?.parentMarketID || '',
|
||||
skip:
|
||||
!featureFlags.SUCCESSOR_MARKETS ||
|
||||
!isSuccessor ||
|
||||
!parentMarketId?.market?.parentMarketID,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
data: assetData,
|
||||
loading: assetLoading,
|
||||
error: assetError,
|
||||
} = useAssetQuery({
|
||||
fetchPolicy: 'network-only',
|
||||
variables: {
|
||||
assetId:
|
||||
(proposal?.terms.change.__typename === 'NewAsset' && proposal?.id) ||
|
||||
(proposal?.terms.change.__typename === 'UpdateAsset' &&
|
||||
proposal.terms.change.assetId) ||
|
||||
'',
|
||||
},
|
||||
skip: !['NewAsset', 'UpdateAsset'].includes(
|
||||
proposal?.terms?.change?.__typename || ''
|
||||
),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
previouslyEnactedMarketProposalsRestData &&
|
||||
proposal?.terms.change.__typename === 'UpdateMarket'
|
||||
) {
|
||||
const change = proposal?.terms?.change as { marketId: string };
|
||||
|
||||
const filteredProposals =
|
||||
// @ts-ignore rest data is not typed
|
||||
previouslyEnactedMarketProposalsRestData.connection.edges.filter(
|
||||
// @ts-ignore rest data is not typed
|
||||
({ node }) =>
|
||||
node?.proposal?.terms?.updateMarket?.marketId === change.marketId
|
||||
);
|
||||
|
||||
const sortedProposals = filteredProposals.sort(
|
||||
// @ts-ignore rest data is not typed
|
||||
(a, b) =>
|
||||
new Date(a?.node?.terms?.enactmentTimestamp).getTime() -
|
||||
new Date(b?.node?.terms?.enactmentTimestamp).getTime()
|
||||
);
|
||||
|
||||
setMostRecentlyEnactedAssociatedMarketProposal(
|
||||
sortedProposals[sortedProposals.length - 1]
|
||||
);
|
||||
}
|
||||
}, [
|
||||
previouslyEnactedMarketProposalsRestData,
|
||||
params.proposalId,
|
||||
proposal?.terms.change.__typename,
|
||||
proposal?.terms.change,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(refetch, 2000);
|
||||
return () => clearInterval(interval);
|
||||
}, [refetch]);
|
||||
|
||||
return (
|
||||
<AsyncRenderer
|
||||
loading={
|
||||
loading ||
|
||||
marketLoading ||
|
||||
assetLoading ||
|
||||
networkParamsLoading ||
|
||||
parentMarketIdLoading ||
|
||||
parentMarketLoading ||
|
||||
(restLoading ? (restLoading as boolean) : false) ||
|
||||
(originalMarketProposalRestLoading
|
||||
? (originalMarketProposalRestLoading as boolean)
|
||||
: false) ||
|
||||
(previouslyEnactedMarketProposalsRestLoading
|
||||
? (previouslyEnactedMarketProposalsRestLoading as boolean)
|
||||
: false)
|
||||
}
|
||||
error={
|
||||
error ||
|
||||
marketError ||
|
||||
assetError ||
|
||||
networkParamsError ||
|
||||
parentMarketIdError ||
|
||||
parentMarketError ||
|
||||
restError ||
|
||||
originalMarketProposalRestError ||
|
||||
previouslyEnactedMarketProposalsRestError
|
||||
}
|
||||
loading={Boolean(loading || restLoading)}
|
||||
error={error || restError}
|
||||
data={{
|
||||
...data,
|
||||
...networkParams,
|
||||
...(marketData ? { newMarketData: marketData } : {}),
|
||||
...(parentMarketData ? { parentMarketData } : {}),
|
||||
...(assetData ? { assetData } : {}),
|
||||
...(restData ? { restData } : {}),
|
||||
...(originalMarketProposalRestData
|
||||
? { originalMarketProposalRestData }
|
||||
: {}),
|
||||
...(previouslyEnactedMarketProposalsRestData
|
||||
? { previouslyEnactedMarketProposalsRestData }
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
{data?.proposal ? (
|
||||
<Proposal
|
||||
proposal={proposal}
|
||||
networkParams={networkParams}
|
||||
restData={restData}
|
||||
marketData={marketData}
|
||||
parentMarketData={parentMarketData}
|
||||
assetData={assetData}
|
||||
originalMarketProposalRestData={originalMarketProposalRestData}
|
||||
mostRecentlyEnactedAssociatedMarketProposal={
|
||||
mostRecentlyEnactedAssociatedMarketProposal
|
||||
}
|
||||
/>
|
||||
<Proposal proposal={data.proposal} restData={restData} />
|
||||
) : (
|
||||
<ProposalNotFound />
|
||||
)}
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
fragment NewMarketProductFields on Proposal {
|
||||
terms {
|
||||
change {
|
||||
... on NewMarket {
|
||||
instrument {
|
||||
product {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment UpdateMarketStates on Proposal {
|
||||
terms {
|
||||
change {
|
||||
... on UpdateMarketState {
|
||||
updateType
|
||||
market {
|
||||
decimalPlaces
|
||||
id
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
product {
|
||||
__typename
|
||||
... on Future {
|
||||
quoteName
|
||||
}
|
||||
... on Perpetual {
|
||||
quoteName
|
||||
}
|
||||
}
|
||||
name
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
updateType
|
||||
price
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment UpdateReferralPrograms on Proposal {
|
||||
terms {
|
||||
change {
|
||||
... on UpdateReferralProgram {
|
||||
benefitTiers {
|
||||
minimumEpochs
|
||||
minimumRunningNotionalTakerVolume
|
||||
referralDiscountFactor
|
||||
referralRewardFactor
|
||||
}
|
||||
endOfProgram: endOfProgramTimestamp
|
||||
windowLength
|
||||
stakingTiers {
|
||||
minimumStakedTokens
|
||||
referralRewardMultiplier
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment UpdateVolumeDiscountPrograms on Proposal {
|
||||
terms {
|
||||
change {
|
||||
... on UpdateVolumeDiscountProgram {
|
||||
benefitTiers {
|
||||
minimumRunningNotionalTakerVolume
|
||||
volumeDiscountFactor
|
||||
}
|
||||
endOfProgramTimestamp
|
||||
windowLength
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment ProposalFields on Proposal {
|
||||
id
|
||||
rationale {
|
||||
title
|
||||
description
|
||||
}
|
||||
reference
|
||||
state
|
||||
datetime
|
||||
rejectionReason
|
||||
party {
|
||||
id
|
||||
}
|
||||
errorDetails
|
||||
terms {
|
||||
closingDatetime
|
||||
enactmentDatetime
|
||||
change {
|
||||
... on NewMarket {
|
||||
instrument {
|
||||
name
|
||||
code
|
||||
product {
|
||||
... on FutureProduct {
|
||||
settlementAsset {
|
||||
symbol
|
||||
}
|
||||
}
|
||||
... on PerpetualProduct {
|
||||
settlementAsset {
|
||||
symbol
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on UpdateMarket {
|
||||
marketId
|
||||
}
|
||||
... on NewAsset {
|
||||
__typename
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
source {
|
||||
... on BuiltinAsset {
|
||||
maxFaucetAmountMint
|
||||
}
|
||||
... on ERC20 {
|
||||
contractAddress
|
||||
withdrawThreshold
|
||||
lifetimeLimit
|
||||
}
|
||||
}
|
||||
}
|
||||
... on UpdateNetworkParameter {
|
||||
networkParameter {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
... on UpdateAsset {
|
||||
quantum
|
||||
assetId
|
||||
source {
|
||||
... on UpdateERC20 {
|
||||
lifetimeLimit
|
||||
withdrawThreshold
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
votes {
|
||||
yes {
|
||||
totalTokens
|
||||
totalNumber
|
||||
totalEquityLikeShareWeight
|
||||
}
|
||||
no {
|
||||
totalTokens
|
||||
totalNumber
|
||||
totalEquityLikeShareWeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query Proposals(
|
||||
$includeNewMarketProductFields: Boolean!
|
||||
$includeUpdateMarketStates: Boolean!
|
||||
$includeUpdateReferralPrograms: Boolean!
|
||||
) {
|
||||
proposalsConnection {
|
||||
edges {
|
||||
node {
|
||||
...ProposalFields
|
||||
...NewMarketProductFields @include(if: $includeNewMarketProductFields)
|
||||
...UpdateMarketStates @include(if: $includeUpdateMarketStates)
|
||||
...UpdateReferralPrograms @include(if: $includeUpdateReferralPrograms)
|
||||
...UpdateVolumeDiscountPrograms
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type NewMarketProductFieldsFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', product?: { __typename: 'FutureProduct' } | { __typename: 'PerpetualProduct' } | { __typename: 'SpotProduct' } | null } } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } };
|
||||
|
||||
export type UpdateMarketStatesFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', decimalPlaces: number, id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename: 'Future', quoteName: string } | { __typename: 'Perpetual', quoteName: string } | { __typename: 'Spot' } } } } } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } };
|
||||
|
||||
export type UpdateReferralProgramsFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram', windowLength: number, endOfProgram: any, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } };
|
||||
|
||||
export type UpdateVolumeDiscountProgramsFragment = { __typename?: 'Proposal', terms: { __typename?: 'ProposalTerms', change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter' } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram', endOfProgramTimestamp: any, windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } } };
|
||||
|
||||
export type ProposalFieldsFragment = { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, product?: { __typename?: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | { __typename?: 'PerpetualProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | { __typename?: 'SpotProduct' } | null } } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } };
|
||||
|
||||
export type ProposalsQueryVariables = Types.Exact<{
|
||||
includeNewMarketProductFields: Types.Scalars['Boolean'];
|
||||
includeUpdateMarketStates: Types.Scalars['Boolean'];
|
||||
includeUpdateReferralPrograms: Types.Scalars['Boolean'];
|
||||
}>;
|
||||
|
||||
|
||||
export type ProposalsQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename: 'NewAsset', name: string, symbol: string, decimals: number, quantum: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string, withdrawThreshold: string, lifetimeLimit: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string, code: string, product?: { __typename: 'FutureProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | { __typename: 'PerpetualProduct', settlementAsset: { __typename?: 'Asset', symbol: string } } | { __typename: 'SpotProduct' } | null } } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset', quantum: string, assetId: string, source: { __typename?: 'UpdateERC20', lifetimeLimit: string, withdrawThreshold: string } } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateMarketState', updateType: Types.MarketUpdateType, price?: string | null, market: { __typename?: 'Market', decimalPlaces: number, id: string, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, product: { __typename: 'Future', quoteName: string } | { __typename: 'Perpetual', quoteName: string } | { __typename: 'Spot' } } } } } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram', windowLength: number, endOfProgram: any, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string, referralRewardFactor: string }>, stakingTiers: Array<{ __typename?: 'StakingTier', minimumStakedTokens: string, referralRewardMultiplier: string }> } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram', endOfProgramTimestamp: any, windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, totalEquityLikeShareWeight: string } } } } | null> | null } | null };
|
||||
|
||||
export const NewMarketProductFieldsFragmentDoc = gql`
|
||||
fragment NewMarketProductFields on Proposal {
|
||||
terms {
|
||||
change {
|
||||
... on NewMarket {
|
||||
instrument {
|
||||
product {
|
||||
__typename
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const UpdateMarketStatesFragmentDoc = gql`
|
||||
fragment UpdateMarketStates on Proposal {
|
||||
terms {
|
||||
change {
|
||||
... on UpdateMarketState {
|
||||
updateType
|
||||
market {
|
||||
decimalPlaces
|
||||
id
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
product {
|
||||
__typename
|
||||
... on Future {
|
||||
quoteName
|
||||
}
|
||||
... on Perpetual {
|
||||
quoteName
|
||||
}
|
||||
}
|
||||
name
|
||||
code
|
||||
}
|
||||
}
|
||||
}
|
||||
updateType
|
||||
price
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const UpdateReferralProgramsFragmentDoc = gql`
|
||||
fragment UpdateReferralPrograms on Proposal {
|
||||
terms {
|
||||
change {
|
||||
... on UpdateReferralProgram {
|
||||
benefitTiers {
|
||||
minimumEpochs
|
||||
minimumRunningNotionalTakerVolume
|
||||
referralDiscountFactor
|
||||
referralRewardFactor
|
||||
}
|
||||
endOfProgram: endOfProgramTimestamp
|
||||
windowLength
|
||||
stakingTiers {
|
||||
minimumStakedTokens
|
||||
referralRewardMultiplier
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const UpdateVolumeDiscountProgramsFragmentDoc = gql`
|
||||
fragment UpdateVolumeDiscountPrograms on Proposal {
|
||||
terms {
|
||||
change {
|
||||
... on UpdateVolumeDiscountProgram {
|
||||
benefitTiers {
|
||||
minimumRunningNotionalTakerVolume
|
||||
volumeDiscountFactor
|
||||
}
|
||||
endOfProgramTimestamp
|
||||
windowLength
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const ProposalFieldsFragmentDoc = gql`
|
||||
fragment ProposalFields on Proposal {
|
||||
id
|
||||
rationale {
|
||||
title
|
||||
description
|
||||
}
|
||||
reference
|
||||
state
|
||||
datetime
|
||||
rejectionReason
|
||||
party {
|
||||
id
|
||||
}
|
||||
errorDetails
|
||||
terms {
|
||||
closingDatetime
|
||||
enactmentDatetime
|
||||
change {
|
||||
... on NewMarket {
|
||||
instrument {
|
||||
name
|
||||
code
|
||||
product {
|
||||
... on FutureProduct {
|
||||
settlementAsset {
|
||||
symbol
|
||||
}
|
||||
}
|
||||
... on PerpetualProduct {
|
||||
settlementAsset {
|
||||
symbol
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on UpdateMarket {
|
||||
marketId
|
||||
}
|
||||
... on NewAsset {
|
||||
__typename
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
source {
|
||||
... on BuiltinAsset {
|
||||
maxFaucetAmountMint
|
||||
}
|
||||
... on ERC20 {
|
||||
contractAddress
|
||||
withdrawThreshold
|
||||
lifetimeLimit
|
||||
}
|
||||
}
|
||||
}
|
||||
... on UpdateNetworkParameter {
|
||||
networkParameter {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
... on UpdateAsset {
|
||||
quantum
|
||||
assetId
|
||||
source {
|
||||
... on UpdateERC20 {
|
||||
lifetimeLimit
|
||||
withdrawThreshold
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
votes {
|
||||
yes {
|
||||
totalTokens
|
||||
totalNumber
|
||||
totalEquityLikeShareWeight
|
||||
}
|
||||
no {
|
||||
totalTokens
|
||||
totalNumber
|
||||
totalEquityLikeShareWeight
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const ProposalsDocument = gql`
|
||||
query Proposals($includeNewMarketProductFields: Boolean!, $includeUpdateMarketStates: Boolean!, $includeUpdateReferralPrograms: Boolean!) {
|
||||
proposalsConnection {
|
||||
edges {
|
||||
node {
|
||||
...ProposalFields
|
||||
...NewMarketProductFields @include(if: $includeNewMarketProductFields)
|
||||
...UpdateMarketStates @include(if: $includeUpdateMarketStates)
|
||||
...UpdateReferralPrograms @include(if: $includeUpdateReferralPrograms)
|
||||
...UpdateVolumeDiscountPrograms
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${ProposalFieldsFragmentDoc}
|
||||
${NewMarketProductFieldsFragmentDoc}
|
||||
${UpdateMarketStatesFragmentDoc}
|
||||
${UpdateReferralProgramsFragmentDoc}
|
||||
${UpdateVolumeDiscountProgramsFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useProposalsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useProposalsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useProposalsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useProposalsQuery({
|
||||
* variables: {
|
||||
* includeNewMarketProductFields: // value for 'includeNewMarketProductFields'
|
||||
* includeUpdateMarketStates: // value for 'includeUpdateMarketStates'
|
||||
* includeUpdateReferralPrograms: // value for 'includeUpdateReferralPrograms'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useProposalsQuery(baseOptions: Apollo.QueryHookOptions<ProposalsQuery, ProposalsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ProposalsQuery, ProposalsQueryVariables>(ProposalsDocument, options);
|
||||
}
|
||||
export function useProposalsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ProposalsQuery, ProposalsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ProposalsQuery, ProposalsQueryVariables>(ProposalsDocument, options);
|
||||
}
|
||||
export type ProposalsQueryHookResult = ReturnType<typeof useProposalsQuery>;
|
||||
export type ProposalsLazyQueryHookResult = ReturnType<typeof useProposalsLazyQuery>;
|
||||
export type ProposalsQueryResult = Apollo.QueryResult<ProposalsQuery, ProposalsQueryVariables>;
|
||||
@@ -1,31 +1,27 @@
|
||||
import flow from 'lodash/flow';
|
||||
import compact from 'lodash/compact';
|
||||
import { Callout, Intent, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { SplashLoader } from '../../../components/splash-loader';
|
||||
import { ProposalsList } from '../components/proposals-list';
|
||||
import { getNodes, removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import { getNodes } from '@vegaprotocol/utils';
|
||||
import {
|
||||
ProposalState,
|
||||
ProtocolUpgradeProposalStatus,
|
||||
} from '@vegaprotocol/types';
|
||||
import { type NodeConnection, type NodeEdge } from '@vegaprotocol/utils';
|
||||
import {
|
||||
useProposalsQuery,
|
||||
type ProposalFieldsFragment,
|
||||
} from './__generated__/Proposals';
|
||||
import { useProposalsQuery } from '../__generated__/Proposals';
|
||||
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { useProtocolUpgradeProposalsQuery } from '@vegaprotocol/proposals';
|
||||
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||
import { type BatchProposal, type Proposal } from '../types';
|
||||
|
||||
export function getNotRejectedProposals(data?: ProposalFieldsFragment[]) {
|
||||
return flow([
|
||||
(data) =>
|
||||
data.filter(
|
||||
(p: ProposalFieldsFragment) => p?.state !== ProposalState.STATE_REJECTED
|
||||
),
|
||||
])(data);
|
||||
export function getNotRejectedProposals(
|
||||
data?: Array<Proposal | BatchProposal>
|
||||
) {
|
||||
if (!data) return [];
|
||||
return data.filter((p) => p.state !== ProposalState.STATE_REJECTED);
|
||||
}
|
||||
|
||||
export function getNotRejectedProtocolUpgradeProposals<
|
||||
@@ -43,17 +39,11 @@ export function getNotRejectedProtocolUpgradeProposals<
|
||||
}
|
||||
|
||||
export const ProposalsContainer = () => {
|
||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||
const { t } = useTranslation();
|
||||
const { data, loading, error } = useProposalsQuery({
|
||||
pollInterval: 5000,
|
||||
fetchPolicy: 'network-only',
|
||||
errorPolicy: 'ignore',
|
||||
variables: {
|
||||
includeNewMarketProductFields: !!featureFlags.PRODUCT_PERPETUALS,
|
||||
includeUpdateMarketStates: !!featureFlags.UPDATE_MARKET_STATE,
|
||||
includeUpdateReferralPrograms: !!featureFlags.REFERRALS,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -69,7 +59,7 @@ export const ProposalsContainer = () => {
|
||||
const proposals = useMemo(
|
||||
() =>
|
||||
getNotRejectedProposals(
|
||||
removePaginationWrapper(data?.proposalsConnection?.edges)
|
||||
compact(data?.proposalsConnection?.edges?.map((e) => e?.proposalNode))
|
||||
),
|
||||
[data]
|
||||
);
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import compact from 'lodash/compact';
|
||||
import { Callout, Intent, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { SplashLoader } from '../../../components/splash-loader';
|
||||
import { RejectedProposalsList } from '../components/proposals-list';
|
||||
import type { ProposalFieldsFragment } from '../proposals/__generated__/Proposals';
|
||||
import { useProposalsQuery } from '../proposals/__generated__/Proposals';
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import { type ProposalFieldsFragment } from '../__generated__/Proposals';
|
||||
import { useProposalsQuery } from '../__generated__/Proposals';
|
||||
import flow from 'lodash/flow';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||
import { type BatchProposal, type Proposal } from '../types';
|
||||
|
||||
const orderByDate = (arr: ProposalFieldsFragment[]) =>
|
||||
orderBy(
|
||||
@@ -22,7 +22,9 @@ const orderByDate = (arr: ProposalFieldsFragment[]) =>
|
||||
['desc', 'desc']
|
||||
);
|
||||
|
||||
export function getRejectedProposals(data?: ProposalFieldsFragment[] | null) {
|
||||
export function getRejectedProposals(
|
||||
data?: Array<Proposal | BatchProposal> | null
|
||||
) {
|
||||
return flow([
|
||||
(data) =>
|
||||
data.filter(
|
||||
@@ -33,23 +35,17 @@ export function getRejectedProposals(data?: ProposalFieldsFragment[] | null) {
|
||||
}
|
||||
|
||||
export const RejectedProposalsContainer = () => {
|
||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||
const { t } = useTranslation();
|
||||
const { data, loading, error } = useProposalsQuery({
|
||||
pollInterval: 5000,
|
||||
fetchPolicy: 'network-only',
|
||||
errorPolicy: 'ignore',
|
||||
variables: {
|
||||
includeNewMarketProductFields: !!featureFlags.PRODUCT_PERPETUALS,
|
||||
includeUpdateMarketStates: !!featureFlags.UPDATE_MARKET_STATE,
|
||||
includeUpdateReferralPrograms: !!featureFlags.REFERRALS,
|
||||
},
|
||||
});
|
||||
|
||||
const proposals = useMemo(
|
||||
() =>
|
||||
getRejectedProposals(
|
||||
removePaginationWrapper(data?.proposalsConnection?.edges)
|
||||
compact(data?.proposalsConnection?.edges?.map((e) => e?.proposalNode))
|
||||
),
|
||||
[data]
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import isArray from 'lodash/isArray';
|
||||
import mergeWith from 'lodash/mergeWith';
|
||||
|
||||
import { type PartialDeep } from 'type-fest';
|
||||
import { type ProposalQuery } from '../proposal/__generated__/Proposal';
|
||||
import { type ProposalQuery } from '../__generated__/Proposals';
|
||||
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { type Proposal } from '../types';
|
||||
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import type { ProposalQuery } from './proposal/__generated__/Proposal';
|
||||
import { type ProposalNode } from '@vegaprotocol/types';
|
||||
import {
|
||||
type BatchProposalFieldsFragment,
|
||||
type ProposalFieldsFragment,
|
||||
} from './__generated__/Proposals';
|
||||
|
||||
/**
|
||||
* The default Proposal type needs extracting from the ProposalNode union type
|
||||
* as lots of fields on the original type don't exist on BatchProposal. Eventually
|
||||
* we will support BatchProposal but for now we don't
|
||||
*/
|
||||
export type Proposal = Extract<
|
||||
ProposalQuery['proposal'],
|
||||
{ __typename?: 'Proposal' }
|
||||
export type Proposal = ProposalFieldsFragment;
|
||||
export type BatchProposal = BatchProposalFieldsFragment;
|
||||
|
||||
export type ProposalChangeType = NonNullable<
|
||||
Proposal['terms']['change']['__typename']
|
||||
>;
|
||||
|
||||
export type ProposalType = NonNullable<ProposalNode['__typename']>;
|
||||
|
||||
@@ -22,7 +22,7 @@ NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
NX_STOP_ORDERS=true
|
||||
NX_ISOLATED_MARGIN=true
|
||||
NX_ISOLATED_MARGIN=false
|
||||
NX_ICEBERG_ORDERS=true
|
||||
NX_METAMASK_SNAPS=true
|
||||
NX_REFERRALS=true
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CompetitionsHeader } from '../../components/competitions/competitions-h
|
||||
import { Intent, Loader, TradingButton } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
import { useGameCards } from '../../lib/hooks/use-game-cards';
|
||||
import { useCurrentEpochInfoQuery } from '../../lib/hooks/__generated__/Epoch';
|
||||
import { useEpochInfoQuery } from '../../lib/hooks/__generated__/Epoch';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Links } from '../../lib/links';
|
||||
import {
|
||||
@@ -25,7 +25,7 @@ export const CompetitionsHome = () => {
|
||||
|
||||
usePageTitle(t('Competitions'));
|
||||
|
||||
const { data: epochData } = useCurrentEpochInfoQuery();
|
||||
const { data: epochData } = useEpochInfoQuery();
|
||||
const currentEpoch = Number(epochData?.epoch.id);
|
||||
|
||||
const { data: gamesData, loading: gamesLoading } = useGameCards({
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
import { useState, type ButtonHTMLAttributes } from 'react';
|
||||
import { useState, type ButtonHTMLAttributes, useRef } from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { Splash, truncateMiddle, Loader } from '@vegaprotocol/ui-toolkit';
|
||||
import { DispatchMetricLabels, type DispatchMetric } from '@vegaprotocol/types';
|
||||
import {
|
||||
Splash,
|
||||
truncateMiddle,
|
||||
Loader,
|
||||
Dialog,
|
||||
Button,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
TransferStatus,
|
||||
type Asset,
|
||||
type RecurringTransfer,
|
||||
} from '@vegaprotocol/types';
|
||||
import classNames from 'classnames';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { Table } from '../../components/table';
|
||||
import { formatNumber, getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
import {
|
||||
addDecimalsFormatNumberQuantum,
|
||||
formatNumber,
|
||||
getDateTimeFormat,
|
||||
} from '@vegaprotocol/utils';
|
||||
import {
|
||||
useTeam,
|
||||
type TeamStats as ITeamStats,
|
||||
@@ -27,6 +43,19 @@ import {
|
||||
useGames,
|
||||
areTeamGames,
|
||||
} from '../../lib/hooks/use-games';
|
||||
import { useEpochInfoQuery } from '../../lib/hooks/__generated__/Epoch';
|
||||
import {
|
||||
type EnrichedTransfer,
|
||||
isScopedToTeams,
|
||||
useGameCards,
|
||||
} from '../../lib/hooks/use-game-cards';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
import {
|
||||
ActiveRewardCard,
|
||||
DispatchMetricInfo,
|
||||
} from '../../components/rewards-container/active-rewards';
|
||||
import { type MarketMap, useMarketsMapProvider } from '@vegaprotocol/markets';
|
||||
import format from 'date-fns/format';
|
||||
|
||||
export const CompetitionsTeam = () => {
|
||||
const t = useT();
|
||||
@@ -49,6 +78,14 @@ const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => {
|
||||
|
||||
const { data: games, loading: gamesLoading } = useGames(teamId);
|
||||
|
||||
const { data: epochData, loading: epochLoading } = useEpochInfoQuery();
|
||||
const { data: transfersData, loading: transfersLoading } = useGameCards({
|
||||
currentEpoch: Number(epochData?.epoch.id),
|
||||
onlyActive: false,
|
||||
});
|
||||
|
||||
const { data: markets } = useMarketsMapProvider();
|
||||
|
||||
// only show spinner on first load so when users join teams its smoother
|
||||
if (!data && loading) {
|
||||
return (
|
||||
@@ -74,6 +111,9 @@ const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => {
|
||||
members={members}
|
||||
games={areTeamGames(games) ? games : undefined}
|
||||
gamesLoading={gamesLoading}
|
||||
transfers={transfersData}
|
||||
transfersLoading={epochLoading || transfersLoading}
|
||||
allMarkets={markets || undefined}
|
||||
refetch={refetch}
|
||||
/>
|
||||
);
|
||||
@@ -86,6 +126,9 @@ const TeamPage = ({
|
||||
members,
|
||||
games,
|
||||
gamesLoading,
|
||||
transfers,
|
||||
transfersLoading,
|
||||
allMarkets,
|
||||
refetch,
|
||||
}: {
|
||||
team: TeamType;
|
||||
@@ -94,6 +137,9 @@ const TeamPage = ({
|
||||
members?: Member[];
|
||||
games?: TeamGame[];
|
||||
gamesLoading?: boolean;
|
||||
transfers?: EnrichedTransfer[];
|
||||
transfersLoading?: boolean;
|
||||
allMarkets?: MarketMap;
|
||||
refetch: () => void;
|
||||
}) => {
|
||||
const t = useT();
|
||||
@@ -141,7 +187,13 @@ const TeamPage = ({
|
||||
</ToggleButton>
|
||||
</div>
|
||||
{showGames ? (
|
||||
<Games games={games} gamesLoading={gamesLoading} />
|
||||
<Games
|
||||
games={games}
|
||||
gamesLoading={gamesLoading}
|
||||
transfers={transfers}
|
||||
transfersLoading={transfersLoading}
|
||||
allMarkets={allMarkets}
|
||||
/>
|
||||
) : (
|
||||
<Members members={members} />
|
||||
)}
|
||||
@@ -153,9 +205,15 @@ const TeamPage = ({
|
||||
const Games = ({
|
||||
games,
|
||||
gamesLoading,
|
||||
transfers,
|
||||
transfersLoading,
|
||||
allMarkets,
|
||||
}: {
|
||||
games?: TeamGame[];
|
||||
gamesLoading?: boolean;
|
||||
transfers?: EnrichedTransfer[];
|
||||
transfersLoading?: boolean;
|
||||
allMarkets?: MarketMap;
|
||||
}) => {
|
||||
const t = useT();
|
||||
|
||||
@@ -171,40 +229,106 @@ const Games = ({
|
||||
return <p>{t('No game results available')}</p>;
|
||||
}
|
||||
|
||||
const dependable = (value: string | JSX.Element) => {
|
||||
if (transfersLoading) return <Loader size="small" />;
|
||||
return value;
|
||||
};
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={[
|
||||
{ name: 'rank', displayName: t('Rank') },
|
||||
{
|
||||
name: 'epoch',
|
||||
displayName: t('Epoch'),
|
||||
headerClassName: 'hidden md:table-cell',
|
||||
className: 'hidden md:table-cell',
|
||||
},
|
||||
{
|
||||
name: 'endtime',
|
||||
displayName: t('End time'),
|
||||
},
|
||||
{ name: 'type', displayName: t('Type') },
|
||||
{ name: 'amount', displayName: t('Amount earned') },
|
||||
{
|
||||
name: 'asset',
|
||||
displayName: t('Reward asset'),
|
||||
},
|
||||
{ name: 'daily', displayName: t('Daily reward amount') },
|
||||
{ name: 'rank', displayName: t('Rank') },
|
||||
{ name: 'amount', displayName: t('Amount earned this epoch') },
|
||||
{ name: 'total', displayName: t('Cumulative amount earned') },
|
||||
{
|
||||
name: 'participatingTeams',
|
||||
displayName: t('No. of participating teams'),
|
||||
headerClassName: 'hidden md:table-cell',
|
||||
className: 'hidden md:table-cell',
|
||||
},
|
||||
{
|
||||
name: 'participatingMembers',
|
||||
displayName: t('No. of participating members'),
|
||||
headerClassName: 'hidden md:table-cell',
|
||||
className: 'hidden md:table-cell',
|
||||
},
|
||||
]}
|
||||
data={games.map((game) => ({
|
||||
rank: game.team.rank,
|
||||
epoch: game.epoch,
|
||||
type: DispatchMetricLabels[game.team.rewardMetric as DispatchMetric],
|
||||
amount: formatNumber(game.team.totalRewardsEarned),
|
||||
participatingTeams: game.entities.length,
|
||||
participatingMembers: game.numberOfParticipants,
|
||||
}))}
|
||||
noCollapse={true}
|
||||
].map((c) => ({ ...c, headerClassName: 'text-left' }))}
|
||||
data={games.map((game) => {
|
||||
let transfer = transfers?.find((t) => {
|
||||
if (!isScopedToTeams(t)) return false;
|
||||
|
||||
const idMatch = t.transfer.gameId === game.id;
|
||||
const metricMatch =
|
||||
t.transfer.kind.dispatchStrategy?.dispatchMetric ===
|
||||
game.team.rewardMetric;
|
||||
|
||||
const start = t.transfer.kind.startEpoch <= game.epoch;
|
||||
const end = t.transfer.kind.endEpoch
|
||||
? t.transfer.kind.endEpoch >= game.epoch
|
||||
: true;
|
||||
|
||||
const rejected = t.transfer.status === TransferStatus.STATUS_REJECTED;
|
||||
|
||||
return idMatch && metricMatch && start && end && !rejected;
|
||||
});
|
||||
if (!transfer || !isScopedToTeams(transfer)) transfer = undefined;
|
||||
const asset = transfer?.transfer.asset;
|
||||
|
||||
const dailyAmount =
|
||||
asset && transfer
|
||||
? addDecimalsFormatNumberQuantum(
|
||||
transfer.transfer.amount,
|
||||
asset.decimals,
|
||||
asset.quantum
|
||||
)
|
||||
: '-';
|
||||
|
||||
const earnedAmount = asset
|
||||
? addDecimalsFormatNumberQuantum(
|
||||
game.team.rewardEarned,
|
||||
asset.decimals,
|
||||
asset.quantum
|
||||
)
|
||||
: '-';
|
||||
|
||||
const totalAmount = asset
|
||||
? addDecimalsFormatNumberQuantum(
|
||||
game.team.totalRewardsEarned,
|
||||
asset.decimals,
|
||||
asset.quantum
|
||||
)
|
||||
: '-';
|
||||
|
||||
const assetSymbol = asset ? <RewardAssetCell asset={asset} /> : '-';
|
||||
|
||||
return {
|
||||
id: game.id,
|
||||
amount: dependable(earnedAmount),
|
||||
asset: dependable(assetSymbol),
|
||||
daily: dependable(dailyAmount),
|
||||
endtime: <EndTimeCell epoch={game.epoch} />,
|
||||
epoch: game.epoch,
|
||||
participatingMembers: game.numberOfParticipants,
|
||||
participatingTeams: game.entities.length,
|
||||
rank: game.team.rank,
|
||||
total: totalAmount,
|
||||
// type: DispatchMetricLabels[game.team.rewardMetric as DispatchMetric],
|
||||
type: dependable(
|
||||
<GameTypeCell transfer={transfer} allMarkets={allMarkets} />
|
||||
),
|
||||
};
|
||||
})}
|
||||
noCollapse={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -286,3 +410,126 @@ const ToggleButton = ({
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const EndTimeCell = ({ epoch }: { epoch?: number }) => {
|
||||
const { data, loading } = useEpochInfoQuery({
|
||||
variables: {
|
||||
epochId: epoch ? epoch.toString() : undefined,
|
||||
},
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
if (loading) return <Loader size="small" />;
|
||||
if (data) {
|
||||
return format(
|
||||
new Date(data.epoch.timestamps.expiry),
|
||||
'yyyy/MM/dd hh:mm:ss'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const RewardAssetCell = ({ asset }: { asset: Asset }) => {
|
||||
const open = useAssetDetailsDialogStore((state) => state.open);
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
open(asset.id, ref.current);
|
||||
}}
|
||||
className="border-b border-dashed border-vega-clight-200 dark:border-vega-cdark-200 text-left text-nowrap whitespace-nowrap"
|
||||
>
|
||||
{asset.symbol}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
const GameTypeCell = ({
|
||||
transfer,
|
||||
allMarkets,
|
||||
}: {
|
||||
transfer?: EnrichedTransfer;
|
||||
allMarkets?: MarketMap;
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
if (!transfer) return '-';
|
||||
return (
|
||||
<>
|
||||
<ActiveRewardCardDialog
|
||||
open={open}
|
||||
onChange={(isOpen) => setOpen(isOpen)}
|
||||
trigger={ref.current}
|
||||
transfer={transfer}
|
||||
allMarkets={allMarkets}
|
||||
/>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setOpen(true);
|
||||
}}
|
||||
ref={ref}
|
||||
className="border-b border-dashed border-vega-clight-200 dark:border-vega-cdark-200 text-left md:truncate md:max-w-[25vw]"
|
||||
>
|
||||
<DispatchMetricInfo transferNode={transfer} allMarkets={allMarkets} />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const ActiveRewardCardDialog = ({
|
||||
open,
|
||||
onChange,
|
||||
trigger,
|
||||
transfer,
|
||||
allMarkets,
|
||||
}: {
|
||||
open: boolean;
|
||||
onChange: (isOpen: boolean) => void;
|
||||
trigger?: HTMLElement | null;
|
||||
transfer: EnrichedTransfer;
|
||||
allMarkets?: MarketMap;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const { data } = useEpochInfoQuery();
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
title={t('Game details')}
|
||||
onChange={(isOpen) => onChange(isOpen)}
|
||||
icon={<VegaIcon name={VegaIconNames.INFO} />}
|
||||
onCloseAutoFocus={(e) => {
|
||||
/**
|
||||
* This mimics radix's default behaviour that focuses the dialog's
|
||||
* trigger after closing itself
|
||||
*/
|
||||
if (trigger) {
|
||||
e.preventDefault();
|
||||
trigger.focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="py-5 max-w-[454px]">
|
||||
<ActiveRewardCard
|
||||
transferNode={transfer}
|
||||
currentEpoch={Number(data?.epoch.id)}
|
||||
kind={transfer.transfer.kind as RecurringTransfer}
|
||||
allMarkets={allMarkets}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-1/4">
|
||||
<Button
|
||||
data-testid="close-asset-details-dialog"
|
||||
fill={true}
|
||||
size="sm"
|
||||
onClick={() => onChange(false)}
|
||||
>
|
||||
{t('Close')}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -56,7 +56,6 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
|
||||
<Last24hPriceChange
|
||||
marketId={market.id}
|
||||
decimalPlaces={market.decimalPlaces}
|
||||
fallback={<span>-</span>}
|
||||
/>
|
||||
</HeaderStat>
|
||||
<HeaderStat heading={t('Volume (24h)')} testId="market-volume">
|
||||
|
||||
@@ -72,7 +72,15 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
||||
<div className="h-full flex flex-col lg:grid grid-rows-[min-content_min-content_1fr_min-content]">
|
||||
<div className="flex flex-col w-full overflow-hidden">
|
||||
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
|
||||
{['chart', 'orderbook', 'trades', 'liquidity', 'fundingPayments']
|
||||
{[
|
||||
'chart',
|
||||
'orderbook',
|
||||
'depth',
|
||||
'trades',
|
||||
'liquidity',
|
||||
'fundingPayments',
|
||||
'funding',
|
||||
]
|
||||
// filter to control available views for the current market
|
||||
// e.g. only perpetuals should get the funding views
|
||||
.filter((_key) => {
|
||||
@@ -163,9 +171,12 @@ const ViewButton = ({
|
||||
onClick: () => void;
|
||||
}) => {
|
||||
const label = useViewLabel(view);
|
||||
const className = classNames('py-2 px-4 min-w-[100px] capitalize text-sm', {
|
||||
'bg-vega-clight-500 dark:bg-vega-cdark-500': isActive,
|
||||
});
|
||||
const className = classNames(
|
||||
'py-2 px-4 capitalize text-sm whitespace-nowrap',
|
||||
{
|
||||
'bg-vega-clight-500 dark:bg-vega-cdark-500': isActive,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<button data-testid={view} onClick={onClick} className={className}>
|
||||
@@ -181,8 +192,8 @@ const useViewLabel = (view: TradingView) => {
|
||||
chart: t('Chart'),
|
||||
depth: t('Depth'),
|
||||
liquidity: t('Liquidity'),
|
||||
funding: t('Funding'),
|
||||
fundingPayments: t('Funding'),
|
||||
funding: t('Funding history'),
|
||||
fundingPayments: t('Funding payments'),
|
||||
orderbook: t('Orderbook'),
|
||||
trades: t('Trades'),
|
||||
positions: t('Positions'),
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useEffect } from 'react';
|
||||
import { useT } from '../../../lib/use-t';
|
||||
import { matchPath, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { Routes } from '../../../lib/links';
|
||||
import { useCurrentEpochInfoQuery } from '../../../lib/hooks/__generated__/Epoch';
|
||||
import { useEpochInfoQuery } from '../../../lib/hooks/__generated__/Epoch';
|
||||
|
||||
const REFETCH_INTERVAL = 60 * 60 * 1000; // 1h
|
||||
const NON_ELIGIBLE_REFERRAL_SET_TOAST_ID = 'non-eligible-referral-set';
|
||||
@@ -23,7 +23,7 @@ const useNonEligibleReferralSet = () => {
|
||||
data: epochData,
|
||||
loading: epochLoading,
|
||||
refetch: epochRefetch,
|
||||
} = useCurrentEpochInfoQuery();
|
||||
} = useEpochInfoQuery();
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
} from './hooks/use-referral';
|
||||
import { ApplyCodeForm, ApplyCodeFormContainer } from './apply-code-form';
|
||||
import { useReferralProgram } from './hooks/use-referral-program';
|
||||
import { useCurrentEpochInfoQuery } from '../../lib/hooks/__generated__/Epoch';
|
||||
import { useEpochInfoQuery } from '../../lib/hooks/__generated__/Epoch';
|
||||
import { QUSDTooltip } from './qusd-tooltip';
|
||||
import { CodeTile, StatTile, Tile } from './tile';
|
||||
import { areTeamGames, useGames } from '../../lib/hooks/use-games';
|
||||
@@ -95,7 +95,7 @@ export const useStats = ({
|
||||
program: ReturnType<typeof useReferralProgram>;
|
||||
}) => {
|
||||
const { benefitTiers } = program;
|
||||
const { data: epochData } = useCurrentEpochInfoQuery({
|
||||
const { data: epochData } = useEpochInfoQuery({
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
const { data: statsData } = useReferralSetStatsQuery({
|
||||
|
||||
@@ -1,49 +1,19 @@
|
||||
import { type TransferNode } from '@vegaprotocol/types';
|
||||
import {
|
||||
ActiveRewardCard,
|
||||
isActiveReward,
|
||||
} from '../rewards-container/active-rewards';
|
||||
import { ActiveRewardCard } from '../rewards-container/active-rewards';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { useAssetsMapProvider } from '@vegaprotocol/assets';
|
||||
import { type EnrichedTransfer } from '../../lib/hooks/use-game-cards';
|
||||
import { useMarketsMapProvider } from '@vegaprotocol/markets';
|
||||
|
||||
export const GamesContainer = ({
|
||||
data,
|
||||
currentEpoch,
|
||||
}: {
|
||||
data: TransferNode[];
|
||||
data: EnrichedTransfer[];
|
||||
currentEpoch: number;
|
||||
}) => {
|
||||
const t = useT();
|
||||
// Re-load markets and assets in the games container to ensure that the
|
||||
// the cards are updated (not grayed out) when the user navigates to the games page
|
||||
const { data: assets } = useAssetsMapProvider();
|
||||
const { data: markets } = useMarketsMapProvider();
|
||||
|
||||
const enrichedTransfers = data
|
||||
.filter((node) => isActiveReward(node, currentEpoch))
|
||||
.map((node) => {
|
||||
if (node.transfer.kind.__typename !== 'RecurringTransfer') {
|
||||
return node;
|
||||
}
|
||||
|
||||
const asset =
|
||||
assets &&
|
||||
assets[
|
||||
node.transfer.kind.dispatchStrategy?.dispatchMetricAssetId || ''
|
||||
];
|
||||
|
||||
const marketsInScope =
|
||||
node.transfer.kind.dispatchStrategy?.marketIdsInScope?.map(
|
||||
(id) => markets && markets[id]
|
||||
);
|
||||
|
||||
return { ...node, asset, markets: marketsInScope };
|
||||
});
|
||||
|
||||
if (!enrichedTransfers || !enrichedTransfers.length) return null;
|
||||
|
||||
if (!enrichedTransfers || enrichedTransfers.length === 0) {
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<p className="mb-6 text-muted">
|
||||
{t('There are currently no games available.')}
|
||||
@@ -53,7 +23,7 @@ export const GamesContainer = ({
|
||||
|
||||
return (
|
||||
<div className="mb-12 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{enrichedTransfers.map((game, i) => {
|
||||
{data.map((game, i) => {
|
||||
// TODO: Remove `kind` prop from ActiveRewardCard
|
||||
const { transfer } = game;
|
||||
if (
|
||||
|
||||
@@ -29,28 +29,35 @@ export const MobileMarketHeader = () => {
|
||||
if (!marketId) return null;
|
||||
|
||||
return (
|
||||
<div className="pl-3 pr-2 flex justify-between gap-2 h-10 bg-vega-clight-700 dark:bg-vega-cdark-700">
|
||||
<div className="pl-3 pr-2 grid grid-cols-2 h-10 bg-vega-clight-700 dark:bg-vega-cdark-700">
|
||||
<FullScreenPopover
|
||||
open={openMarket}
|
||||
onOpenChange={(x) => {
|
||||
setOpenMarket(x);
|
||||
}}
|
||||
trigger={
|
||||
<h1 className="flex gap-1 sm:gap-2 md:gap-4 items-center text-base leading-3 md:text-lg whitespace-nowrap">
|
||||
{data
|
||||
? data.tradableInstrument.instrument.code
|
||||
: t('Select market')}
|
||||
<span
|
||||
<button
|
||||
data-testid="popover-trigger"
|
||||
className="min-w-0 flex gap-1 items-center"
|
||||
>
|
||||
<h1 className="whitespace-nowrap overflow-hidden text-ellipsis items-center">
|
||||
<span className="">
|
||||
{data
|
||||
? data.tradableInstrument.instrument.code
|
||||
: t('Select market')}
|
||||
</span>
|
||||
</h1>
|
||||
<VegaIcon
|
||||
name={VegaIconNames.CHEVRON_DOWN}
|
||||
size={16}
|
||||
className={classNames(
|
||||
'transition-transform ease-in-out duration-300 flex',
|
||||
'origin-center transition-transform ease-in-out duration-300 flex',
|
||||
{
|
||||
'rotate-180': openMarket,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={16} />
|
||||
</span>
|
||||
</h1>
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<MarketSelector
|
||||
@@ -64,34 +71,40 @@ export const MobileMarketHeader = () => {
|
||||
setOpenPrice(x);
|
||||
}}
|
||||
trigger={
|
||||
<span className="flex gap-2 items-end md:text-md whitespace-nowrap leading-3">
|
||||
<button
|
||||
data-testid="popover-trigger"
|
||||
className="min-w-0 flex gap-2 items-center justify-end"
|
||||
>
|
||||
{data && (
|
||||
<>
|
||||
<span className="text-xs">
|
||||
<Last24hPriceChange
|
||||
marketId={data.id}
|
||||
decimalPlaces={data.decimalPlaces}
|
||||
/>
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<MarketMarkPrice
|
||||
marketId={data.id}
|
||||
decimalPlaces={data.decimalPlaces}
|
||||
/>
|
||||
<VegaIcon
|
||||
name={VegaIconNames.CHEVRON_DOWN}
|
||||
size={16}
|
||||
className={classNames(
|
||||
'transition-transform ease-in-out duration-300',
|
||||
{
|
||||
'rotate-180': openPrice,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<span className="min-w-0 flex flex-col items-end gap-0">
|
||||
<span className="text-sm">
|
||||
<MarketMarkPrice
|
||||
marketId={data.id}
|
||||
decimalPlaces={data.decimalPlaces}
|
||||
/>
|
||||
</span>
|
||||
<span className="text-xs">
|
||||
<Last24hPriceChange
|
||||
marketId={data.id}
|
||||
decimalPlaces={data.decimalPlaces}
|
||||
fallback={<span />} // dont render anything so price is vertically centered
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
<VegaIcon
|
||||
name={VegaIconNames.CHEVRON_DOWN}
|
||||
size={16}
|
||||
className={classNames(
|
||||
'min-w-0 transition-transform ease-in-out duration-300',
|
||||
{
|
||||
'rotate-180': openPrice,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
>
|
||||
{data && (
|
||||
@@ -104,11 +117,11 @@ export const MobileMarketHeader = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export interface PopoverProps extends PopoverPrimitive.PopoverProps {
|
||||
interface PopoverProps extends PopoverPrimitive.PopoverProps {
|
||||
trigger: React.ReactNode | string;
|
||||
}
|
||||
|
||||
export const FullScreenPopover = ({
|
||||
const FullScreenPopover = ({
|
||||
trigger,
|
||||
children,
|
||||
open,
|
||||
@@ -116,7 +129,7 @@ export const FullScreenPopover = ({
|
||||
}: PopoverProps) => {
|
||||
return (
|
||||
<PopoverPrimitive.Root open={open} onOpenChange={onOpenChange}>
|
||||
<PopoverPrimitive.Trigger data-testid="popover-trigger">
|
||||
<PopoverPrimitive.Trigger asChild={true}>
|
||||
{trigger}
|
||||
</PopoverPrimitive.Trigger>
|
||||
<PopoverPrimitive.Portal>
|
||||
|
||||
@@ -73,6 +73,7 @@ query ActiveRewards(
|
||||
reference
|
||||
status
|
||||
timestamp
|
||||
gameId
|
||||
kind {
|
||||
... on RecurringTransfer {
|
||||
startEpoch
|
||||
|
||||
@@ -18,7 +18,7 @@ export type ActiveRewardsQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ActiveRewardsQuery = { __typename?: 'Query', transfersConnection?: { __typename?: 'TransferConnection', edges?: Array<{ __typename?: 'TransferEdge', node: { __typename?: 'TransferNode', transfer: { __typename?: 'Transfer', amount: string, id: string, from: string, fromAccountType: Types.AccountType, to: string, toAccountType: Types.AccountType, reference?: string | null, status: Types.TransferStatus, timestamp: any, reason?: string | null, asset?: { __typename?: 'Asset', id: string, symbol: string, decimals: number, name: string, quantum: string, status: Types.AssetStatus } | null, kind: { __typename?: 'OneOffGovernanceTransfer' } | { __typename?: 'OneOffTransfer' } | { __typename?: 'RecurringGovernanceTransfer' } | { __typename?: 'RecurringTransfer', startEpoch: number, endEpoch?: number | null, dispatchStrategy?: { __typename?: 'DispatchStrategy', dispatchMetric: Types.DispatchMetric, dispatchMetricAssetId: string, marketIdsInScope?: Array<string> | null, entityScope: Types.EntityScope, individualScope?: Types.IndividualScope | null, teamScope?: Array<string | null> | null, nTopPerformers?: string | null, stakingRequirement: string, notionalTimeWeightedAveragePositionRequirement: string, windowLength: number, lockPeriod: number, distributionStrategy: Types.DistributionStrategy, rankTable?: Array<{ __typename?: 'RankTable', startRank: number, shareRatio: number } | null> | null } | null } }, fees?: Array<{ __typename?: 'TransferFee', transferId: string, amount: string, epoch: number } | null> | null } } | null> | null } | null };
|
||||
export type ActiveRewardsQuery = { __typename?: 'Query', transfersConnection?: { __typename?: 'TransferConnection', edges?: Array<{ __typename?: 'TransferEdge', node: { __typename?: 'TransferNode', transfer: { __typename?: 'Transfer', amount: string, id: string, from: string, fromAccountType: Types.AccountType, to: string, toAccountType: Types.AccountType, reference?: string | null, status: Types.TransferStatus, timestamp: any, gameId?: string | null, reason?: string | null, asset?: { __typename?: 'Asset', id: string, symbol: string, decimals: number, name: string, quantum: string, status: Types.AssetStatus } | null, kind: { __typename?: 'OneOffGovernanceTransfer' } | { __typename?: 'OneOffTransfer' } | { __typename?: 'RecurringGovernanceTransfer' } | { __typename?: 'RecurringTransfer', startEpoch: number, endEpoch?: number | null, dispatchStrategy?: { __typename?: 'DispatchStrategy', dispatchMetric: Types.DispatchMetric, dispatchMetricAssetId: string, marketIdsInScope?: Array<string> | null, entityScope: Types.EntityScope, individualScope?: Types.IndividualScope | null, teamScope?: Array<string | null> | null, nTopPerformers?: string | null, stakingRequirement: string, notionalTimeWeightedAveragePositionRequirement: string, windowLength: number, lockPeriod: number, distributionStrategy: Types.DistributionStrategy, rankTable?: Array<{ __typename?: 'RankTable', startRank: number, shareRatio: number } | null> | null } | null } }, fees?: Array<{ __typename?: 'TransferFee', transferId: string, amount: string, epoch: number } | null> | null } } | null> | null } | null };
|
||||
|
||||
export type RewardsHistoryQueryVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
@@ -144,6 +144,7 @@ export const ActiveRewardsDocument = gql`
|
||||
reference
|
||||
status
|
||||
timestamp
|
||||
gameId
|
||||
kind {
|
||||
... on RecurringTransfer {
|
||||
startEpoch
|
||||
|
||||
@@ -251,12 +251,7 @@ const StatusIndicator = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const ActiveRewardCard = ({
|
||||
transferNode,
|
||||
currentEpoch,
|
||||
kind,
|
||||
allMarkets,
|
||||
}: {
|
||||
type ActiveRewardCardProps = {
|
||||
transferNode: TransferNode & {
|
||||
asset?: AssetFieldsFragment | null;
|
||||
markets?: (MarketFieldsFragment | null)[];
|
||||
@@ -264,7 +259,13 @@ export const ActiveRewardCard = ({
|
||||
currentEpoch: number;
|
||||
kind: RecurringTransfer;
|
||||
allMarkets?: Record<string, MarketFieldsFragment | null>;
|
||||
}) => {
|
||||
};
|
||||
export const ActiveRewardCard = ({
|
||||
transferNode,
|
||||
currentEpoch,
|
||||
kind,
|
||||
allMarkets,
|
||||
}: ActiveRewardCardProps) => {
|
||||
const t = useT();
|
||||
|
||||
const { transfer } = transferNode;
|
||||
@@ -485,6 +486,62 @@ export const ActiveRewardCard = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const DispatchMetricInfo = ({
|
||||
transferNode,
|
||||
allMarkets,
|
||||
}: {
|
||||
transferNode: ActiveRewardCardProps['transferNode'];
|
||||
allMarkets?: ActiveRewardCardProps['allMarkets'];
|
||||
}) => {
|
||||
const dispatchStrategy =
|
||||
transferNode.transfer.kind.__typename === 'RecurringTransfer'
|
||||
? transferNode.transfer.kind.dispatchStrategy
|
||||
: null;
|
||||
|
||||
const dispatchAsset = transferNode.transfer.asset;
|
||||
|
||||
const marketIdsInScope = dispatchStrategy?.marketIdsInScope;
|
||||
const firstMarketData = transferNode.markets?.[0];
|
||||
const specificMarkets = useMemo(() => {
|
||||
if (
|
||||
!firstMarketData ||
|
||||
!marketIdsInScope ||
|
||||
marketIdsInScope.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (marketIdsInScope.length > 1) {
|
||||
const marketNames =
|
||||
allMarkets &&
|
||||
marketIdsInScope
|
||||
.map((id) => allMarkets[id]?.tradableInstrument?.instrument?.name)
|
||||
.join(', ');
|
||||
|
||||
return (
|
||||
<Tooltip description={marketNames}>
|
||||
<span>Specific markets</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
const name = firstMarketData?.tradableInstrument?.instrument?.name;
|
||||
if (name) {
|
||||
return <span>{name}</span>;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [firstMarketData, marketIdsInScope, allMarkets]);
|
||||
|
||||
if (!dispatchStrategy) return null;
|
||||
|
||||
return (
|
||||
<span data-testid="dispatch-metric-info">
|
||||
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]} •{' '}
|
||||
<span>{specificMarkets || dispatchAsset?.name}</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const RewardRequirements = ({
|
||||
dispatchStrategy,
|
||||
assetDecimalPlaces = 0,
|
||||
|
||||
@@ -93,6 +93,9 @@ export const Table = forwardRef<
|
||||
key={i}
|
||||
className={classNames(dataEntry['className'] as string, {
|
||||
'max-md:flex flex-col w-full': !noCollapse,
|
||||
// collapsed (mobile) row divider
|
||||
'first:border-t-0 max-md:border-t border-vega-clight-500 dark:border-vega-cdark-500 first:mt-0 mt-1':
|
||||
!noCollapse,
|
||||
})}
|
||||
onClick={() => {
|
||||
if (onRowClick) {
|
||||
|
||||
@@ -9,6 +9,16 @@ leverage_input = "#leverage-input"
|
||||
tab_positions = "tab-positions"
|
||||
margin_row = '[col-id="margin"]'
|
||||
|
||||
current_margin = "deal-ticket-fee-current-margin"
|
||||
available_collateral = "deal-ticket-fee-available-collateral"
|
||||
additional_margin_required = "deal-ticket-fee-additional-margin-required"
|
||||
liquidation_estimate = "deal-ticket-fee-liquidation-estimate"
|
||||
dialog_content = "dialog-content"
|
||||
cross_margin = "cross-margin"
|
||||
isolated_margin = "isolated-margin"
|
||||
confirm_cross_margin_mode = "confirm-cross-margin-mode"
|
||||
confirm_isolated_margin_mode = "confirm-isolated-margin-mode"
|
||||
|
||||
|
||||
def create_position(vega: VegaServiceNull, market_id):
|
||||
submit_order(vega, "Key 1", market_id, "SIDE_SELL", 100, 110)
|
||||
@@ -17,6 +27,7 @@ def create_position(vega: VegaServiceNull, market_id):
|
||||
vega.wait_for_total_catchup
|
||||
|
||||
|
||||
@pytest.mark.skip("tempory disabling of isolated margin")
|
||||
@pytest.mark.usefixtures("auth", "risk_accepted")
|
||||
def test_switch_cross_isolated_margin(
|
||||
continuous_market, vega: VegaServiceNull, page: Page):
|
||||
@@ -29,10 +40,9 @@ def test_switch_cross_isolated_margin(
|
||||
expect(page.get_by_test_id(tooltip_content).nth(0)).to_have_text(
|
||||
"Liquidation: 582.81328Margin: 874.21992General account: 998,084.95183"
|
||||
)
|
||||
page.get_by_role("button", name="Isolated 10x").click()
|
||||
page.locator(leverage_input).clear()
|
||||
page.locator(leverage_input).type("1")
|
||||
page.get_by_role("button", name="Confirm").click()
|
||||
page.get_by_test_id(isolated_margin).click()
|
||||
page.locator(leverage_input).fill("1")
|
||||
page.get_by_test_id(confirm_isolated_margin_mode).click()
|
||||
wait_for_toast_confirmation(page)
|
||||
next_epoch(vega=vega)
|
||||
expect(page.get_by_test_id("toast-content")).to_have_text(
|
||||
@@ -45,9 +55,53 @@ def test_switch_cross_isolated_margin(
|
||||
expect(page.get_by_test_id(tooltip_content).nth(0)).to_have_text(
|
||||
"Liquidation: 583.62409Margin: 11,109.99996Order: 11,000.00"
|
||||
)
|
||||
page.get_by_role("button", name="Cross").click()
|
||||
page.get_by_role("button", name="Confirm").click()
|
||||
page.get_by_test_id(cross_margin).click()
|
||||
page.get_by_test_id(confirm_cross_margin_mode).click()
|
||||
wait_for_toast_confirmation(page)
|
||||
next_epoch(vega=vega)
|
||||
expect(page.locator(margin_row).nth(1)).to_have_text(
|
||||
"22,109.99996Cross1.0x")
|
||||
|
||||
|
||||
@pytest.mark.skip("tempory disabling of isolated margin")
|
||||
@pytest.mark.usefixtures("auth", "risk_accepted")
|
||||
def test_check_cross_isolated_margin_info(
|
||||
continuous_market, vega: VegaServiceNull, page: Page):
|
||||
create_position(vega, continuous_market)
|
||||
page.goto(f"/#/markets/{continuous_market}")
|
||||
expect(page.get_by_test_id(current_margin)).to_have_text(
|
||||
"Current margin874.21992 tDAI")
|
||||
expect(page.get_by_test_id(available_collateral)
|
||||
).to_have_text("Available collateral998,084.95183 tDAI")
|
||||
expect(page.get_by_test_id(additional_margin_required)
|
||||
).to_have_text("Additional margin required0.00 tDAI")
|
||||
expect(page.get_by_test_id(liquidation_estimate)
|
||||
).to_have_text("Liquidation estimate- BTC")
|
||||
page.get_by_test_id(isolated_margin).click()
|
||||
page.locator(leverage_input).fill("6")
|
||||
expect(page.get_by_test_id(dialog_content).get_by_test_id("notification")).to_have_text(
|
||||
"You have an existing position and and open order on this market.Changing the margin mode and leverage will move 2,703.11341 tDAI from your general account to fund the position.")
|
||||
expect(page.get_by_test_id(dialog_content).get_by_test_id(current_margin)).to_have_text(
|
||||
"Current margin874.21992 tDAI")
|
||||
expect(page.get_by_test_id(dialog_content).get_by_test_id(available_collateral)
|
||||
).to_have_text("Available collateral998,084.95183 tDAI")
|
||||
expect(page.get_by_test_id(dialog_content).get_by_test_id(additional_margin_required)
|
||||
).to_have_text("Additional margin required2,703.11341 tDAI")
|
||||
expect(page.get_by_test_id(dialog_content).get_by_test_id(liquidation_estimate)
|
||||
).to_have_text("Liquidation estimate95.23553 BTC")
|
||||
page.get_by_test_id(confirm_isolated_margin_mode).click()
|
||||
wait_for_toast_confirmation(page)
|
||||
next_epoch(vega=vega)
|
||||
expect(page.get_by_test_id("toast-content")).to_have_text(
|
||||
"ConfirmedYour transaction has been confirmedView in block explorerUpdate margin modeBTC:DAI_2023Isolated margin mode, leverage: 6.0x")
|
||||
page.get_by_test_id(cross_margin).click()
|
||||
expect(page.get_by_test_id(dialog_content).get_by_test_id(
|
||||
"notification")).not_to_be_visible()
|
||||
expect(page.get_by_test_id(dialog_content).get_by_test_id(current_margin)).to_have_text(
|
||||
"Current margin4,223.33332 tDAI")
|
||||
expect(page.get_by_test_id(dialog_content).get_by_test_id(available_collateral)
|
||||
).to_have_text("Available collateral995,381.83843 tDAI")
|
||||
expect(page.get_by_test_id(dialog_content).get_by_test_id(additional_margin_required)
|
||||
).to_have_text("Additional margin required0.00 tDAI")
|
||||
expect(page.get_by_test_id(dialog_content).get_by_test_id(liquidation_estimate)
|
||||
).to_have_text("Liquidation estimate0.00 BTC")
|
||||
|
||||
@@ -35,9 +35,9 @@ def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page):
|
||||
# 6002-MDET-003
|
||||
expect(page.get_by_test_id("market-price")).to_have_text("Mark Price0.00")
|
||||
# 6002-MDET-004
|
||||
expect(page.get_by_test_id("market-change")).to_have_text("Change (24h)0.00%0.00")
|
||||
expect(page.get_by_test_id("market-change")).to_have_text("Change (24h)-")
|
||||
# 6002-MDET-005
|
||||
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)- (- BTC)")
|
||||
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)-")
|
||||
# 6002-MDET-008
|
||||
expect(page.get_by_test_id("market-settlement-asset")).to_have_text(
|
||||
"Settlement assettDAI"
|
||||
|
||||
@@ -4,7 +4,7 @@ from playwright.sync_api import Page, expect
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from vega_sim.service import MarketStateUpdateType
|
||||
from datetime import datetime, timedelta
|
||||
from conftest import init_vega
|
||||
from conftest import init_vega, cleanup_container
|
||||
from actions.utils import change_keys
|
||||
from actions.vega import submit_multiple_orders
|
||||
from fixtures.market import setup_perps_market
|
||||
@@ -17,8 +17,9 @@ col_amount = '[col-id="amount"]'
|
||||
class TestPerpetuals:
|
||||
@pytest.fixture(scope="class")
|
||||
def vega(self, request):
|
||||
with init_vega(request) as vega:
|
||||
yield vega
|
||||
with init_vega(request) as vega_instance:
|
||||
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
|
||||
yield vega_instance
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def perps_market(self, vega: VegaServiceNull):
|
||||
|
||||
@@ -8,10 +8,13 @@ from fixtures.market import setup_continuous_market
|
||||
from conftest import auth_setup, init_page, init_vega, risk_accepted_setup
|
||||
from wallet_config import PARTY_A, PARTY_B, PARTY_C, PARTY_D, MM_WALLET
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega(request):
|
||||
with init_vega(request) as vega_instance:
|
||||
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
|
||||
request.addfinalizer(
|
||||
lambda: cleanup_container(vega_instance)
|
||||
) # Register the cleanup function
|
||||
yield vega_instance
|
||||
|
||||
|
||||
@@ -125,7 +128,7 @@ def setup_teams_and_games(vega: VegaServiceNull):
|
||||
n_top_performers=1,
|
||||
amount=100,
|
||||
factor=1.0,
|
||||
window_length=15
|
||||
window_length=15,
|
||||
)
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
@@ -213,23 +216,22 @@ def create_team(vega: VegaServiceNull):
|
||||
|
||||
|
||||
def test_team_page_games_table(team_page: Page):
|
||||
team_page.pause()
|
||||
team_page.get_by_test_id("games-toggle").click()
|
||||
expect(team_page.get_by_test_id("games-toggle")).to_have_text("Results (10)")
|
||||
expect(team_page.get_by_test_id("rank-0")).to_have_text("1")
|
||||
expect(team_page.get_by_test_id("epoch-0")).to_have_text("19")
|
||||
expect(team_page.get_by_test_id("type-0")
|
||||
).to_have_text("Price maker fees paid")
|
||||
#TODO skipped as the amount is wrong
|
||||
#expect(team_page.get_by_test_id("amount-0")).to_have_text("74") # 50,000,000 on 74.1
|
||||
expect(team_page.get_by_test_id("type-0")).to_have_text(
|
||||
"Price maker fees paid • tDAI "
|
||||
)
|
||||
# TODO skipped as the amount is wrong
|
||||
# expect(team_page.get_by_test_id("amount-0")).to_have_text("74") # 50,000,000 on 74.1
|
||||
expect(team_page.get_by_test_id("participatingTeams-0")).to_have_text("2")
|
||||
expect(team_page.get_by_test_id("participatingMembers-0")).to_have_text("3")
|
||||
|
||||
|
||||
def test_team_page_members_table(team_page: Page):
|
||||
team_page.get_by_test_id("members-toggle").click()
|
||||
expect(team_page.get_by_test_id("members-toggle")
|
||||
).to_have_text("Members (4)")
|
||||
expect(team_page.get_by_test_id("members-toggle")).to_have_text("Members (4)")
|
||||
expect(team_page.get_by_test_id("referee-0")).to_be_visible()
|
||||
expect(team_page.get_by_test_id("joinedAt-0")).to_be_visible()
|
||||
expect(team_page.get_by_test_id("joinedAtEpoch-0")).to_have_text("9")
|
||||
@@ -267,8 +269,7 @@ def test_leaderboard(competitions_page: Page, setup_teams_and_games):
|
||||
competitions_page.get_by_test_id("rank-0").locator(".text-yellow-300")
|
||||
).to_have_count(1)
|
||||
expect(
|
||||
competitions_page.get_by_test_id(
|
||||
"rank-1").locator(".text-vega-clight-500")
|
||||
competitions_page.get_by_test_id("rank-1").locator(".text-vega-clight-500")
|
||||
).to_have_count(1)
|
||||
expect(competitions_page.get_by_test_id("team-0")).to_have_text(team_name)
|
||||
expect(competitions_page.get_by_test_id("status-1")).to_have_text("Open")
|
||||
@@ -282,17 +283,16 @@ def test_leaderboard(competitions_page: Page, setup_teams_and_games):
|
||||
|
||||
|
||||
def test_game_card(competitions_page: Page):
|
||||
expect(competitions_page.get_by_test_id(
|
||||
"active-rewards-card")).to_have_count(2)
|
||||
expect(competitions_page.get_by_test_id("active-rewards-card")).to_have_count(2)
|
||||
game_1 = competitions_page.get_by_test_id("active-rewards-card").first
|
||||
expect(game_1).to_be_visible()
|
||||
expect(game_1.get_by_test_id("entity-scope")).to_have_text("Individual")
|
||||
expect(game_1.get_by_test_id("locked-for")).to_have_text("1 epoch")
|
||||
expect(game_1.get_by_test_id("reward-value")).to_have_text("100.00")
|
||||
expect(game_1.get_by_test_id("distribution-strategy")
|
||||
).to_have_text("Pro rata")
|
||||
expect(game_1.get_by_test_id("dispatch-metric-info")
|
||||
).to_have_text("Price maker fees paid • tDAI")
|
||||
expect(game_1.get_by_test_id("distribution-strategy")).to_have_text("Pro rata")
|
||||
expect(game_1.get_by_test_id("dispatch-metric-info")).to_have_text(
|
||||
"Price maker fees paid • tDAI"
|
||||
)
|
||||
expect(game_1.get_by_test_id("assessed-over")).to_have_text("15 epochs")
|
||||
expect(game_1.get_by_test_id("scope")).to_have_text("In team")
|
||||
expect(game_1.get_by_test_id("staking-requirement")).to_have_text("0.00")
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
query CurrentEpochInfo {
|
||||
epoch {
|
||||
query EpochInfo($epochId: ID) {
|
||||
epoch(id: $epochId) {
|
||||
id
|
||||
timestamps {
|
||||
start
|
||||
end
|
||||
expiry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ fragment GameFields on Game {
|
||||
id
|
||||
epoch
|
||||
numberOfParticipants
|
||||
rewardAssetId
|
||||
entities {
|
||||
... on TeamGameEntity {
|
||||
...TeamEntity
|
||||
|
||||
+20
-16
@@ -3,47 +3,51 @@ import * as Types from '@vegaprotocol/types';
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type CurrentEpochInfoQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
export type EpochInfoQueryVariables = Types.Exact<{
|
||||
epochId?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type CurrentEpochInfoQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null } } };
|
||||
export type EpochInfoQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null, expiry?: any | null } } };
|
||||
|
||||
|
||||
export const CurrentEpochInfoDocument = gql`
|
||||
query CurrentEpochInfo {
|
||||
epoch {
|
||||
export const EpochInfoDocument = gql`
|
||||
query EpochInfo($epochId: ID) {
|
||||
epoch(id: $epochId) {
|
||||
id
|
||||
timestamps {
|
||||
start
|
||||
end
|
||||
expiry
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useCurrentEpochInfoQuery__
|
||||
* __useEpochInfoQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useCurrentEpochInfoQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useCurrentEpochInfoQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* To run a query within a React component, call `useEpochInfoQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useEpochInfoQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useCurrentEpochInfoQuery({
|
||||
* const { data, loading, error } = useEpochInfoQuery({
|
||||
* variables: {
|
||||
* epochId: // value for 'epochId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useCurrentEpochInfoQuery(baseOptions?: Apollo.QueryHookOptions<CurrentEpochInfoQuery, CurrentEpochInfoQueryVariables>) {
|
||||
export function useEpochInfoQuery(baseOptions?: Apollo.QueryHookOptions<EpochInfoQuery, EpochInfoQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<CurrentEpochInfoQuery, CurrentEpochInfoQueryVariables>(CurrentEpochInfoDocument, options);
|
||||
return Apollo.useQuery<EpochInfoQuery, EpochInfoQueryVariables>(EpochInfoDocument, options);
|
||||
}
|
||||
export function useCurrentEpochInfoLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<CurrentEpochInfoQuery, CurrentEpochInfoQueryVariables>) {
|
||||
export function useEpochInfoLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<EpochInfoQuery, EpochInfoQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<CurrentEpochInfoQuery, CurrentEpochInfoQueryVariables>(CurrentEpochInfoDocument, options);
|
||||
return Apollo.useLazyQuery<EpochInfoQuery, EpochInfoQueryVariables>(EpochInfoDocument, options);
|
||||
}
|
||||
export type CurrentEpochInfoQueryHookResult = ReturnType<typeof useCurrentEpochInfoQuery>;
|
||||
export type CurrentEpochInfoLazyQueryHookResult = ReturnType<typeof useCurrentEpochInfoLazyQuery>;
|
||||
export type CurrentEpochInfoQueryResult = Apollo.QueryResult<CurrentEpochInfoQuery, CurrentEpochInfoQueryVariables>;
|
||||
export type EpochInfoQueryHookResult = ReturnType<typeof useEpochInfoQuery>;
|
||||
export type EpochInfoLazyQueryHookResult = ReturnType<typeof useEpochInfoLazyQuery>;
|
||||
export type EpochInfoQueryResult = Apollo.QueryResult<EpochInfoQuery, EpochInfoQueryVariables>;
|
||||
+3
-2
@@ -5,14 +5,14 @@ import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type TeamEntityFragment = { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: Types.DispatchMetric, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string, membersParticipating: Array<{ __typename?: 'IndividualGameEntity', individual: string, rank: number }> } };
|
||||
|
||||
export type GameFieldsFragment = { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: Types.DispatchMetric, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string, membersParticipating: Array<{ __typename?: 'IndividualGameEntity', individual: string, rank: number }> } }> };
|
||||
export type GameFieldsFragment = { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, rewardAssetId: string, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: Types.DispatchMetric, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string, membersParticipating: Array<{ __typename?: 'IndividualGameEntity', individual: string, rank: number }> } }> };
|
||||
|
||||
export type GamesQueryVariables = Types.Exact<{
|
||||
epochFrom?: Types.InputMaybe<Types.Scalars['Int']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type GamesQuery = { __typename?: 'Query', games: { __typename?: 'GamesConnection', edges?: Array<{ __typename?: 'GameEdge', node: { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: Types.DispatchMetric, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string, membersParticipating: Array<{ __typename?: 'IndividualGameEntity', individual: string, rank: number }> } }> } } | null> | null } };
|
||||
export type GamesQuery = { __typename?: 'Query', games: { __typename?: 'GamesConnection', edges?: Array<{ __typename?: 'GameEdge', node: { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, rewardAssetId: string, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: Types.DispatchMetric, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string, membersParticipating: Array<{ __typename?: 'IndividualGameEntity', individual: string, rank: number }> } }> } } | null> | null } };
|
||||
|
||||
export const TeamEntityFragmentDoc = gql`
|
||||
fragment TeamEntity on TeamGameEntity {
|
||||
@@ -35,6 +35,7 @@ export const GameFieldsFragmentDoc = gql`
|
||||
id
|
||||
epoch
|
||||
numberOfParticipants
|
||||
rewardAssetId
|
||||
entities {
|
||||
... on TeamGameEntity {
|
||||
...TeamEntity
|
||||
|
||||
@@ -2,12 +2,35 @@ import compact from 'lodash/compact';
|
||||
import { useActiveRewardsQuery } from '../../components/rewards-container/__generated__/Rewards';
|
||||
import { isActiveReward } from '../../components/rewards-container/active-rewards';
|
||||
import {
|
||||
type RecurringTransfer,
|
||||
type TransferNode,
|
||||
EntityScope,
|
||||
IndividualScope,
|
||||
type TransferNode,
|
||||
} from '@vegaprotocol/types';
|
||||
import {
|
||||
type AssetFieldsFragment,
|
||||
useAssetsMapProvider,
|
||||
} from '@vegaprotocol/assets';
|
||||
import {
|
||||
type MarketFieldsFragment,
|
||||
useMarketsMapProvider,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { type ApolloError } from '@apollo/client';
|
||||
|
||||
const isScopedToTeams = (node: TransferNode) =>
|
||||
export type EnrichedTransfer = TransferNode & {
|
||||
asset?: AssetFieldsFragment | null;
|
||||
markets?: (MarketFieldsFragment | null)[];
|
||||
};
|
||||
|
||||
type RecurringTransferKind = EnrichedTransfer & {
|
||||
transfer: {
|
||||
kind: RecurringTransfer;
|
||||
};
|
||||
};
|
||||
|
||||
export const isScopedToTeams = (
|
||||
node: TransferNode
|
||||
): node is RecurringTransferKind =>
|
||||
node.transfer.kind.__typename === 'RecurringTransfer' &&
|
||||
// scoped to teams
|
||||
(node.transfer.kind.dispatchStrategy?.entityScope ===
|
||||
@@ -25,7 +48,7 @@ export const useGameCards = ({
|
||||
}: {
|
||||
currentEpoch: number;
|
||||
onlyActive: boolean;
|
||||
}) => {
|
||||
}): { data: EnrichedTransfer[]; loading: boolean; error?: ApolloError } => {
|
||||
const { data, loading, error } = useActiveRewardsQuery({
|
||||
variables: {
|
||||
isReward: true,
|
||||
@@ -33,16 +56,37 @@ export const useGameCards = ({
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
const { data: assets, loading: assetsLoading } = useAssetsMapProvider();
|
||||
const { data: markets, loading: marketsLoading } = useMarketsMapProvider();
|
||||
|
||||
const games = compact(data?.transfersConnection?.edges?.map((n) => n?.node))
|
||||
.map((n) => n as TransferNode)
|
||||
.filter((node) => {
|
||||
const active = onlyActive ? isActiveReward(node, currentEpoch) : true;
|
||||
return active && isScopedToTeams(node);
|
||||
})
|
||||
.map((node) => {
|
||||
if (node.transfer.kind.__typename !== 'RecurringTransfer') {
|
||||
return node;
|
||||
}
|
||||
|
||||
const asset =
|
||||
assets &&
|
||||
assets[
|
||||
node.transfer.kind.dispatchStrategy?.dispatchMetricAssetId || ''
|
||||
];
|
||||
|
||||
const marketsInScope =
|
||||
node.transfer.kind.dispatchStrategy?.marketIdsInScope?.map(
|
||||
(id) => markets && markets[id]
|
||||
);
|
||||
|
||||
return { ...node, asset, markets: marketsInScope };
|
||||
});
|
||||
|
||||
return {
|
||||
data: games,
|
||||
loading,
|
||||
loading: loading || assetsLoading || marketsLoading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
} from './__generated__/Games';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import { useCurrentEpochInfoQuery } from './__generated__/Epoch';
|
||||
import { useEpochInfoQuery } from './__generated__/Epoch';
|
||||
import { type ApolloError } from '@apollo/client';
|
||||
|
||||
const TAKE_EPOCHS = 30; // TODO: should this be DEFAULT_AGGREGATION_EPOCHS?
|
||||
@@ -39,7 +39,7 @@ export const useGames = (teamId?: string, epochFrom?: number): GamesData => {
|
||||
data: epochData,
|
||||
loading: epochLoading,
|
||||
error: epochError,
|
||||
} = useCurrentEpochInfoQuery({
|
||||
} = useEpochInfoQuery({
|
||||
skip: Boolean(epochFrom),
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type FirstDataRenderedEvent,
|
||||
type SortChangedEvent,
|
||||
type GridReadyEvent,
|
||||
GridApi,
|
||||
type GridApi,
|
||||
} from 'ag-grid-community';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
|
||||
@@ -222,6 +222,7 @@ const CrossMarginModeDialog = ({
|
||||
<NoWalletWarning noWalletConnected={!partyId} isReadOnly={isReadOnly} />
|
||||
<Button
|
||||
className="w-full"
|
||||
data-testid="confirm-cross-margin-mode"
|
||||
onClick={() => {
|
||||
partyId &&
|
||||
!isReadOnly &&
|
||||
@@ -329,7 +330,11 @@ const IsolatedMarginModeDialog = ({
|
||||
marginFactor={`${1 / leverage}`}
|
||||
/>
|
||||
<NoWalletWarning noWalletConnected={!partyId} isReadOnly={isReadOnly} />
|
||||
<Button className="w-full" type="submit">
|
||||
<Button
|
||||
className="w-full"
|
||||
type="submit"
|
||||
data-testid="confirm-isolated-margin-mode"
|
||||
>
|
||||
{t('Confirm')}
|
||||
</Button>
|
||||
</form>
|
||||
@@ -363,6 +368,7 @@ export const MarginModeSelector = ({ marketId }: { marketId: string }) => {
|
||||
<div className="mb-4 grid h-8 leading-8 font-alpha text-xs grid-cols-2">
|
||||
<button
|
||||
type="button"
|
||||
data-testid="cross-margin"
|
||||
onClick={() => setDialog('cross')}
|
||||
className={classnames('rounded', {
|
||||
[enabledModeClassName]:
|
||||
@@ -374,6 +380,7 @@ export const MarginModeSelector = ({ marketId }: { marketId: string }) => {
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="isolated-margin"
|
||||
onClick={() => setDialog('isolated')}
|
||||
className={classnames('rounded', {
|
||||
[enabledModeClassName]:
|
||||
|
||||
@@ -373,7 +373,10 @@ export const compileFeatureFlags = (refresh = false): FeatureFlags => {
|
||||
windowOrDefault('NX_STOP_ORDERS', process.env['NX_STOP_ORDERS']) as string
|
||||
),
|
||||
ISOLATED_MARGIN: TRUTHY.includes(
|
||||
windowOrDefault('NX_STOP_ORDERS', process.env['NX_STOP_ORDERS']) as string
|
||||
windowOrDefault(
|
||||
'NX_ISOLATED_MARGIN',
|
||||
process.env['NX_ISOLATED_MARGIN']
|
||||
) as string
|
||||
),
|
||||
SUCCESSOR_MARKETS: TRUTHY.includes(
|
||||
windowOrDefault(
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"approvers": "Approvers",
|
||||
"as soon as possible": "now",
|
||||
"Asset change": "Asset change",
|
||||
"AssetID": "Asset ID",
|
||||
"Asset ID: <lozenge>{{id}}</lozenge>": "Asset ID: <lozenge>{{id}}</lozenge>",
|
||||
"assets": "Assets",
|
||||
"assetSpecification": "Asset specification",
|
||||
"associate": "Associate",
|
||||
@@ -81,6 +81,7 @@
|
||||
"back": "back",
|
||||
"backToStaking": "Back to Staking",
|
||||
"Balance": "Balance",
|
||||
"Batch proposal": "Batch proposal",
|
||||
"BenefitTierMinimumActivityStreak": "Minimum activity streak",
|
||||
"BenefitTierMinimumActivityStreakDescription": "The minimum number of times the party needs to have completed the activity",
|
||||
"BenefitTierMinimumEpochs": "Minimum epochs",
|
||||
@@ -109,7 +110,7 @@
|
||||
"CancelTransfer": "Cancel transfer",
|
||||
"CancelTransferProposal": "Cancel transfer proposal",
|
||||
"castYourVote": "Cast your vote",
|
||||
"Change": "Change",
|
||||
"Change <lozenge>{{key}}</lozenge> to <lozenge>{{value}}</lozenge>": "Change <lozenge>{{key}}</lozenge> to <lozenge>{{value}}</lozenge>",
|
||||
"changeVote": "Change vote",
|
||||
"Check to see if you can redeem unlocked VEGA tokens": "Check to see if you can redeem unlocked $VEGA tokens",
|
||||
"Check your vesting VEGA tokens": "Check your vesting $VEGA tokens",
|
||||
@@ -162,7 +163,10 @@
|
||||
"created": "Created",
|
||||
"CreateProposalAndDownloadJSONToShare": "Create proposal and download JSON to share",
|
||||
"currently": "currently",
|
||||
"currentlySetTo": "Currently expected to ",
|
||||
"Currently expected to <0>pass</0>": "Currently expected to <0>pass</0>",
|
||||
"Currently expected to <0>fail</0>": "Currently expected to <0>fail</0>",
|
||||
"Currently expected to pass: conditions met for {{count}} of {{total}} proposals": "Currently expected to pass: conditions met for {{count}} of {{total}} Proposals",
|
||||
"Currently expected to fail: {{count}} of {{total}} proposals are passing": "Currently expected to fail: {{count}} of {{total}} proposals are passing",
|
||||
"CurrentValue": "Current value",
|
||||
"dataIsIdentical": "Data is identical",
|
||||
"date": "Date",
|
||||
@@ -287,7 +291,7 @@
|
||||
"Keep track of locked tokens in your wallet with the VEGA (VESTING) token.": "Keep track of locked tokens in your wallet with the $VEGA (VESTING) token.",
|
||||
"latestProposals": "Latest proposals",
|
||||
"learnMore": "Learn more",
|
||||
"left to vote": "left to vote",
|
||||
"{{time}} left to vote": "{{time}} left to vote",
|
||||
"Link transaction": "Link transaction",
|
||||
"liquidityComingSoon": "Liquidity rewards coming soon",
|
||||
"liquidityIntro": "You can read about our incentive program in this <linkToPost>blog post</linkToPost>.",
|
||||
@@ -421,6 +425,7 @@
|
||||
"nextEpoch": "Next epoch",
|
||||
"No holders": "No holders",
|
||||
"No token": "No token",
|
||||
"not met": "not met",
|
||||
"noClosedProposals": "There are no enacted or rejected proposals",
|
||||
"Node invalid": "Node invalid",
|
||||
"nodeQueryFailed": "Could not get data for validator {{node}}",
|
||||
@@ -528,6 +533,8 @@
|
||||
"ProductMaturityIsPassed": "Product maturity is passed",
|
||||
"Proposal": "Proposal",
|
||||
"proposal": "Proposal",
|
||||
"Proposal passed: conditions met for {{count}} of {{total}} proposals": "Proposal passed: conditions met for {{count}} of {{total}} proposals",
|
||||
"Proposal failed: {{count}} of {{total}} proposals passed": "Proposal failed: {{count}} of {{total}} proposals passed",
|
||||
"Proposal rejected": "Proposal rejected",
|
||||
"proposalCancelTransferDetails": "Cancel governance transfer details",
|
||||
"proposalChange": "Change <code>{{key}}</code> to <code>{{value}}</code>",
|
||||
@@ -713,6 +720,7 @@
|
||||
"submitProposal": "Submit proposal",
|
||||
"submittingProposal": "Submitting proposal",
|
||||
"successfullAssociationMessage": "Vega key {{vegaKey}} can now participate in governance and nominate a validator with your associated $VEGA.",
|
||||
"Successor market to": "Successor market to",
|
||||
"Switch to form for immediate removal": "Switch to remove now",
|
||||
"Switch to form for removal at end of epoch": "Switch to remove at end of epoch",
|
||||
"Symbol": "Symbol",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"{{liquidityPriceRange}} of mid price": "{{liquidityPriceRange}} of mid price",
|
||||
"{{probability}} probability price bounds": "{{probability}} probability price bounds",
|
||||
"24 hour change is unavailable at this time. The price change in the last 120 hours is:": "24 hour change is unavailable at this time. The price change in the last 120 hours is:",
|
||||
"24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}}": "24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}}",
|
||||
"24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}} for a total of {{candleVolumePrice}} {{quoteUnit}}": "24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}} for a total of {{candleVolumePrice}} {{quoteUnit}}",
|
||||
"A concept derived from traditional markets. It is a calculated value for the ‘current market price’ on a market.": "A concept derived from traditional markets. It is a calculated value for the ‘current market price’ on a market.",
|
||||
"A number that will be calculated by an appropriate stochastic risk model, dependent on the type of risk model used and its parameters.": "A number that will be calculated by an appropriate stochastic risk model, dependent on the type of risk model used and its parameters.",
|
||||
"A sliding penalty for how much an LP bond is slashed if an LP fails to reach the minimum SLA. This is a network parameter.": "A sliding penalty for how much an LP bond is slashed if an LP fails to reach the minimum SLA. This is a network parameter.",
|
||||
@@ -49,6 +49,9 @@
|
||||
"Market": "Market",
|
||||
"Market data": "Market data",
|
||||
"Market governance": "Market governance",
|
||||
"Market has not been active for 24 hours. The price change between {{start}} and {{end}} is:": "Market has not been active for 24 hours. The price change between {{start}} and {{end}} is:",
|
||||
"Market has not been active for 24 hours. The volume traded between {{start}} and {{end}} is:": "Market has not been active for 24 hours. The volume traded between {{start}} and {{end}} is:",
|
||||
"Market has not been active for 24 hours. The volume traded between {{start}} and {{end}} is {{candleVolumeValue}} for a total of {{candleVolumePrice}} {{quoteUnit}}": "Market has not been active for 24 hours. The volume traded between {{start}} and {{end}} is {{candleVolumeValue}} for a total of {{candleVolumePrice}} {{quoteUnit}}",
|
||||
"Market ID": "Market ID",
|
||||
"Market price": "Market price",
|
||||
"Market specification": "Market specification",
|
||||
|
||||
@@ -455,5 +455,12 @@
|
||||
"Go back to the team's profile": "Go back to the team's profile",
|
||||
"Go back to the competitions": "Go back to the competitions",
|
||||
"Your team ID:": "Your team ID:",
|
||||
"Changes successfully saved to your team.": "Changes successfully saved to your team."
|
||||
"Changes successfully saved to your team.": "Changes successfully saved to your team.",
|
||||
"Results {{games}}": "Results {{games}}",
|
||||
"End time": "End time",
|
||||
"Reward asset": "Reward asset",
|
||||
"Daily reward amount": "Daily reward amount",
|
||||
"Amount earned this epoch": "Amount earned this epoch",
|
||||
"Cumulative amount earned": "Cumulative amount earned",
|
||||
"Game details": "Game details"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { type ReactNode } from 'react';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumberPercentage,
|
||||
isNumeric,
|
||||
getDateTimeFormat,
|
||||
priceChange,
|
||||
priceChangePercentage,
|
||||
} from '@vegaprotocol/utils';
|
||||
@@ -14,29 +15,57 @@ import { useT } from '../../use-t';
|
||||
|
||||
interface Props {
|
||||
marketId?: string;
|
||||
decimalPlaces?: number;
|
||||
initialValue?: string[];
|
||||
isHeader?: boolean;
|
||||
noUpdate?: boolean;
|
||||
// render prop for no price change
|
||||
fallback?: React.ReactNode;
|
||||
decimalPlaces: number;
|
||||
fallback?: ReactNode;
|
||||
}
|
||||
|
||||
export const Last24hPriceChange = ({
|
||||
marketId,
|
||||
decimalPlaces,
|
||||
initialValue,
|
||||
fallback,
|
||||
}: Props) => {
|
||||
const t = useT();
|
||||
const { oneDayCandles, error, fiveDaysCandles } = useCandles({
|
||||
const { oneDayCandles, fiveDaysCandles, error } = useCandles({
|
||||
marketId,
|
||||
});
|
||||
if (
|
||||
fiveDaysCandles &&
|
||||
fiveDaysCandles.length > 0 &&
|
||||
(!oneDayCandles || oneDayCandles?.length === 0)
|
||||
) {
|
||||
|
||||
const nonIdeal = fallback || <span>{'-'}</span>;
|
||||
|
||||
if (error || !oneDayCandles || !fiveDaysCandles) {
|
||||
return nonIdeal;
|
||||
}
|
||||
|
||||
if (fiveDaysCandles.length < 24) {
|
||||
return (
|
||||
<Tooltip
|
||||
description={
|
||||
<span className="justify-start">
|
||||
{t(
|
||||
'Market has not been active for 24 hours. The price change between {{start}} and {{end}} is:',
|
||||
{
|
||||
start: getDateTimeFormat().format(
|
||||
new Date(fiveDaysCandles[0].periodStart)
|
||||
),
|
||||
end: getDateTimeFormat().format(
|
||||
new Date(
|
||||
fiveDaysCandles[fiveDaysCandles.length - 1].periodStart
|
||||
)
|
||||
),
|
||||
}
|
||||
)}
|
||||
<PriceChangeCell
|
||||
candles={fiveDaysCandles.map((c) => c.close) || []}
|
||||
decimalPlaces={decimalPlaces}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<span>{nonIdeal}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (oneDayCandles.length < 24) {
|
||||
return (
|
||||
<Tooltip
|
||||
description={
|
||||
@@ -51,16 +80,12 @@ export const Last24hPriceChange = ({
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<span>{fallback}</span>
|
||||
<span>{nonIdeal}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !isNumeric(decimalPlaces)) {
|
||||
return <span>{fallback}</span>;
|
||||
}
|
||||
|
||||
const candles = oneDayCandles?.map((c) => c.close) || initialValue || [];
|
||||
const candles = oneDayCandles?.map((c) => c.close) || [];
|
||||
const change = priceChange(candles);
|
||||
const changePercentage = priceChangePercentage(candles);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { calcCandleVolume, calcCandleVolumePrice } from '../../market-utils';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumber,
|
||||
getDateTimeFormat,
|
||||
isNumeric,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
@@ -26,15 +27,17 @@ export const Last24hVolume = ({
|
||||
quoteUnit,
|
||||
}: Props) => {
|
||||
const t = useT();
|
||||
const { oneDayCandles, fiveDaysCandles } = useCandles({
|
||||
const { oneDayCandles, fiveDaysCandles, error } = useCandles({
|
||||
marketId,
|
||||
});
|
||||
|
||||
if (
|
||||
fiveDaysCandles &&
|
||||
fiveDaysCandles.length > 0 &&
|
||||
(!oneDayCandles || oneDayCandles?.length === 0)
|
||||
) {
|
||||
const nonIdeal = <span>{'-'}</span>;
|
||||
|
||||
if (error || !oneDayCandles || !fiveDaysCandles) {
|
||||
return nonIdeal;
|
||||
}
|
||||
|
||||
if (fiveDaysCandles.length < 24) {
|
||||
const candleVolume = calcCandleVolume(fiveDaysCandles);
|
||||
const candleVolumePrice = calcCandleVolumePrice(
|
||||
fiveDaysCandles,
|
||||
@@ -55,14 +58,59 @@ export const Last24hVolume = ({
|
||||
<div>
|
||||
<span className="flex flex-col">
|
||||
{t(
|
||||
'24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}} ({{candleVolumePrice}} {{quoteUnit}})',
|
||||
'Market has not been active for 24 hours. The volume traded between {{start}} and {{end}} is {{candleVolumeValue}} for a total of {{candleVolumePrice}} {{quoteUnit}}',
|
||||
{
|
||||
start: getDateTimeFormat().format(
|
||||
new Date(fiveDaysCandles[0].periodStart)
|
||||
),
|
||||
end: getDateTimeFormat().format(
|
||||
new Date(
|
||||
fiveDaysCandles[fiveDaysCandles.length - 1].periodStart
|
||||
)
|
||||
),
|
||||
candleVolumeValue,
|
||||
candleVolumePrice,
|
||||
quoteUnit,
|
||||
}
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<span>{nonIdeal}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (oneDayCandles.length < 24) {
|
||||
const candleVolume = calcCandleVolume(fiveDaysCandles);
|
||||
const candleVolumePrice = calcCandleVolumePrice(
|
||||
fiveDaysCandles,
|
||||
marketDecimals,
|
||||
positionDecimalPlaces
|
||||
);
|
||||
const candleVolumeValue =
|
||||
candleVolume && isNumeric(positionDecimalPlaces)
|
||||
? addDecimalsFormatNumber(
|
||||
candleVolume,
|
||||
positionDecimalPlaces,
|
||||
formatDecimals
|
||||
)
|
||||
: '-';
|
||||
return (
|
||||
<Tooltip
|
||||
description={
|
||||
<div>
|
||||
<span className="flex flex-col">
|
||||
{t(
|
||||
'24 hour change is unavailable at this time. The volume change in the last 120 hours is {{candleVolumeValue}} for a total of ({{candleVolumePrice}} {{quoteUnit}})',
|
||||
{ candleVolumeValue, candleVolumePrice, quoteUnit }
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<span>-</span>
|
||||
<span>{nonIdeal}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -139,6 +139,8 @@ query MarketInfo($marketId: ID!) {
|
||||
state
|
||||
tradingMode
|
||||
linearSlippageFactor
|
||||
parentMarketID
|
||||
successorMarketID
|
||||
proposal {
|
||||
id
|
||||
rationale {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -15,7 +15,6 @@ const mockData = [
|
||||
periodStart: today.toISOString(),
|
||||
__typename: 'Candle',
|
||||
},
|
||||
null,
|
||||
{
|
||||
high: '6309988',
|
||||
low: '6296335',
|
||||
|
||||
@@ -18,7 +18,12 @@ export const useCandles = ({ marketId }: { marketId?: string }) => {
|
||||
skip: !marketId,
|
||||
});
|
||||
|
||||
const fiveDaysCandles = data?.filter(Boolean);
|
||||
const fiveDaysCandles = data?.filter((c) => {
|
||||
if (c.open === '' || c.close === '' || c.high === '' || c.close === '') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const oneDayCandles = fiveDaysCandles?.filter((candle) =>
|
||||
isCandleLessThan24hOld(candle, yesterday)
|
||||
|
||||
@@ -47,8 +47,9 @@ export const marketsProvider = makeDataProvider<
|
||||
errorPolicy: 'all',
|
||||
});
|
||||
|
||||
export type MarketMap = Record<string, Market>;
|
||||
export const marketsMapProvider = makeDerivedDataProvider<
|
||||
Record<string, Market>,
|
||||
MarketMap,
|
||||
never,
|
||||
undefined
|
||||
>(
|
||||
@@ -59,7 +60,7 @@ export const marketsMapProvider = makeDerivedDataProvider<
|
||||
markets[market.id] = market;
|
||||
return markets;
|
||||
},
|
||||
{} as Record<string, Market>
|
||||
{} as MarketMap
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import {
|
||||
OpenVolumeData,
|
||||
openVolumeDataProvider,
|
||||
type OpenVolumeData,
|
||||
} from './positions-data-providers';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
|
||||
|
||||
Generated
+23
-1
@@ -1640,6 +1640,8 @@ export type Game = {
|
||||
id: Scalars['ID'];
|
||||
/** Number of participants that took part in the game during the epoch. */
|
||||
numberOfParticipants: Scalars['Int'];
|
||||
/** ID of asset in which the rewards were paid. */
|
||||
rewardAssetId: Scalars['ID'];
|
||||
};
|
||||
|
||||
/** Edge type containing the game metrics and cursor information returned by a GameConnection */
|
||||
@@ -1704,10 +1706,14 @@ export type IndividualGameEntity = {
|
||||
rank: Scalars['Int'];
|
||||
/** The rewards earned by the individual during the epoch */
|
||||
rewardEarned: Scalars['String'];
|
||||
/** The rewards earned by the individual during the epoch in quantum value */
|
||||
rewardEarnedQuantum: Scalars['String'];
|
||||
/** The reward metric applied to the game */
|
||||
rewardMetric: DispatchMetric;
|
||||
/** Total rewards earned by the individual during the game */
|
||||
totalRewardsEarned: Scalars['String'];
|
||||
/** Total rewards earned by the individual during the game in quantum value */
|
||||
totalRewardsEarnedQuantum: Scalars['String'];
|
||||
/** The volume traded by the individual */
|
||||
volume: Scalars['String'];
|
||||
};
|
||||
@@ -4733,10 +4739,18 @@ export type QuantumRewardsPerEpoch = {
|
||||
__typename?: 'QuantumRewardsPerEpoch';
|
||||
/** Epoch for which this information is valid. */
|
||||
epoch: Scalars['Int'];
|
||||
/** Total of rewards accumulated over the epoch period expressed in quantum value. */
|
||||
/** Total of rewards accumulated over the epoch period expressed in quantum value. */
|
||||
totalQuantumRewards: Scalars['String'];
|
||||
};
|
||||
|
||||
export type QuantumVolumesPerEpoch = {
|
||||
__typename?: 'QuantumVolumesPerEpoch';
|
||||
/** Epoch for which this information is valid. */
|
||||
epoch: Scalars['Int'];
|
||||
/** Total volume across all markets, accumulated over the epoch period, expressed in quantum value. */
|
||||
totalQuantumVolumes: Scalars['String'];
|
||||
};
|
||||
|
||||
/** Queries allow a caller to read data and filter data via GraphQL. */
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
@@ -6412,12 +6426,16 @@ export type TeamGameEntity = {
|
||||
rank: Scalars['Int'];
|
||||
/** Total rewards earned by the team during the epoch */
|
||||
rewardEarned: Scalars['String'];
|
||||
/** Total rewards earned by the team during the epoch in quantum value */
|
||||
rewardEarnedQuantum: Scalars['String'];
|
||||
/** Reward metric applied to the game. */
|
||||
rewardMetric: DispatchMetric;
|
||||
/** Breakdown of the team members and their contributions to the total team metrics. */
|
||||
team: TeamParticipation;
|
||||
/** Total rewards earned by the team for the game */
|
||||
totalRewardsEarned: Scalars['String'];
|
||||
/** Total rewards earned by the team for the game in quantum value */
|
||||
totalRewardsEarnedQuantum: Scalars['String'];
|
||||
/** Total volume traded by the team */
|
||||
volume: Scalars['String'];
|
||||
};
|
||||
@@ -6431,6 +6449,8 @@ export type TeamMemberStatistics = {
|
||||
partyId: Scalars['String'];
|
||||
/** List of rewards over the requested epoch period, expressed in quantum value for each epoch */
|
||||
quantumRewards: Array<QuantumRewardsPerEpoch>;
|
||||
/** List of trading volume totals per epoch, for the requested epoch period, expressed in quantum value */
|
||||
quantumVolumes: Array<QuantumVolumesPerEpoch>;
|
||||
/** Total number of games played. */
|
||||
totalGamesPlayed: Scalars['Int'];
|
||||
/** Total of rewards accumulated over the requested epoch period, expressed in quantum value. */
|
||||
@@ -6533,6 +6553,8 @@ export type TeamStatistics = {
|
||||
gamesPlayed: Array<Scalars['String']>;
|
||||
/** List of rewards over the requested epoch period, expressed in quantum value for each epoch */
|
||||
quantumRewards: Array<QuantumRewardsPerEpoch>;
|
||||
/** List of trading volume totals per epoch, over the requested epoch period, expressed in quantum value */
|
||||
quantumVolumes: Array<QuantumVolumesPerEpoch>;
|
||||
/** Team ID the statistics are related to. */
|
||||
teamId: Scalars['String'];
|
||||
/** Total of games played. */
|
||||
|
||||
@@ -66,12 +66,12 @@ export const KeyValueTableRow = ({
|
||||
id,
|
||||
}: KeyValueTableRowProps) => {
|
||||
const dlClassName = classNames(
|
||||
'flex gap-1 flex-wrap justify-between py-1 text-sm',
|
||||
'flex gap-1 flex-wrap justify-between py-1 text-sm items-start',
|
||||
{
|
||||
'border-b border-neutral-300 dark:border-neutral-700': !noBorder,
|
||||
},
|
||||
{ 'flex-col items-start': !inline },
|
||||
{ 'flex-row items-center': inline },
|
||||
{ 'flex-col': !inline },
|
||||
{ 'flex-row': inline },
|
||||
className
|
||||
);
|
||||
const dtClassNames = classNames(
|
||||
|
||||
Reference in New Issue
Block a user