diff --git a/apps/explorer/src/app/routes/network-parameters/index.tsx b/apps/explorer/src/app/routes/network-parameters/index.tsx index 90c3f470a..097b457e6 100644 --- a/apps/explorer/src/app/routes/network-parameters/index.tsx +++ b/apps/explorer/src/app/routes/network-parameters/index.tsx @@ -1 +1,2 @@ export * from './network-parameters'; +export * from './structure-network-params'; diff --git a/apps/explorer/src/app/routes/network-parameters/network-parameters.spec.tsx b/apps/explorer/src/app/routes/network-parameters/network-parameters.spec.tsx index 933f1224d..901c5f22d 100644 --- a/apps/explorer/src/app/routes/network-parameters/network-parameters.spec.tsx +++ b/apps/explorer/src/app/routes/network-parameters/network-parameters.spec.tsx @@ -1,82 +1,105 @@ import { render, screen } from '@testing-library/react'; import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters'; import { NetworkParametersTable } from './network-parameters'; +import { MemoryRouter } from 'react-router-dom'; + +const renderComponent = (data: NetworkParamsQuery | undefined) => { + return render( + + + + ); +}; + +const mockData = { + networkParametersConnection: { + edges: [ + { + node: { + key: 'spam.protection.delegation.min.tokens', + value: '3', + }, + }, + { + node: { + key: 'spam.protection.voting.min.tokens', + value: '1', + }, + }, + { + node: { + key: 'reward.staking.delegation.minimumValidatorStake', + value: '2', + }, + }, + { + node: { + key: 'reward.asset', + value: + 'fc7fd956078fb1fc9db5c19b88f0874c4299b2a7639ad05a47a28c0aef291b55', + }, + }, + ], + }, +}; describe('NetworkParametersTable', () => { - it('renders correctly when it has network params', () => { - const data: NetworkParamsQuery = { - networkParametersConnection: { - edges: [ - { - node: { - __typename: 'NetworkParameter', - key: 'market.liquidityProvision.minLpStakeQuantumMultiple', - value: '1', - }, - }, - { - node: { - __typename: 'NetworkParameter', - key: 'market.fee.factors.infrastructureFee', - value: '0.0005', - }, - }, - ], + it('renders headers correctly', () => { + renderComponent(mockData); + + const allHeadings = screen.getAllByRole('heading'); + expect( + allHeadings.map((h) => { + return { + text: h.textContent, + level: h.tagName, + testId: h.getAttribute('data-testid'), + }; + }) + ).toEqual([ + { + text: 'Network Parameters', + level: 'H1', + testId: 'network-param-header', }, - }; - render(); - expect(screen.getByTestId('network-param-header')).toHaveTextContent( - 'Network Parameters' - ); - const rows = screen.getAllByTestId('key-value-table-row'); - expect(rows[0].children[0]).toHaveTextContent( - 'market.fee.factors.infrastructureFee' - ); - expect(rows[1].children[0]).toHaveTextContent( - 'market.liquidityProvision.minLpStakeQuantumMultiple' - ); - expect(rows[0].children[1]).toHaveTextContent('0.0005'); - expect(rows[1].children[1]).toHaveTextContent('1'); + { text: 'Spam', level: 'H1', testId: 'spam' }, + { text: 'Protection', level: 'H2', testId: 'spam-protection' }, + { text: 'Delegation', level: 'H3', testId: 'spam-protection-delegation' }, + { text: 'Min', level: 'H4', testId: 'spam-protection-delegation-min' }, + { text: 'Voting', level: 'H3', testId: 'spam-protection-voting' }, + { text: 'Min', level: 'H4', testId: 'spam-protection-voting-min' }, + { text: 'Reward', level: 'H1', testId: 'reward' }, + { text: 'Staking', level: 'H2', testId: 'reward-staking' }, + { text: 'Delegation', level: 'H3', testId: 'reward-staking-delegation' }, + ]); }); - it('renders the rows in ascending order', () => { - const data: NetworkParamsQuery = { - networkParametersConnection: { - edges: [ - { - node: { - __typename: 'NetworkParameter', - key: 'market.fee.factors.infrastructureFee', - value: '0.0005', - }, - }, - { - node: { - __typename: 'NetworkParameter', - key: 'market.liquidityProvision.minLpStakeQuantumMultiple', - value: '1', - }, - }, - ], - }, - }; - render(); - expect(screen.getByTestId('network-param-header')).toHaveTextContent( - 'Network Parameters' + it('renders network params correctly', () => { + renderComponent(mockData); + + const delegationMinTokensRow = screen.getByTestId( + 'spam-protection-delegation-min-tokens' ); - const rows = screen.getAllByTestId('key-value-table-row'); - expect(rows[0].children[0]).toHaveTextContent( - 'market.fee.factors.infrastructureFee' + expect(delegationMinTokensRow).toHaveTextContent('0.000000000000000003'); + + const votingMinTokensRow = screen.getByTestId( + 'spam-protection-voting-min-tokens' ); - expect(rows[1].children[0]).toHaveTextContent( - 'market.liquidityProvision.minLpStakeQuantumMultiple' + expect(votingMinTokensRow).toHaveTextContent('0.000000000000000001'); + + const minimumValidatorStakeRow = screen.getByTestId( + 'reward-staking-delegation-minimumValidatorStake' + ); + expect(minimumValidatorStakeRow).toHaveTextContent('2'); + + const assetRow = screen.getByTestId('reward-asset'); + expect(assetRow).toHaveTextContent( + 'fc7fd956078fb1fc9db5c19b88f0874c4299b2a7639ad05a47a28c0aef291b55' ); - expect(rows[0].children[1]).toHaveTextContent('0.0005'); - expect(rows[1].children[1]).toHaveTextContent('1'); }); it('does not render rows when is loading', () => { - render(); + renderComponent(undefined); expect(screen.getByTestId('network-param-header')).toHaveTextContent( 'Network Parameters' ); diff --git a/apps/explorer/src/app/routes/network-parameters/network-parameters.tsx b/apps/explorer/src/app/routes/network-parameters/network-parameters.tsx index 8eb9522a0..71d99d2c2 100644 --- a/apps/explorer/src/app/routes/network-parameters/network-parameters.tsx +++ b/apps/explorer/src/app/routes/network-parameters/network-parameters.tsx @@ -1,6 +1,8 @@ +import startCase from 'lodash/startCase'; +import classNames from 'classnames'; +import { Link } from 'react-router-dom'; import { AsyncRenderer, - KeyValueTable, KeyValueTableRow, SyntaxHighlighter, } from '@vegaprotocol/ui-toolkit'; @@ -12,11 +14,12 @@ import { } from '@vegaprotocol/utils'; import { t } from '@vegaprotocol/i18n'; import { RouteTitle } from '../../components/route-title'; -import orderBy from 'lodash/orderBy'; import { useNetworkParamsQuery } from '@vegaprotocol/network-parameters'; -import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters'; import { useScrollToLocation } from '../../hooks/scroll-to-location'; import { useDocumentTitle } from '../../hooks/use-document-title'; +import { structureNetworkParams } from './structure-network-params'; +import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters'; +import type { GroupedParams } from './structure-network-params'; const PERCENTAGE_PARAMS = [ 'governance.proposal.asset.requiredMajority', @@ -58,6 +61,54 @@ const BIG_NUMBER_PARAMS = [ 'governance.proposal.updateAsset.minVoterBalance', ]; +export const renderGroupedParams = ( + group: GroupedParams, + level: number, + parentKeys: string[] = [] +) => { + const Header = `h${level}` as keyof JSX.IntrinsicElements; + const headerStyles = classNames('uppercase font-semibold', { + 'pt-6 text-3xl underline': level === 1, + 'pt-3 text-2xl': level === 2, + 'pt-2 text-lg': level === 3, + 'pt-2 text-default': level === 4, + }); + + return Object.entries(group).map(([key, value]) => { + const fullPath = [...parentKeys, key].join('.'); + const isLeafNode = typeof value !== 'object'; + const id = parentKeys.concat([key]).join('-'); + + return ( +
+ {!isLeafNode && ( +
+ +
+ {startCase(key)} +
+ +
+ )} + {isLeafNode ? ( + typeof value === 'string' ? ( +
+ +
+ ) : null + ) : ( +
+ {renderGroupedParams(value, level + 1, [...parentKeys, key])} +
+ )} +
+ ); + }); +}; + export const NetworkParameterRow = ({ row: { key, value }, }: { @@ -77,7 +128,9 @@ export const NetworkParameterRow = ({ > {key} {isSyntaxRow ? ( - +
+ +
) : isNaN(Number(value)) ? ( value ) : BIG_NUMBER_PARAMS.includes(key) ? ( @@ -113,17 +166,12 @@ export const NetworkParametersTable = ({ loading={loading} error={error} render={(data) => { - const ascParams = orderBy( - removePaginationWrapper(data.networkParametersConnection.edges), - (param) => param.key, - 'asc' + const flatParams = removePaginationWrapper( + data.networkParametersConnection.edges ); + const groupedParams = structureNetworkParams(flatParams); return ( - - {(ascParams || []).map((row) => ( - - ))} - +
{renderGroupedParams(groupedParams, 1)}
); }} /> diff --git a/apps/explorer/src/app/routes/network-parameters/structure-network-params.spec.tsx b/apps/explorer/src/app/routes/network-parameters/structure-network-params.spec.tsx new file mode 100644 index 000000000..34f2010a5 --- /dev/null +++ b/apps/explorer/src/app/routes/network-parameters/structure-network-params.spec.tsx @@ -0,0 +1,109 @@ +import type { GroupedParams } from './structure-network-params'; +import { + structureParams, + sortGroupedParams, + structureNetworkParams, +} from './structure-network-params'; + +describe('structureParams', () => { + it('should correctly structure params', () => { + const input = [ + { key: 'spam.protection.delegation.min.tokens', value: '10' }, + { key: 'spam.protection.voting.min.tokens', value: '5' }, + ]; + const output: GroupedParams = { + spam: { + protection: { + delegation: { + min: { + tokens: '10', + }, + }, + voting: { + min: { + tokens: '5', + }, + }, + }, + }, + }; + expect(structureParams(input)).toEqual(output); + }); + + it('should handle top-level keys correctly', () => { + const input = [{ key: 'levelOne', value: '10' }]; + const output = { + levelOne: '10', + }; + expect(structureParams(input)).toEqual(output); + }); +}); + +describe('sortGroupedParams', () => { + it('should correctly sort grouped params', () => { + const input: GroupedParams = { + spam: { + protection: { + delegation: { + min: { + tokens: '10', + }, + }, + }, + }, + reward: '50', + }; + const output: GroupedParams = { + reward: '50', + spam: { + protection: { + delegation: { + min: { + tokens: '10', + }, + }, + }, + }, + }; + expect(sortGroupedParams(input)).toEqual(output); + }); + + it('should handle already sorted keys', () => { + const input = { + a: '10', + b: { + c: '5', + d: '6', + }, + }; + expect(sortGroupedParams(input)).toEqual(input); + }); +}); + +describe('structureNetworkParams', () => { + it('should structure and sort network params correctly', () => { + const input = [ + { key: 'spam.protection.delegation.min.tokens', value: '10' }, + { key: 'reward.asset', value: '50' }, + ]; + const output: GroupedParams = { + reward: { + asset: '50', + }, + spam: { + protection: { + delegation: { + min: { + tokens: '10', + }, + }, + }, + }, + }; + expect(structureNetworkParams(input)).toEqual(output); + }); + + it('should return an empty object if no params are provided', () => { + expect(structureNetworkParams([])).toEqual({}); + }); +}); diff --git a/apps/explorer/src/app/routes/network-parameters/structure-network-params.tsx b/apps/explorer/src/app/routes/network-parameters/structure-network-params.tsx new file mode 100644 index 000000000..f59adeb35 --- /dev/null +++ b/apps/explorer/src/app/routes/network-parameters/structure-network-params.tsx @@ -0,0 +1,107 @@ +/** + * Categorizes and sorts an array of key-value pairs of network params into a nested object structure. + * + * The function takes an array of network params where keys are dot-delimited + * strings representing nested categories (e.g., 'spam.protection.delegation.min.tokens'). + * + * Why this is necessary: + * A flat key-value structure wouldn't provide the hierarchical information needed. + * Organizing network parameters like this allows the rendering of nested headers + * and their corresponding network params. + * + * It also ensures that items with the minimum amount of nesting are ordered first. This + * allows us to render these items first, before more deeply nested items are rendered with + * subheaders. This creates a more intuitive UI. + * + * For example, given the input: + * [ + * { key: 'spam.protection.delegation.min.tokens', value: '10' }, + * { key: 'spam.protection.voting.min.tokens', value: '5' }, + * { key: 'reward.staking.delegation.minimumValidatorStake', value: '2' } + * { key: 'reward.asset', value: 'fc7fd956078fb1fc9db5c19b88f0874c4299b2a7639ad05a47a28c0aef291b55' } + * ] + * + * The output will be: + * { + * spam: { + * protection: { + * delegation: { + * min: { + * tokens: '10' + * } + * }, + * voting: { + * min: { + * tokens: '5' + * } + * } + * } + * }, + * reward: { + * asset: 'fc7fd956078fb1fc9db5c19b88f0874c4299b2a7639ad05a47a28c0aef291b55', + * staking: { + * delegation: { + * minimumValidatorStake: '2' + * } + * } + * } + * } + * + * @param {Array} params - An array of key-value pairs to categorize and sort. + * @returns {GroupedParams} - A nested object that groups the key-value pairs. + */ + +export type GroupedParams = { + [key: string]: string | GroupedParams; +}; + +export const structureParams = ( + params: { key: string; value: string }[] +): GroupedParams => { + const grouped: GroupedParams = {}; + + params.forEach(({ key, value }) => { + const parts = key.split('.'); + let node: GroupedParams = grouped; + + parts.forEach((part, i) => { + if (typeof node[part] === 'undefined') { + node[part] = i === parts.length - 1 ? value : {}; + } + + if (typeof node[part] === 'object') { + node = node[part] as GroupedParams; + } + }); + }); + + return grouped; +}; + +export const sortGroupedParams = ( + groupedParams: GroupedParams +): GroupedParams => { + const sorted: GroupedParams = {}; + + // Sort top-level keys first + Object.entries(groupedParams).forEach(([key, value]) => { + if (typeof value === 'string') { + sorted[key] = value; + } + }); + + Object.entries(groupedParams).forEach(([key, value]) => { + if (typeof value === 'object') { + sorted[key] = sortGroupedParams(value); + } + }); + + return sorted; +}; + +export const structureNetworkParams = ( + params: { key: string; value: string }[] +) => { + const grouped = structureParams(params); + return sortGroupedParams(grouped); +}; diff --git a/apps/governance/src/components/wallet-card/wallet-card.tsx b/apps/governance/src/components/wallet-card/wallet-card.tsx index aa8b06e5f..6a1b1e5fa 100644 --- a/apps/governance/src/components/wallet-card/wallet-card.tsx +++ b/apps/governance/src/components/wallet-card/wallet-card.tsx @@ -54,7 +54,7 @@ export const WalletCardRow = ({ }) => { const ref = React.useRef(null); useAnimateValue(ref, value); - const [integers, decimalsPlaces] = useNumberParts(value, decimals); + const [integers, decimalsPlaces, separator] = useNumberParts(value, decimals); return (
- {integers}. + + {integers} + {separator} + {decimalsPlaces} )} @@ -110,7 +113,10 @@ export const WalletCardAsset = ({ border, subheading, }: WalletCardAssetProps) => { - const [integers, decimalsPlaces] = useNumberParts(balance, decimals); + const [integers, decimalsPlaces, separator] = useNumberParts( + balance, + decimals + ); return (
@@ -132,7 +138,10 @@ export const WalletCardAsset = ({
- {integers}. + + {integers} + {separator} + {decimalsPlaces}
diff --git a/apps/governance/src/i18n/translations/dev.json b/apps/governance/src/i18n/translations/dev.json index f8b65b066..95a131119 100644 --- a/apps/governance/src/i18n/translations/dev.json +++ b/apps/governance/src/i18n/translations/dev.json @@ -201,6 +201,8 @@ "STATE_WAITING_FOR_NODE_VOTE": "Waiting for node vote", "UpdateNetworkParameter": "Network parameter", "NewFreeform": "Freeform", + "setToPass": "Set to pass", + "setToFail": "Set to fail", "tokenVotes": "Token votes", "liquidityVotes": "Liquidity votes", "castYourVote": "Cast your vote", @@ -209,13 +211,23 @@ "against": "Against", "majorityRequired": "Majority Required", "participation": "Participation", - "met": "Met", - "notMet": "Not Met", + "majorityThreshold": "majority threshold", + "participationThreshold": "participation threshold", + "met": "met", + "notMet": "not met", "governanceRequired": "Required", "daysLeft": "{{daysLeft}} left to vote.", "toVote": "to vote", "voteFor": "Vote for", "voteAgainst": "Vote against", + "tokenVote": "Token vote", + "tokenVotesFor": "Token votes for", + "tokenVotesAgainst": "Token votes against", + "totalTokensVoted": "Total tokens voted", + "liquidityProviderVote": "Liquidity provider vote", + "liquidityProviderVotesFor": "LP votes for", + "liquidityProviderVotesAgainst": "LP votes against", + "totalLiquidityProviderTokensVoted": "Total LP tokens voted", "votingThresholdInfo": "If the token vote passes the participation threshold it will be the deciding vote. If not, the outcome will be determined by liquidity providers on this market.", "noGovernanceTokens": "You need some VEGA tokens to participate in governance", "youVoted": "You voted", diff --git a/apps/governance/src/routes/home/index.tsx b/apps/governance/src/routes/home/index.tsx index 064bdabb2..f6c8b4f69 100644 --- a/apps/governance/src/routes/home/index.tsx +++ b/apps/governance/src/routes/home/index.tsx @@ -31,10 +31,6 @@ import { orderByDate, orderByUpgradeBlockHeight, } from '../proposals/components/proposals-list/proposals-list'; -import { - NetworkParams, - useNetworkParams, -} from '@vegaprotocol/network-parameters'; import { BigNumber } from '../../lib/bignumber'; const nodesToShow = 6; @@ -47,57 +43,34 @@ const HomeProposals = ({ protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[]; }) => { const { t } = useTranslation(); - const { - params: networkParams, - loading: networkParamsLoading, - error: networkParamsError, - } = useNetworkParams([ - NetworkParams.governance_proposal_market_requiredMajority, - NetworkParams.governance_proposal_updateMarket_requiredMajority, - NetworkParams.governance_proposal_updateMarket_requiredMajorityLP, - NetworkParams.governance_proposal_asset_requiredMajority, - NetworkParams.governance_proposal_updateAsset_requiredMajority, - NetworkParams.governance_proposal_updateNetParam_requiredMajority, - NetworkParams.governance_proposal_freeform_requiredMajority, - ]); return ( - -
- -

{t('homeProposalsIntro')}

-
- - {t(`readMoreGovernance`)} - -
+
+ +

{t('homeProposalsIntro')}

+
+ + {t(`readMoreGovernance`)} + +
- -
    - {protocolUpgradeProposals.map((proposal, index) => ( - - ))} + +
      + {protocolUpgradeProposals.map((proposal, index) => ( + + ))} - {proposals.map((proposal) => ( - - ))} -
    + {proposals.map((proposal) => ( + + ))} +
-
- - - -
-
- +
+ + + +
+
); }; diff --git a/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.spec.tsx b/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.spec.tsx index be59889e9..def3377e3 100644 --- a/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.spec.tsx +++ b/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.spec.tsx @@ -16,7 +16,6 @@ import { ProposalHeader } from './proposal-header'; import { lastWeek, nextWeek, - mockNetworkParams, mockWalletContext, createUserVoteQueryMock, } from '../../test-helpers/mocks'; @@ -48,7 +47,6 @@ const renderComponent = ( diff --git a/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.tsx b/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.tsx index 8507212e5..c6b968566 100644 --- a/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.tsx +++ b/apps/governance/src/routes/proposals/components/proposal-detail-header/proposal-header.tsx @@ -8,22 +8,19 @@ import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import { truncateMiddle } from '../../../../lib/truncate-middle'; import { CurrentProposalState } from '../current-proposal-state'; import { ProposalInfoLabel } from '../proposal-info-label'; -import { ProposalVotingStatus } from '../proposal-voting-status'; -import type { NetworkParamsResult } from '@vegaprotocol/network-parameters'; import { useSuccessorMarketProposalDetails } from '@vegaprotocol/proposals'; import { FLAGS } from '@vegaprotocol/environment'; import Routes from '../../../routes'; import { Link } from 'react-router-dom'; import type { VoteState } from '../vote-details/use-user-vote'; +import { VoteBreakdown } from '../vote-breakdown'; export const ProposalHeader = ({ proposal, - networkParams, isListItem = true, voteState, }: { proposal: ProposalFieldsFragment | ProposalQuery['proposal']; - networkParams: Partial; isListItem?: boolean; voteState?: VoteState | null; }) => { @@ -146,7 +143,7 @@ export const ProposalHeader = ({ className="flex items-center gap-2" data-testid={`user-voted-${voteState.toLowerCase()}`} > -
+
@@ -185,7 +182,7 @@ export const ProposalHeader = ({
)} - + ); }; diff --git a/apps/governance/src/routes/proposals/components/proposal-votes-table/index.tsx b/apps/governance/src/routes/proposals/components/proposal-votes-table/index.tsx deleted file mode 100644 index cdae0a4e4..000000000 --- a/apps/governance/src/routes/proposals/components/proposal-votes-table/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export { ProposalVotesTable } from './proposal-votes-table'; diff --git a/apps/governance/src/routes/proposals/components/proposal-votes-table/proposal-votes-table.spec.tsx b/apps/governance/src/routes/proposals/components/proposal-votes-table/proposal-votes-table.spec.tsx deleted file mode 100644 index 6edce2761..000000000 --- a/apps/governance/src/routes/proposals/components/proposal-votes-table/proposal-votes-table.spec.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { render, screen, fireEvent } from '@testing-library/react'; -import { MockedProvider } from '@apollo/client/testing'; -import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider'; -import { ProposalVotesTable } from './proposal-votes-table'; -import { ProposalType } from '../proposal/proposal'; -import { - generateNoVotes, - generateProposal, - generateYesVotes, -} from '../../test-helpers/generate-proposals'; - -const defaultProposal = generateProposal(); -const defaultProposalType = ProposalType.PROPOSAL_NETWORK_PARAMETER; -const updateMarketProposal = generateProposal({ - terms: { - change: { - __typename: 'UpdateMarket', - marketId: '12345', - }, - }, - votes: { - __typename: 'ProposalVotes', - yes: generateYesVotes(10), - no: generateNoVotes(0), - }, -}); -const updateMarketProposalType = ProposalType.PROPOSAL_UPDATE_MARKET; - -const renderComponent = ( - proposal = defaultProposal, - proposalType = defaultProposalType -) => - render( - - - - - - ); - -describe('Proposal Votes Table', () => { - it('should render successfully', () => { - const { baseElement } = renderComponent(); - expect(baseElement).toBeTruthy(); - }); - - it('should show vote breakdown fields, excluding custom update market fields', () => { - renderComponent(); - fireEvent.click(screen.getByTestId('vote-breakdown-toggle')); - expect(screen.getByText('Expected to pass')).toBeInTheDocument(); - expect(screen.getByText('Token majority met')).toBeInTheDocument(); - expect(screen.getByText('Token participation met')).toBeInTheDocument(); - expect(screen.getByText('Tokens for proposal')).toBeInTheDocument(); - expect(screen.getByText('Total Supply')).toBeInTheDocument(); - expect(screen.getByText('Tokens against proposal')).toBeInTheDocument(); - expect(screen.getByText('Participation required')).toBeInTheDocument(); - expect(screen.getByText('Majority Required')).toBeInTheDocument(); - expect(screen.getByText('Number of voting parties')).toBeInTheDocument(); - expect(screen.getByText('Total tokens voted')).toBeInTheDocument(); - expect( - screen.getByText('Total tokens voted percentage') - ).toBeInTheDocument(); - expect(screen.getByText('Number of votes for')).toBeInTheDocument(); - expect(screen.getByText('Number of votes against')).toBeInTheDocument(); - expect(screen.getByText('Yes percentage')).toBeInTheDocument(); - expect(screen.getByText('No percentage')).toBeInTheDocument(); - expect(screen.queryByText('Liquidity majority met')).toBeNull(); - expect(screen.queryByText('Liquidity participation met')).toBeNull(); - expect(screen.queryByText('Liquidity shares for proposal')).toBeNull(); - }); - - it('displays different breakdown fields for update market proposal', () => { - renderComponent(updateMarketProposal, updateMarketProposalType); - fireEvent.click(screen.getByTestId('vote-breakdown-toggle')); - expect(screen.getByText('Liquidity majority met')).toBeInTheDocument(); - expect(screen.getByText('Liquidity participation met')).toBeInTheDocument(); - expect( - screen.getByText('Liquidity shares for proposal') - ).toBeInTheDocument(); - expect(screen.queryByText('Number of voting parties')).toBeNull(); - expect(screen.queryByText('Total tokens voted')).toBeNull(); - expect(screen.queryByText('Total tokens voted percentage')).toBeNull(); - expect(screen.queryByText('Number of votes for')).toBeNull(); - expect(screen.queryByText('Number of votes against')).toBeNull(); - expect(screen.queryByText('Yes percentage')).toBeNull(); - expect(screen.queryByText('No percentage')).toBeNull(); - }); - - it('displays if an update market proposal will pass by token vote', () => { - renderComponent(updateMarketProposal, updateMarketProposalType); - fireEvent.click(screen.getByTestId('vote-breakdown-toggle')); - expect(screen.getByText('👍 by token vote')).toBeInTheDocument(); - }); - - it('displays if an update market proposal will pass by LP vote', () => { - renderComponent( - generateProposal({ - terms: { - change: { - __typename: 'UpdateMarket', - marketId: '12345', - }, - }, - votes: { - __typename: 'ProposalVotes', - yes: { - ...generateYesVotes(0, 1, '10'), - }, - no: { - ...generateNoVotes(0, 1, '0'), - }, - }, - }), - updateMarketProposalType - ); - fireEvent.click(screen.getByTestId('vote-breakdown-toggle')); - expect(screen.getByText('👍 by liquidity vote')).toBeInTheDocument(); - }); -}); diff --git a/apps/governance/src/routes/proposals/components/proposal-votes-table/proposal-votes-table.tsx b/apps/governance/src/routes/proposals/components/proposal-votes-table/proposal-votes-table.tsx deleted file mode 100644 index e1d2af14a..000000000 --- a/apps/governance/src/routes/proposals/components/proposal-votes-table/proposal-votes-table.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { - KeyValueTable, - KeyValueTableRow, - Thumbs, - RoundedWrapper, -} from '@vegaprotocol/ui-toolkit'; -import { formatNumber, formatNumberPercentage } from '@vegaprotocol/utils'; -import { SubHeading } from '../../../../components/heading'; -import { useVoteInformation } from '../../hooks'; -import { useAppState } from '../../../../contexts/app-state/app-state-context'; -import { ProposalType } from '../proposal/proposal'; -import { CollapsibleToggle } from '../../../../components/collapsible-toggle'; -import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; -import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; - -interface ProposalVotesTableProps { - proposal: ProposalFieldsFragment | ProposalQuery['proposal']; - proposalType: ProposalType | null; -} - -export const ProposalVotesTable = ({ - proposal, - proposalType, -}: ProposalVotesTableProps) => { - const { t } = useTranslation(); - const { - appState: { totalSupply }, - } = useAppState(); - const [showDetails, setShowDetails] = useState(false); - const { - willPassByTokenVote, - willPassByLPVote, - totalTokensPercentage, - participationMet, - participationLPMet, - totalTokensVoted, - noPercentage, - yesPercentage, - noTokens, - yesTokens, - yesEquityLikeShareWeight, - yesVotes, - noVotes, - totalVotes, - requiredMajorityPercentage, - requiredParticipation, - majorityMet, - majorityLPMet, - } = useVoteInformation({ proposal }); - - const isUpdateMarket = proposalType === ProposalType.PROPOSAL_UPDATE_MARKET; - const updateMarketWillPass = willPassByTokenVote || willPassByLPVote; - const updateMarketVotePassMethod = willPassByTokenVote - ? t('byTokenVote') - : t('byLiquidityVote'); - - return ( - <> - - - - - {showDetails && ( - - - - {t('expectedToPass')} - {isUpdateMarket ? ( - updateMarketWillPass ? ( - - ) : ( - - ) - ) : willPassByTokenVote ? ( - - ) : ( - - )} - - - {t('majorityMet')} - {majorityMet ? : } - - {isUpdateMarket && ( - - {t('majorityLPMet')} - {majorityLPMet ? : } - - )} - - {t('participationMet')} - {participationMet ? : } - - {isUpdateMarket && ( - - {t('participationLPMet')} - {participationLPMet ? ( - - ) : ( - - )} - - )} - - {t('tokenForProposal')} - {formatNumber(yesTokens, 2)} - - {isUpdateMarket && ( - - {t('tokenLPForProposal')} - {formatNumber(yesEquityLikeShareWeight, 2)} - - )} - - {t('totalSupply')} - {formatNumber(totalSupply, 2)} - - - {t('tokensAgainstProposal')} - {formatNumber(noTokens, 2)} - - - {t('participationRequired')} - {formatNumberPercentage(requiredParticipation)} - - - {t('majorityRequired')} - {formatNumberPercentage(requiredMajorityPercentage)} - - {!isUpdateMarket && ( - <> - - {t('numberOfVotingParties')} - {formatNumber(totalVotes, 0)} - - - {t('totalTokensVotes')} - {formatNumber(totalTokensVoted, 2)} - - - {t('totalTokenVotedPercentage')} - {formatNumberPercentage(totalTokensPercentage, 2)} - - - {t('numberOfForVotes')} - {formatNumber(yesVotes, 0)} - - - {t('numberOfAgainstVotes')} - {formatNumber(noVotes, 0)} - - - {t('yesPercentage')} - {formatNumberPercentage(yesPercentage, 2)} - - - {t('noPercentage')} - {formatNumberPercentage(noPercentage, 2)} - - - )} - - - )} - - ); -}; diff --git a/apps/governance/src/routes/proposals/components/proposal-voting-status/index.tsx b/apps/governance/src/routes/proposals/components/proposal-voting-status/index.tsx deleted file mode 100644 index bb9ad609b..000000000 --- a/apps/governance/src/routes/proposals/components/proposal-voting-status/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './proposal-voting-status'; diff --git a/apps/governance/src/routes/proposals/components/proposal-voting-status/proposal-voting-status.spec.tsx b/apps/governance/src/routes/proposals/components/proposal-voting-status/proposal-voting-status.spec.tsx deleted file mode 100644 index 0fee11edc..000000000 --- a/apps/governance/src/routes/proposals/components/proposal-voting-status/proposal-voting-status.spec.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import { BrowserRouter as Router } from 'react-router-dom'; -import { MockedProvider } from '@apollo/client/testing'; -import { VegaWalletContext } from '@vegaprotocol/wallet'; -import { - lastWeek, - mockWalletContext, - networkParamsQueryMock, - nextWeek, - mockNetworkParams, -} from '../../test-helpers/mocks'; -import { ProposalVotingStatus } from './proposal-voting-status'; -import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; -import type { MockedResponse } from '@apollo/client/testing'; -import { - generateNoVotes, - generateProposal, - generateYesVotes, -} from '../../test-helpers/generate-proposals'; -import { ProposalState } from '@vegaprotocol/types'; -import { BigNumber } from '../../../../lib/bignumber'; -import type { AppState } from '../../../../contexts/app-state/app-state-context'; - -const mockTotalSupply = new BigNumber(100); -// Note - giving a fixedTokenValue of 1 means a ratio of 1:1 votes to tokens, making sums easier :) -const fixedTokenValue = 1000000000000000000; - -const mockAppState: AppState = { - totalAssociated: new BigNumber('50063005'), - decimals: 18, - totalSupply: mockTotalSupply, - vegaWalletManageOverlay: false, - transactionOverlay: false, - bannerMessage: '', - disconnectNotice: false, -}; - -jest.mock('../../../../contexts/app-state/app-state-context', () => ({ - useAppState: () => ({ - appState: mockAppState, - }), -})); - -const renderComponent = ( - proposal: ProposalQuery['proposal'], - // eslint-disable-next-line @typescript-eslint/no-explicit-any - mocks: MockedResponse[] = [networkParamsQueryMock] -) => - render( - - - - - - - - ); - -describe('ProposalVotingStatus', () => { - beforeAll(() => { - jest.useFakeTimers(); - jest.setSystemTime(0); - }); - afterAll(() => { - jest.useRealTimers(); - }); - - it('Renders majority reached', () => { - const yesVotes = 100; - const noVotes = 0; - - renderComponent( - generateProposal({ - state: ProposalState.STATE_PASSED, - terms: { - closingDatetime: lastWeek.toString(), - enactmentDatetime: nextWeek.toString(), - }, - votes: { - __typename: 'ProposalVotes', - yes: generateYesVotes(yesVotes, fixedTokenValue), - no: generateNoVotes(noVotes, fixedTokenValue), - }, - }) - ); - expect(screen.getByTestId('majority-reached')).toBeInTheDocument(); - }); - - it('Renders majority not reached', () => { - const yesVotes = 20; - const noVotes = 80; - - renderComponent( - generateProposal({ - state: ProposalState.STATE_PASSED, - terms: { - closingDatetime: lastWeek.toString(), - enactmentDatetime: nextWeek.toString(), - }, - votes: { - __typename: 'ProposalVotes', - yes: generateYesVotes(yesVotes, fixedTokenValue), - no: generateNoVotes(noVotes, fixedTokenValue), - }, - }) - ); - expect(screen.getByTestId('majority-not-reached')).toBeInTheDocument(); - }); - - it('Renders participation reached', () => { - const yesVotes = 1000; - const noVotes = 0; - - renderComponent( - generateProposal({ - state: ProposalState.STATE_PASSED, - terms: { - closingDatetime: lastWeek.toString(), - enactmentDatetime: nextWeek.toString(), - }, - votes: { - __typename: 'ProposalVotes', - yes: generateYesVotes(yesVotes, fixedTokenValue), - no: generateNoVotes(noVotes, fixedTokenValue), - }, - }) - ); - expect(screen.getByTestId('participation-reached')).toBeInTheDocument(); - }); - - it('Renders participation not reached', () => { - const yesVotes = 0; - const noVotes = 0; - - renderComponent( - generateProposal({ - terms: { - closingDatetime: lastWeek.toString(), - enactmentDatetime: nextWeek.toString(), - }, - votes: { - __typename: 'ProposalVotes', - yes: generateYesVotes(yesVotes, fixedTokenValue), - no: generateNoVotes(noVotes, fixedTokenValue), - }, - }) - ); - expect(screen.getByTestId('participation-not-reached')).toBeInTheDocument(); - }); -}); diff --git a/apps/governance/src/routes/proposals/components/proposal-voting-status/proposal-voting-status.tsx b/apps/governance/src/routes/proposals/components/proposal-voting-status/proposal-voting-status.tsx deleted file mode 100644 index a00600a9d..000000000 --- a/apps/governance/src/routes/proposals/components/proposal-voting-status/proposal-voting-status.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import classNames from 'classnames'; -import { useTranslation } from 'react-i18next'; -import { Icon } from '@vegaprotocol/ui-toolkit'; -import { useVoteInformation } from '../../hooks'; -import { BigNumber } from '../../../../lib/bignumber'; -import type { NetworkParamsResult } from '@vegaprotocol/network-parameters'; -import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; -import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; - -const statusClasses = (reached: boolean) => - classNames('flex items-center gap-2 px-4 py-2 rounded-md', { - 'bg-vega-green-700': reached, - 'bg-vega-red-700': !reached, - }); - -const MajorityStatus = ({ - reached, - requiredMajority, -}: { - reached: boolean; - requiredMajority: string | null | undefined; -}) => { - const { t } = useTranslation(); - - return ( -
- {reached ? : } - {reached ? ( -
- {requiredMajority ? ( - <> - {new BigNumber(requiredMajority).times(100).toString()}%{' '} - {t('majorityVotedForProposal')} - - ) : ( - t('requiredMajorityVotedForProposal') - )} -
- ) : ( -
- {requiredMajority ? ( - <> - {new BigNumber(requiredMajority).times(100).toString()}%{' '} - {t('majorityNotVotedForProposal')} - - ) : ( - t('requiredMajorityNotVotedForProposal') - )} -
- )} -
- ); -}; - -const ParticipationStatus = ({ reached }: { reached: boolean }) => { - const { t } = useTranslation(); - - return ( -
- {reached ? ( - <> - -
- {t('minParticipationReached')} -
- - ) : ( - <> - -
- {t('minParticipationNotReached')} -
- - )} -
- ); -}; - -export const ProposalVotingStatus = ({ - proposal, - networkParams, -}: { - proposal: ProposalFieldsFragment | ProposalQuery['proposal']; - networkParams: Partial; -}) => { - const { t } = useTranslation(); - const { majorityMet, majorityLPMet, participationMet, participationLPMet } = - useVoteInformation({ - proposal, - }); - - if (!proposal) { - return null; - } - - const isUpdateMarket = proposal?.terms.change.__typename === 'UpdateMarket'; - - let requiredVotingMajority = null; - let requiredVotingMajorityLP = null; - - if (networkParams) { - switch (proposal.terms.change.__typename) { - case 'NewMarket': - requiredVotingMajority = - networkParams.governance_proposal_market_requiredMajority; - break; - case 'UpdateMarket': - requiredVotingMajority = - networkParams.governance_proposal_updateMarket_requiredMajority; - requiredVotingMajorityLP = - networkParams.governance_proposal_updateMarket_requiredMajorityLP; - break; - case 'NewAsset': - requiredVotingMajority = - networkParams.governance_proposal_asset_requiredMajority; - break; - case 'UpdateAsset': - requiredVotingMajority = - networkParams.governance_proposal_updateAsset_requiredMajority; - break; - case 'UpdateNetworkParameter': - requiredVotingMajority = - networkParams.governance_proposal_updateNetParam_requiredMajority; - break; - case 'NewFreeform': - requiredVotingMajority = - networkParams.governance_proposal_freeform_requiredMajority; - break; - } - } - - if (isUpdateMarket) { - return ( -
-

{t('Token vote')}

-
- {' '} - -
- -

{t('Liquidity provider vote')}

-
- {' '} - -
-
- ); - } - - return ( -
- {' '} - -
- ); -}; diff --git a/apps/governance/src/routes/proposals/components/proposal/proposal.spec.tsx b/apps/governance/src/routes/proposals/components/proposal/proposal.spec.tsx index db81de7a8..dceb58bd9 100644 --- a/apps/governance/src/routes/proposals/components/proposal/proposal.spec.tsx +++ b/apps/governance/src/routes/proposals/components/proposal/proposal.spec.tsx @@ -34,12 +34,6 @@ jest.mock('../proposal-change-table', () => ({ jest.mock('../proposal-json', () => ({ ProposalJson: () =>
, })); -jest.mock('../proposal-votes-table', () => ({ - ProposalVotesTable: () =>
, -})); -jest.mock('../vote-details', () => ({ - VoteDetails: () =>
, -})); jest.mock('../list-asset', () => ({ ListAsset: () =>
, })); @@ -104,8 +98,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-votes-table')).toBeInTheDocument(); - expect(screen.getByTestId('proposal-vote-details')).toBeInTheDocument(); expect(screen.queryByTestId('proposal-list-asset')).not.toBeInTheDocument(); }); diff --git a/apps/governance/src/routes/proposals/components/proposal/proposal.tsx b/apps/governance/src/routes/proposals/components/proposal/proposal.tsx index 64790df1f..ebd3b0da7 100644 --- a/apps/governance/src/routes/proposals/components/proposal/proposal.tsx +++ b/apps/governance/src/routes/proposals/components/proposal/proposal.tsx @@ -5,9 +5,8 @@ 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 { ProposalVotesTable } from '../proposal-votes-table'; import { ProposalAssetDetails } from '../proposal-asset-details'; -import { VoteDetails } from '../vote-details'; +import { UserVote } from '../vote-details'; import { ListAsset } from '../list-asset'; import Routes from '../../../routes'; import { ProposalMarketData } from '../proposal-market-data'; @@ -22,14 +21,6 @@ import type { NetworkParamsResult } from '@vegaprotocol/network-parameters'; import { useVoteSubmit } from '@vegaprotocol/proposals'; import { useUserVote } from '../vote-details/use-user-vote'; -export enum ProposalType { - PROPOSAL_NEW_MARKET = 'PROPOSAL_NEW_MARKET', - PROPOSAL_UPDATE_MARKET = 'PROPOSAL_UPDATE_MARKET', - PROPOSAL_NEW_ASSET = 'PROPOSAL_NEW_ASSET', - PROPOSAL_UPDATE_ASSET = 'PROPOSAL_UPDATE_ASSET', - PROPOSAL_NETWORK_PARAMETER = 'PROPOSAL_NETWORK_PARAMETER', - PROPOSAL_FREEFORM = 'PROPOSAL_FREEFORM', -} export interface ProposalProps { proposal: ProposalFieldsFragment | ProposalQuery['proposal']; networkParams: Partial; @@ -80,39 +71,32 @@ export const Proposal = ({ } let minVoterBalance = null; - let proposalType = null; if (networkParams) { switch (proposal.terms.change.__typename) { case 'NewMarket': minVoterBalance = networkParams.governance_proposal_market_minVoterBalance; - proposalType = ProposalType.PROPOSAL_NEW_MARKET; break; case 'UpdateMarket': minVoterBalance = networkParams.governance_proposal_updateMarket_minVoterBalance; - proposalType = ProposalType.PROPOSAL_UPDATE_MARKET; break; case 'NewAsset': minVoterBalance = networkParams.governance_proposal_asset_minVoterBalance; - proposalType = ProposalType.PROPOSAL_NEW_ASSET; break; case 'UpdateAsset': minVoterBalance = networkParams.governance_proposal_updateAsset_minVoterBalance; - proposalType = ProposalType.PROPOSAL_UPDATE_ASSET; break; case 'UpdateNetworkParameter': minVoterBalance = networkParams.governance_proposal_updateNetParam_minVoterBalance; - proposalType = ProposalType.PROPOSAL_NETWORK_PARAMETER; break; case 'NewFreeform': minVoterBalance = networkParams.governance_proposal_freeform_minVoterBalance; - proposalType = ProposalType.PROPOSAL_FREEFORM; break; } } @@ -140,91 +124,81 @@ export const Proposal = ({ -
-
- -
- - {proposal.terms.change.__typename === 'NewAsset' && - proposal.terms.change.source.__typename === 'ERC20' && - proposal.id ? ( - - ) : null} - -
- -
- - {newMarketData && ( -
- -
- )} - - {proposal.terms.change.__typename === 'UpdateMarket' && ( -
- -
- )} - - {(proposal.terms.change.__typename === 'NewAsset' || - proposal.terms.change.__typename === 'UpdateAsset') && - asset && ( -
- -
- )} - -
- -
+
+
-
-
- - - -
+ {proposal.terms.change.__typename === 'NewAsset' && + proposal.terms.change.source.__typename === 'ERC20' && + proposal.id ? ( + + ) : null} +
+ +
+ + {newMarketData && (
- +
+ )} + + {proposal.terms.change.__typename === 'UpdateMarket' && ( +
+ +
+ )} + + {(proposal.terms.change.__typename === 'NewAsset' || + proposal.terms.change.__typename === 'UpdateAsset') && + asset && ( +
+ +
+ )} + +
+ + + +
+ +
+
); diff --git a/apps/governance/src/routes/proposals/components/proposals-list-item/proposals-list-item-details.spec.tsx b/apps/governance/src/routes/proposals/components/proposals-list-item/proposals-list-item-details.spec.tsx index 6092f66e5..bbc0edb46 100644 --- a/apps/governance/src/routes/proposals/components/proposals-list-item/proposals-list-item-details.spec.tsx +++ b/apps/governance/src/routes/proposals/components/proposals-list-item/proposals-list-item-details.spec.tsx @@ -6,11 +6,7 @@ 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 { - generateNoVotes, - generateProposal, - generateYesVotes, -} from '../../test-helpers/generate-proposals'; +import { generateProposal } from '../../test-helpers/generate-proposals'; import { ProposalsListItemDetails } from './proposals-list-item-details'; import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats'; import { @@ -93,84 +89,6 @@ describe('Proposals list item details', () => { ); }); - it('Renders proposal state: Update market proposal - Currently expected to pass by LP vote', () => { - renderComponent( - generateProposal({ - state: ProposalState.STATE_OPEN, - terms: { - change: { - __typename: 'UpdateMarket', - }, - }, - votes: { - yes: { - ...generateYesVotes(0), - totalEquityLikeShareWeight: '1000', - }, - no: { - ...generateNoVotes(0), - totalEquityLikeShareWeight: '0', - }, - }, - }) - ); - expect(screen.getByTestId('vote-status')).toHaveTextContent( - 'Currently expected to pass by LP vote' - ); - }); - - it('Renders proposal state: Update market proposal - Currently expected to pass by token vote', () => { - renderComponent( - generateProposal({ - state: ProposalState.STATE_OPEN, - terms: { - change: { - __typename: 'UpdateMarket', - }, - }, - votes: { - yes: { - ...generateYesVotes(1000, 1000), - totalEquityLikeShareWeight: '0', - }, - no: { - ...generateNoVotes(0), - totalEquityLikeShareWeight: '0', - }, - }, - }) - ); - expect(screen.getByTestId('vote-status')).toHaveTextContent( - 'Currently expected to pass by token vote' - ); - }); - - it('Renders proposal state: Update market proposal - Currently expected to fail', () => { - renderComponent( - generateProposal({ - state: ProposalState.STATE_OPEN, - terms: { - change: { - __typename: 'UpdateMarket', - }, - }, - votes: { - yes: { - ...generateYesVotes(0), - totalEquityLikeShareWeight: '0', - }, - no: { - ...generateNoVotes(0), - totalEquityLikeShareWeight: '0', - }, - }, - }) - ); - expect(screen.getByTestId('vote-status')).toHaveTextContent( - 'Currently expected to fail' - ); - }); - it('Renders proposal state: Open - 5 minutes left to vote', () => { renderComponent( generateProposal({ @@ -213,43 +131,6 @@ describe('Proposals list item details', () => { ); }); - it('Renders proposal state: Open - majority not reached', () => { - renderComponent( - generateProposal({ - state: ProposalState.STATE_OPEN, - terms: { - enactmentDatetime: nextWeek.toString(), - }, - votes: { - no: generateNoVotes(1, 1000000000000000000), - yes: generateYesVotes(1, 1000000000000000000), - }, - }) - ); - expect(screen.getByTestId('vote-status')).toHaveTextContent( - 'Currently expected to fail' - ); - }); - - it('Renders proposal state: Open - will pass', () => { - renderComponent( - generateProposal({ - state: ProposalState.STATE_OPEN, - votes: { - __typename: 'ProposalVotes', - yes: generateYesVotes(3000, 1000000000000000000), - no: generateNoVotes(0), - }, - terms: { - closingDatetime: nextWeek.toString(), - }, - }) - ); - expect(screen.getByTestId('vote-status')).toHaveTextContent( - 'Currently expected to pass' - ); - }); - it('Renders proposal state: Rejected', () => { renderComponent( generateProposal({ diff --git a/apps/governance/src/routes/proposals/components/proposals-list-item/proposals-list-item-details.tsx b/apps/governance/src/routes/proposals/components/proposals-list-item/proposals-list-item-details.tsx index 97ab568c4..4cb854c62 100644 --- a/apps/governance/src/routes/proposals/components/proposals-list-item/proposals-list-item-details.tsx +++ b/apps/governance/src/routes/proposals/components/proposals-list-item/proposals-list-item-details.tsx @@ -11,7 +11,6 @@ import { import Routes from '../../../routes'; import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; -import { useVoteInformation } from '../../hooks'; export const ProposalsListItemDetails = ({ proposal, @@ -20,18 +19,10 @@ export const ProposalsListItemDetails = ({ }) => { const { t } = useTranslation(); const state = proposal?.state; - const { willPassByTokenVote, willPassByLPVote } = useVoteInformation({ - proposal, - }); - const updateMarketWillPass = willPassByTokenVote || willPassByLPVote; - const updateMarketVotePassMethod = willPassByTokenVote - ? t('byTokenVote') - : t('byLPVote'); const nowToEnactmentInHours = differenceInHours( new Date(proposal?.terms.closingDatetime), new Date() ); - const isUpdateMarket = proposal?.terms.change.__typename === 'UpdateMarket'; let voteDetails: ReactNode; let voteStatus: ReactNode; @@ -78,31 +69,11 @@ export const ProposalsListItemDetails = ({ } case ProposalState.STATE_OPEN: { voteDetails = ( - + {formatDistanceToNowStrict(new Date(proposal?.terms.closingDatetime))}{' '} {t('left to vote')} ); - voteStatus = - (isUpdateMarket && - (updateMarketWillPass ? ( - <> - {t('currentlySetTo')} {t('pass')} {updateMarketVotePassMethod} - - ) : ( - <> - {t('currentlySetTo')} {t('fail')} - - ))) || - (willPassByTokenVote ? ( - <> - {t('currentlySetTo')} {t('pass')} - - ) : ( - <> - {t('currentlySetTo')} {t('fail')} - - )); break; } case ProposalState.STATE_REJECTED: { diff --git a/apps/governance/src/routes/proposals/components/proposals-list-item/proposals-list-item.tsx b/apps/governance/src/routes/proposals/components/proposals-list-item/proposals-list-item.tsx index ba9664323..815aa3f3f 100644 --- a/apps/governance/src/routes/proposals/components/proposals-list-item/proposals-list-item.tsx +++ b/apps/governance/src/routes/proposals/components/proposals-list-item/proposals-list-item.tsx @@ -4,28 +4,19 @@ import { ProposalsListItemDetails } from './proposals-list-item-details'; import { useUserVote } from '../vote-details/use-user-vote'; import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; -import type { NetworkParamsResult } from '@vegaprotocol/network-parameters'; interface ProposalsListItemProps { proposal?: ProposalFieldsFragment | ProposalQuery['proposal'] | null; - networkParams: Partial | null; } -export const ProposalsListItem = ({ - proposal, - networkParams, -}: ProposalsListItemProps) => { +export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => { const { voteState } = useUserVote(proposal?.id); - if (!proposal || !proposal.id || !networkParams) return null; + if (!proposal || !proposal.id) return null; return (
  • - +
  • diff --git a/apps/governance/src/routes/proposals/components/proposals-list/proposals-list.tsx b/apps/governance/src/routes/proposals/components/proposals-list/proposals-list.tsx index 68484bcbc..4cfac6cc3 100644 --- a/apps/governance/src/routes/proposals/components/proposals-list/proposals-list.tsx +++ b/apps/governance/src/routes/proposals/components/proposals-list/proposals-list.tsx @@ -8,7 +8,6 @@ import { ProtocolUpgradeProposalsListItem } from '../protocol-upgrade-proposals- import { ProposalsListFilter } from '../proposals-list-filter'; import Routes from '../../../routes'; import { - AsyncRenderer, Button, Toggle, VegaIcon, @@ -20,10 +19,6 @@ import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals'; import { DocsLinks, ExternalLinks } from '@vegaprotocol/environment'; -import { - NetworkParams, - useNetworkParams, -} from '@vegaprotocol/network-parameters'; interface ProposalsListProps { proposals: Array; @@ -75,20 +70,6 @@ export const ProposalsList = ({ lastBlockHeight, }: ProposalsListProps) => { const { t } = useTranslation(); - const { - params: networkParams, - loading: networkParamsLoading, - error: networkParamsError, - } = useNetworkParams([ - NetworkParams.governance_proposal_market_requiredMajority, - NetworkParams.governance_proposal_updateMarket_requiredMajority, - NetworkParams.governance_proposal_updateMarket_requiredMajorityLP, - NetworkParams.governance_proposal_asset_requiredMajority, - NetworkParams.governance_proposal_updateAsset_requiredMajority, - NetworkParams.governance_proposal_updateNetParam_requiredMajority, - NetworkParams.governance_proposal_freeform_requiredMajority, - ]); - const [filterString, setFilterString] = useState(''); const [closedProposalsView, setClosedProposalsView] = useState( @@ -153,181 +134,166 @@ export const ProposalsList = ({ p?.party?.id?.toString().includes(filterString); return ( - -
    -
    - +
    +
    + - {DocsLinks && ( -
    - - - -
    - )} -
    - -

    - {t( - `The Vega network is governed by the community. View active proposals, vote on them or propose changes to the network. Network upgrades are proposed and approved by validators.` - )}{' '} - - {t(`Find out more about Vega governance`)} - -

    - - {proposals.length > 0 && ( - { - setFilterString(value); - if (value.length > 0) { - // If the filter is engaged, ensure the user is viewing governance proposals, - // as network upgrades do not have IDs to filter by and will be excluded. - setClosedProposalsView( - ClosedProposalsViewOptions.NetworkGovernance - ); - } - }} - /> + {DocsLinks && ( +
    + + + +
    )} +
    -
    - +

    + {t( + `The Vega network is governed by the community. View active proposals, vote on them or propose changes to the network. Network upgrades are proposed and approved by validators.` + )}{' '} + + {t(`Find out more about Vega governance`)} + +

    - {sortedProposals.open.length > 0 || - sortedProtocolUpgradeProposals.open.length > 0 ? ( -
      - {filterString.length < 1 && - sortedProtocolUpgradeProposals.open.map((proposal) => ( - - ))} + {proposals.length > 0 && ( + { + setFilterString(value); + if (value.length > 0) { + // If the filter is engaged, ensure the user is viewing governance proposals, + // as network upgrades do not have IDs to filter by and will be excluded. + setClosedProposalsView( + ClosedProposalsViewOptions.NetworkGovernance + ); + } + }} + /> + )} - {sortedProposals.open.filter(filterPredicate).map((proposal) => ( - + + + {sortedProposals.open.length > 0 || + sortedProtocolUpgradeProposals.open.length > 0 ? ( +
        + {filterString.length < 1 && + sortedProtocolUpgradeProposals.open.map((proposal) => ( + ))} -
      - ) : ( -

      - {t('noOpenProposals')} -

      - )} -
    -
    - - {sortedProposals.closed.length > 0 || - sortedProtocolUpgradeProposals.closed.length > 0 ? ( - <> - { - // We need both the closed proposals and closed protocol upgrade - // proposals to be present for there to be a toggle. It also gets - // hidden if the user has filtered the list, as the upgrade proposals - // do not have the necessary fields for filtering. - sortedProposals.closed.length > 0 && - sortedProtocolUpgradeProposals.closed.length > 0 && - filterString.length < 1 && ( -
    -
    - - setClosedProposalsView( - e.target.value as ClosedProposalsViewOptions - ) - } - /> -
    + {sortedProposals.open.filter(filterPredicate).map((proposal) => ( + + ))} + + ) : ( +

    + {t('noOpenProposals')} +

    + )} +
    + +
    + + {sortedProposals.closed.length > 0 || + sortedProtocolUpgradeProposals.closed.length > 0 ? ( + <> + { + // We need both the closed proposals and closed protocol upgrade + // proposals to be present for there to be a toggle. It also gets + // hidden if the user has filtered the list, as the upgrade proposals + // do not have the necessary fields for filtering. + sortedProposals.closed.length > 0 && + sortedProtocolUpgradeProposals.closed.length > 0 && + filterString.length < 1 && ( +
    +
    + + setClosedProposalsView( + e.target.value as ClosedProposalsViewOptions + ) + } + />
    - ) - } +
    + ) + } -
      - {closedProposalsView === - ClosedProposalsViewOptions.NetworkUpgrades && ( -
      - {sortedProtocolUpgradeProposals.closed.map((proposal) => ( - + {closedProposalsView === + ClosedProposalsViewOptions.NetworkUpgrades && ( +
      + {sortedProtocolUpgradeProposals.closed.map((proposal) => ( + + ))} +
      + )} + + {closedProposalsView === + ClosedProposalsViewOptions.NetworkGovernance && ( +
      + {sortedProposals.closed + .filter(filterPredicate) + .map((proposal) => ( + ))} -
      - )} +
      + )} +
    + + ) : ( +

    + {t('noClosedProposals')} +

    + )} +
    - {closedProposalsView === - ClosedProposalsViewOptions.NetworkGovernance && ( -
    - {sortedProposals.closed - .filter(filterPredicate) - .map((proposal) => ( - - ))} -
    - )} - - - ) : ( -

    - {t('noClosedProposals')} -

    - )} - - - - {t('seeRejectedProposals')} - -
    - + + {t('seeRejectedProposals')} + +
    ); }; diff --git a/apps/governance/src/routes/proposals/components/proposals-list/rejected-proposals-list.tsx b/apps/governance/src/routes/proposals/components/proposals-list/rejected-proposals-list.tsx index 31ae42f45..0b128df9d 100644 --- a/apps/governance/src/routes/proposals/components/proposals-list/rejected-proposals-list.tsx +++ b/apps/governance/src/routes/proposals/components/proposals-list/rejected-proposals-list.tsx @@ -1,15 +1,10 @@ import { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { AsyncRenderer } from '@vegaprotocol/ui-toolkit'; import { Heading } from '../../../../components/heading'; import { ProposalsListItem } from '../proposals-list-item'; import { ProposalsListFilter } from '../proposals-list-filter'; import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; -import { - NetworkParams, - useNetworkParams, -} from '@vegaprotocol/network-parameters'; interface ProposalsListProps { proposals: Array; @@ -17,19 +12,6 @@ interface ProposalsListProps { export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => { const { t } = useTranslation(); - const { - params: networkParams, - loading: networkParamsLoading, - error: networkParamsError, - } = useNetworkParams([ - NetworkParams.governance_proposal_market_requiredMajority, - NetworkParams.governance_proposal_updateMarket_requiredMajority, - NetworkParams.governance_proposal_updateMarket_requiredMajorityLP, - NetworkParams.governance_proposal_asset_requiredMajority, - NetworkParams.governance_proposal_updateAsset_requiredMajority, - NetworkParams.governance_proposal_updateNetParam_requiredMajority, - NetworkParams.governance_proposal_freeform_requiredMajority, - ]); const [filterString, setFilterString] = useState(''); const filterPredicate = ( @@ -39,11 +21,7 @@ export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => { p?.party?.id?.toString().includes(filterString); return ( - + <> { {proposals.length > 0 ? (
      {proposals.filter(filterPredicate).map((proposal) => ( - + ))}
    ) : ( @@ -66,6 +40,6 @@ export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {

    )} -
    + ); }; diff --git a/apps/governance/src/routes/proposals/components/vote-breakdown/index.ts b/apps/governance/src/routes/proposals/components/vote-breakdown/index.ts new file mode 100644 index 000000000..73b0db8e5 --- /dev/null +++ b/apps/governance/src/routes/proposals/components/vote-breakdown/index.ts @@ -0,0 +1 @@ +export * from './vote-breakdown'; diff --git a/apps/governance/src/routes/proposals/components/vote-breakdown/vote-breakdown.spec.tsx b/apps/governance/src/routes/proposals/components/vote-breakdown/vote-breakdown.spec.tsx new file mode 100644 index 000000000..623c81f67 --- /dev/null +++ b/apps/governance/src/routes/proposals/components/vote-breakdown/vote-breakdown.spec.tsx @@ -0,0 +1,348 @@ +import { render, screen } from '@testing-library/react'; +import { BrowserRouter as Router } from 'react-router-dom'; +import { MockedProvider } from '@apollo/client/testing'; +import { VegaWalletContext } from '@vegaprotocol/wallet'; +import { + lastWeek, + mockWalletContext, + networkParamsQueryMock, + nextWeek, +} from '../../test-helpers/mocks'; +import { VoteBreakdown } from './vote-breakdown'; +import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; +import type { MockedResponse } from '@apollo/client/testing'; +import { + generateNoVotes, + generateProposal, + generateYesVotes, +} from '../../test-helpers/generate-proposals'; +import { ProposalState } from '@vegaprotocol/types'; +import { BigNumber } from '../../../../lib/bignumber'; +import type { AppState } from '../../../../contexts/app-state/app-state-context'; + +const mockTotalSupply = new BigNumber(100); +// Note - giving a fixedTokenValue of 1 means a ratio of 1:1 votes to tokens, making sums easier :) +const fixedTokenValue = 1000000000000000000; + +const mockAppState: AppState = { + totalAssociated: new BigNumber('50063005'), + decimals: 18, + totalSupply: mockTotalSupply, + vegaWalletManageOverlay: false, + transactionOverlay: false, + bannerMessage: '', + disconnectNotice: false, +}; + +jest.mock('../../../../contexts/app-state/app-state-context', () => ({ + useAppState: () => ({ + appState: mockAppState, + }), +})); + +const renderComponent = ( + proposal: ProposalQuery['proposal'], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + mocks: MockedResponse[] = [networkParamsQueryMock] +) => + render( + + + + + + + + ); + +describe('VoteBreakdown', () => { + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(0); + }); + afterAll(() => { + jest.useRealTimers(); + }); + + it('Renders majority reached', () => { + const yesVotes = 100; + const noVotes = 0; + + renderComponent( + generateProposal({ + state: ProposalState.STATE_PASSED, + terms: { + closingDatetime: lastWeek.toString(), + enactmentDatetime: nextWeek.toString(), + }, + votes: { + __typename: 'ProposalVotes', + yes: generateYesVotes(yesVotes, fixedTokenValue), + no: generateNoVotes(noVotes, fixedTokenValue), + }, + }) + ); + expect(screen.getByTestId('token-majority-met')).toBeInTheDocument(); + }); + + it('Renders majority not reached', () => { + const yesVotes = 20; + const noVotes = 80; + + renderComponent( + generateProposal({ + state: ProposalState.STATE_PASSED, + terms: { + closingDatetime: lastWeek.toString(), + enactmentDatetime: nextWeek.toString(), + }, + votes: { + __typename: 'ProposalVotes', + yes: generateYesVotes(yesVotes, fixedTokenValue), + no: generateNoVotes(noVotes, fixedTokenValue), + }, + }) + ); + expect(screen.getByTestId('token-majority-not-met')).toBeInTheDocument(); + }); + + it('Renders participation reached', () => { + const yesVotes = 1000; + const noVotes = 0; + + renderComponent( + generateProposal({ + state: ProposalState.STATE_PASSED, + terms: { + closingDatetime: lastWeek.toString(), + enactmentDatetime: nextWeek.toString(), + }, + votes: { + __typename: 'ProposalVotes', + yes: generateYesVotes(yesVotes, fixedTokenValue), + no: generateNoVotes(noVotes, fixedTokenValue), + }, + }) + ); + expect(screen.getByTestId('token-participation-met')).toBeInTheDocument(); + }); + + it('Renders participation not reached', () => { + const yesVotes = 0; + const noVotes = 0; + + renderComponent( + generateProposal({ + terms: { + closingDatetime: lastWeek.toString(), + enactmentDatetime: nextWeek.toString(), + }, + votes: { + __typename: 'ProposalVotes', + yes: generateYesVotes(yesVotes, fixedTokenValue), + no: generateNoVotes(noVotes, fixedTokenValue), + }, + }) + ); + expect( + screen.getByTestId('token-participation-not-met') + ).toBeInTheDocument(); + }); + + it('Renders proposal state: Update market proposal - Currently expected to pass by LP vote', () => { + renderComponent( + generateProposal({ + state: ProposalState.STATE_OPEN, + terms: { + change: { + __typename: 'UpdateMarket', + }, + }, + votes: { + yes: { + ...generateYesVotes(0), + totalEquityLikeShareWeight: '1000', + }, + no: { + ...generateNoVotes(0), + totalEquityLikeShareWeight: '0', + }, + }, + }) + ); + expect(screen.getByTestId('vote-status')).toHaveTextContent( + 'Currently expected to pass by liquidity vote' + ); + }); + + it('Renders proposal state: Update market proposal - Currently expected to pass by token vote', () => { + renderComponent( + generateProposal({ + state: ProposalState.STATE_OPEN, + terms: { + change: { + __typename: 'UpdateMarket', + }, + }, + votes: { + yes: { + ...generateYesVotes(1000, fixedTokenValue), + totalEquityLikeShareWeight: '0', + }, + no: { + ...generateNoVotes(0, fixedTokenValue), + totalEquityLikeShareWeight: '0', + }, + }, + }) + ); + expect(screen.getByTestId('vote-status')).toHaveTextContent( + 'Currently expected to pass by token vote' + ); + }); + + it('Renders proposal state: Update market proposal - Currently expected to fail', () => { + renderComponent( + generateProposal({ + state: ProposalState.STATE_OPEN, + terms: { + change: { + __typename: 'UpdateMarket', + }, + }, + votes: { + yes: { + ...generateYesVotes(0), + totalEquityLikeShareWeight: '0', + }, + no: { + ...generateNoVotes(0), + totalEquityLikeShareWeight: '0', + }, + }, + }) + ); + expect(screen.getByTestId('vote-status')).toHaveTextContent( + 'Currently expected to fail' + ); + }); + + it('Progress bar displays status - token majority', () => { + const yesVotes = 80; + const noVotes = 20; + + renderComponent( + generateProposal({ + state: ProposalState.STATE_PASSED, + terms: { + closingDatetime: lastWeek.toString(), + enactmentDatetime: nextWeek.toString(), + }, + votes: { + __typename: 'ProposalVotes', + yes: generateYesVotes(yesVotes, fixedTokenValue), + no: generateNoVotes(noVotes, fixedTokenValue), + }, + }) + ); + + const element = screen.getByTestId('token-majority-progress'); + const style = window.getComputedStyle(element); + + expect(style.width).toBe(`${yesVotes}%`); + }); + + it('Progress bar displays status - token participation', () => { + const yesVotes = 40; + const noVotes = 20; + const totalVotes = yesVotes + noVotes; + const totalSupplyValue = mockTotalSupply.toNumber(); + const expectedProgress = (totalVotes / totalSupplyValue) * 100; // Here it should be 60% + + renderComponent( + generateProposal({ + state: ProposalState.STATE_PASSED, + terms: { + closingDatetime: lastWeek.toString(), + enactmentDatetime: nextWeek.toString(), + }, + votes: { + __typename: 'ProposalVotes', + yes: generateYesVotes(yesVotes, fixedTokenValue), + no: generateNoVotes(noVotes, fixedTokenValue), + }, + }) + ); + + const element = screen.getByTestId('token-participation-progress'); + const style = window.getComputedStyle(element); + + expect(style.width).toBe(`${expectedProgress}%`); + }); + + it('Progress bar displays status - LP majority', () => { + const yesVotesLP = 800; + const noVotesLP = 200; + const expectedProgress = (yesVotesLP / (yesVotesLP + noVotesLP)) * 100; // 80% + + renderComponent( + generateProposal({ + state: ProposalState.STATE_PASSED, + terms: { + change: { + __typename: 'UpdateMarket', + }, + }, + votes: { + __typename: 'ProposalVotes', + yes: { + ...generateYesVotes(0), + totalEquityLikeShareWeight: `${yesVotesLP}`, + }, + no: { + ...generateNoVotes(0), + totalEquityLikeShareWeight: `${noVotesLP}`, + }, + }, + }) + ); + + const element = screen.getByTestId('lp-majority-progress'); + const style = window.getComputedStyle(element); + expect(style.width).toBe(`${expectedProgress}%`); + }); + + it('Progress bar displays status - LP participation', () => { + const yesVotesLP = 400; + const noVotesLP = 600; + const totalVotesLP = yesVotesLP + noVotesLP; + const totalLPSupply = 1000; + const expectedProgress = (totalVotesLP / totalLPSupply) * 100; // 100% + + renderComponent( + generateProposal({ + state: ProposalState.STATE_PASSED, + terms: { + change: { + __typename: 'UpdateMarket', + }, + }, + votes: { + __typename: 'ProposalVotes', + yes: { + ...generateYesVotes(0), + totalEquityLikeShareWeight: `${yesVotesLP}`, + }, + no: { + ...generateNoVotes(0), + totalEquityLikeShareWeight: `${noVotesLP}`, + }, + }, + }) + ); + + const element = screen.getByTestId('lp-participation-progress'); + const style = window.getComputedStyle(element); + expect(style.width).toBe(`${expectedProgress}%`); + }); +}); diff --git a/apps/governance/src/routes/proposals/components/vote-breakdown/vote-breakdown.tsx b/apps/governance/src/routes/proposals/components/vote-breakdown/vote-breakdown.tsx new file mode 100644 index 000000000..6f0e36e00 --- /dev/null +++ b/apps/governance/src/routes/proposals/components/vote-breakdown/vote-breakdown.tsx @@ -0,0 +1,378 @@ +import classNames from 'classnames'; +import BigNumber from 'bignumber.js'; +import { useTranslation } from 'react-i18next'; +import { useVoteInformation } from '../../hooks'; +import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit'; +import { formatNumber, toBigNum } from '@vegaprotocol/utils'; +import { ProposalState } from '@vegaprotocol/types'; +import type { ReactNode } from 'react'; +import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; +import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; + +interface VoteBreakdownProps { + proposal: ProposalFieldsFragment | ProposalQuery['proposal']; +} + +interface VoteProgressProps { + percentageFor: BigNumber; + colourfulBg?: boolean; + testId?: string; + children?: ReactNode; +} + +const VoteProgress = ({ + percentageFor, + colourfulBg, + testId, + children, +}: VoteProgressProps) => { + const containerClasses = classNames( + 'relative h-10 rounded-md border border-vega-dark-300 overflow-hidden', + colourfulBg ? 'bg-vega-pink' : 'bg-vega-dark-400' + ); + + const progressClasses = classNames( + 'absolute h-full top-0 left-0', + colourfulBg ? 'bg-vega-green' : 'bg-white' + ); + + const textClasses = classNames( + 'absolute top-0 left-0 w-full h-full flex items-center justify-start px-3 text-black' + ); + + return ( +
    +
    +
    {children}
    +
    + ); +}; + +interface StatusProps { + reached: boolean; + threshold: BigNumber; + text: string; + testId?: string; +} + +const Status = ({ reached, threshold, text, testId }: StatusProps) => { + const { t } = useTranslation(); + + return ( +
    + {reached ? ( +
    + + + {threshold.toString()}% {text} {t('met')} + +
    + ) : ( +
    + + + {threshold.toString()}% {text} {t('not met')} + +
    + )} +
    + ); +}; + +export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => { + const { + totalTokensPercentage, + participationMet, + totalTokensVoted, + totalLPTokensPercentage, + noPercentage, + noLPPercentage, + yesPercentage, + yesLPPercentage, + yesTokens, + noTokens, + yesEquityLikeShareWeight, + noEquityLikeShareWeight, + totalEquityLikeShareWeight, + requiredMajorityPercentage, + requiredMajorityLPPercentage, + requiredParticipation, + requiredParticipationLP, + participationLPMet, + majorityMet, + majorityLPMet, + willPassByTokenVote, + willPassByLPVote, + } = useVoteInformation({ proposal }); + + const { t } = useTranslation(); + const defaultDP = 2; + const isProposalOpen = proposal?.state === ProposalState.STATE_OPEN; + const isUpdateMarket = proposal?.terms?.change?.__typename === 'UpdateMarket'; + const participationThresholdProgress = BigNumber.min( + totalTokensPercentage.dividedBy(requiredParticipation).multipliedBy(100), + new BigNumber(100) + ); + const lpParticipationThresholdProgress = + requiredParticipationLP && + BigNumber.min( + totalLPTokensPercentage + .dividedBy(requiredParticipationLP) + .multipliedBy(100), + new BigNumber(100) + ); + const willPass = willPassByTokenVote || willPassByLPVote; + const updateMarketVotePassMethod = willPassByTokenVote + ? t('byTokenVote') + : t('byLiquidityVote'); + + const sectionWrapperClasses = classNames('grid sm:grid-cols-2 gap-6'); + const headingClasses = classNames('mb-2 text-vega-dark-400'); + const progressDetailsClasses = classNames( + 'flex justify-between flex-wrap mt-2 text-sm' + ); + + return ( +
    + {isProposalOpen && ( +
    + + {willPass ? ( + + ) : ( + + )} + + {t('currentlySetTo')} + {willPass ? ( + + {t('pass')} + {isUpdateMarket && {updateMarketVotePassMethod}} + + ) : ( + {t('fail')} + )} +
    + )} + + {isUpdateMarket && ( +
    +

    {t('liquidityProviderVote')}

    +
    +
    + + + + +
    +
    + {t('liquidityProviderVotesFor')}: + + + + + ( + {yesLPPercentage.toFixed(defaultDP)}% + } + > + + + ) + +
    + +
    + {t('liquidityProviderVotesAgainst')}: + + + + + ( + {noLPPercentage.toFixed(defaultDP)}% + } + > + + + ) + +
    +
    +
    + +
    + + + + +
    +
    + {t('totalLiquidityProviderTokensVoted')}: + + + + + ({totalEquityLikeShareWeight.toFixed(defaultDP)}%) + +
    +
    +
    +
    +
    + )} + + {isUpdateMarket &&

    {t('tokenVote')}

    } +
    +
    + + + + +
    +
    + {t('tokenVotesFor')}: + + + + + ( + {yesPercentage.toFixed(defaultDP)}%} + > + + + ) + +
    + +
    + {t('tokenVotesAgainst')}: + + + + + ( + {noPercentage.toFixed(defaultDP)}%} + > + + + ) + +
    +
    +
    + +
    + + + + +
    +
    + {t('totalTokensVoted')}: + + + + ({totalTokensPercentage.toFixed(defaultDP)}%) +
    +
    +
    +
    +
    + ); +}; diff --git a/apps/governance/src/routes/proposals/components/vote-details/index.tsx b/apps/governance/src/routes/proposals/components/vote-details/index.tsx index b8d4db866..eb79e6910 100644 --- a/apps/governance/src/routes/proposals/components/vote-details/index.tsx +++ b/apps/governance/src/routes/proposals/components/vote-details/index.tsx @@ -1 +1 @@ -export { VoteDetails } from './vote-details'; +export { UserVote } from './user-vote'; diff --git a/apps/governance/src/routes/proposals/components/vote-details/user-vote.tsx b/apps/governance/src/routes/proposals/components/vote-details/user-vote.tsx new file mode 100644 index 000000000..70e764d18 --- /dev/null +++ b/apps/governance/src/routes/proposals/components/vote-details/user-vote.tsx @@ -0,0 +1,78 @@ +import { useTranslation } from 'react-i18next'; +import { Icon, ExternalLink } from '@vegaprotocol/ui-toolkit'; +import { useVegaWallet } from '@vegaprotocol/wallet'; +import { ProposalState } from '@vegaprotocol/types'; +import { ConnectToVega } from '../../../../components/connect-to-vega'; +import { VoteButtonsContainer } from './vote-buttons'; +import { SubHeading } from '../../../../components/heading'; +import type { VoteValue } from '@vegaprotocol/types'; +import type { DialogProps, VegaTxState } from '@vegaprotocol/wallet'; +import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; +import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; +import type { VoteState } from './use-user-vote'; + +interface UserVoteProps { + proposal: ProposalFieldsFragment | ProposalQuery['proposal']; + minVoterBalance: string | null | undefined; + spamProtectionMinTokens: string | null | undefined; + transaction: VegaTxState | null; + submit: (voteValue: VoteValue, proposalId: string | null) => Promise; + dialog: (props: DialogProps) => JSX.Element; + voteState: VoteState | null; + voteDatetime: Date | null; +} + +export const UserVote = ({ + proposal, + minVoterBalance, + spamProtectionMinTokens, + submit, + transaction, + dialog, + voteState, + voteDatetime, +}: UserVoteProps) => { + const { pubKey } = useVegaWallet(); + + const { t } = useTranslation(); + + return ( +
    + {proposal?.state === ProposalState.STATE_OPEN ? ( + + ) : ( + + )} + + {pubKey ? ( + proposal && ( + + ) + ) : ( +
    +
    +
    + +
    {t('connectAVegaWalletToVote')}
    +
    + + {t('findOutMoreAboutHowToVote')} + +
    + +
    + )} +
    + ); +}; diff --git a/apps/governance/src/routes/proposals/components/vote-details/vote-buttons.tsx b/apps/governance/src/routes/proposals/components/vote-details/vote-buttons.tsx index 7199c9365..911c2d41a 100644 --- a/apps/governance/src/routes/proposals/components/vote-details/vote-buttons.tsx +++ b/apps/governance/src/routes/proposals/components/vote-details/vote-buttons.tsx @@ -190,14 +190,15 @@ export const VoteButtons = ({ (voteState === VoteState.Yes || voteState === VoteState.No) && (

    {t('youVoted')}:{' '} - + {t(`voteState_${voteState}`)} {' '} {voteDatetime ? ( - {format(voteDatetime, DATE_FORMAT_LONG)}. + on {format(voteDatetime, DATE_FORMAT_LONG)}. ) : null} {proposalVotable ? ( { setChangeVote(true); diff --git a/apps/governance/src/routes/proposals/components/vote-details/vote-details.tsx b/apps/governance/src/routes/proposals/components/vote-details/vote-details.tsx deleted file mode 100644 index b8fbb7c0a..000000000 --- a/apps/governance/src/routes/proposals/components/vote-details/vote-details.tsx +++ /dev/null @@ -1,255 +0,0 @@ -import { useTranslation } from 'react-i18next'; -import { formatDistanceToNow } from 'date-fns'; -import { RoundedWrapper, Icon, ExternalLink } from '@vegaprotocol/ui-toolkit'; -import { useVegaWallet } from '@vegaprotocol/wallet'; -import { ProposalState } from '@vegaprotocol/types'; -import { VoteProgress } from '@vegaprotocol/proposals'; -import { formatNumber } from '../../../../lib/format-number'; -import { ConnectToVega } from '../../../../components/connect-to-vega'; -import { useVoteInformation } from '../../hooks'; -import { CurrentProposalStatus } from '../current-proposal-status'; -import { VoteButtonsContainer } from './vote-buttons'; -import { SubHeading } from '../../../../components/heading'; -import { ProposalType } from '../proposal/proposal'; -import type { VoteValue } from '@vegaprotocol/types'; -import type { DialogProps, VegaTxState } from '@vegaprotocol/wallet'; -import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; -import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; -import type { VoteState } from './use-user-vote'; - -interface VoteDetailsProps { - proposal: ProposalFieldsFragment | ProposalQuery['proposal']; - minVoterBalance: string | null | undefined; - spamProtectionMinTokens: string | null | undefined; - proposalType: ProposalType | null; - transaction: VegaTxState | null; - submit: (voteValue: VoteValue, proposalId: string | null) => Promise; - dialog: (props: DialogProps) => JSX.Element; - voteState: VoteState | null; - voteDatetime: Date | null; -} - -export const VoteDetails = ({ - proposal, - minVoterBalance, - spamProtectionMinTokens, - proposalType, - submit, - transaction, - dialog, - voteState, - voteDatetime, -}: VoteDetailsProps) => { - const { pubKey } = useVegaWallet(); - const { - totalTokensPercentage, - participationMet, - totalTokensVoted, - totalLPTokensPercentage, - noPercentage, - noLPPercentage, - yesPercentage, - yesLPPercentage, - yesTokens, - noTokens, - requiredMajorityPercentage, - requiredMajorityLPPercentage, - requiredParticipation, - requiredParticipationLP, - participationLPMet, - } = useVoteInformation({ proposal }); - - const { t } = useTranslation(); - - const defaultDecimals = 2; - const daysLeft = t('daysLeft', { - daysLeft: formatDistanceToNow(new Date(proposal?.terms.closingDatetime)), - }); - - return ( - <> - {proposalType === ProposalType.PROPOSAL_UPDATE_MARKET && ( -

    - -

    - - - - {'. '} - {proposal?.state === ProposalState.STATE_OPEN ? daysLeft : null} -

    - - - - - - - - - - - - - - - -
    - {t('for')} - - - - {t('against')} -
    - {yesLPPercentage.toFixed(defaultDecimals)}% - - {t('majorityRequired')}{' '} - {requiredMajorityLPPercentage.toFixed(defaultDecimals)}% - - {noLPPercentage.toFixed(defaultDecimals)}% -
    - -

    - {t('participation')} - {': '} - {participationLPMet ? ( - {t('met')} - ) : ( - {t('notMet')} - )}{' '} - {formatNumber(totalLPTokensPercentage, defaultDecimals)}% - - {requiredParticipationLP && ( - <> - ({formatNumber(requiredParticipationLP, defaultDecimals)}%{' '} - {t('governanceRequired')}) - - )} - -

    -
    - )} -
    - -

    - - - - {'. '} - {proposal?.state === ProposalState.STATE_OPEN ? daysLeft : null} -

    - - - - - - - - - - - - - - - - - - - - -
    {t('for')} - - {t('against')}
    - {yesPercentage.toFixed(defaultDecimals)}% - - {t('majorityRequired')}{' '} - {requiredMajorityPercentage.toFixed(defaultDecimals)}% - - {noPercentage.toFixed(defaultDecimals)}% -
    - {' '} - {formatNumber(yesTokens, defaultDecimals)}{' '} - - {formatNumber(noTokens, defaultDecimals)} -
    -

    - {t('participation')} - {': '} - {participationMet ? ( - {t('met')} - ) : ( - {t('notMet')} - )}{' '} - {formatNumber(totalTokensVoted, defaultDecimals)}{' '} - {formatNumber(totalTokensPercentage, defaultDecimals)}% - - ({formatNumber(requiredParticipation, defaultDecimals)}%{' '} - {t('governanceRequired')}) - -

    - {proposalType === ProposalType.PROPOSAL_UPDATE_MARKET && ( -

    {t('votingThresholdInfo')}

    - )} - -
    - {proposal?.state === ProposalState.STATE_OPEN ? ( - - ) : ( - - )} - - {pubKey ? ( - proposal && ( - - ) - ) : ( - -
    -
    - -
    {t('connectAVegaWalletToVote')}
    -
    - - {t('findOutMoreAboutHowToVote')} - -
    - -
    - )} -
    -
    - - ); -}; diff --git a/apps/trading-e2e/src/integration/order-book.cy.ts b/apps/trading-e2e/src/integration/order-book.cy.ts deleted file mode 100644 index 34cb68fce..000000000 --- a/apps/trading-e2e/src/integration/order-book.cy.ts +++ /dev/null @@ -1,117 +0,0 @@ -const orderbookTab = 'Orderbook'; -const orderbookTable = 'tab-orderbook'; -const askPrice = 'price-9894185'; -const bidPrice = 'price-9889001'; -const askVolume = 'ask-vol-9894185'; -const bidVolume = 'bid-vol-9889001'; -const askCumulative = 'cumulative-vol-9894185'; -const bidCumulative = 'cumulative-vol-9889001'; -const midPrice = 'last-traded-4612690000'; -const priceResolution = 'resolution'; -const dealTicketPrice = 'order-price'; -const dealTicketSize = 'order-size'; -const resPrice = 'price-990'; - -describe('order book', { tags: '@smoke' }, () => { - before(() => { - cy.setOnBoardingViewed(); - cy.mockTradingPage(); - cy.mockSubscription(); - cy.visit('/#/markets/market-0'); - cy.wait('@Markets'); - }); - - beforeEach(() => { - cy.mockTradingPage(); - }); - - it('show order book', () => { - // 6003-ORDB-001 - // 6003-ORDB-002 - cy.getByTestId(orderbookTab).click(); - cy.getByTestId(orderbookTable).should('be.visible'); - cy.getByTestId(orderbookTable).should('not.be.empty'); - }); - - it('show orders prices', () => { - // 6003-ORDB-003 - cy.getByTestId(askPrice).should('have.text', '98.94185'); - cy.getByTestId(bidPrice).should('have.text', '98.89001'); - }); - - it('show prices volumes', () => { - // 6003-ORDB-004 - cy.getByTestId(askVolume).should('have.text', '1'); - cy.getByTestId(bidVolume).should('have.text', '1'); - }); - - it('show prices cumulative volumes', () => { - // 6003-ORDB-005 - cy.getByTestId(askCumulative).should('have.text', '38'); - cy.getByTestId(bidCumulative).should('have.text', '7'); - }); - - it('show mid price', () => { - // 6003-ORDB-006 - cy.getByTestId(midPrice).should('have.text', '46,126.90'); - }); - - it('sort prices descending', () => { - // 6003-ORDB-007 - const prices: number[] = []; - cy.getByTestId(orderbookTable).within(() => { - cy.get('[data-testid*=price]') - .each(($el) => { - prices.push(Number($el.text())); - }) - .then(() => { - expect(prices).to.deep.equal(prices.sort((a, b) => b - a)); - }); - }); - }); - - it('copy price to deal ticket form', () => { - // 6003-ORDB-009 - cy.getByTestId(askPrice).click(); - cy.getByTestId(dealTicketPrice).should('have.value', '98.94185'); - }); - - it('copy size to deal ticket form', () => { - // 6003-ORDB-009 - cy.getByTestId(bidCumulative).click(); - cy.getByTestId(dealTicketSize).should('have.value', '7'); - }); - - it('copy size to deal ticket form', () => { - // 6003-ORDB-009 - cy.getByTestId(bidVolume).click(); - cy.getByTestId(dealTicketSize).should('have.value', '1'); - }); - - it('change price resolution', () => { - // 6003-ORDB-008 - const resolutions = [ - '0.00000', - '0.0000', - '0.000', - '0.00', - '0.0', - '0', - '10', - '100', - '1,000', - '10,000', - ]; - cy.getByTestId(priceResolution).click(); - cy.get('[role="menu"]') - .find('[role="menuitem"]') - .each(($el, index) => { - expect($el.text()).to.equal(resolutions[index]); - }); - - cy.get('[role="menuitem"]').eq(4).click(); - cy.getByTestId(resPrice).should('have.text', '99.0'); - cy.getByTestId(askPrice).should('not.exist'); - cy.getByTestId(bidPrice).should('not.exist'); - }); -}); diff --git a/apps/trading-e2e/src/integration/trading-deal-ticket-order.cy.ts b/apps/trading-e2e/src/integration/trading-deal-ticket-order.cy.ts index fe82aee3e..d2bd1c0d6 100644 --- a/apps/trading-e2e/src/integration/trading-deal-ticket-order.cy.ts +++ b/apps/trading-e2e/src/integration/trading-deal-ticket-order.cy.ts @@ -24,6 +24,9 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => { beforeEach(() => { cy.mockTradingPage(); + cy.getByTestId('deal-ticket-fee-margin-required').within(() => { + cy.get('button').click(); + }); }); describe('limit order', () => { diff --git a/apps/trading/.env.mainnet b/apps/trading/.env.mainnet index df13750fa..b02a11b93 100644 --- a/apps/trading/.env.mainnet +++ b/apps/trading/.env.mainnet @@ -16,12 +16,12 @@ NX_VEGA_CONSOLE_URL=https://console.vega.xyz NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-mainnet/codfcglpplgmmlokgilfkpcjnmkbfiel NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-mainnet # TAG name of the current app version - TODO: bump to the latest upon release -NX_APP_VERSION=v0.20.21-core-0.71.6 +NX_APP_VERSION=v0.21.0-core-0.72.14 # Cosmic elevator flags -NX_SUCCESSOR_MARKETS=false -NX_STOP_ORDERS=false -# NX_ICEBERG_ORDERS +NX_SUCCESSOR_MARKETS=true +NX_STOP_ORDERS=true +NX_ICEBERG_ORDERS=true # NX_PRODUCT_PERPETUALS NX_METAMASK_SNAPS=false diff --git a/apps/trading/.env.mainnet-mirror b/apps/trading/.env.mainnet-mirror index 9a575f33c..bde8e334b 100644 --- a/apps/trading/.env.mainnet-mirror +++ b/apps/trading/.env.mainnet-mirror @@ -19,9 +19,9 @@ NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-ma NX_APP_VERSION=v0.20.19-core-0.71.6 # Cosmic elevator flags -NX_SUCCESSOR_MARKETS=false -NX_STOP_ORDERS=false -# NX_ICEBERG_ORDERS +NX_SUCCESSOR_MARKETS=true +NX_STOP_ORDERS=true +NX_ICEBERG_ORDERS=true # NX_PRODUCT_PERPETUALS NX_METAMASK_SNAPS=false diff --git a/apps/trading/.env.stagnet1 b/apps/trading/.env.stagnet1 index 4a9bc1e46..f2e4129c8 100644 --- a/apps/trading/.env.stagnet1 +++ b/apps/trading/.env.stagnet1 @@ -19,6 +19,6 @@ NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fa # Cosmic elevator flags NX_SUCCESSOR_MARKETS=true NX_STOP_ORDERS=true -# NX_ICEBERG_ORDERS +NX_ICEBERG_ORDERS=true # NX_PRODUCT_PERPETUALS NX_METAMASK_SNAPS=true diff --git a/apps/trading/.env.validators-testnet b/apps/trading/.env.validators-testnet index c8e48e938..21af40de4 100644 --- a/apps/trading/.env.validators-testnet +++ b/apps/trading/.env.validators-testnet @@ -19,9 +19,9 @@ NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fa NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground # Cosmic elevator flags -NX_SUCCESSOR_MARKETS=false -NX_STOP_ORDERS=false -# NX_ICEBERG_ORDERS +NX_SUCCESSOR_MARKETS=true +NX_STOP_ORDERS=true +NX_ICEBERG_ORDERS=true # NX_PRODUCT_PERPETUALS NX_METAMASK_SNAPS=false diff --git a/apps/trading/client-pages/markets/markets-page.tsx b/apps/trading/client-pages/markets/markets-page.tsx index b2403ccc9..b2f3e7bef 100644 --- a/apps/trading/client-pages/markets/markets-page.tsx +++ b/apps/trading/client-pages/markets/markets-page.tsx @@ -1,27 +1,49 @@ import React, { useEffect } from 'react'; import { titlefy } from '@vegaprotocol/utils'; import { t } from '@vegaprotocol/i18n'; -import { LocalStoragePersistTabs as Tabs, Tab } from '@vegaprotocol/ui-toolkit'; +import { + LocalStoragePersistTabs as Tabs, + Tab, + TradingAnchorButton, +} from '@vegaprotocol/ui-toolkit'; import { Markets } from './markets'; import { Proposed } from './proposed'; import { usePageTitleStore } from '../../stores'; import { Closed } from './closed'; +import { + DApp, + TOKEN_NEW_MARKET_PROPOSAL, + useLinks, +} from '@vegaprotocol/environment'; export const MarketsPage = () => { const { updateTitle } = usePageTitleStore((store) => ({ updateTitle: store.updateTitle, })); + + const tokenLink = useLinks(DApp.Token); + const externalLink = tokenLink(TOKEN_NEW_MARKET_PROPOSAL); + useEffect(() => { updateTitle(titlefy(['Markets'])); }, [updateTitle]); + return (
    -
    +
    - + + {t('Propose a new market')} + + } + > diff --git a/apps/trading/client-pages/markets/proposed.tsx b/apps/trading/client-pages/markets/proposed.tsx index 009190e17..67a20cbc9 100644 --- a/apps/trading/client-pages/markets/proposed.tsx +++ b/apps/trading/client-pages/markets/proposed.tsx @@ -1,24 +1,6 @@ -import { t } from '@vegaprotocol/i18n'; -import { - DApp, - TOKEN_NEW_MARKET_PROPOSAL, - useLinks, -} from '@vegaprotocol/environment'; import { ProposalsList } from '@vegaprotocol/proposals'; -import { ExternalLink } from '@vegaprotocol/ui-toolkit'; import { SuccessorMarketRenderer } from './successor-market-cell'; export const Proposed = () => { - const tokenLink = useLinks(DApp.Token); - const externalLink = tokenLink(TOKEN_NEW_MARKET_PROPOSAL); - return ( - <> -
    - -
    - - {t('Propose a new market')} - - - ); + return ; }; diff --git a/apps/trading/client-pages/portfolio/account-history-container.tsx b/apps/trading/client-pages/portfolio/account-history-container.tsx index 1dfb3d75f..90e2a88b6 100644 --- a/apps/trading/client-pages/portfolio/account-history-container.tsx +++ b/apps/trading/client-pages/portfolio/account-history-container.tsx @@ -4,7 +4,7 @@ import { useVegaWallet } from '@vegaprotocol/wallet'; import compact from 'lodash/compact'; import uniqBy from 'lodash/uniqBy'; import type { ChangeEvent } from 'react'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import type { AccountHistoryQuery } from './__generated__/AccountHistory'; import { useAccountHistoryQuery } from './__generated__/AccountHistory'; import * as Schema from '@vegaprotocol/types'; @@ -12,12 +12,13 @@ import type { AssetFieldsFragment } from '@vegaprotocol/assets'; import { useAssetsDataProvider } from '@vegaprotocol/assets'; import { AsyncRenderer, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, Splash, Toggle, + TradingButton, + TradingDropdown, + TradingDropdownContent, + TradingDropdownItem, + TradingDropdownTrigger, } from '@vegaprotocol/ui-toolkit'; import { AccountTypeMapping } from '@vegaprotocol/types'; import { PriceChart } from 'pennant'; @@ -151,6 +152,7 @@ const AccountHistoryManager = ({ ) : null; }, [accounts, marketFilterCb]); + const resolveMarket = useCallback( (m: Market) => { setMarket(m); @@ -175,111 +177,110 @@ const AccountHistoryManager = ({ }), [pubKey, asset, accountType, range, market?.id] ); + const { data } = useAccountHistoryQuery({ variables, skip: !asset || !pubKey, }); - const accountTypeMenu = useMemo(() => { - return ( - - {accountType - ? `${ - AccountTypeMapping[ - accountType as keyof typeof Schema.AccountType - ] - } Account` - : t('Select account type')} - - } - > - - {[ - Schema.AccountType.ACCOUNT_TYPE_GENERAL, - Schema.AccountType.ACCOUNT_TYPE_BOND, - Schema.AccountType.ACCOUNT_TYPE_MARGIN, - ].map((type) => ( - setAccountType(type as Schema.AccountType)} - > - {AccountTypeMapping[type as keyof typeof Schema.AccountType]} - - ))} - - - ); - }, [accountType]); - const assetsMenu = useMemo(() => { - return ( - - {asset ? asset.symbol : t('Select asset')} - - } - > - - {assets.map((a) => ( - setAssetId(a.id)}> - {a.symbol} - - ))} - - - ); - }, [asset, assets, setAssetId]); - const marketsMenu = useMemo(() => { - return accountType === Schema.AccountType.ACCOUNT_TYPE_MARGIN && - markets?.length ? ( - - {market - ? market.tradableInstrument.instrument.code - : t('Select market')} - - } - > - - {market && ( - setMarket(null)}> - {t('All markets')} - - )} - {markets?.map((m) => ( - resolveMarket(m)}> - {m.tradableInstrument.instrument.code} - - ))} - - - ) : null; - }, [markets, market, accountType, resolveMarket]); - - useEffect(() => { - const itemAsset = market && getAsset(market); - if ( - accountType !== Schema.AccountType.ACCOUNT_TYPE_MARGIN || - itemAsset?.id !== asset?.id - ) { - setMarket(null); - } - }, [accountType, asset?.id, market]); - return ( -
    -
    -
    - <> - {accountTypeMenu} - {assetsMenu} - {marketsMenu} - +
    +
    +
    + + + {accountType + ? `${ + AccountTypeMapping[ + accountType as keyof typeof Schema.AccountType + ] + } Account` + : t('Select account type')} + + + } + > + + {[ + Schema.AccountType.ACCOUNT_TYPE_GENERAL, + Schema.AccountType.ACCOUNT_TYPE_BOND, + Schema.AccountType.ACCOUNT_TYPE_MARGIN, + ].map((type) => ( + { + setAccountType(type as Schema.AccountType); + + // if not a margin account clear any market selection + if (type !== Schema.AccountType.ACCOUNT_TYPE_MARGIN) { + setMarket(null); + } + }} + > + {AccountTypeMapping[type as keyof typeof Schema.AccountType]} + + ))} + + + + + + {asset ? asset.symbol : t('Select asset')} + + + } + > + + {assets.map((a) => ( + { + setAssetId(a.id); + + // if the selected asset is different to the selected market clear the market + if (market && a.id !== getAsset(market).id) { + setMarket(null); + } + }} + > + {a.symbol} + + ))} + + + + + {market + ? market.tradableInstrument.instrument.code + : t('Select market')} + + + } + > + + {market && ( + setMarket(null)}> + {t('All markets')} + + )} + {markets?.map((m) => ( + resolveMarket(m)} + > + {m.tradableInstrument.instrument.code} + + ))} + +
    -
    +
    ) => setRange(e.target.value as keyof typeof DateRange) } + size="sm" />
    -
    +
    {asset && ( - +
    + +
    )}
    diff --git a/apps/trading/components/market-selector/asset-dropdown.tsx b/apps/trading/components/market-selector/asset-dropdown.tsx index aed0b0cb4..d0bcefb0c 100644 --- a/apps/trading/components/market-selector/asset-dropdown.tsx +++ b/apps/trading/components/market-selector/asset-dropdown.tsx @@ -1,13 +1,12 @@ import { t } from '@vegaprotocol/i18n'; import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuItemIndicator, - DropdownMenuTrigger, - VegaIcon, - VegaIconNames, + TradingDropdown, + TradingDropdownCheckboxItem, + TradingDropdownContent, + TradingDropdownItemIndicator, + TradingDropdownTrigger, } from '@vegaprotocol/ui-toolkit'; +import { MarketSelectorButton } from './market-selector-button'; type Assets = Array<{ id: string; symbol: string }>; @@ -25,17 +24,19 @@ export const AssetDropdown = ({ } return ( - - - + + + {triggerText({ assets, checkedAssets })} + + } > - - {assets.filter(Boolean).map((a) => { + + {assets?.map((a) => { return ( - { @@ -46,16 +47,16 @@ export const AssetDropdown = ({ data-testid={`asset-id-${a.id}`} > {a.symbol} - - + + ); })} - - + + ); }; -const TriggerText = ({ +const triggerText = ({ assets, checkedAssets, }: { @@ -72,9 +73,5 @@ const TriggerText = ({ text = t(`${checkedAssets.length} Assets`); } - return ( - - {text} - - ); + return text; }; diff --git a/apps/trading/components/market-selector/index.ts b/apps/trading/components/market-selector/index.ts index ddd159890..6a16773bb 100644 --- a/apps/trading/components/market-selector/index.ts +++ b/apps/trading/components/market-selector/index.ts @@ -1,2 +1,3 @@ export * from './market-selector'; export * from './market-selector-item'; +export * from './market-selector-button'; diff --git a/apps/trading/components/market-selector/market-selector-button.tsx b/apps/trading/components/market-selector/market-selector-button.tsx new file mode 100644 index 000000000..90ae6cacf --- /dev/null +++ b/apps/trading/components/market-selector/market-selector-button.tsx @@ -0,0 +1,23 @@ +import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit'; +import classNames from 'classnames'; +import type { ButtonHTMLAttributes } from 'react'; +import { forwardRef } from 'react'; + +export const MarketSelectorButton = forwardRef< + HTMLButtonElement, + ButtonHTMLAttributes +>((props, ref) => ( + +)); +MarketSelectorButton.displayName = 'MarketSelectorButton'; diff --git a/apps/trading/components/market-selector/market-selector-item.tsx b/apps/trading/components/market-selector/market-selector-item.tsx index 094e0a7e6..65583f6fa 100644 --- a/apps/trading/components/market-selector/market-selector-item.tsx +++ b/apps/trading/components/market-selector/market-selector-item.tsx @@ -31,7 +31,7 @@ export const MarketSelectorItem = ({
    -

    +

    {market.tradableInstrument.instrument.code}{' '} {allProducts && productType && ( @@ -107,25 +106,23 @@ const MarketData = ({

    )}

    - {instrument.product && ( -
    - {price} {symbol} -
    - )}
    + {price} {symbol} +
    +
    {volume}
    -
    +
    {oneDayCandles && ( +
    {Object.keys(Product).map((t) => { - const classes = classNames('px-3 py-1.5 rounded', { - 'bg-vega-clight-500 dark:bg-vega-cdark-500 text-default': - t === product, - 'text-secondary': t !== product, - }); + const classes = classNames( + 'text-sm px-3 py-1.5 rounded hover:text-vega-clight-50 dark:hover:text-vega-cdark-50', + { + 'bg-vega-clight-500 dark:bg-vega-cdark-500 text-default': + t === product, + 'text-secondary': t !== product, + } + ); return ( -
    - - ); - } - - return <>{children}; -}; diff --git a/apps/trading/components/welcome-dialog/telemetry-approval.tsx b/apps/trading/components/welcome-dialog/telemetry-approval.tsx index 5b38c2963..0c21a4cd3 100644 --- a/apps/trading/components/welcome-dialog/telemetry-approval.tsx +++ b/apps/trading/components/welcome-dialog/telemetry-approval.tsx @@ -16,29 +16,32 @@ export const TelemetryApproval = ({ setTelemetryValue, }: Props) => { return ( -
    +
    -
    +

    {t( 'Help us identify bugs and improve Vega Governance by sharing anonymous usage data.' )} -

    -
    - -
    -
    {t('Anonymous')}
    -
    {t('Your identity is always anonymous on Vega')}
    +

    +
    + +
    +
    {t('Anonymous')}
    +

    + {t('Your identity is always anonymous on Vega')} +

    - -
    - -
    -
    {t('Optional')}
    -
    {t('You can opt out any time via settings')}
    +
    + +
    +
    {t('Optional')}
    +

    + {t('You can opt out any time via settings')} +

    -
    +
    setTelemetryValue('false')} size="small" diff --git a/apps/trading/components/welcome-dialog/welcome-dialog.tsx b/apps/trading/components/welcome-dialog/welcome-dialog.tsx index a3c42cdf1..7f7cbb06e 100644 --- a/apps/trading/components/welcome-dialog/welcome-dialog.tsx +++ b/apps/trading/components/welcome-dialog/welcome-dialog.tsx @@ -92,7 +92,7 @@ export const WelcomeDialog = () => { intent: Intent.Primary, content: ( <> -

    +

    {t('Improve vega console')}

    - {t('Deposit')} - - + {t('Withdraw')} - - + {t('Transfer')} - - + {t('View usage breakdown')} - - + { openAssetDialog(assetId, e.target as HTMLElement); }} > {t('View asset details')} - - + + {assetContractAddress && ( - + - + )} ); diff --git a/libs/accounts/src/lib/accounts-table.tsx b/libs/accounts/src/lib/accounts-table.tsx index 847868230..413eb9338 100644 --- a/libs/accounts/src/lib/accounts-table.tsx +++ b/libs/accounts/src/lib/accounts-table.tsx @@ -11,9 +11,13 @@ import type { VegaValueFormatterParams, } from '@vegaprotocol/datagrid'; import { COL_DEFS } from '@vegaprotocol/datagrid'; -import { Button, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit'; - -import { TooltipCellComponent } from '@vegaprotocol/ui-toolkit'; +import { + Intent, + TradingButton, + VegaIcon, + VegaIconNames, + TooltipCellComponent, +} from '@vegaprotocol/ui-toolkit'; import { AgGridLazy as AgGrid } from '@vegaprotocol/datagrid'; import type { IGetRowsParams, @@ -186,7 +190,7 @@ export const AccountTable = ({ ) : ( <> {valueFormatted} - + {t('0.00%')} @@ -248,26 +252,26 @@ export const AccountTable = ({ colId: 'accounts-actions', field: 'asset.id', ...COL_DEFS.actions, - minWidth: showDepositButton ? 130 : COL_DEFS.actions.minWidth, - maxWidth: showDepositButton ? 130 : COL_DEFS.actions.maxWidth, + minWidth: showDepositButton ? 105 : COL_DEFS.actions.minWidth, + maxWidth: showDepositButton ? 105 : COL_DEFS.actions.maxWidth, cellRenderer: ({ value: assetId, node, }: VegaICellRendererParams) => { if (!assetId) return null; - if (node.rowPinned && node.data?.total === '0') { + if (node.rowPinned && node.data?.balance === '0') { return ( - +
    ); } diff --git a/libs/accounts/src/lib/transfer-form.tsx b/libs/accounts/src/lib/transfer-form.tsx index a3e81c4c7..f0adcba94 100644 --- a/libs/accounts/src/lib/transfer-form.tsx +++ b/libs/accounts/src/lib/transfer-form.tsx @@ -8,7 +8,6 @@ import { } from '@vegaprotocol/utils'; import { t } from '@vegaprotocol/i18n'; import { - Button, TradingFormGroup, TradingInput, TradingInputError, @@ -16,6 +15,7 @@ import { TradingSelect, Tooltip, TradingCheckbox, + TradingButton, } from '@vegaprotocol/ui-toolkit'; import type { Transfer } from '@vegaprotocol/wallet'; import { normalizeTransfer } from '@vegaprotocol/wallet'; @@ -276,9 +276,9 @@ export const TransferForm = ({ decimals={asset?.decimals} /> )} - + ); }; @@ -309,8 +309,8 @@ export const TransferFee = ({ const totalValue = new BigNumber(transferAmount).plus(fee).toString(); return ( -
    -
    +
    +
    -
    +
    -
    +
    !curr); onChange(); }} - className="ml-auto text-sm absolute top-0 right-0 underline" + className="absolute top-0 right-0 ml-auto text-sm underline" > {isInput ? t('Select from wallet') : t('Enter manually')} diff --git a/libs/assets/src/lib/asset-details-dialog.tsx b/libs/assets/src/lib/asset-details-dialog.tsx index 1fb26fe48..9f2fa7503 100644 --- a/libs/assets/src/lib/asset-details-dialog.tsx +++ b/libs/assets/src/lib/asset-details-dialog.tsx @@ -2,9 +2,10 @@ import { t } from '@vegaprotocol/i18n'; import { Button, Dialog, - Icon, Splash, SyntaxHighlighter, + VegaIcon, + VegaIconNames, } from '@vegaprotocol/ui-toolkit'; import { create } from 'zustand'; import { AssetDetailsTable } from './asset-details-table'; @@ -82,7 +83,7 @@ export const AssetDetailsDialog = ({ return ( } + icon={} open={open} onChange={(isOpen) => onChange(isOpen)} onCloseAutoFocus={(e) => { @@ -97,7 +98,7 @@ export const AssetDetailsDialog = ({ }} > {content} -

    +

    {t( 'There is 1 unit of the settlement asset (%s) to every 1 quote unit.', [assetSymbol] diff --git a/libs/assets/src/lib/asset-details-table.tsx b/libs/assets/src/lib/asset-details-table.tsx index eca9a30a3..f6ef156a9 100644 --- a/libs/assets/src/lib/asset-details-table.tsx +++ b/libs/assets/src/lib/asset-details-table.tsx @@ -3,11 +3,8 @@ import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; import { t } from '@vegaprotocol/i18n'; import type * as Schema from '@vegaprotocol/types'; import type { KeyValueTableRowProps } from '@vegaprotocol/ui-toolkit'; -import { - CopyWithTooltip, - Icon, - truncateMiddle, -} from '@vegaprotocol/ui-toolkit'; +import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit'; +import { CopyWithTooltip, truncateMiddle } from '@vegaprotocol/ui-toolkit'; import { KeyValueTable, KeyValueTableRow, @@ -118,7 +115,7 @@ export const rows: Rows = [ {' '} diff --git a/libs/candles-chart/src/lib/candles-menu.spec.tsx b/libs/candles-chart/src/lib/candles-menu.spec.tsx index 85f8b5b24..9289581bd 100644 --- a/libs/candles-chart/src/lib/candles-menu.spec.tsx +++ b/libs/candles-chart/src/lib/candles-menu.spec.tsx @@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event'; import { CandlesMenu } from './candles-menu'; describe('CandlesMenu', () => { - it('should render with volume study showing by default', async () => { + it('should render with the correct default studies', async () => { render(); await userEvent.click( @@ -13,5 +13,21 @@ describe('CandlesMenu', () => { ); expect(await screen.findByRole('menu')).toBeInTheDocument(); expect(screen.getByText('Volume')).toHaveAttribute('data-state', 'checked'); + expect(screen.getByText('MACD')).toHaveAttribute('data-state', 'checked'); + }); + + it('should render with the correct default overlays', async () => { + render(); + + await userEvent.click( + screen.getByRole('button', { + name: 'Overlays', + }) + ); + expect(await screen.findByRole('menu')).toBeInTheDocument(); + expect(screen.getByText('Moving average')).toHaveAttribute( + 'data-state', + 'checked' + ); }); }); diff --git a/libs/candles-chart/src/lib/use-candles-chart-settings.ts b/libs/candles-chart/src/lib/use-candles-chart-settings.ts index 78e5f81f9..cce6fb5b5 100644 --- a/libs/candles-chart/src/lib/use-candles-chart-settings.ts +++ b/libs/candles-chart/src/lib/use-candles-chart-settings.ts @@ -15,8 +15,8 @@ interface StoredSettings { const DEFAULT_CHART_SETTINGS = { interval: Interval.I15M, type: ChartType.CANDLE, - overlays: [], - studies: [Study.VOLUME], + overlays: [Overlay.MOVING_AVERAGE], + studies: [Study.MACD, Study.VOLUME], }; export const useCandlesChartSettingsStore = create< diff --git a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx index 776426450..4275a10cd 100644 --- a/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx +++ b/libs/deal-ticket/src/components/deal-ticket/deal-ticket-fee-details.tsx @@ -12,6 +12,8 @@ import { formatRange, formatValue } from '@vegaprotocol/utils'; import { marketMarginDataProvider } from '@vegaprotocol/accounts'; import { useDataProvider } from '@vegaprotocol/data-provider'; +import * as Accordion from '@radix-ui/react-accordion'; + import { MARGIN_DIFF_TOOLTIP_TEXT, DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT, @@ -22,6 +24,7 @@ import { } from '../../constants'; import { useEstimateFees } from '../../hooks'; import { KeyValue } from './key-value'; +import { TOOLTIP_TRIGGER_CLASS_NAME } from '@vegaprotocol/ui-toolkit'; const emptyValue = '-'; @@ -242,55 +245,72 @@ export const DealTicketMarginDetails = ({ return ( <> - - - {deductionFromCollateral} - setBreakdownDialog(true) : undefined - } - value={formatValue(marginAccountBalance, assetDecimals)} - symbol={assetSymbol} - labelDescription={MARGIN_ACCOUNT_TOOLTIP_TEXT} - formattedValue={formatValue( - marginAccountBalance, - assetDecimals, - quantum - )} - /> + + + + {t('Margin required')} + + } + value={formatRange( + marginRequiredBestCase, + marginRequiredWorstCase, + assetDecimals + )} + formattedValue={formatRange( + marginRequiredBestCase, + marginRequiredWorstCase, + assetDecimals, + quantum + )} + labelDescription={MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol)} + symbol={assetSymbol} + /> + + + {deductionFromCollateral} + setBreakdownDialog(true) + : undefined + } + value={formatValue(marginAccountBalance, assetDecimals)} + symbol={assetSymbol} + labelDescription={MARGIN_ACCOUNT_TOOLTIP_TEXT} + formattedValue={formatValue( + marginAccountBalance, + assetDecimals, + quantum + )} + /> + + + {projectedMargin} { target: { value: '8' }, }); - fireEvent.click( - screen.getByText('Deposit', { selector: '[type="submit"]' }) - ); + fireEvent.click(screen.getByRole('button', { name: 'Deposit' })); await waitFor(() => { expect(props.submitDeposit).toHaveBeenCalledWith({ diff --git a/libs/deposits/src/lib/deposit-form.tsx b/libs/deposits/src/lib/deposit-form.tsx index 0044592c2..0bee8999f 100644 --- a/libs/deposits/src/lib/deposit-form.tsx +++ b/libs/deposits/src/lib/deposit-form.tsx @@ -13,7 +13,6 @@ import { import { t } from '@vegaprotocol/i18n'; import { useLocalStorage } from '@vegaprotocol/react-helpers'; import { - Button, TradingFormGroup, TradingInput, TradingInputError, @@ -23,6 +22,7 @@ import { ButtonLink, TradingSelect, truncateMiddle, + TradingButton, } from '@vegaprotocol/ui-toolkit'; import { useVegaWallet } from '@vegaprotocol/wallet'; import { useWeb3React } from '@web3-react/core'; @@ -186,14 +186,14 @@ export const DepositForm = ({ ); } return ( - + ); }} /> @@ -435,15 +435,14 @@ const FormButton = ({ approved, selectedAsset }: FormButtonProps) => { />

    )} - + ); }; @@ -455,7 +454,7 @@ const UseButton = (props: UseButtonProps) => {
    )} - +
    ); diff --git a/libs/market-depth/src/lib/orderbook-controls.tsx b/libs/market-depth/src/lib/orderbook-controls.tsx index 62c3f6b0a..b41e146e2 100644 --- a/libs/market-depth/src/lib/orderbook-controls.tsx +++ b/libs/market-depth/src/lib/orderbook-controls.tsx @@ -7,7 +7,7 @@ import { TradingDropdownContent, TradingDropdownItem, } from '@vegaprotocol/ui-toolkit'; -import { formatNumberFixed } from '@vegaprotocol/utils'; +import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; export const OrderbookControls = ({ lastTradedPrice, @@ -15,27 +15,14 @@ export const OrderbookControls = ({ decimalPlaces, setResolution, }: { - lastTradedPrice: string | undefined; + lastTradedPrice: string; resolution: number; decimalPlaces: number; setResolution: (resolution: number) => void; }) => { const [isOpen, setOpen] = useState(false); - const resolutions = new Array( - Math.max(lastTradedPrice?.toString().length ?? 0, decimalPlaces + 1) - ) - .fill(null) - .map((v, i) => Math.pow(10, i)); - - const formatResolution = (r: number) => { - return formatNumberFixed( - Math.log10(r) - decimalPlaces > 0 - ? Math.pow(10, Math.log10(r) - decimalPlaces) - : 0, - decimalPlaces - Math.log10(r) - ); - }; + const resolutions = createResolutions(lastTradedPrice, decimalPlaces); const increaseResolution = () => { const index = resolutions.indexOf(resolution); @@ -56,7 +43,7 @@ export const OrderbookControls = ({ } > {resolutions.map((r) => ( - setResolution(r)}> - {formatResolution(r)} + setResolution(r)} + className="justify-end" + > + {formatResolution(r, decimalPlaces)} ))} @@ -99,7 +92,7 @@ export const OrderbookControls = ({
    ); }; + +export const formatResolution = (r: number, decimalPlaces: number) => { + let num = addDecimalsFormatNumber(r, decimalPlaces); + + // Remove trailing zeroes + num = num.replace(/\.?0+$/, ''); + + return num; +}; + +/** + * Create a list of resolutions based on the largest and smallest + * possible values using the last traded price and the market + * decimal places + */ +export const createResolutions = ( + lastTradedPrice: string, + decimalPlaces: number +) => { + // number of levels determined by either the number + // of digits in the last traded price OR the number of decimal + // places. For example: + // + // last traded = 1 (0.001) + // dps = 3 + // result = 3 + // + // last traded = 100001 (1000.01 + // dps = 2 + // result = 6 + const levelCount = Math.max(lastTradedPrice.length ?? 0, decimalPlaces + 1); + const generatedResolutions = new Array(levelCount) + .fill(null) + .map((_, i) => Math.pow(10, i)); + const customResolutions = [2, 5, 20, 50, 200, 500]; + const combined = customResolutions.concat(generatedResolutions); + combined.sort((a, b) => a - b); + + // Remove any resolutions higher than the generated ones as + // we dont want a custom resolution higher than necessary + const resolutions = combined.filter((r) => { + return r <= generatedResolutions[generatedResolutions.length - 1]; + }); + + return resolutions; +}; diff --git a/libs/market-depth/src/lib/orderbook-data.ts b/libs/market-depth/src/lib/orderbook-data.ts index 31be1491a..2b81eb6e5 100644 --- a/libs/market-depth/src/lib/orderbook-data.ts +++ b/libs/market-depth/src/lib/orderbook-data.ts @@ -12,7 +12,7 @@ export interface OrderbookRowData { cumulativeVol: number; } -export const getPriceLevel = (price: string | bigint, resolution: number) => { +export const getPriceLevel = (price: string, resolution: number) => { const p = BigInt(price); const r = BigInt(resolution); let priceLevel = (p / r) * r; @@ -43,7 +43,7 @@ const updateCumulativeVolumeByType = ( }; export const compactRows = ( - data: PriceLevelFieldsFragment[] | null | undefined, + data: PriceLevelFieldsFragment[], dataType: VolumeType, resolution: number ) => { diff --git a/libs/market-depth/src/lib/orderbook-row.tsx b/libs/market-depth/src/lib/orderbook-row.tsx index 82cc238c1..e6e2ffcb2 100644 --- a/libs/market-depth/src/lib/orderbook-row.tsx +++ b/libs/market-depth/src/lib/orderbook-row.tsx @@ -13,6 +13,7 @@ interface OrderbookRowProps { cumulativeVolume: number; decimalPlaces: number; positionDecimalPlaces: number; + priceFormatDecimalPlaces: number; price: string; onClick: (args: { price?: string; size?: string }) => void; type: VolumeType; @@ -26,6 +27,7 @@ export const OrderbookRow = memo( cumulativeVolume, decimalPlaces, positionDecimalPlaces, + priceFormatDecimalPlaces, price, onClick, type, @@ -35,6 +37,7 @@ export const OrderbookRow = memo( const txtId = type === VolumeType.bid ? 'bid' : 'ask'; const cols = width >= HIDE_CUMULATIVE_VOL_WIDTH ? 3 : width >= HIDE_VOL_WIDTH ? 2 : 1; + return (
    { jest.clearAllMocks(); mockOffsetSize(800, 768); }); - it('markPrice should be in the middle', async () => { + + it('lastTradedPrice should be in the middle', async () => { render( { expect( await screen.findByTestId(`last-traded-${params.lastTradedPrice}`) ).toBeInTheDocument(); + // Before resolution change the price is 122.934 await userEvent.click(screen.getByTestId('price-122901')); expect(onClickSpy).toBeCalledWith({ price: '122.901' }); @@ -86,15 +89,16 @@ describe('Orderbook', () => { expect(orderbookData.compactRows).toHaveBeenCalledWith( mockedData.bids, VolumeType.bid, - 10 + 2 ); expect(orderbookData.compactRows).toHaveBeenCalledWith( mockedData.asks, VolumeType.ask, - 10 + 2 ); - await userEvent.click(screen.getByTestId('price-12294')); - expect(onClickSpy).toBeCalledWith({ price: '122.94' }); + + await userEvent.click(screen.getByTestId('price-122938')); + expect(onClickSpy).toBeCalledWith({ price: '122.938' }); }); it('plus - minus buttons should change resolution', async () => { @@ -114,26 +118,30 @@ describe('Orderbook', () => { 1 ); expect(screen.getByTestId('minus-button')).toBeDisabled(); - userEvent.click(screen.getByTestId('plus-button')); + await userEvent.click(screen.getByTestId('plus-button')); + expect((orderbookData.compactRows as jest.Mock).mock.lastCall[2]).toEqual( + 2 + ); + + await userEvent.click(screen.getByTestId('plus-button')); + expect((orderbookData.compactRows as jest.Mock).mock.lastCall[2]).toEqual( + 5 + ); + + expect(screen.getByTestId('minus-button')).not.toBeDisabled(); + await userEvent.click(screen.getByTestId('minus-button')); await waitFor(() => { expect((orderbookData.compactRows as jest.Mock).mock.lastCall[2]).toEqual( - 10 + 2 ); }); expect(screen.getByTestId('minus-button')).not.toBeDisabled(); - userEvent.click(screen.getByTestId('minus-button')); - await waitFor(() => { - expect((orderbookData.compactRows as jest.Mock).mock.lastCall[2]).toEqual( - 1 - ); - }); - expect(screen.getByTestId('minus-button')).toBeDisabled(); - await userEvent.click(screen.getByTestId('resolution')); + await userEvent.click(screen.getByTestId('resolution')); await waitFor(() => { expect(screen.getByRole('menu')).toBeInTheDocument(); }); - await userEvent.click(screen.getAllByRole('menuitem')[5]); + await userEvent.click(screen.getAllByRole('menuitem')[11]); await waitFor(() => { expect((orderbookData.compactRows as jest.Mock).mock.lastCall[2]).toEqual( 100000 @@ -223,3 +231,58 @@ describe('OrderbookMid', () => { expect(screen.getByTestId('icon-arrow-down')).toBeInTheDocument(); }); }); + +describe('createResolutions', () => { + it('create resolutions relative to the market', () => { + expect( + createResolutions( + '1', // 0.001 + 3 + ) + ).toEqual([1, 2, 5, 10, 20, 50, 100, 200, 500, 1000]); + + expect( + createResolutions( + '190017', // 1900.17 + 2 + ) + ).toEqual([1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 10000, 100000]); + + expect( + createResolutions( + '123456789', // 1234.56789 + 5 + ) + ).toEqual([ + 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 10000, 100000, 1000000, + 10000000, 100000000, + ]); + }); + + it('removes resolutions that arent precise enough for the market', () => { + expect( + createResolutions( + '1', // 0.01 + 2 + ) + ).toEqual([1, 2, 5, 10, 20, 50, 100]); + }); +}); + +describe('formatResolution', () => { + it('formats less than 1', () => { + expect(formatResolution(1, 2)).toEqual('0.01'); + expect(formatResolution(1, 3)).toEqual('0.001'); + expect(formatResolution(2, 4)).toEqual('0.0002'); + expect(formatResolution(5, 8)).toEqual('0.00000005'); + expect(formatResolution(10000, 5)).toEqual('0.1'); + }); + + it('formats greater than 1', () => { + expect(formatResolution(1000, 2)).toEqual('10'); + expect(formatResolution(100000, 4)).toEqual('10'); + expect(formatResolution(10000000, 2)).toEqual('100,000'); + expect(formatResolution(500, 2)).toEqual('5'); + expect(formatResolution(500, 1)).toEqual('50'); + }); +}); diff --git a/libs/market-depth/src/lib/orderbook.tsx b/libs/market-depth/src/lib/orderbook.tsx index 145ddbb30..8d4878817 100644 --- a/libs/market-depth/src/lib/orderbook.tsx +++ b/libs/market-depth/src/lib/orderbook.tsx @@ -23,6 +23,7 @@ const OrderbookSide = ({ type, decimalPlaces, positionDecimalPlaces, + priceFormatDecimalPlaces, onClick, width, maxVol, @@ -31,6 +32,7 @@ const OrderbookSide = ({ resolution: number; decimalPlaces: number; positionDecimalPlaces: number; + priceFormatDecimalPlaces: number; type: VolumeType; onClick: (args: { price?: string; size?: string }) => void; width: number; @@ -53,10 +55,11 @@ const OrderbookSide = ({ {rows.map((data) => (
    @@ -203,6 +212,7 @@ export const Orderbook = ({ resolution={resolution} decimalPlaces={decimalPlaces} positionDecimalPlaces={positionDecimalPlaces} + priceFormatDecimalPlaces={priceFormatDecimalPlaces} onClick={onClick} width={width} maxVol={maxVol} @@ -220,6 +230,7 @@ export const Orderbook = ({ resolution={resolution} decimalPlaces={decimalPlaces} positionDecimalPlaces={positionDecimalPlaces} + priceFormatDecimalPlaces={priceFormatDecimalPlaces} onClick={onClick} width={width} maxVol={maxVol} diff --git a/libs/markets/src/lib/markets-provider.ts b/libs/markets/src/lib/markets-provider.ts index 5fe37a720..021b78fca 100644 --- a/libs/markets/src/lib/markets-provider.ts +++ b/libs/markets/src/lib/markets-provider.ts @@ -199,7 +199,7 @@ export const allMarketsWithLiveDataProvider = makeDerivedDataProvider< return data.find( (market) => market.id === - (parts[1].delta as MarketDataUpdateFieldsFragment).marketId + (parts[1].delta as MarketDataUpdateFieldsFragment)?.marketId ); } ); diff --git a/libs/orders/src/lib/components/order-list/order-edit-dialog.tsx b/libs/orders/src/lib/components/order-list/order-edit-dialog.tsx index 4710035b7..4ccfd2aa9 100644 --- a/libs/orders/src/lib/components/order-list/order-edit-dialog.tsx +++ b/libs/orders/src/lib/components/order-list/order-edit-dialog.tsx @@ -12,9 +12,10 @@ import { TradingFormGroup, TradingInput, TradingInputError, - Button, Dialog, - Icon, + VegaIcon, + VegaIconNames, + TradingButton, } from '@vegaprotocol/ui-toolkit'; import { useForm } from 'react-hook-form'; import type { Order } from '../order-data-provider'; @@ -37,7 +38,7 @@ export const OrderEditDialog = ({ order, onSubmit, }: OrderEditDialogProps) => { - const headerClassName = 'text-lg font-bold text-black dark:text-white'; + const headerClassName = 'text-xs font-bold text-black dark:text-white'; const { register, formState: { errors }, @@ -57,13 +58,13 @@ export const OrderEditDialog = ({ open={isOpen} onChange={onChange} title={t('Edit order')} - icon={} + icon={} >
    {order.market && (

    {t(`Market`)}

    -

    {t(`${order.market.tradableInstrument.instrument.name}`)}

    +

    {order.market.tradableInstrument.instrument.code}

    )} {order.type === Schema.OrderType.TYPE_LIMIT && order.market && ( @@ -149,9 +150,7 @@ export const OrderEditDialog = ({ )}
    - + {t('Update')} ); diff --git a/libs/orders/src/lib/components/order-list/order-list.tsx b/libs/orders/src/lib/components/order-list/order-list.tsx index aca064bd0..a98821a53 100644 --- a/libs/orders/src/lib/components/order-list/order-list.tsx +++ b/libs/orders/src/lib/components/order-list/order-list.tsx @@ -10,7 +10,7 @@ import { ActionsDropdown, ButtonLink, TradingDropdownCopyItem, - DropdownMenuItem, + TradingDropdownItem, VegaIcon, VegaIconNames, } from '@vegaprotocol/ui-toolkit'; @@ -271,19 +271,20 @@ export const OrderListTable = memo< { colId: 'amend', ...COL_DEFS.actions, - minWidth: showAllActions ? 90 : COL_DEFS.actions.minWidth, - maxWidth: showAllActions ? 90 : COL_DEFS.actions.minWidth, + minWidth: showAllActions ? 80 : COL_DEFS.actions.minWidth, + maxWidth: showAllActions ? 80 : COL_DEFS.actions.minWidth, cellRenderer: ({ data }: { data?: Order }) => { if (!data) return null; return ( -
    +
    {isOrderAmendable(data) && !props.isReadOnly && ( <> {!data.icebergOrder && ( onEdit(data)} + title={t('Edit order')} > @@ -291,6 +292,7 @@ export const OrderListTable = memo< onCancel(data)} + title={t('Cancel order')} > @@ -301,14 +303,14 @@ export const OrderListTable = memo< value={data.id} text={t('Copy order ID')} /> - onView(data)} > {t('View order details')} - +
    ); diff --git a/libs/orders/src/lib/components/stop-orders-table/stop-orders-table.tsx b/libs/orders/src/lib/components/stop-orders-table/stop-orders-table.tsx index 9af429a0e..5c8a9e049 100644 --- a/libs/orders/src/lib/components/stop-orders-table/stop-orders-table.tsx +++ b/libs/orders/src/lib/components/stop-orders-table/stop-orders-table.tsx @@ -12,7 +12,7 @@ import { ButtonLink, VegaIcon, VegaIconNames, - DropdownMenuItem, + TradingDropdownItem, TradingDropdownCopyItem, Pill, } from '@vegaprotocol/ui-toolkit'; @@ -246,7 +246,7 @@ export const StopOrdersTable = memo( if (!data) return null; return ( -
    +
    {data.status === Schema.StopOrderStatus.STATUS_PENDING && !props.isReadOnly && ( - @@ -273,7 +273,7 @@ export const StopOrdersTable = memo( > {t('View order details')} - + )}
    diff --git a/libs/positions/src/lib/positions-table.spec.tsx b/libs/positions/src/lib/positions-table.spec.tsx index 7dc85c5d1..ed2aebf4c 100644 --- a/libs/positions/src/lib/positions-table.spec.tsx +++ b/libs/positions/src/lib/positions-table.spec.tsx @@ -61,7 +61,7 @@ describe('Positions', () => { 'Market', 'Size / Notional', 'Entry / Mark', - 'Margin', + 'Margin / Leverage', 'Liquidation', 'Realised PNL', 'Unrealised PNL', diff --git a/libs/positions/src/lib/positions-table.tsx b/libs/positions/src/lib/positions-table.tsx index 11d7ce1c1..a7ef20e93 100644 --- a/libs/positions/src/lib/positions-table.tsx +++ b/libs/positions/src/lib/positions-table.tsx @@ -292,7 +292,7 @@ export const PositionsTable = ({ }, }, { - headerName: t('Margin'), + headerName: t('Margin / Leverage'), colId: 'margin', type: 'rightAligned', cellClass: 'font-mono text-right', @@ -456,13 +456,14 @@ export const PositionsTable = ({ ...COL_DEFS.actions, cellRenderer: ({ data }: VegaICellRendererParams) => { return ( -
    +
    {data?.openVolume && data?.openVolume !== '0' && data.partyId === pubKey ? ( data && onClose(data)} + title={t('Close position')} > @@ -548,9 +549,9 @@ const WarningCell = ({ showIcon?: boolean; }) => { return ( -
    +
    {showIcon && ( - + )} diff --git a/libs/proposals/src/components/proposals-list/proposals-list.tsx b/libs/proposals/src/components/proposals-list/proposals-list.tsx index 38ab2b753..9cdb01aef 100644 --- a/libs/proposals/src/components/proposals-list/proposals-list.tsx +++ b/libs/proposals/src/components/proposals-list/proposals-list.tsx @@ -35,16 +35,13 @@ export const ProposalsList = ({ const { columnDefs, defaultColDef } = useColumnDefs(); return ( -
    - data.id} - overlayNoRowsTemplate={t('No markets')} - components={{ SuccessorMarketRenderer, MarketNameProposalCell }} - /> -
    + data.id} + overlayNoRowsTemplate={t('No markets')} + components={{ SuccessorMarketRenderer, MarketNameProposalCell }} + /> ); }; diff --git a/libs/react-helpers/src/hooks/use-number-parts.ts b/libs/react-helpers/src/hooks/use-number-parts.ts index 606e854a8..2c3ba399d 100644 --- a/libs/react-helpers/src/hooks/use-number-parts.ts +++ b/libs/react-helpers/src/hooks/use-number-parts.ts @@ -5,6 +5,6 @@ import { toNumberParts } from '@vegaprotocol/utils'; export const useNumberParts = ( value: BigNumber | null | undefined, decimals: number -): [integers: string, decimalPlaces: string] => { +): [integers: string, decimalPlaces: string, separator: string | undefined] => { return useMemo(() => toNumberParts(value, decimals), [decimals, value]); }; diff --git a/libs/ui-toolkit/src/components/icon/blueprint-icons/icon.tsx b/libs/ui-toolkit/src/components/icon/blueprint-icons/icon.tsx index ca238b76a..5118d720f 100644 --- a/libs/ui-toolkit/src/components/icon/blueprint-icons/icon.tsx +++ b/libs/ui-toolkit/src/components/icon/blueprint-icons/icon.tsx @@ -7,7 +7,7 @@ export type { IconName } from '@blueprintjs/icons'; export interface IconProps { name: IconName; className?: string; - size?: 2 | 3 | 4 | 6 | 8 | 10 | 12 | 14 | 16; + size?: 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 | 14 | 16; ariaLabel?: string; } @@ -23,6 +23,7 @@ export const Icon = ({ size = 4, name, className, ariaLabel }: IconProps) => { 'w-2 h-2': size === 2, 'w-3 h-3': size === 3, 'w-4 h-4': size === 4, + 'w-5 h-5': size === 5, 'w-6 h-6': size === 6, 'w-8 h-8': size === 8, 'w-10 h-10': size === 10, diff --git a/libs/ui-toolkit/src/components/icon/vega-icons/svg-icons/icon-info.tsx b/libs/ui-toolkit/src/components/icon/vega-icons/svg-icons/icon-info.tsx index 84467122f..4005187f7 100644 --- a/libs/ui-toolkit/src/components/icon/vega-icons/svg-icons/icon-info.tsx +++ b/libs/ui-toolkit/src/components/icon/vega-icons/svg-icons/icon-info.tsx @@ -1,11 +1,6 @@ export const IconInfo = ({ size = 14 }: { size: number }) => { return ( - + ); diff --git a/libs/ui-toolkit/src/components/toast/toast.tsx b/libs/ui-toolkit/src/components/toast/toast.tsx index 89d7155cf..5c4b1c5c9 100644 --- a/libs/ui-toolkit/src/components/toast/toast.tsx +++ b/libs/ui-toolkit/src/components/toast/toast.tsx @@ -10,7 +10,7 @@ import { useCallback } from 'react'; import { useLayoutEffect } from 'react'; import { useRef } from 'react'; import { Intent } from '../../utils/intent'; -import { Icon } from '../icon'; +import { Icon, VegaIcon, VegaIconNames } from '../icon'; import { Loader } from '../loader'; import { t } from '@vegaprotocol/i18n'; @@ -317,18 +317,14 @@ export const Toast = ({ } )} > -
    +
    diff --git a/libs/ui-toolkit/src/components/toast/toasts-container.tsx b/libs/ui-toolkit/src/components/toast/toasts-container.tsx index 00fc88e4e..ae380b16d 100644 --- a/libs/ui-toolkit/src/components/toast/toasts-container.tsx +++ b/libs/ui-toolkit/src/components/toast/toasts-container.tsx @@ -3,7 +3,7 @@ import { usePrevious } from '@vegaprotocol/react-helpers'; import classNames from 'classnames'; import type { Ref } from 'react'; import { useLayoutEffect, useRef } from 'react'; -import { Button } from '../button'; +import { TradingButton } from '../trading-button'; import { Toast } from './toast'; import type { Toasts } from './use-toasts'; import { ToastPosition, useToasts, useToastsConfiguration } from './use-toasts'; @@ -87,26 +87,27 @@ export const ToastsContainer = ({ ); })} - + { + closeAll(); + }} + > + {t('Dismiss all')} + +
    ); diff --git a/libs/ui-toolkit/src/components/tooltip/tooltip.tsx b/libs/ui-toolkit/src/components/tooltip/tooltip.tsx index 9c629d785..fdbae9814 100644 --- a/libs/ui-toolkit/src/components/tooltip/tooltip.tsx +++ b/libs/ui-toolkit/src/components/tooltip/tooltip.tsx @@ -20,6 +20,9 @@ export interface TooltipProps { sideOffset?: number; } +export const TOOLTIP_TRIGGER_CLASS_NAME = + 'underline underline-offset-2 decoration-neutral-400 dark:decoration-neutral-400 decoration-dashed'; + // Conditionally rendered tooltip if description content is provided. export const Tooltip = ({ children, @@ -32,10 +35,7 @@ export const Tooltip = ({ description ? ( - + {children} {description && ( diff --git a/libs/ui-toolkit/src/components/trading-dropdown/trading-dropdown.tsx b/libs/ui-toolkit/src/components/trading-dropdown/trading-dropdown.tsx index 7d3db2ad3..fba456688 100644 --- a/libs/ui-toolkit/src/components/trading-dropdown/trading-dropdown.tsx +++ b/libs/ui-toolkit/src/components/trading-dropdown/trading-dropdown.tsx @@ -146,7 +146,7 @@ export const TradingDropdownItemIndicator = forwardRef< diff --git a/libs/ui-toolkit/src/components/trading-input/input.tsx b/libs/ui-toolkit/src/components/trading-input/input.tsx index 20345f08a..e50b4eda1 100644 --- a/libs/ui-toolkit/src/components/trading-input/input.tsx +++ b/libs/ui-toolkit/src/components/trading-input/input.tsx @@ -84,10 +84,8 @@ const getAffixElement = ({ 'absolute z-10 top-0 bottom-0 flex items-center', { 'fill-black dark:fill-white': prependIconName || appendIconName, - 'left-3': prependIconName, - 'right-3': appendIconName, - 'left-1': prependElement, - 'right-1': appendElement, + 'left-3': prependIconName || prependElement, + 'right-3': appendIconName || appendElement, } ); diff --git a/libs/ui-toolkit/src/components/trading-select/select.tsx b/libs/ui-toolkit/src/components/trading-select/select.tsx index a8869a661..acae3b473 100644 --- a/libs/ui-toolkit/src/components/trading-select/select.tsx +++ b/libs/ui-toolkit/src/components/trading-select/select.tsx @@ -2,7 +2,7 @@ import type { Ref, SelectHTMLAttributes } from 'react'; import { useRef } from 'react'; import { forwardRef } from 'react'; import classNames from 'classnames'; -import { Icon } from '..'; +import { VegaIcon, VegaIconNames } from '..'; import { defaultSelectElement } from '../../utils/shared'; import * as SelectPrimitive from '@radix-ui/react-select'; @@ -16,7 +16,7 @@ export interface TradingSelectProps export const TradingSelect = forwardRef( ({ className, hasError, ...props }, ref) => ( -
    +