Compare commits

..
74 changed files with 1706 additions and 2321 deletions
+2 -2
View File
@@ -23,7 +23,7 @@ NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
#Test configuration variables
CYPRESS_FAIRGROUND=false
@@ -31,4 +31,4 @@ LC_ALL="en_US.UTF-8"
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
NX_METAMASK_SNAPS=true
+2 -2
View File
@@ -20,7 +20,7 @@ NX_TRANCHES_SERVICE_URL=https://tranches-stagnet1-k8s.ops.vega.xyz
NX_VEGA_REST_URL=http://localhost:3008/api/v2/
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
@@ -30,4 +30,4 @@ CYPRESS_FAIRGROUND=false
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_METAMASK_SNAPS=false
NX_METAMASK_SNAPS=false
+2 -2
View File
@@ -15,11 +15,11 @@ NX_VEGA_REST_URL=https://api.n00.devnet1.vega.xyz/api/v2/
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
NX_METAMASK_SNAPS=true
+1 -1
View File
@@ -15,7 +15,7 @@ NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_REST_URL=https://api.vega.community/api/v2/
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-mainnet
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
+1 -1
View File
@@ -14,7 +14,7 @@ NX_TRANCHES_SERVICE_URL=https://tranches-mainnet-mirror-k8s.ops.vega.xyz
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/mainnet/announcements.json
NX_VEGA_REST_URL=https://api.mainnet-mirror.vega.rocks/api/v2/
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-mainnet
NX_TENDERMINT_URL=https://be.mainnet-mirror.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
+1 -1
View File
@@ -11,7 +11,7 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
NX_VEGA_REST_URL=https://api.n00.stagnet1.vega.xyz/api/v2/
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
+1 -1
View File
@@ -16,7 +16,7 @@ NX_VEGA_REST_URL=https://api.n07.testnet.vega.xyz/api/v2/
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
+1 -1
View File
@@ -13,7 +13,7 @@ NX_VEGA_REST_URL=https://api-validators-testnet.vega.rocks/api/v2/
NX_SENTRY_DSN=https://4b8c8a8ba07742648aa4dfe1b8d17e40@o286262.ingest.sentry.io/5882996
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.
+14 -2
View File
@@ -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",
+23 -50
View File
@@ -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 (
<AsyncRenderer
loading={networkParamsLoading}
error={networkParamsError}
data={networkParams}
>
<section className="mb-16" data-testid="home-proposals">
<Heading title={t('vegaGovernance')} />
<h3 className="mb-6">{t('homeProposalsIntro')}</h3>
<div className="mb-8">
<ExternalLink href={ExternalLinks.GOVERNANCE_PAGE}>
{t(`readMoreGovernance`)}
</ExternalLink>
</div>
<section className="mb-16" data-testid="home-proposals">
<Heading title={t('vegaGovernance')} />
<h3 className="mb-6">{t('homeProposalsIntro')}</h3>
<div className="mb-8">
<ExternalLink href={ExternalLinks.GOVERNANCE_PAGE}>
{t(`readMoreGovernance`)}
</ExternalLink>
</div>
<SubHeading title={t('latestProposals')} />
<ul data-testid="home-proposal-list" className="grid gap-6">
{protocolUpgradeProposals.map((proposal, index) => (
<ProtocolUpgradeProposalsListItem key={index} proposal={proposal} />
))}
<SubHeading title={t('latestProposals')} />
<ul data-testid="home-proposal-list" className="grid gap-6">
{protocolUpgradeProposals.map((proposal, index) => (
<ProtocolUpgradeProposalsListItem key={index} proposal={proposal} />
))}
{proposals.map((proposal) => (
<ProposalsListItem
key={proposal.id}
proposal={proposal}
networkParams={networkParams}
/>
))}
</ul>
{proposals.map((proposal) => (
<ProposalsListItem key={proposal.id} proposal={proposal} />
))}
</ul>
<div className="mt-6">
<Link to={`${Routes.PROPOSALS}`}>
<Button size="md">{t('homeProposalsButtonText')}</Button>
</Link>
</div>
</section>
</AsyncRenderer>
<div className="mt-6">
<Link to={`${Routes.PROPOSALS}`}>
<Button size="md">{t('homeProposalsButtonText')}</Button>
</Link>
</div>
</section>
);
};
@@ -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 = (
<ProposalHeader
proposal={proposal}
isListItem={isListItem}
networkParams={mockNetworkParams}
voteState={voteState}
/>
</VegaWalletContext.Provider>
@@ -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<NetworkParamsResult>;
isListItem?: boolean;
voteState?: VoteState | null;
}) => {
@@ -146,7 +143,7 @@ export const ProposalHeader = ({
className="flex items-center gap-2"
data-testid={`user-voted-${voteState.toLowerCase()}`}
>
<div className="text-vega-green" data-testid="you-voted-icon">
<div data-testid="you-voted-icon">
<VegaIcon name={VegaIconNames.VOTE} size={24} />
</div>
<div>
@@ -185,7 +182,7 @@ export const ProposalHeader = ({
</div>
)}
<ProposalVotingStatus proposal={proposal} networkParams={networkParams} />
<VoteBreakdown proposal={proposal} />
</>
);
};
@@ -1 +0,0 @@
export { ProposalVotesTable } from './proposal-votes-table';
@@ -1,119 +0,0 @@
import { render, screen, fireEvent } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { ProposalVotesTable } from './proposal-votes-table';
import { ProposalType } from '../proposal/proposal';
import {
generateNoVotes,
generateProposal,
generateYesVotes,
} from '../../test-helpers/generate-proposals';
const defaultProposal = generateProposal();
const defaultProposalType = ProposalType.PROPOSAL_NETWORK_PARAMETER;
const updateMarketProposal = generateProposal({
terms: {
change: {
__typename: 'UpdateMarket',
marketId: '12345',
},
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(10),
no: generateNoVotes(0),
},
});
const updateMarketProposalType = ProposalType.PROPOSAL_UPDATE_MARKET;
const renderComponent = (
proposal = defaultProposal,
proposalType = defaultProposalType
) =>
render(
<MockedProvider>
<AppStateProvider>
<ProposalVotesTable proposal={proposal} proposalType={proposalType} />
</AppStateProvider>
</MockedProvider>
);
describe('Proposal Votes Table', () => {
it('should render successfully', () => {
const { baseElement } = renderComponent();
expect(baseElement).toBeTruthy();
});
it('should show vote breakdown fields, excluding custom update market fields', () => {
renderComponent();
fireEvent.click(screen.getByTestId('vote-breakdown-toggle'));
expect(screen.getByText('Expected to pass')).toBeInTheDocument();
expect(screen.getByText('Token majority met')).toBeInTheDocument();
expect(screen.getByText('Token participation met')).toBeInTheDocument();
expect(screen.getByText('Tokens for proposal')).toBeInTheDocument();
expect(screen.getByText('Total Supply')).toBeInTheDocument();
expect(screen.getByText('Tokens against proposal')).toBeInTheDocument();
expect(screen.getByText('Participation required')).toBeInTheDocument();
expect(screen.getByText('Majority Required')).toBeInTheDocument();
expect(screen.getByText('Number of voting parties')).toBeInTheDocument();
expect(screen.getByText('Total tokens voted')).toBeInTheDocument();
expect(
screen.getByText('Total tokens voted percentage')
).toBeInTheDocument();
expect(screen.getByText('Number of votes for')).toBeInTheDocument();
expect(screen.getByText('Number of votes against')).toBeInTheDocument();
expect(screen.getByText('Yes percentage')).toBeInTheDocument();
expect(screen.getByText('No percentage')).toBeInTheDocument();
expect(screen.queryByText('Liquidity majority met')).toBeNull();
expect(screen.queryByText('Liquidity participation met')).toBeNull();
expect(screen.queryByText('Liquidity shares for proposal')).toBeNull();
});
it('displays different breakdown fields for update market proposal', () => {
renderComponent(updateMarketProposal, updateMarketProposalType);
fireEvent.click(screen.getByTestId('vote-breakdown-toggle'));
expect(screen.getByText('Liquidity majority met')).toBeInTheDocument();
expect(screen.getByText('Liquidity participation met')).toBeInTheDocument();
expect(
screen.getByText('Liquidity shares for proposal')
).toBeInTheDocument();
expect(screen.queryByText('Number of voting parties')).toBeNull();
expect(screen.queryByText('Total tokens voted')).toBeNull();
expect(screen.queryByText('Total tokens voted percentage')).toBeNull();
expect(screen.queryByText('Number of votes for')).toBeNull();
expect(screen.queryByText('Number of votes against')).toBeNull();
expect(screen.queryByText('Yes percentage')).toBeNull();
expect(screen.queryByText('No percentage')).toBeNull();
});
it('displays if an update market proposal will pass by token vote', () => {
renderComponent(updateMarketProposal, updateMarketProposalType);
fireEvent.click(screen.getByTestId('vote-breakdown-toggle'));
expect(screen.getByText('👍 by token vote')).toBeInTheDocument();
});
it('displays if an update market proposal will pass by LP vote', () => {
renderComponent(
generateProposal({
terms: {
change: {
__typename: 'UpdateMarket',
marketId: '12345',
},
},
votes: {
__typename: 'ProposalVotes',
yes: {
...generateYesVotes(0, 1, '10'),
},
no: {
...generateNoVotes(0, 1, '0'),
},
},
}),
updateMarketProposalType
);
fireEvent.click(screen.getByTestId('vote-breakdown-toggle'));
expect(screen.getByText('👍 by liquidity vote')).toBeInTheDocument();
});
});
@@ -1,177 +0,0 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
KeyValueTable,
KeyValueTableRow,
Thumbs,
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import { formatNumber, formatNumberPercentage } from '@vegaprotocol/utils';
import { SubHeading } from '../../../../components/heading';
import { useVoteInformation } from '../../hooks';
import { useAppState } from '../../../../contexts/app-state/app-state-context';
import { ProposalType } from '../proposal/proposal';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
interface ProposalVotesTableProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
proposalType: ProposalType | null;
}
export const ProposalVotesTable = ({
proposal,
proposalType,
}: ProposalVotesTableProps) => {
const { t } = useTranslation();
const {
appState: { totalSupply },
} = useAppState();
const [showDetails, setShowDetails] = useState(false);
const {
willPassByTokenVote,
willPassByLPVote,
totalTokensPercentage,
participationMet,
participationLPMet,
totalTokensVoted,
noPercentage,
yesPercentage,
noTokens,
yesTokens,
yesEquityLikeShareWeight,
yesVotes,
noVotes,
totalVotes,
requiredMajorityPercentage,
requiredParticipation,
majorityMet,
majorityLPMet,
} = useVoteInformation({ proposal });
const isUpdateMarket = proposalType === ProposalType.PROPOSAL_UPDATE_MARKET;
const updateMarketWillPass = willPassByTokenVote || willPassByLPVote;
const updateMarketVotePassMethod = willPassByTokenVote
? t('byTokenVote')
: t('byLiquidityVote');
return (
<>
<CollapsibleToggle
toggleState={showDetails}
setToggleState={setShowDetails}
dataTestId="vote-breakdown-toggle"
>
<SubHeading title={t('voteBreakdown')} />
</CollapsibleToggle>
{showDetails && (
<RoundedWrapper marginBottomLarge={true} paddingBottom={true}>
<KeyValueTable
data-testid="proposal-votes-table"
numerical={true}
headingLevel={4}
>
<KeyValueTableRow>
{t('expectedToPass')}
{isUpdateMarket ? (
updateMarketWillPass ? (
<Thumbs up={true} text={updateMarketVotePassMethod} />
) : (
<Thumbs up={false} />
)
) : willPassByTokenVote ? (
<Thumbs up={true} />
) : (
<Thumbs up={false} />
)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('majorityMet')}
{majorityMet ? <Thumbs up={true} /> : <Thumbs up={false} />}
</KeyValueTableRow>
{isUpdateMarket && (
<KeyValueTableRow>
{t('majorityLPMet')}
{majorityLPMet ? <Thumbs up={true} /> : <Thumbs up={false} />}
</KeyValueTableRow>
)}
<KeyValueTableRow>
{t('participationMet')}
{participationMet ? <Thumbs up={true} /> : <Thumbs up={false} />}
</KeyValueTableRow>
{isUpdateMarket && (
<KeyValueTableRow>
{t('participationLPMet')}
{participationLPMet ? (
<Thumbs up={true} />
) : (
<Thumbs up={false} />
)}
</KeyValueTableRow>
)}
<KeyValueTableRow>
{t('tokenForProposal')}
{formatNumber(yesTokens, 2)}
</KeyValueTableRow>
{isUpdateMarket && (
<KeyValueTableRow>
{t('tokenLPForProposal')}
{formatNumber(yesEquityLikeShareWeight, 2)}
</KeyValueTableRow>
)}
<KeyValueTableRow>
{t('totalSupply')}
{formatNumber(totalSupply, 2)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('tokensAgainstProposal')}
{formatNumber(noTokens, 2)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('participationRequired')}
{formatNumberPercentage(requiredParticipation)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('majorityRequired')}
{formatNumberPercentage(requiredMajorityPercentage)}
</KeyValueTableRow>
{!isUpdateMarket && (
<>
<KeyValueTableRow>
{t('numberOfVotingParties')}
{formatNumber(totalVotes, 0)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('totalTokensVotes')}
{formatNumber(totalTokensVoted, 2)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('totalTokenVotedPercentage')}
{formatNumberPercentage(totalTokensPercentage, 2)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('numberOfForVotes')}
{formatNumber(yesVotes, 0)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('numberOfAgainstVotes')}
{formatNumber(noVotes, 0)}
</KeyValueTableRow>
<KeyValueTableRow>
{t('yesPercentage')}
{formatNumberPercentage(yesPercentage, 2)}
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
{t('noPercentage')}
{formatNumberPercentage(noPercentage, 2)}
</KeyValueTableRow>
</>
)}
</KeyValueTable>
</RoundedWrapper>
)}
</>
);
};
@@ -1 +0,0 @@
export * from './proposal-voting-status';
@@ -1,153 +0,0 @@
import { render, screen } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import {
lastWeek,
mockWalletContext,
networkParamsQueryMock,
nextWeek,
mockNetworkParams,
} from '../../test-helpers/mocks';
import { ProposalVotingStatus } from './proposal-voting-status';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MockedResponse } from '@apollo/client/testing';
import {
generateNoVotes,
generateProposal,
generateYesVotes,
} from '../../test-helpers/generate-proposals';
import { ProposalState } from '@vegaprotocol/types';
import { BigNumber } from '../../../../lib/bignumber';
import type { AppState } from '../../../../contexts/app-state/app-state-context';
const mockTotalSupply = new BigNumber(100);
// Note - giving a fixedTokenValue of 1 means a ratio of 1:1 votes to tokens, making sums easier :)
const fixedTokenValue = 1000000000000000000;
const mockAppState: AppState = {
totalAssociated: new BigNumber('50063005'),
decimals: 18,
totalSupply: mockTotalSupply,
vegaWalletManageOverlay: false,
transactionOverlay: false,
bannerMessage: '',
disconnectNotice: false,
};
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
useAppState: () => ({
appState: mockAppState,
}),
}));
const renderComponent = (
proposal: ProposalQuery['proposal'],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mocks: MockedResponse<any>[] = [networkParamsQueryMock]
) =>
render(
<Router>
<MockedProvider mocks={mocks}>
<VegaWalletContext.Provider value={mockWalletContext}>
<ProposalVotingStatus
proposal={proposal}
networkParams={mockNetworkParams}
/>
</VegaWalletContext.Provider>
</MockedProvider>
</Router>
);
describe('ProposalVotingStatus', () => {
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(0);
});
afterAll(() => {
jest.useRealTimers();
});
it('Renders majority reached', () => {
const yesVotes = 100;
const noVotes = 0;
renderComponent(
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(yesVotes, fixedTokenValue),
no: generateNoVotes(noVotes, fixedTokenValue),
},
})
);
expect(screen.getByTestId('majority-reached')).toBeInTheDocument();
});
it('Renders majority not reached', () => {
const yesVotes = 20;
const noVotes = 80;
renderComponent(
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(yesVotes, fixedTokenValue),
no: generateNoVotes(noVotes, fixedTokenValue),
},
})
);
expect(screen.getByTestId('majority-not-reached')).toBeInTheDocument();
});
it('Renders participation reached', () => {
const yesVotes = 1000;
const noVotes = 0;
renderComponent(
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(yesVotes, fixedTokenValue),
no: generateNoVotes(noVotes, fixedTokenValue),
},
})
);
expect(screen.getByTestId('participation-reached')).toBeInTheDocument();
});
it('Renders participation not reached', () => {
const yesVotes = 0;
const noVotes = 0;
renderComponent(
generateProposal({
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(yesVotes, fixedTokenValue),
no: generateNoVotes(noVotes, fixedTokenValue),
},
})
);
expect(screen.getByTestId('participation-not-reached')).toBeInTheDocument();
});
});
@@ -1,174 +0,0 @@
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { Icon } from '@vegaprotocol/ui-toolkit';
import { useVoteInformation } from '../../hooks';
import { BigNumber } from '../../../../lib/bignumber';
import type { NetworkParamsResult } from '@vegaprotocol/network-parameters';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
const statusClasses = (reached: boolean) =>
classNames('flex items-center gap-2 px-4 py-2 rounded-md', {
'bg-vega-green-700': reached,
'bg-vega-red-700': !reached,
});
const MajorityStatus = ({
reached,
requiredMajority,
}: {
reached: boolean;
requiredMajority: string | null | undefined;
}) => {
const { t } = useTranslation();
return (
<div
className={statusClasses(reached)}
data-testid="proposal-majority-status"
>
{reached ? <Icon name="tick" /> : <Icon name="cross" />}
{reached ? (
<div data-testid="majority-reached">
{requiredMajority ? (
<>
{new BigNumber(requiredMajority).times(100).toString()}%{' '}
{t('majorityVotedForProposal')}
</>
) : (
t('requiredMajorityVotedForProposal')
)}
</div>
) : (
<div data-testid="majority-not-reached">
{requiredMajority ? (
<>
{new BigNumber(requiredMajority).times(100).toString()}%{' '}
{t('majorityNotVotedForProposal')}
</>
) : (
t('requiredMajorityNotVotedForProposal')
)}
</div>
)}
</div>
);
};
const ParticipationStatus = ({ reached }: { reached: boolean }) => {
const { t } = useTranslation();
return (
<div className={statusClasses(reached)}>
{reached ? (
<>
<Icon name="tick" />
<div data-testid="participation-reached">
{t('minParticipationReached')}
</div>
</>
) : (
<>
<Icon name="cross" />
<div data-testid="participation-not-reached">
{t('minParticipationNotReached')}
</div>
</>
)}
</div>
);
};
export const ProposalVotingStatus = ({
proposal,
networkParams,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
networkParams: Partial<NetworkParamsResult>;
}) => {
const { t } = useTranslation();
const { majorityMet, majorityLPMet, participationMet, participationLPMet } =
useVoteInformation({
proposal,
});
if (!proposal) {
return null;
}
const isUpdateMarket = proposal?.terms.change.__typename === 'UpdateMarket';
let requiredVotingMajority = null;
let requiredVotingMajorityLP = null;
if (networkParams) {
switch (proposal.terms.change.__typename) {
case 'NewMarket':
requiredVotingMajority =
networkParams.governance_proposal_market_requiredMajority;
break;
case 'UpdateMarket':
requiredVotingMajority =
networkParams.governance_proposal_updateMarket_requiredMajority;
requiredVotingMajorityLP =
networkParams.governance_proposal_updateMarket_requiredMajorityLP;
break;
case 'NewAsset':
requiredVotingMajority =
networkParams.governance_proposal_asset_requiredMajority;
break;
case 'UpdateAsset':
requiredVotingMajority =
networkParams.governance_proposal_updateAsset_requiredMajority;
break;
case 'UpdateNetworkParameter':
requiredVotingMajority =
networkParams.governance_proposal_updateNetParam_requiredMajority;
break;
case 'NewFreeform':
requiredVotingMajority =
networkParams.governance_proposal_freeform_requiredMajority;
break;
}
}
if (isUpdateMarket) {
return (
<div className="mb-6">
<p>{t('Token vote')}</p>
<div
className="grid grid-cols-2 gap-4 items-center"
data-testid="token-vote-statuses"
>
<MajorityStatus
reached={majorityMet}
requiredMajority={requiredVotingMajority}
/>{' '}
<ParticipationStatus reached={participationMet} />
</div>
<p className="mt-4">{t('Liquidity provider vote')}</p>
<div
className="grid grid-cols-2 gap-4 items-center"
data-testid="lp-vote-statuses"
>
<MajorityStatus
reached={majorityLPMet}
requiredMajority={requiredVotingMajorityLP}
/>{' '}
<ParticipationStatus reached={participationLPMet} />
</div>
</div>
);
}
return (
<div className="grid grid-cols-2 gap-4 items-center mb-6">
<MajorityStatus
reached={majorityMet}
requiredMajority={requiredVotingMajority}
/>{' '}
<ParticipationStatus reached={participationMet} />
</div>
);
};
@@ -34,12 +34,6 @@ jest.mock('../proposal-change-table', () => ({
jest.mock('../proposal-json', () => ({
ProposalJson: () => <div data-testid="proposal-json"></div>,
}));
jest.mock('../proposal-votes-table', () => ({
ProposalVotesTable: () => <div data-testid="proposal-votes-table"></div>,
}));
jest.mock('../vote-details', () => ({
VoteDetails: () => <div data-testid="proposal-vote-details"></div>,
}));
jest.mock('../list-asset', () => ({
ListAsset: () => <div data-testid="proposal-list-asset"></div>,
}));
@@ -104,8 +98,6 @@ it('renders each section', async () => {
expect(await screen.findByTestId('proposal-header')).toBeInTheDocument();
expect(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();
});
@@ -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<NetworkParamsResult>;
@@ -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 = ({
<ProposalHeader
proposal={proposal}
isListItem={false}
networkParams={networkParams}
voteState={voteState}
/>
<div id="details">
<div className="my-10">
<ProposalChangeTable proposal={proposal} />
</div>
{proposal.terms.change.__typename === 'NewAsset' &&
proposal.terms.change.source.__typename === 'ERC20' &&
proposal.id ? (
<ListAsset
assetId={proposal.id}
withdrawalThreshold={proposal.terms.change.source.withdrawThreshold}
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
/>
) : null}
<div className="mb-4">
<ProposalDescription description={proposal.rationale.description} />
</div>
{newMarketData && (
<div className="mb-4">
<ProposalMarketData
marketData={newMarketData}
parentMarketData={parentMarketData ? parentMarketData : undefined}
/>
</div>
)}
{proposal.terms.change.__typename === 'UpdateMarket' && (
<div className="mb-4">
<ProposalMarketChanges
originalProposal={
originalMarketProposalRestData?.data?.proposal?.terms?.newMarket
?.changes || {}
}
latestEnactedProposal={
mostRecentlyEnactedAssociatedMarketProposal?.node?.proposal
?.terms?.updateMarket?.changes || {}
}
updatedProposal={
restData?.data?.proposal?.terms?.updateMarket?.changes || {}
}
/>
</div>
)}
{(proposal.terms.change.__typename === 'NewAsset' ||
proposal.terms.change.__typename === 'UpdateAsset') &&
asset && (
<div className="mb-4">
<ProposalAssetDetails asset={asset} />
</div>
)}
<div className="mb-6">
<ProposalJson proposal={restData?.data?.proposal} />
</div>
<div className="my-10">
<ProposalChangeTable proposal={proposal} />
</div>
<div id="voting">
<div className="mb-10">
<RoundedWrapper paddingBottom={true}>
<VoteDetails
proposal={proposal}
proposalType={proposalType}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={
networkParams?.spam_protection_voting_min_tokens
}
submit={submit}
dialog={Dialog}
transaction={transaction}
voteState={voteState}
voteDatetime={voteDatetime}
/>
</RoundedWrapper>
</div>
{proposal.terms.change.__typename === 'NewAsset' &&
proposal.terms.change.source.__typename === 'ERC20' &&
proposal.id ? (
<ListAsset
assetId={proposal.id}
withdrawalThreshold={proposal.terms.change.source.withdrawThreshold}
lifetimeLimit={proposal.terms.change.source.lifetimeLimit}
/>
) : null}
<div className="mb-4">
<ProposalDescription description={proposal.rationale.description} />
</div>
{newMarketData && (
<div className="mb-4">
<ProposalVotesTable proposal={proposal} proposalType={proposalType} />
<ProposalMarketData
marketData={newMarketData}
parentMarketData={parentMarketData ? parentMarketData : undefined}
/>
</div>
)}
{proposal.terms.change.__typename === 'UpdateMarket' && (
<div className="mb-4">
<ProposalMarketChanges
originalProposal={
originalMarketProposalRestData?.data?.proposal?.terms?.newMarket
?.changes || {}
}
latestEnactedProposal={
mostRecentlyEnactedAssociatedMarketProposal?.node?.proposal?.terms
?.updateMarket?.changes || {}
}
updatedProposal={
restData?.data?.proposal?.terms?.updateMarket?.changes || {}
}
/>
</div>
)}
{(proposal.terms.change.__typename === 'NewAsset' ||
proposal.terms.change.__typename === 'UpdateAsset') &&
asset && (
<div className="mb-4">
<ProposalAssetDetails asset={asset} />
</div>
)}
<div className="mb-10">
<RoundedWrapper paddingBottom={true}>
<UserVote
proposal={proposal}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={
networkParams?.spam_protection_voting_min_tokens
}
submit={submit}
dialog={Dialog}
transaction={transaction}
voteState={voteState}
voteDatetime={voteDatetime}
/>
</RoundedWrapper>
</div>
<div className="mb-6">
<ProposalJson proposal={restData?.data?.proposal} />
</div>
</section>
);
@@ -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({
@@ -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 = (
<span className={nowToEnactmentInHours < 6 ? 'text-vega-pink' : ''}>
<span className={nowToEnactmentInHours < 6 ? 'text-vega-orange' : ''}>
{formatDistanceToNowStrict(new Date(proposal?.terms.closingDatetime))}{' '}
{t('left to vote')}
</span>
);
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: {
@@ -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<NetworkParamsResult> | 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 (
<li id={proposal.id} data-testid="proposals-list-item">
<RoundedWrapper paddingBottom={true} heightFull={true}>
<ProposalHeader
proposal={proposal}
networkParams={networkParams}
voteState={voteState}
/>
<ProposalHeader proposal={proposal} voteState={voteState} />
<ProposalsListItemDetails proposal={proposal} />
</RoundedWrapper>
</li>
@@ -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<ProposalFieldsFragment | ProposalQuery['proposal']>;
@@ -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<ClosedProposalsViewOptions>(
@@ -153,181 +134,166 @@ export const ProposalsList = ({
p?.party?.id?.toString().includes(filterString);
return (
<AsyncRenderer
loading={networkParamsLoading}
error={networkParamsError}
data={networkParams}
>
<div data-testid="proposals-list">
<div className="grid xs:grid-cols-2 items-center">
<Heading
centerContent={false}
marginBottom={false}
title={t('pageTitleProposals')}
/>
<div data-testid="proposals-list">
<div className="grid xs:grid-cols-2 items-center">
<Heading
centerContent={false}
marginBottom={false}
title={t('pageTitleProposals')}
/>
{DocsLinks && (
<div
className="xs:justify-self-end"
data-testid="new-proposal-link"
>
<ExternalLink href={DocsLinks.PROPOSALS_GUIDE}>
<Button variant="primary" size="sm">
<div className="flex items-center gap-1">
{t('NewProposal')}
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} size={13} />
</div>
</Button>
</ExternalLink>
</div>
)}
</div>
<p className="mb-8">
{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.`
)}{' '}
<ExternalLink
data-testid="proposal-documentation-link"
href={ExternalLinks.GOVERNANCE_PAGE}
className="text-white"
>
{t(`Find out more about Vega governance`)}
</ExternalLink>
</p>
{proposals.length > 0 && (
<ProposalsListFilter
filterString={filterString}
setFilterString={(value) => {
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 && (
<div className="xs:justify-self-end" data-testid="new-proposal-link">
<ExternalLink href={DocsLinks.PROPOSALS_GUIDE}>
<Button variant="primary" size="sm">
<div className="flex items-center gap-1">
{t('NewProposal')}
<VegaIcon name={VegaIconNames.OPEN_EXTERNAL} size={13} />
</div>
</Button>
</ExternalLink>
</div>
)}
</div>
<section className="-mx-4 p-4 mb-8 bg-vega-dark-100">
<SubHeading title={t('openProposals')} />
<p className="mb-8">
{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.`
)}{' '}
<ExternalLink
data-testid="proposal-documentation-link"
href={ExternalLinks.GOVERNANCE_PAGE}
className="text-white"
>
{t(`Find out more about Vega governance`)}
</ExternalLink>
</p>
{sortedProposals.open.length > 0 ||
sortedProtocolUpgradeProposals.open.length > 0 ? (
<ul data-testid="open-proposals">
{filterString.length < 1 &&
sortedProtocolUpgradeProposals.open.map((proposal) => (
<ProtocolUpgradeProposalsListItem
key={proposal.upgradeBlockHeight}
proposal={proposal}
/>
))}
{proposals.length > 0 && (
<ProposalsListFilter
filterString={filterString}
setFilterString={(value) => {
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) => (
<ProposalsListItem
key={proposal?.id}
<section className="-mx-4 p-4 mb-8 bg-vega-dark-100">
<SubHeading title={t('openProposals')} />
{sortedProposals.open.length > 0 ||
sortedProtocolUpgradeProposals.open.length > 0 ? (
<ul data-testid="open-proposals">
{filterString.length < 1 &&
sortedProtocolUpgradeProposals.open.map((proposal) => (
<ProtocolUpgradeProposalsListItem
key={proposal.upgradeBlockHeight}
proposal={proposal}
networkParams={networkParams}
/>
))}
</ul>
) : (
<p className="mb-0" data-testid="no-open-proposals">
{t('noOpenProposals')}
</p>
)}
</section>
<section className="relative">
<SubHeading title={t('closedProposals')} />
{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 && (
<div
className="grid w-full justify-end xl:-mt-12 pb-6"
data-testid="toggle-closed-proposals"
>
<div className="w-[440px]">
<Toggle
name="closed-proposals-toggle"
toggles={[
{
label: t(
ClosedProposalsViewOptions.NetworkGovernance
),
value:
ClosedProposalsViewOptions.NetworkGovernance,
},
{
label: t(
ClosedProposalsViewOptions.NetworkUpgrades
),
value: ClosedProposalsViewOptions.NetworkUpgrades,
},
]}
checkedValue={closedProposalsView}
onChange={(e) =>
setClosedProposalsView(
e.target.value as ClosedProposalsViewOptions
)
}
/>
</div>
{sortedProposals.open.filter(filterPredicate).map((proposal) => (
<ProposalsListItem key={proposal?.id} proposal={proposal} />
))}
</ul>
) : (
<p className="mb-0" data-testid="no-open-proposals">
{t('noOpenProposals')}
</p>
)}
</section>
<section className="relative">
<SubHeading title={t('closedProposals')} />
{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 && (
<div
className="grid w-full justify-end xl:-mt-12 pb-6"
data-testid="toggle-closed-proposals"
>
<div className="w-[440px]">
<Toggle
name="closed-proposals-toggle"
toggles={[
{
label: t(
ClosedProposalsViewOptions.NetworkGovernance
),
value: ClosedProposalsViewOptions.NetworkGovernance,
},
{
label: t(
ClosedProposalsViewOptions.NetworkUpgrades
),
value: ClosedProposalsViewOptions.NetworkUpgrades,
},
]}
checkedValue={closedProposalsView}
onChange={(e) =>
setClosedProposalsView(
e.target.value as ClosedProposalsViewOptions
)
}
/>
</div>
)
}
</div>
)
}
<ul data-testid="closed-proposals">
{closedProposalsView ===
ClosedProposalsViewOptions.NetworkUpgrades && (
<div data-testid="closed-upgrade-proposals">
{sortedProtocolUpgradeProposals.closed.map((proposal) => (
<ProtocolUpgradeProposalsListItem
key={proposal.upgradeBlockHeight}
<ul data-testid="closed-proposals">
{closedProposalsView ===
ClosedProposalsViewOptions.NetworkUpgrades && (
<div data-testid="closed-upgrade-proposals">
{sortedProtocolUpgradeProposals.closed.map((proposal) => (
<ProtocolUpgradeProposalsListItem
key={proposal.upgradeBlockHeight}
proposal={proposal}
/>
))}
</div>
)}
{closedProposalsView ===
ClosedProposalsViewOptions.NetworkGovernance && (
<div data-testid="closed-governance-proposals">
{sortedProposals.closed
.filter(filterPredicate)
.map((proposal) => (
<ProposalsListItem
key={proposal?.id}
proposal={proposal}
/>
))}
</div>
)}
</div>
)}
</ul>
</>
) : (
<p className="mb-0" data-testid="no-closed-proposals">
{t('noClosedProposals')}
</p>
)}
</section>
{closedProposalsView ===
ClosedProposalsViewOptions.NetworkGovernance && (
<div data-testid="closed-governance-proposals">
{sortedProposals.closed
.filter(filterPredicate)
.map((proposal) => (
<ProposalsListItem
key={proposal?.id}
proposal={proposal}
networkParams={networkParams}
/>
))}
</div>
)}
</ul>
</>
) : (
<p className="mb-0" data-testid="no-closed-proposals">
{t('noClosedProposals')}
</p>
)}
</section>
<Link className="underline" to={Routes.PROPOSALS_REJECTED}>
{t('seeRejectedProposals')}
</Link>
</div>
</AsyncRenderer>
<Link className="underline" to={Routes.PROPOSALS_REJECTED}>
{t('seeRejectedProposals')}
</Link>
</div>
);
};
@@ -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<ProposalQuery['proposal'] | ProposalFieldsFragment>;
@@ -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 (
<AsyncRenderer
loading={networkParamsLoading}
error={networkParamsError}
data={networkParams}
>
<>
<Heading title={t('pageTitleRejectedProposals')} />
<ProposalsListFilter
filterString={filterString}
@@ -53,11 +31,7 @@ export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
{proposals.length > 0 ? (
<ul data-testid="rejected-proposals">
{proposals.filter(filterPredicate).map((proposal) => (
<ProposalsListItem
key={proposal?.id}
proposal={proposal}
networkParams={networkParams}
/>
<ProposalsListItem key={proposal?.id} proposal={proposal} />
))}
</ul>
) : (
@@ -66,6 +40,6 @@ export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
</p>
)}
</section>
</AsyncRenderer>
</>
);
};
@@ -0,0 +1 @@
export * from './vote-breakdown';
@@ -0,0 +1,348 @@
import { render, screen } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import {
lastWeek,
mockWalletContext,
networkParamsQueryMock,
nextWeek,
} from '../../test-helpers/mocks';
import { VoteBreakdown } from './vote-breakdown';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MockedResponse } from '@apollo/client/testing';
import {
generateNoVotes,
generateProposal,
generateYesVotes,
} from '../../test-helpers/generate-proposals';
import { ProposalState } from '@vegaprotocol/types';
import { BigNumber } from '../../../../lib/bignumber';
import type { AppState } from '../../../../contexts/app-state/app-state-context';
const mockTotalSupply = new BigNumber(100);
// Note - giving a fixedTokenValue of 1 means a ratio of 1:1 votes to tokens, making sums easier :)
const fixedTokenValue = 1000000000000000000;
const mockAppState: AppState = {
totalAssociated: new BigNumber('50063005'),
decimals: 18,
totalSupply: mockTotalSupply,
vegaWalletManageOverlay: false,
transactionOverlay: false,
bannerMessage: '',
disconnectNotice: false,
};
jest.mock('../../../../contexts/app-state/app-state-context', () => ({
useAppState: () => ({
appState: mockAppState,
}),
}));
const renderComponent = (
proposal: ProposalQuery['proposal'],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mocks: MockedResponse<any>[] = [networkParamsQueryMock]
) =>
render(
<Router>
<MockedProvider mocks={mocks}>
<VegaWalletContext.Provider value={mockWalletContext}>
<VoteBreakdown proposal={proposal} />
</VegaWalletContext.Provider>
</MockedProvider>
</Router>
);
describe('VoteBreakdown', () => {
beforeAll(() => {
jest.useFakeTimers();
jest.setSystemTime(0);
});
afterAll(() => {
jest.useRealTimers();
});
it('Renders majority reached', () => {
const yesVotes = 100;
const noVotes = 0;
renderComponent(
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(yesVotes, fixedTokenValue),
no: generateNoVotes(noVotes, fixedTokenValue),
},
})
);
expect(screen.getByTestId('token-majority-met')).toBeInTheDocument();
});
it('Renders majority not reached', () => {
const yesVotes = 20;
const noVotes = 80;
renderComponent(
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(yesVotes, fixedTokenValue),
no: generateNoVotes(noVotes, fixedTokenValue),
},
})
);
expect(screen.getByTestId('token-majority-not-met')).toBeInTheDocument();
});
it('Renders participation reached', () => {
const yesVotes = 1000;
const noVotes = 0;
renderComponent(
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(yesVotes, fixedTokenValue),
no: generateNoVotes(noVotes, fixedTokenValue),
},
})
);
expect(screen.getByTestId('token-participation-met')).toBeInTheDocument();
});
it('Renders participation not reached', () => {
const yesVotes = 0;
const noVotes = 0;
renderComponent(
generateProposal({
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(yesVotes, fixedTokenValue),
no: generateNoVotes(noVotes, fixedTokenValue),
},
})
);
expect(
screen.getByTestId('token-participation-not-met')
).toBeInTheDocument();
});
it('Renders proposal state: Update market proposal - Currently expected to pass by LP vote', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
terms: {
change: {
__typename: 'UpdateMarket',
},
},
votes: {
yes: {
...generateYesVotes(0),
totalEquityLikeShareWeight: '1000',
},
no: {
...generateNoVotes(0),
totalEquityLikeShareWeight: '0',
},
},
})
);
expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Currently expected to pass by liquidity vote'
);
});
it('Renders proposal state: Update market proposal - Currently expected to pass by token vote', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
terms: {
change: {
__typename: 'UpdateMarket',
},
},
votes: {
yes: {
...generateYesVotes(1000, fixedTokenValue),
totalEquityLikeShareWeight: '0',
},
no: {
...generateNoVotes(0, fixedTokenValue),
totalEquityLikeShareWeight: '0',
},
},
})
);
expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Currently expected to pass by token vote'
);
});
it('Renders proposal state: Update market proposal - Currently expected to fail', () => {
renderComponent(
generateProposal({
state: ProposalState.STATE_OPEN,
terms: {
change: {
__typename: 'UpdateMarket',
},
},
votes: {
yes: {
...generateYesVotes(0),
totalEquityLikeShareWeight: '0',
},
no: {
...generateNoVotes(0),
totalEquityLikeShareWeight: '0',
},
},
})
);
expect(screen.getByTestId('vote-status')).toHaveTextContent(
'Currently expected to fail'
);
});
it('Progress bar displays status - token majority', () => {
const yesVotes = 80;
const noVotes = 20;
renderComponent(
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(yesVotes, fixedTokenValue),
no: generateNoVotes(noVotes, fixedTokenValue),
},
})
);
const element = screen.getByTestId('token-majority-progress');
const style = window.getComputedStyle(element);
expect(style.width).toBe(`${yesVotes}%`);
});
it('Progress bar displays status - token participation', () => {
const yesVotes = 40;
const noVotes = 20;
const totalVotes = yesVotes + noVotes;
const totalSupplyValue = mockTotalSupply.toNumber();
const expectedProgress = (totalVotes / totalSupplyValue) * 100; // Here it should be 60%
renderComponent(
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
closingDatetime: lastWeek.toString(),
enactmentDatetime: nextWeek.toString(),
},
votes: {
__typename: 'ProposalVotes',
yes: generateYesVotes(yesVotes, fixedTokenValue),
no: generateNoVotes(noVotes, fixedTokenValue),
},
})
);
const element = screen.getByTestId('token-participation-progress');
const style = window.getComputedStyle(element);
expect(style.width).toBe(`${expectedProgress}%`);
});
it('Progress bar displays status - LP majority', () => {
const yesVotesLP = 800;
const noVotesLP = 200;
const expectedProgress = (yesVotesLP / (yesVotesLP + noVotesLP)) * 100; // 80%
renderComponent(
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
change: {
__typename: 'UpdateMarket',
},
},
votes: {
__typename: 'ProposalVotes',
yes: {
...generateYesVotes(0),
totalEquityLikeShareWeight: `${yesVotesLP}`,
},
no: {
...generateNoVotes(0),
totalEquityLikeShareWeight: `${noVotesLP}`,
},
},
})
);
const element = screen.getByTestId('lp-majority-progress');
const style = window.getComputedStyle(element);
expect(style.width).toBe(`${expectedProgress}%`);
});
it('Progress bar displays status - LP participation', () => {
const yesVotesLP = 400;
const noVotesLP = 600;
const totalVotesLP = yesVotesLP + noVotesLP;
const totalLPSupply = 1000;
const expectedProgress = (totalVotesLP / totalLPSupply) * 100; // 100%
renderComponent(
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
change: {
__typename: 'UpdateMarket',
},
},
votes: {
__typename: 'ProposalVotes',
yes: {
...generateYesVotes(0),
totalEquityLikeShareWeight: `${yesVotesLP}`,
},
no: {
...generateNoVotes(0),
totalEquityLikeShareWeight: `${noVotesLP}`,
},
},
})
);
const element = screen.getByTestId('lp-participation-progress');
const style = window.getComputedStyle(element);
expect(style.width).toBe(`${expectedProgress}%`);
});
});
@@ -0,0 +1,378 @@
import classNames from 'classnames';
import BigNumber from 'bignumber.js';
import { useTranslation } from 'react-i18next';
import { useVoteInformation } from '../../hooks';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import { formatNumber, toBigNum } from '@vegaprotocol/utils';
import { ProposalState } from '@vegaprotocol/types';
import type { ReactNode } from 'react';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
interface VoteBreakdownProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}
interface VoteProgressProps {
percentageFor: BigNumber;
colourfulBg?: boolean;
testId?: string;
children?: ReactNode;
}
const VoteProgress = ({
percentageFor,
colourfulBg,
testId,
children,
}: VoteProgressProps) => {
const containerClasses = classNames(
'relative h-10 rounded-md border border-vega-dark-300 overflow-hidden',
colourfulBg ? 'bg-vega-pink' : 'bg-vega-dark-400'
);
const progressClasses = classNames(
'absolute h-full top-0 left-0',
colourfulBg ? 'bg-vega-green' : 'bg-white'
);
const textClasses = classNames(
'absolute top-0 left-0 w-full h-full flex items-center justify-start px-3 text-black'
);
return (
<div className={containerClasses}>
<div
className={progressClasses}
style={{ width: `${percentageFor}%` }}
data-testid={testId}
></div>
<div className={textClasses}>{children}</div>
</div>
);
};
interface StatusProps {
reached: boolean;
threshold: BigNumber;
text: string;
testId?: string;
}
const Status = ({ reached, threshold, text, testId }: StatusProps) => {
const { t } = useTranslation();
return (
<div data-testid={testId}>
{reached ? (
<div className="flex items-center gap-2">
<Icon name="tick" size={4} />
<span>
{threshold.toString()}% {text} {t('met')}
</span>
</div>
) : (
<div className="flex items-center gap-2">
<Icon name="cross" size={4} />
<span>
{threshold.toString()}% {text} {t('not met')}
</span>
</div>
)}
</div>
);
};
export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
const {
totalTokensPercentage,
participationMet,
totalTokensVoted,
totalLPTokensPercentage,
noPercentage,
noLPPercentage,
yesPercentage,
yesLPPercentage,
yesTokens,
noTokens,
yesEquityLikeShareWeight,
noEquityLikeShareWeight,
totalEquityLikeShareWeight,
requiredMajorityPercentage,
requiredMajorityLPPercentage,
requiredParticipation,
requiredParticipationLP,
participationLPMet,
majorityMet,
majorityLPMet,
willPassByTokenVote,
willPassByLPVote,
} = useVoteInformation({ proposal });
const { t } = useTranslation();
const defaultDP = 2;
const isProposalOpen = proposal?.state === ProposalState.STATE_OPEN;
const isUpdateMarket = proposal?.terms?.change?.__typename === 'UpdateMarket';
const participationThresholdProgress = BigNumber.min(
totalTokensPercentage.dividedBy(requiredParticipation).multipliedBy(100),
new BigNumber(100)
);
const lpParticipationThresholdProgress =
requiredParticipationLP &&
BigNumber.min(
totalLPTokensPercentage
.dividedBy(requiredParticipationLP)
.multipliedBy(100),
new BigNumber(100)
);
const willPass = willPassByTokenVote || willPassByLPVote;
const updateMarketVotePassMethod = willPassByTokenVote
? t('byTokenVote')
: t('byLiquidityVote');
const sectionWrapperClasses = classNames('grid sm:grid-cols-2 gap-6');
const headingClasses = classNames('mb-2 text-vega-dark-400');
const progressDetailsClasses = classNames(
'flex justify-between flex-wrap mt-2 text-sm'
);
return (
<div className="mb-6">
{isProposalOpen && (
<div
data-testid="vote-status"
className="flex items-center gap-1 mb-2 text-bold"
>
<span>
{willPass ? (
<Icon name="tick" size={5} className="text-vega-green" />
) : (
<Icon name="cross" size={5} className="text-vega-pink" />
)}
</span>
<span>{t('currentlySetTo')} </span>
{willPass ? (
<span>
<span className="text-vega-green">{t('pass')}</span>
{isUpdateMarket && <span> {updateMarketVotePassMethod}</span>}
</span>
) : (
<span className="text-vega-pink">{t('fail')}</span>
)}
</div>
)}
{isUpdateMarket && (
<div className="mb-4">
<h3 className={headingClasses}>{t('liquidityProviderVote')}</h3>
<div className={sectionWrapperClasses}>
<section data-testid="lp-majority-breakdown">
<VoteProgress
percentageFor={yesLPPercentage}
colourfulBg={true}
testId="lp-majority-progress"
>
<Status
reached={majorityLPMet}
threshold={requiredMajorityLPPercentage}
text={t('majorityThreshold')}
testId={
majorityLPMet ? 'lp-majority-met' : 'lp-majority-not-met'
}
/>
</VoteProgress>
<div className={progressDetailsClasses}>
<div className="flex items-center gap-1">
<span>{t('liquidityProviderVotesFor')}:</span>
<Tooltip
description={formatNumber(
yesEquityLikeShareWeight,
defaultDP
)}
>
<button>
{yesEquityLikeShareWeight
.dividedBy(toBigNum(10 ** 6, 0))
.toFixed(1)}
M
</button>
</Tooltip>
<span>
(
<Tooltip
description={
<span>{yesLPPercentage.toFixed(defaultDP)}%</span>
}
>
<button>{yesLPPercentage.toFixed(0)}%</button>
</Tooltip>
)
</span>
</div>
<div className="flex items-center gap-1">
<span>{t('liquidityProviderVotesAgainst')}:</span>
<Tooltip
description={formatNumber(
noEquityLikeShareWeight,
defaultDP
)}
>
<button>
{noEquityLikeShareWeight
.dividedBy(toBigNum(10 ** 6, 0))
.toFixed(1)}
M
</button>
</Tooltip>
<span>
(
<Tooltip
description={
<span>{noLPPercentage.toFixed(defaultDP)}%</span>
}
>
<button>{noLPPercentage.toFixed(0)}%</button>
</Tooltip>
)
</span>
</div>
</div>
</section>
<section data-testid="lp-participation-breakdown">
<VoteProgress
percentageFor={
lpParticipationThresholdProgress || new BigNumber(0)
}
testId="lp-participation-progress"
>
<Status
reached={participationLPMet}
threshold={requiredParticipationLP || new BigNumber(1)}
text={t('participationThreshold')}
testId={
participationLPMet
? 'lp-participation-met'
: 'lp-participation-not-met'
}
/>
</VoteProgress>
<div className="flex mt-2 text-sm">
<div className="flex items-center gap-1">
<span>{t('totalLiquidityProviderTokensVoted')}:</span>
<Tooltip
description={formatNumber(
totalEquityLikeShareWeight,
defaultDP
)}
>
<button>
{totalEquityLikeShareWeight
.dividedBy(toBigNum(10 ** 6, 0))
.toFixed(1)}
M
</button>
</Tooltip>
<span>
({totalEquityLikeShareWeight.toFixed(defaultDP)}%)
</span>
</div>
</div>
</section>
</div>
</div>
)}
{isUpdateMarket && <h3 className={headingClasses}>{t('tokenVote')}</h3>}
<div className={sectionWrapperClasses}>
<section data-testid="token-majority-breakdown">
<VoteProgress
percentageFor={yesPercentage}
colourfulBg={true}
testId="token-majority-progress"
>
<Status
reached={majorityMet}
threshold={requiredMajorityPercentage}
text={t('majorityThreshold')}
testId={
majorityMet ? 'token-majority-met' : 'token-majority-not-met'
}
/>
</VoteProgress>
<div className={progressDetailsClasses}>
<div className="flex items-center gap-1">
<span>{t('tokenVotesFor')}:</span>
<Tooltip description={formatNumber(yesTokens, defaultDP)}>
<button>
{yesTokens.dividedBy(toBigNum(10 ** 6, 0)).toFixed(1)}M
</button>
</Tooltip>
<span>
(
<Tooltip
description={<span>{yesPercentage.toFixed(defaultDP)}%</span>}
>
<button>{yesPercentage.toFixed(0)}%</button>
</Tooltip>
)
</span>
</div>
<div className="flex items-center gap-1">
<span>{t('tokenVotesAgainst')}:</span>
<Tooltip description={formatNumber(noTokens, defaultDP)}>
<button>
{noTokens.dividedBy(toBigNum(10 ** 6, 0)).toFixed(1)}M
</button>
</Tooltip>
<span>
(
<Tooltip
description={<span>{noPercentage.toFixed(defaultDP)}%</span>}
>
<button>{noPercentage.toFixed(0)}%</button>
</Tooltip>
)
</span>
</div>
</div>
</section>
<section data-testid="token-participation-breakdown">
<VoteProgress
percentageFor={participationThresholdProgress}
testId="token-participation-progress"
>
<Status
reached={participationMet}
threshold={requiredParticipation}
text={t('participationThreshold')}
testId={
participationMet
? 'token-participation-met'
: 'token-participation-not-met'
}
/>
</VoteProgress>
<div className="flex mt-2 text-sm">
<div className="flex items-center gap-1">
<span>{t('totalTokensVoted')}:</span>
<Tooltip description={formatNumber(totalTokensVoted, defaultDP)}>
<button>
{totalTokensVoted.dividedBy(toBigNum(10 ** 6, 0)).toFixed(1)}M
</button>
</Tooltip>
<span>({totalTokensPercentage.toFixed(defaultDP)}%)</span>
</div>
</div>
</section>
</div>
</div>
);
};
@@ -1 +1 @@
export { VoteDetails } from './vote-details';
export { UserVote } from './user-vote';
@@ -0,0 +1,78 @@
import { useTranslation } from 'react-i18next';
import { Icon, ExternalLink } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { ProposalState } from '@vegaprotocol/types';
import { ConnectToVega } from '../../../../components/connect-to-vega';
import { VoteButtonsContainer } from './vote-buttons';
import { SubHeading } from '../../../../components/heading';
import type { VoteValue } from '@vegaprotocol/types';
import type { DialogProps, VegaTxState } from '@vegaprotocol/wallet';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { VoteState } from './use-user-vote';
interface UserVoteProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
minVoterBalance: string | null | undefined;
spamProtectionMinTokens: string | null | undefined;
transaction: VegaTxState | null;
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
dialog: (props: DialogProps) => JSX.Element;
voteState: VoteState | null;
voteDatetime: Date | null;
}
export const UserVote = ({
proposal,
minVoterBalance,
spamProtectionMinTokens,
submit,
transaction,
dialog,
voteState,
voteDatetime,
}: UserVoteProps) => {
const { pubKey } = useVegaWallet();
const { t } = useTranslation();
return (
<section data-testid="user-vote">
{proposal?.state === ProposalState.STATE_OPEN ? (
<SubHeading title={t('castYourVote')} />
) : (
<SubHeading title={t('yourVote')} />
)}
{pubKey ? (
proposal && (
<VoteButtonsContainer
voteState={voteState}
voteDatetime={voteDatetime}
proposalState={proposal.state}
proposalId={proposal.id ?? ''}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={spamProtectionMinTokens}
className="flex"
submit={submit}
transaction={transaction}
dialog={dialog}
/>
)
) : (
<div className="pb-2">
<div className="mb-4">
<div className="flex items-center gap-2 mb-2">
<Icon name={'info-sign'} />
<div>{t('connectAVegaWalletToVote')}</div>
</div>
<ExternalLink href="https://blog.vega.xyz/how-to-vote-on-vega-2195d1e52ec5">
{t('findOutMoreAboutHowToVote')}
</ExternalLink>
</div>
<ConnectToVega />
</div>
)}
</section>
);
};
@@ -190,14 +190,15 @@ export const VoteButtons = ({
(voteState === VoteState.Yes || voteState === VoteState.No) && (
<p data-testid="you-voted">
<span>{t('youVoted')}:</span>{' '}
<span className="text-white font-bold">
<span className="mx-1 text-white font-bold uppercase">
{t(`voteState_${voteState}`)}
</span>{' '}
{voteDatetime ? (
<span>{format(voteDatetime, DATE_FORMAT_LONG)}. </span>
<span>on {format(voteDatetime, DATE_FORMAT_LONG)}. </span>
) : null}
{proposalVotable ? (
<ButtonLink
className="text-white"
data-testid="change-vote-button"
onClick={() => {
setChangeVote(true);
@@ -1,255 +0,0 @@
import { useTranslation } from 'react-i18next';
import { formatDistanceToNow } from 'date-fns';
import { RoundedWrapper, Icon, ExternalLink } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { ProposalState } from '@vegaprotocol/types';
import { VoteProgress } from '@vegaprotocol/proposals';
import { formatNumber } from '../../../../lib/format-number';
import { ConnectToVega } from '../../../../components/connect-to-vega';
import { useVoteInformation } from '../../hooks';
import { CurrentProposalStatus } from '../current-proposal-status';
import { VoteButtonsContainer } from './vote-buttons';
import { SubHeading } from '../../../../components/heading';
import { ProposalType } from '../proposal/proposal';
import type { VoteValue } from '@vegaprotocol/types';
import type { DialogProps, VegaTxState } from '@vegaprotocol/wallet';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { VoteState } from './use-user-vote';
interface VoteDetailsProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
minVoterBalance: string | null | undefined;
spamProtectionMinTokens: string | null | undefined;
proposalType: ProposalType | null;
transaction: VegaTxState | null;
submit: (voteValue: VoteValue, proposalId: string | null) => Promise<void>;
dialog: (props: DialogProps) => JSX.Element;
voteState: VoteState | null;
voteDatetime: Date | null;
}
export const VoteDetails = ({
proposal,
minVoterBalance,
spamProtectionMinTokens,
proposalType,
submit,
transaction,
dialog,
voteState,
voteDatetime,
}: VoteDetailsProps) => {
const { pubKey } = useVegaWallet();
const {
totalTokensPercentage,
participationMet,
totalTokensVoted,
totalLPTokensPercentage,
noPercentage,
noLPPercentage,
yesPercentage,
yesLPPercentage,
yesTokens,
noTokens,
requiredMajorityPercentage,
requiredMajorityLPPercentage,
requiredParticipation,
requiredParticipationLP,
participationLPMet,
} = useVoteInformation({ proposal });
const { t } = useTranslation();
const defaultDecimals = 2;
const daysLeft = t('daysLeft', {
daysLeft: formatDistanceToNow(new Date(proposal?.terms.closingDatetime)),
});
return (
<>
{proposalType === ProposalType.PROPOSAL_UPDATE_MARKET && (
<section>
<SubHeading title={t('liquidityVotes')} />
<p data-testid="liquidity-votes-status">
<span>
<CurrentProposalStatus proposal={proposal} />
</span>
{'. '}
{proposal?.state === ProposalState.STATE_OPEN ? daysLeft : null}
</p>
<table className="w-full mb-8">
<thead>
<tr>
<th className="text-vega-green w-[18%] text-left">
{t('for')}
</th>
<th>
<VoteProgress
threshold={requiredMajorityLPPercentage}
progress={yesLPPercentage}
/>
</th>
<th className="text-danger w-[18%] text-right">
{t('against')}
</th>
</tr>
</thead>
<tbody>
<tr>
<td
className="text-left"
data-testid="vote-progress-indicator-percentage-for"
>
{yesLPPercentage.toFixed(defaultDecimals)}%
</td>
<td className="text-center text-white">
{t('majorityRequired')}{' '}
{requiredMajorityLPPercentage.toFixed(defaultDecimals)}%
</td>
<td
className="text-right"
data-testid="vote-progress-indicator-percentage-against"
>
{noLPPercentage.toFixed(defaultDecimals)}%
</td>
</tr>
</tbody>
</table>
<p className="mb-6">
{t('participation')}
{': '}
{participationLPMet ? (
<span className="text-vega-green mx-4">{t('met')}</span>
) : (
<span className="text-danger mx-4">{t('notMet')}</span>
)}{' '}
{formatNumber(totalLPTokensPercentage, defaultDecimals)}%
<span className="ml-4">
{requiredParticipationLP && (
<>
({formatNumber(requiredParticipationLP, defaultDecimals)}%{' '}
{t('governanceRequired')})
</>
)}
</span>
</p>
</section>
)}
<section data-testid="votes-table">
<SubHeading title={t('tokenVotes')} />
<p data-testid="token-votes-status">
<span>
<CurrentProposalStatus proposal={proposal} />
</span>
{'. '}
{proposal?.state === ProposalState.STATE_OPEN ? daysLeft : null}
</p>
<table className="w-full mb-4">
<thead>
<tr>
<th className="text-vega-green w-[18%] text-left">{t('for')}</th>
<th>
<VoteProgress
threshold={requiredMajorityPercentage}
progress={yesPercentage}
/>
</th>
<th className="text-danger w-[18%] text-right">{t('against')}</th>
</tr>
</thead>
<tbody>
<tr>
<td
className="text-left"
data-testid="vote-progress-indicator-percentage-for"
>
{yesPercentage.toFixed(defaultDecimals)}%
</td>
<td className="text-center text-white">
{t('majorityRequired')}{' '}
{requiredMajorityPercentage.toFixed(defaultDecimals)}%
</td>
<td
className="text-right"
data-testid="vote-progress-indicator-percentage-against"
>
{noPercentage.toFixed(defaultDecimals)}%
</td>
</tr>
<tr>
<td data-testid="vote-progress-indicator-tokens-for">
{' '}
{formatNumber(yesTokens, defaultDecimals)}{' '}
</td>
<td></td>
<td
data-testid="vote-progress-indicator-tokens-against"
className="text-right"
>
{formatNumber(noTokens, defaultDecimals)}
</td>
</tr>
</tbody>
</table>
<p className="mb-6">
{t('participation')}
{': '}
{participationMet ? (
<span className="text-vega-green mx-4">{t('met')}</span>
) : (
<span className="text-danger mx-4">{t('notMet')}</span>
)}{' '}
{formatNumber(totalTokensVoted, defaultDecimals)}{' '}
{formatNumber(totalTokensPercentage, defaultDecimals)}%
<span className="ml-4">
({formatNumber(requiredParticipation, defaultDecimals)}%{' '}
{t('governanceRequired')})
</span>
</p>
{proposalType === ProposalType.PROPOSAL_UPDATE_MARKET && (
<p>{t('votingThresholdInfo')}</p>
)}
<section className="mt-10">
{proposal?.state === ProposalState.STATE_OPEN ? (
<SubHeading title={t('castYourVote')} />
) : (
<SubHeading title={t('yourVote')} />
)}
{pubKey ? (
proposal && (
<VoteButtonsContainer
voteState={voteState}
voteDatetime={voteDatetime}
proposalState={proposal.state}
proposalId={proposal.id ?? ''}
minVoterBalance={minVoterBalance}
spamProtectionMinTokens={spamProtectionMinTokens}
className="flex"
submit={submit}
transaction={transaction}
dialog={dialog}
/>
)
) : (
<RoundedWrapper paddingBottom={true}>
<div className="mb-4">
<div className="flex items-center gap-2 mb-2">
<Icon name={'info-sign'} />
<div>{t('connectAVegaWalletToVote')}</div>
</div>
<ExternalLink href="https://blog.vega.xyz/how-to-vote-on-vega-2195d1e52ec5">
{t('findOutMoreAboutHowToVote')}
</ExternalLink>
</div>
<ConnectToVega />
</RoundedWrapper>
)}
</section>
</section>
</>
);
};
@@ -1,475 +0,0 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import type { DataSourceDefinition } from '@vegaprotocol/types';
import {
MarketState,
MarketStateMapping,
PropertyKeyType,
} from '@vegaprotocol/types';
import { addDays, subDays } from 'date-fns';
import {
chainIdQuery,
statisticsQuery,
createDataConnection,
oracleSpecDataConnectionQuery,
createMarketFragment,
marketsQuery,
marketsDataQuery,
createMarketsDataFragment,
assetQuery,
networkParamsQuery,
nodeGuardQuery,
} from '@vegaprotocol/mock';
import {
addDecimalsFormatNumber,
getDateTimeFormat,
} from '@vegaprotocol/utils';
describe('Closed markets', { tags: '@smoke' }, () => {
const settlementDataProperty = 'settlement-data-property';
const settlementDataPropertyKey = {
__typename: 'PropertyKey' as const,
name: settlementDataProperty,
type: PropertyKeyType.TYPE_INTEGER,
numberDecimalPlaces: 2,
};
const settlementDataSourceData: DataSourceDefinition = {
sourceType: {
sourceType: {
filters: [
{
__typename: 'Filter',
key: settlementDataPropertyKey,
},
],
},
},
};
const rowSelector =
'[data-testid="tab-closed-markets"] .ag-center-cols-container .ag-row';
const assetsResult = assetQuery();
// @ts-ignore asset definitely exists
const settlementAsset = assetsResult.assetsConnection.edges[0].node;
const settledMarket = createMarketFragment({
id: '0',
state: MarketState.STATE_SETTLED,
marketTimestamps: {
open: subDays(new Date(), 10).toISOString(),
close: subDays(new Date(), 4).toISOString(),
},
tradableInstrument: {
instrument: {
product: {
dataSourceSpecBinding: {
settlementDataProperty,
},
dataSourceSpecForTradingTermination: {
id: 'market-1-trading-termination-oracle-id',
},
dataSourceSpecForSettlementData: {
id: 'market-1-settlement-data-oracle-id',
data: settlementDataSourceData,
},
settlementAsset,
},
},
},
});
const terminatedMarket = createMarketFragment({
id: '1',
state: MarketState.STATE_TRADING_TERMINATED,
marketTimestamps: {
open: subDays(new Date(), 10).toISOString(),
close: null, // market
},
tradableInstrument: {
instrument: {
metadata: {
tags: [
`settlement-expiry-date:${addDays(new Date(), 4).toISOString()}`,
],
},
product: {
dataSourceSpecBinding: {
settlementDataProperty,
},
dataSourceSpecForSettlementData: {
id: 'market-1-settlement-data-oracle-id',
data: settlementDataSourceData,
},
},
},
},
});
const delayedSettledMarket = createMarketFragment({
id: '2',
state: MarketState.STATE_TRADING_TERMINATED,
marketTimestamps: {
open: subDays(new Date(), 10).toISOString(),
close: null, // market
},
tradableInstrument: {
instrument: {
metadata: {
tags: [
`settlement-expiry-date:${subDays(new Date(), 2).toISOString()}`,
],
},
product: {
dataSourceSpecBinding: {
settlementDataProperty,
},
dataSourceSpecForSettlementData: {
id: 'market-1-settlement-data-oracle-id',
data: settlementDataSourceData,
},
},
},
},
});
const unknownMarket = createMarketFragment({
id: '3',
state: MarketState.STATE_SETTLED,
});
const closedMarketsResult = [
{
node: settledMarket,
},
{
node: terminatedMarket,
},
{
node: delayedSettledMarket,
},
{ node: unknownMarket },
{
node: createMarketFragment({ id: '4', state: MarketState.STATE_PENDING }),
},
{
node: createMarketFragment({ id: '5', state: MarketState.STATE_ACTIVE }),
},
];
const settledMarketData = createMarketsDataFragment({
market: {
id: settledMarket.id,
},
bestBidPrice: '1000',
bestOfferPrice: '2000',
markPrice: '1500',
});
const closedMarketsDataResult = [
{
node: {
data: settledMarketData,
},
},
{
node: {
data: createMarketsDataFragment({
market: {
id: terminatedMarket.id,
},
}),
},
},
{
node: {
data: createMarketsDataFragment({
market: {
id: delayedSettledMarket.id,
},
}),
},
},
{
node: {
data: createMarketsDataFragment({
market: {
id: unknownMarket.id,
},
}),
},
},
];
const specDataConnection = createDataConnection();
before(() => {
cy.setOnBoardingViewed();
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ChainId', chainIdQuery());
aliasGQLQuery(req, 'Statistics', statisticsQuery());
aliasGQLQuery(req, 'NodeGuard', nodeGuardQuery());
aliasGQLQuery(req, 'NetworkParams', networkParamsQuery());
aliasGQLQuery(
req,
'Markets',
marketsQuery({
marketsConnection: {
edges: closedMarketsResult,
},
})
);
aliasGQLQuery(
req,
'MarketsData',
marketsDataQuery({
marketsConnection: {
edges: closedMarketsDataResult,
},
})
);
aliasGQLQuery(
req,
'OracleSpecDataConnection',
oracleSpecDataConnectionQuery()
);
});
cy.mockSubscription();
cy.visit('/#/markets/all');
cy.get('[data-testid="Closed markets"]').click();
});
it('renders a settled market', () => {
const expectedMarkets = closedMarketsResult.filter((edge) => {
return [
MarketState.STATE_SETTLED,
MarketState.STATE_TRADING_TERMINATED,
].includes(edge.node.state);
});
const product = settledMarket.tradableInstrument.instrument.product;
// rows should be filtered to only include settled/terminated markets
cy.get(rowSelector).should('have.length', expectedMarkets.length);
// check each column in the first row renders correctly
// 6001-MARK-001
cy.get(rowSelector)
.first()
.find('[col-id="code"]')
.find('[data-testid="market-code"]')
.should('have.text', settledMarket.tradableInstrument.instrument.code);
// 6001-MARK-071
cy.get(rowSelector)
.first()
.find('[title="Future"]')
.should('have.text', 'Futr');
// 6001-MARK-002
cy.get(rowSelector)
.first()
.find('[col-id="name"]')
.should('have.text', settledMarket.tradableInstrument.instrument.name);
// 6001-MARK-003
cy.get(rowSelector)
.first()
.find('[col-id="state"]')
.should('have.text', MarketStateMapping[settledMarket.state]);
// 6001-MARK-004
// 6001-MARK-005
// 6001-MARK-009
// 6001-MARK-008
// 6001-MARK-010
cy.get(rowSelector)
.first()
.find('[col-id="settlementDate"]')
.find('[data-testid="link"]')
.should(($el) => {
const href = $el.attr('href');
expect(href).to.match(
new RegExp(
`/oracles/${product.dataSourceSpecForTradingTermination.id}`
)
);
})
.should('have.text', '4 days ago')
.should(
'have.attr',
'title',
getDateTimeFormat().format(
new Date(settledMarket.marketTimestamps.close)
)
);
// 6001-MARK-011
cy.get(rowSelector)
.first()
.find('[col-id="bestBidPrice"]')
.should(
'have.text',
addDecimalsFormatNumber(
settledMarketData.bestBidPrice,
settledMarket.decimalPlaces
)
);
// 6001-MARK-012
cy.get(rowSelector)
.first()
.find('[col-id="bestOfferPrice"]')
.should(
'have.text',
addDecimalsFormatNumber(
settledMarketData.bestOfferPrice,
settledMarket.decimalPlaces
)
);
// 6001-MARK-013
cy.get(rowSelector).first().find('[col-id="markPrice"]').should(
'have.text',
addDecimalsFormatNumber(
settledMarketData.markPrice,
settledMarket.decimalPlaces
)
);
// 6001-MARK-014
// 6001-MARK-015
// 6001-MARK-016
cy.get(rowSelector)
.first()
.find('[col-id="settlementDataOracleId"]')
.find('[data-testid="link"]')
.should(($el) => {
const href = $el.attr('href');
expect(href).to.match(
new RegExp(`/oracles/${product.dataSourceSpecForSettlementData.id}`)
);
})
.should(
'have.text',
addDecimalsFormatNumber(
// @ts-ignore cannot deep un-partial
specDataConnection.externalData.data.data[0].value,
settlementDataPropertyKey.numberDecimalPlaces
)
);
// 6001-MARK-018
cy.get(rowSelector)
.first()
.find('[col-id="settlementAsset"]')
.should('have.text', product.settlementAsset.symbol);
// 6001-MARK-020
cy.get('.ag-pinned-right-cols-container')
.find('[col-id="market-actions"]')
.first()
.find('button svg')
.should('exist');
if (Cypress.env('NX_SUCCESSOR_MARKETS')) {
cy.get(rowSelector)
.find('[col-id="successorMarket"]')
.first()
.should('have.text', '-');
}
});
// test market list for market in terminated state
it('renders a terminated market', () => {
cy.get(rowSelector)
.eq(1)
.find('[col-id="state"]')
.should('have.text', MarketStateMapping[terminatedMarket.state]);
// 6001-MARK-006
// 6001-MARK-007
cy.get(rowSelector)
.eq(1)
.find('[col-id="settlementDate"]')
.find('[data-testid="link"]')
.should('have.text', 'Expected in 4 days');
});
it('renders a terminated market which was expected to have settled', () => {
cy.get(rowSelector)
.eq(2)
.find('[col-id="settlementDate"]')
.should('have.class', 'text-danger')
.find('[data-testid="link"]')
.should('have.text', 'Expected 2 days ago');
});
it('renders terminated market which doesnt have settlement date metadata', () => {
cy.get(rowSelector)
.eq(3)
.find('[col-id="settlementDate"]')
.find('[data-testid="link"]')
.should('have.text', 'Unknown');
});
it('can open asset detail dialog', () => {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Asset', assetsResult);
});
cy.get(rowSelector)
.first()
.find('[col-id="settlementAsset"]')
.find('button')
.click();
// 6001-MARK-019
cy.get('[data-testid="dialog-title"]').should(
'have.text',
`Asset details - ${settlementAsset.symbol}`
);
cy.get('[data-testid="dialog-close"]').click();
});
it('can open row actions', () => {
cy.get('.ag-pinned-right-cols-container')
.find('[col-id="market-actions"]')
.first()
.find('button')
.click();
const dropdownContent = '[data-testid="market-actions-content"]';
const dropdownContentItem = '[role="menuitem"]';
cy.get(dropdownContent)
.find(dropdownContentItem)
.eq(0)
// Cannot click the copy button as it falls back to window.prompt, blocking the test.
.should('have.text', 'Copy Market ID');
cy.get(dropdownContent)
.find(dropdownContentItem)
.eq(1)
.find('a')
.then(($el) => {
const href = $el.attr('href');
expect(/\/markets\/0/.test(href || '')).to.equal(true);
})
.should('have.text', 'View on Explorer');
});
});
describe('no closed markets', { tags: '@smoke', testIsolation: true }, () => {
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/all');
cy.get('[data-testid="Closed markets"]').click();
});
it('can see no markets message', () => {
// 6001-MARK-034
cy.getByTestId('tab-closed-markets').should('contain.text', 'No markets');
});
});
@@ -133,11 +133,7 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
validateMarketDataRow(4, 'Decimals', '5');
validateMarketDataRow(5, 'Quantum', '1');
validateMarketDataRow(6, 'Status', 'Enabled');
validateMarketDataRow(
7,
'Contract address',
'0x0158031158Bb4dF2AD02eAA31e8963E84EA978a4'
);
validateMarketDataRow(7, 'Contract address', '0x0158…78a4');
validateMarketDataRow(8, 'Withdrawal threshold', '0.0005');
validateMarketDataRow(9, 'Lifetime limit', '1,230');
validateMarketDataRow(10, 'Infrastructure fee account balance', '0.00001');
@@ -6,7 +6,7 @@ const askVolume = 'ask-vol-9894185';
const bidVolume = 'bid-vol-9889001';
const askCumulative = 'cumulative-vol-9894185';
const bidCumulative = 'cumulative-vol-9889001';
const midPrice = 'middle-mark-price-4612690000';
const midPrice = 'last-traded-4612690000';
const priceResolution = 'resolution';
const dealTicketPrice = 'order-price';
const dealTicketSize = 'order-size';
@@ -43,11 +43,10 @@ describe('ethereum wallet', { tags: '@smoke', testIsolation: true }, () => {
// 0004-EWAL-005
// 0004-EWAL-006
const ethWalletAddress = Cypress.env('ETHEREUM_WALLET_ADDRESS');
cy.getByTestId('Deposits').click();
cy.getByTestId('deposit-button').click();
connectEthereumWallet('MetaMask');
cy.getByTestId('ethereum-address').should('have.text', ethWalletAddress);
cy.getByTestId('ethereum-address').should('have.text', '0xEe7D…d94F');
cy.getByTestId('disconnect-ethereum-wallet')
.should('have.text', 'Disconnect')
.click();
+1 -1
View File
@@ -13,7 +13,7 @@ NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/main/announcements.json
NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
# Cosmic elevator flags
+1 -1
View File
@@ -13,7 +13,7 @@ NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/test/announcements.json
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
NX_ETH_LOCAL_PROVIDER_URL=http://localhost:8545/
NX_ETH_WALLET_MNEMONIC="ozone access unlock valid olympic save include omit supply green clown session"
+1 -1
View File
@@ -14,7 +14,7 @@ NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega-dev-releases/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
+1 -1
View File
@@ -14,7 +14,7 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
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/pl/firefox/addon/vega-wallet-mainnet
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
+1 -1
View File
@@ -14,7 +14,7 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.mainnet-mirror.vega.rocks
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-mainnet/codfcglpplgmmlokgilfkpcjnmkbfiel
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet-mainnet
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.19-core-0.71.6
+1 -1
View File
@@ -14,7 +14,7 @@ NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
+1 -1
View File
@@ -15,7 +15,7 @@ NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/annou
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
+1 -1
View File
@@ -16,7 +16,7 @@ NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
NX_VEGA_CONSOLE_URL=https://trading.validators-testnet.vega.rocks
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
@@ -69,6 +69,7 @@ describe('MarketSelectorItem', () => {
targetStake: '1000000',
trigger: AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED,
priceMonitoringBounds: null,
lastTradedPrice: '100',
};
const candles = [
+1 -1
View File
@@ -97,7 +97,7 @@ export const AssetDetailsDialog = ({
}}
>
{content}
<p className="text-sm my-4">
<p className="text-xs my-4">
{t(
'There is 1 unit of the settlement asset (%s) to every 1 quote unit.',
[assetSymbol]
+9 -3
View File
@@ -3,7 +3,11 @@ 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 } from '@vegaprotocol/ui-toolkit';
import {
CopyWithTooltip,
Icon,
truncateMiddle,
} from '@vegaprotocol/ui-toolkit';
import {
KeyValueTable,
KeyValueTableRow,
@@ -56,7 +60,7 @@ export const rows: Rows = [
key: AssetDetail.ID,
label: t('ID'),
tooltip: '',
value: (asset) => asset.id,
value: (asset) => truncateMiddle(asset.id),
},
{
key: AssetDetail.TYPE,
@@ -109,7 +113,9 @@ export const rows: Rows = [
return (
<>
<EtherscanLink address={asset.source.contractAddress} />{' '}
<EtherscanLink address={asset.source.contractAddress}>
{truncateMiddle(asset.source.contractAddress)}
</EtherscanLink>{' '}
<CopyWithTooltip text={asset.source.contractAddress}>
<button title={t('Copy address to clipboard')}>
<Icon size={3} name="duplicate" />
+2 -2
View File
@@ -1,4 +1,4 @@
import { TradingOption } from '@vegaprotocol/ui-toolkit';
import { TradingOption, truncateMiddle } from '@vegaprotocol/ui-toolkit';
import type { AssetFieldsFragment } from './__generated__/Asset';
import classNames from 'classnames';
import { t } from '@vegaprotocol/i18n';
@@ -45,7 +45,7 @@ export const AssetOption = ({ asset, balance }: AssetOptionProps) => {
{balance}
<div className="text-[12px] font-mono w-full text-left break-all">
<span className="text-vega-light-300 dark:text-vega-dark-300">
{asset.id}
{truncateMiddle(asset.id)}
</span>
</div>
</div>
@@ -432,9 +432,9 @@ describe('StopOrder', () => {
});
it('sets expiry time/date to now if expiry is changed to checked', async () => {
const now = Math.round(Date.now() / 1000) * 1000;
const now = 24 * 60 * 60 * 1000;
render(generateJsx());
jest.spyOn(global.Date, 'now').mockImplementationOnce(() => now);
jest.spyOn(global.Date, 'now').mockImplementation(() => now);
await userEvent.click(screen.getByTestId(expire));
// expiry time/date was empty it should be set to now
@@ -590,9 +590,9 @@ describe('DealTicket', () => {
it('sets expiry time/date to now if expiry is changed to checked', async () => {
const datePicker = 'date-picker-field';
const now = Math.round(Date.now() / 1000) * 1000;
const now = 24 * 60 * 60 * 1000;
render(generateJsx());
jest.spyOn(global.Date, 'now').mockImplementationOnce(() => now);
jest.spyOn(global.Date, 'now').mockImplementation(() => now);
await userEvent.selectOptions(
screen.getByTestId('order-tif'),
Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
@@ -496,11 +496,12 @@ export const DealTicket = ({
onSelect={(value) => {
// If GTT is selected and no expiresAt time is set, or its
// behind current time then reset the value to current time
const now = Date.now();
if (
value === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT &&
(!expiresAt || new Date(expiresAt).getTime() < Date.now())
(!expiresAt || new Date(expiresAt).getTime() < now)
) {
setValue('expiresAt', formatForInput(new Date()), {
setValue('expiresAt', formatForInput(new Date(now)), {
shouldValidate: true,
});
}
+15 -2
View File
@@ -2,7 +2,11 @@ import type { Asset } from '@vegaprotocol/assets';
import { EtherscanLink } from '@vegaprotocol/environment';
import { t } from '@vegaprotocol/i18n';
import { Intent, Notification } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '@vegaprotocol/utils';
import {
formatNumber,
getUnlimitedThreshold,
quantumDecimalPlaces,
} from '@vegaprotocol/utils';
import type { EthStoredTxState } from '@vegaprotocol/web3';
import { EthTxStatus, useEthTransactionStore } from '@vegaprotocol/web3';
import BigNumber from 'bignumber.js';
@@ -188,6 +192,15 @@ const ApprovalTxFeedback = ({
}
if (tx.status === EthTxStatus.Confirmed) {
const approvedAllowanceValue = (
allowance || new BigNumber(0)
).isGreaterThan(getUnlimitedThreshold(selectedAsset.decimals))
? '∞'
: formatNumber(
allowance?.toString() || 0,
quantumDecimalPlaces(selectedAsset.quantum, selectedAsset.decimals)
);
return (
<div className="mb-4">
<Notification
@@ -198,7 +211,7 @@ const ApprovalTxFeedback = ({
<p>
{t('You approved deposits of up to %s %s.', [
selectedAsset?.symbol,
formatNumber(allowance?.toString() || 0),
approvedAllowanceValue,
])}
</p>
{txLink && <p>{txLink}</p>}
+3 -2
View File
@@ -15,6 +15,7 @@ import { useWeb3ConnectStore } from '@vegaprotocol/web3';
import { useWeb3React } from '@web3-react/core';
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import type { DepositBalances } from './use-deposit-balances';
import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
jest.mock('@vegaprotocol/wallet');
jest.mock('@vegaprotocol/web3');
@@ -90,7 +91,7 @@ describe('Deposit form', () => {
// Assert default values (including) from/to provided by useVegaWallet and useWeb3React
expect(screen.getByText('From (Ethereum address)')).toBeInTheDocument();
expect(screen.getByTestId('ethereum-address')).toHaveTextContent(
MOCK_ETH_ADDRESS
truncateMiddle(MOCK_ETH_ADDRESS)
);
expect(screen.getByLabelText('Asset')).toHaveValue('');
expect(screen.getByLabelText('To (Vega key)')).toHaveValue('');
@@ -353,7 +354,7 @@ describe('Deposit form', () => {
).not.toBeInTheDocument();
expect(screen.getByText('From (Ethereum address)')).toBeInTheDocument();
expect(screen.getByTestId('ethereum-address')).toHaveTextContent(
MOCK_ETH_ADDRESS
truncateMiddle(MOCK_ETH_ADDRESS)
);
});
+2 -1
View File
@@ -22,6 +22,7 @@ import {
Intent,
ButtonLink,
TradingSelect,
truncateMiddle,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useWeb3React } from '@web3-react/core';
@@ -173,7 +174,7 @@ export const DepositForm = ({
return (
<div className="text-sm" aria-describedby="ethereum-address">
<p className="mb-1 break-all" data-testid="ethereum-address">
{account}
{truncateMiddle(account)}
</p>
<DisconnectEthereumButton
onDisconnect={() => {
+10 -4
View File
@@ -7,7 +7,7 @@ import {
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import type BigNumber from 'bignumber.js';
import { formatNumber } from '@vegaprotocol/utils';
import { formatNumber, quantumDecimalPlaces } from '@vegaprotocol/utils';
// Note: all of the values here are with correct asset's decimals
// See `libs/deposits/src/lib/use-deposit-balances.ts`
@@ -35,7 +35,10 @@ export const DepositLimits = ({
label: t('Balance available'),
rawValue: balance,
value: balance ? (
<CompactNumber number={balance} decimals={asset.decimals} />
<CompactNumber
number={balance}
decimals={quantumDecimalPlaces(asset.quantum, asset.decimals)}
/>
) : (
'-'
),
@@ -73,7 +76,7 @@ export const DepositLimits = ({
value: !exempt ? (
<CompactNumber
number={max.minus(deposited)}
decimals={asset.decimals}
decimals={quantumDecimalPlaces(asset.quantum, asset.decimals)}
/>
) : (
<div data-testid="exempt">{t('Exempt')}</div>
@@ -97,7 +100,10 @@ export const DepositLimits = ({
),
rawValue: allowance,
value: allowance ? (
<CompactNumber number={allowance} decimals={asset.decimals} />
<CompactNumber
number={allowance}
decimals={quantumDecimalPlaces(asset.quantum, asset.decimals)}
/>
) : (
'-'
),
@@ -0,0 +1,109 @@
import { useState } from 'react';
import {
VegaIcon,
VegaIconNames,
TradingDropdown,
TradingDropdownTrigger,
TradingDropdownContent,
TradingDropdownItem,
} from '@vegaprotocol/ui-toolkit';
import { formatNumberFixed } from '@vegaprotocol/utils';
export const OrderbookControls = ({
lastTradedPrice,
resolution,
decimalPlaces,
setResolution,
}: {
lastTradedPrice: string | undefined;
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 increaseResolution = () => {
const index = resolutions.indexOf(resolution);
if (index < resolutions.length - 1) {
setResolution(resolutions[index + 1]);
}
};
const decreaseResolution = () => {
const index = resolutions.indexOf(resolution);
if (index > 0) {
setResolution(resolutions[index - 1]);
}
};
return (
<div className="flex h-6">
<button
onClick={increaseResolution}
disabled={resolutions.indexOf(resolution) >= resolutions.length - 1}
className="flex items-center px-2 border-r cursor-pointer border-default"
data-testid="plus-button"
>
<VegaIcon size={12} name={VegaIconNames.PLUS} />
</button>
<TradingDropdown
open={isOpen}
onOpenChange={(open) => setOpen(open)}
trigger={
<TradingDropdownTrigger data-testid="resolution">
<button
className="flex items-center px-2 text-left gap-1"
style={{
minWidth: `${
Math.max.apply(
null,
resolutions.map((item) => formatResolution(item).length)
) + 5
}ch`,
}}
>
<VegaIcon
size={12}
name={
isOpen ? VegaIconNames.CHEVRON_UP : VegaIconNames.CHEVRON_DOWN
}
/>
{formatResolution(resolution)}
</button>
</TradingDropdownTrigger>
}
>
<TradingDropdownContent align="start">
{resolutions.map((r) => (
<TradingDropdownItem key={r} onClick={() => setResolution(r)}>
{formatResolution(r)}
</TradingDropdownItem>
))}
</TradingDropdownContent>
</TradingDropdown>
<button
onClick={decreaseResolution}
disabled={resolutions.indexOf(resolution) <= 0}
className="flex items-center px-2 cursor-pointer border-x border-default"
data-testid="minus-button"
>
<VegaIcon size={12} name={VegaIconNames.MINUS} />
</button>
</div>
);
};
@@ -31,21 +31,12 @@ describe('compactRows', () => {
it('counts cumulative vol', () => {
const asks = compactRows(sell, VolumeType.ask, 10);
const bids = compactRows(buy, VolumeType.bid, 10);
expect(asks[0].cumulativeVol.value).toEqual(4950);
expect(bids[0].cumulativeVol.value).toEqual(579);
expect(asks[10].cumulativeVol.value).toEqual(390);
expect(bids[10].cumulativeVol.value).toEqual(4950);
expect(bids[bids.length - 1].cumulativeVol.value).toEqual(4950);
expect(asks[asks.length - 1].cumulativeVol.value).toEqual(390);
});
it('updates relative data', () => {
const asks = compactRows(sell, VolumeType.ask, 10);
const bids = compactRows(buy, VolumeType.bid, 10);
expect(asks[0].cumulativeVol.relativeValue).toEqual(100);
expect(bids[0].cumulativeVol.relativeValue).toEqual(12);
expect(asks[10].cumulativeVol.relativeValue).toEqual(8);
expect(bids[10].cumulativeVol.relativeValue).toEqual(100);
expect(asks[0].cumulativeVol).toEqual(4950);
expect(bids[0].cumulativeVol).toEqual(579);
expect(asks[10].cumulativeVol).toEqual(390);
expect(bids[10].cumulativeVol).toEqual(4950);
expect(bids[bids.length - 1].cumulativeVol).toEqual(4950);
expect(asks[asks.length - 1].cumulativeVol).toEqual(390);
});
});
+18 -36
View File
@@ -5,15 +5,11 @@ export enum VolumeType {
bid,
ask,
}
export interface CumulativeVol {
value: number;
relativeValue?: number;
}
export interface OrderbookRowData {
price: string;
value: number;
cumulativeVol: CumulativeVol;
volume: number;
cumulativeVol: number;
}
export const getPriceLevel = (price: string | bigint, resolution: number) => {
@@ -26,25 +22,6 @@ export const getPriceLevel = (price: string | bigint, resolution: number) => {
return priceLevel.toString();
};
const getMaxVolumes = (orderbookData: OrderbookRowData[]) => ({
cumulativeVol: Math.max(
orderbookData[0]?.cumulativeVol.value,
orderbookData[orderbookData.length - 1]?.cumulativeVol.value
),
});
// round instead of ceil so we will not show 0 if value if different than 0
const toPercentValue = (value?: number) => Math.ceil((value ?? 0) * 100);
const updateRelativeData = (data: OrderbookRowData[]) => {
const { cumulativeVol } = getMaxVolumes(data);
data.forEach((data, i) => {
data.cumulativeVol.relativeValue = toPercentValue(
data.cumulativeVol.value / cumulativeVol
);
});
};
const updateCumulativeVolumeByType = (
data: OrderbookRowData[],
dataType: VolumeType
@@ -53,14 +30,13 @@ const updateCumulativeVolumeByType = (
const maxIndex = data.length - 1;
if (dataType === VolumeType.bid) {
for (let i = 0; i <= maxIndex; i++) {
data[i].cumulativeVol.value =
data[i].value + (i !== 0 ? data[i - 1].cumulativeVol.value : 0);
data[i].cumulativeVol =
data[i].volume + (i !== 0 ? data[i - 1].cumulativeVol : 0);
}
} else {
for (let i = maxIndex; i >= 0; i--) {
data[i].cumulativeVol.value =
data[i].value +
(i !== maxIndex ? data[i + 1].cumulativeVol.value : 0);
data[i].cumulativeVol =
data[i].volume + (i !== maxIndex ? data[i + 1].cumulativeVol : 0);
}
}
}
@@ -75,6 +51,7 @@ export const compactRows = (
getPriceLevel(row.price, resolution)
);
const orderbookData: OrderbookRowData[] = [];
Object.keys(groupedByLevel).forEach((price) => {
const { volume } = groupedByLevel[price].pop() as PriceLevelFieldsFragment;
let value = Number(volume);
@@ -83,7 +60,11 @@ export const compactRows = (
value += Number(subRow.volume);
subRow = groupedByLevel[price].pop();
}
orderbookData.push({ price, value, cumulativeVol: { value: 0 } });
orderbookData.push({
price,
volume: value,
cumulativeVol: 0,
});
});
orderbookData.sort((a, b) => {
@@ -95,8 +76,9 @@ export const compactRows = (
}
return 1;
});
updateCumulativeVolumeByType(orderbookData, dataType);
updateRelativeData(orderbookData);
return orderbookData;
};
@@ -140,7 +122,7 @@ export interface MockDataGeneratorParams {
numberOfSellRows: number;
numberOfBuyRows: number;
overlap: number;
midPrice?: string;
lastTradedPrice: string;
bestStaticBidPrice: number;
bestStaticOfferPrice: number;
}
@@ -148,14 +130,14 @@ export interface MockDataGeneratorParams {
export const generateMockData = ({
numberOfSellRows,
numberOfBuyRows,
midPrice,
lastTradedPrice,
overlap,
bestStaticBidPrice,
bestStaticOfferPrice,
}: MockDataGeneratorParams) => {
let matrix = new Array(numberOfSellRows).fill(undefined);
let price =
Number(midPrice) + (numberOfSellRows - Math.ceil(overlap / 2) + 1);
Number(lastTradedPrice) + (numberOfSellRows - Math.ceil(overlap / 2) + 1);
const sell: PriceLevelFieldsFragment[] = matrix.map((row, i) => ({
price: (price -= 1).toString(),
volume: (numberOfSellRows - i + 1).toString(),
@@ -171,7 +153,7 @@ export const generateMockData = ({
return {
asks: sell,
bids: buy,
midPrice,
lastTradedPrice,
bestStaticBidPrice: bestStaticBidPrice.toString(),
bestStaticOfferPrice: bestStaticOfferPrice.toString(),
};
+12 -10
View File
@@ -17,7 +17,7 @@ export type OrderbookData = {
interface OrderbookManagerProps {
marketId: string;
onClick?: (args: { price?: string; size?: string }) => void;
onClick: (args: { price?: string; size?: string }) => void;
}
export const OrderbookManager = ({
@@ -61,15 +61,17 @@ export const OrderbookManager = ({
data={data}
reload={reload}
>
<Orderbook
bids={data?.depth.buy ?? []}
asks={data?.depth.sell ?? []}
decimalPlaces={market?.decimalPlaces ?? 0}
positionDecimalPlaces={market?.positionDecimalPlaces ?? 0}
assetSymbol={market?.tradableInstrument.instrument.product.quoteName}
onClick={onClick}
midPrice={marketData?.midPrice}
/>
{market && marketData && (
<Orderbook
bids={data?.depth.buy ?? []}
asks={data?.depth.sell ?? []}
decimalPlaces={market.decimalPlaces}
positionDecimalPlaces={market.positionDecimalPlaces}
assetSymbol={market.tradableInstrument.instrument.product.quoteName}
onClick={onClick}
lastTradedPrice={marketData.lastTradedPrice}
/>
)}
</AsyncRenderer>
);
};
+105 -114
View File
@@ -1,158 +1,149 @@
import React, { memo } from 'react';
import type { ReactNode } from 'react';
import { memo } from 'react';
import { addDecimal, addDecimalsFixedFormatNumber } from '@vegaprotocol/utils';
import { NumericCell, PriceCell } from '@vegaprotocol/datagrid';
import { NumericCell } from '@vegaprotocol/datagrid';
import { VolumeType } from './orderbook-data';
import classNames from 'classnames';
const HIDE_VOL_WIDTH = 190;
const HIDE_CUMULATIVE_VOL_WIDTH = 260;
interface OrderbookRowProps {
value: number;
cumulativeValue?: number;
cumulativeRelativeValue?: number;
volume: number;
cumulativeVolume: number;
decimalPlaces: number;
positionDecimalPlaces: number;
price: string;
onClick?: (args: { price?: string; size?: string }) => void;
onClick: (args: { price?: string; size?: string }) => void;
type: VolumeType;
width: number;
maxVol: number;
}
const HIDE_VOL_WIDTH = 150;
const HIDE_CUMULATIVE_VOL_WIDTH = 220;
const CumulationBar = ({
cumulativeValue = 0,
type,
}: {
cumulativeValue?: number;
type: VolumeType;
}) => {
return (
<div
data-testid={`${VolumeType.bid === type ? 'bid' : 'ask'}-bar`}
className={classNames(
'absolute top-0 left-0 h-full',
type === VolumeType.bid
? 'bg-market-green-300 dark:bg-market-green/50'
: 'bg-market-red-300 dark:bg-market-red/30'
)}
style={{
width: `${cumulativeValue}%`,
}}
/>
);
};
const CumulativeVol = memo(
export const OrderbookRow = memo(
({
testId,
positionDecimalPlaces,
cumulativeValue,
onClick,
}: {
ask?: number;
bid?: number;
cumulativeValue?: number;
testId?: string;
className?: string;
positionDecimalPlaces: number;
onClick?: (size?: string | number) => void;
}) => {
const volume = cumulativeValue ? (
<NumericCell
testId={testId}
value={cumulativeValue}
valueFormatted={addDecimalsFixedFormatNumber(
cumulativeValue,
positionDecimalPlaces ?? 0
)}
/>
) : null;
return onClick && volume ? (
<button
onClick={() => onClick(cumulativeValue)}
className="hover:dark:bg-neutral-800 hover:bg-neutral-200 text-right pr-1"
>
{volume}
</button>
) : (
<div className="pr-1" data-testid={testId}>
{volume}
</div>
);
}
);
CumulativeVol.displayName = 'OrderBookCumulativeVol';
export const OrderbookRow = React.memo(
({
value,
cumulativeValue,
cumulativeRelativeValue,
volume,
cumulativeVolume,
decimalPlaces,
positionDecimalPlaces,
price,
onClick,
type,
width,
maxVol,
}: OrderbookRowProps) => {
const txtId = type === VolumeType.bid ? 'bid' : 'ask';
const cols =
width >= HIDE_CUMULATIVE_VOL_WIDTH ? 3 : width >= HIDE_VOL_WIDTH ? 2 : 1;
return (
<div className="relative pr-1">
<CumulationBar cumulativeValue={cumulativeRelativeValue} type={type} />
<div className="relative px-1">
<CumulationBar
cumulativeVolume={cumulativeVolume}
type={type}
maxVol={maxVol}
/>
<div
data-testid={`${txtId}-rows-container`}
className={classNames('grid gap-1 text-right', `grid-cols-${cols}`)}
>
<PriceCell
testId={`price-${price}`}
value={BigInt(price)}
onClick={() =>
onClick && onClick({ price: addDecimal(price, decimalPlaces) })
}
valueFormatted={addDecimalsFixedFormatNumber(price, decimalPlaces)}
className={
type === VolumeType.ask
? 'text-market-red dark:text-market-red'
: 'text-market-green-600 dark:text-market-green'
}
/>
{width >= HIDE_VOL_WIDTH && (
<PriceCell
testId={`${txtId}-vol-${price}`}
onClick={(value) =>
onClick &&
value &&
onClick({
size: addDecimal(value, positionDecimalPlaces),
})
}
value={value}
<OrderBookRowCell
onClick={() => onClick({ price: addDecimal(price, decimalPlaces) })}
>
<NumericCell
testId={`price-${price}`}
value={BigInt(price)}
valueFormatted={addDecimalsFixedFormatNumber(
value,
positionDecimalPlaces
price,
decimalPlaces
)}
className={classNames({
'text-market-red dark:text-market-red': type === VolumeType.ask,
'text-market-green-600 dark:text-market-green':
type === VolumeType.bid,
})}
/>
</OrderBookRowCell>
{width >= HIDE_VOL_WIDTH && (
<OrderBookRowCell
onClick={() =>
onClick({ size: addDecimal(volume, positionDecimalPlaces) })
}
>
<NumericCell
testId={`${txtId}-vol-${price}`}
value={volume}
valueFormatted={addDecimalsFixedFormatNumber(
volume,
positionDecimalPlaces ?? 0
)}
/>
</OrderBookRowCell>
)}
{width >= HIDE_CUMULATIVE_VOL_WIDTH && (
<CumulativeVol
testId={`cumulative-vol-${price}`}
<OrderBookRowCell
onClick={() =>
onClick &&
cumulativeValue &&
onClick({
size: addDecimal(cumulativeValue, positionDecimalPlaces),
size: addDecimal(cumulativeVolume, positionDecimalPlaces),
})
}
positionDecimalPlaces={positionDecimalPlaces}
cumulativeValue={cumulativeValue}
/>
>
<NumericCell
testId={`cumulative-vol-${price}`}
value={cumulativeVolume}
valueFormatted={addDecimalsFixedFormatNumber(
cumulativeVolume,
positionDecimalPlaces
)}
/>
</OrderBookRowCell>
)}
</div>
</div>
);
}
);
OrderbookRow.displayName = 'OrderbookRow';
const OrderBookRowCell = ({
children,
onClick,
}: {
children: ReactNode;
onClick: () => void;
}) => {
return (
<button
className="overflow-hidden text-right text-ellipsis whitespace-nowrap hover:dark:bg-neutral-800 hover:bg-neutral-200"
onClick={onClick}
>
{children}
</button>
);
};
const CumulationBar = ({
cumulativeVolume = 0,
type,
maxVol,
}: {
cumulativeVolume: number;
type: VolumeType;
maxVol: number;
}) => {
const width = (cumulativeVolume / maxVol) * 100;
return (
<div
data-testid={`${VolumeType.bid === type ? 'bid' : 'ask'}-bar`}
className={classNames(
'absolute top-0 left-0 h-full',
type === VolumeType.bid
? 'bg-market-green/10 dark:bg-market-green/10'
: 'bg-market-red/10 dark:bg-market-red/10'
)}
style={{
width: `${width}%`,
}}
/>
);
};
+53 -7
View File
@@ -1,7 +1,7 @@
import { render, waitFor, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { generateMockData, VolumeType } from './orderbook-data';
import { Orderbook } from './orderbook';
import { Orderbook, OrderbookMid } from './orderbook';
import * as orderbookData from './orderbook-data';
function mockOffsetSize(width: number, height: number) {
@@ -24,7 +24,7 @@ describe('Orderbook', () => {
numberOfSellRows: 100,
numberOfBuyRows: 100,
step: 1,
midPrice: '122900',
lastTradedPrice: '122900',
bestStaticBidPrice: 122905,
bestStaticOfferPrice: 122895,
decimalPlaces: 3,
@@ -44,13 +44,14 @@ describe('Orderbook', () => {
positionDecimalPlaces={0}
{...generateMockData(params)}
assetSymbol="USD"
onClick={jest.fn()}
/>
);
await waitFor(() =>
screen.getByTestId(`middle-mark-price-${params.midPrice}`)
screen.getByTestId(`last-traded-${params.lastTradedPrice}`)
);
expect(
screen.getByTestId(`middle-mark-price-${params.midPrice}`)
screen.getByTestId(`last-traded-${params.lastTradedPrice}`)
).toHaveTextContent('122.90');
});
@@ -68,10 +69,10 @@ describe('Orderbook', () => {
/>
);
expect(
await screen.findByTestId(`middle-mark-price-${params.midPrice}`)
await screen.findByTestId(`last-traded-${params.lastTradedPrice}`)
).toBeInTheDocument();
// Before resolution change the price is 122.934
await userEvent.click(await screen.getByTestId('price-122901'));
await userEvent.click(screen.getByTestId('price-122901'));
expect(onClickSpy).toBeCalledWith({ price: '122.901' });
await userEvent.click(screen.getByTestId('resolution'));
@@ -92,7 +93,7 @@ describe('Orderbook', () => {
VolumeType.ask,
10
);
await userEvent.click(await screen.getByTestId('price-12294'));
await userEvent.click(screen.getByTestId('price-12294'));
expect(onClickSpy).toBeCalledWith({ price: '122.94' });
});
@@ -177,3 +178,48 @@ describe('Orderbook', () => {
});
});
});
describe('OrderbookMid', () => {
const props = {
lastTradedPrice: '100',
decimalPlaces: 0,
assetSymbol: 'BTC',
bestAskPrice: '101',
bestBidPrice: '99',
};
it('renders no change until lastTradedPrice changes', () => {
const { rerender } = render(<OrderbookMid {...props} />);
expect(screen.getByTestId(/last-traded/)).toHaveTextContent(
props.lastTradedPrice
);
expect(screen.getByText(props.assetSymbol)).toBeInTheDocument();
expect(screen.queryByTestId(/icon-/)).not.toBeInTheDocument();
expect(screen.getByTestId('spread')).toHaveTextContent('(2)');
// rerender with no change should not show the icon
rerender(<OrderbookMid {...props} />);
expect(screen.queryByTestId(/icon-/)).not.toBeInTheDocument();
rerender(
<OrderbookMid {...props} lastTradedPrice="101" bestAskPrice="102" />
);
expect(screen.getByTestId('icon-arrow-up')).toBeInTheDocument();
expect(screen.getByTestId('spread')).toHaveTextContent('(3)');
// rerender again with the same price, should still be set to 'up'
rerender(
<OrderbookMid
{...props}
lastTradedPrice="101"
bestAskPrice="102"
bestBidPrice="98"
/>
);
expect(screen.getByTestId('icon-arrow-up')).toBeInTheDocument();
expect(screen.getByTestId('spread')).toHaveTextContent('(4)');
rerender(<OrderbookMid {...props} lastTradedPrice="100" />);
expect(screen.getByTestId('icon-arrow-down')).toBeInTheDocument();
});
});
@@ -9,9 +9,9 @@ type Props = Omit<MockDataGeneratorParams, 'resolution'> & {
const OrderbookMockDataProvider = ({ decimalPlaces, ...props }: Props) => {
return (
<div className="absolute inset-0 dark:bg-black dark:text-neutral-200 bg-white text-neutral-800">
<div className="absolute inset-0 bg-white dark:bg-black dark:text-neutral-200 text-neutral-800">
<div
className="absolute left-0 top-0 bottom-0"
className="absolute top-0 bottom-0 left-0"
style={{ width: '400px' }}
>
<Orderbook
@@ -19,6 +19,7 @@ const OrderbookMockDataProvider = ({ decimalPlaces, ...props }: Props) => {
decimalPlaces={decimalPlaces}
{...generateMockData({ ...props })}
assetSymbol="USD"
onClick={() => undefined}
/>
</div>
</div>
+112 -151
View File
@@ -1,25 +1,15 @@
import { useMemo, useRef, useState } from 'react';
import ReactVirtualizedAutoSizer from 'react-virtualized-auto-sizer';
import {
addDecimalsFormatNumber,
formatNumberFixed,
} from '@vegaprotocol/utils';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { usePrevious } from '@vegaprotocol/react-helpers';
import { OrderbookRow } from './orderbook-row';
import type { OrderbookRowData } from './orderbook-data';
import { compactRows, VolumeType } from './orderbook-data';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Splash,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { Splash, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import type { PriceLevelFieldsFragment } from './__generated__/MarketDepth';
import { OrderbookControls } from './orderbook-controls';
// Sets row height, will be used to calculate number of rows that can be
// displayed each side of the book without overflow
@@ -27,20 +17,7 @@ export const rowHeight = 17;
const rowGap = 1;
const midHeight = 30;
type PriceChange = 'up' | 'down' | 'none';
const PRICE_CHANGE_ICON_MAP: Readonly<Record<PriceChange, VegaIconNames>> = {
up: VegaIconNames.ARROW_UP,
down: VegaIconNames.ARROW_DOWN,
none: VegaIconNames.BULLET,
};
const PRICE_CHANGE_CLASS_MAP: Readonly<Record<PriceChange, string>> = {
up: 'text-market-green-600 dark:text-market-green',
down: 'text-market-red dark:text-market-red',
none: 'text-vega-blue-500',
};
const OrderbookTable = ({
const OrderbookSide = ({
rows,
resolution,
type,
@@ -48,14 +25,16 @@ const OrderbookTable = ({
positionDecimalPlaces,
onClick,
width,
maxVol,
}: {
rows: OrderbookRowData[];
resolution: number;
decimalPlaces: number;
positionDecimalPlaces: number;
type: VolumeType;
onClick?: (args: { price?: string; size?: string }) => void;
onClick: (args: { price?: string; size?: string }) => void;
width: number;
maxVol: number;
}) => {
return (
<div
@@ -78,11 +57,11 @@ const OrderbookTable = ({
onClick={onClick}
decimalPlaces={decimalPlaces - Math.log10(resolution)}
positionDecimalPlaces={positionDecimalPlaces}
value={data.value}
cumulativeValue={data.cumulativeVol.value}
cumulativeRelativeValue={data.cumulativeVol.relativeValue}
volume={data.volume}
cumulativeVolume={data.cumulativeVol}
type={type}
width={width}
maxVol={maxVol}
/>
))}
</div>
@@ -90,31 +69,88 @@ const OrderbookTable = ({
);
};
export const OrderbookMid = ({
lastTradedPrice,
decimalPlaces,
assetSymbol,
bestAskPrice,
bestBidPrice,
}: {
lastTradedPrice: string;
decimalPlaces: number;
assetSymbol: string;
bestAskPrice: string;
bestBidPrice: string;
}) => {
const previousLastTradedPrice = usePrevious(lastTradedPrice);
const priceChangeRef = useRef<'up' | 'down' | 'none'>('none');
const spread = (BigInt(bestAskPrice) - BigInt(bestBidPrice)).toString();
if (previousLastTradedPrice !== lastTradedPrice) {
priceChangeRef.current =
Number(previousLastTradedPrice) > Number(lastTradedPrice) ? 'down' : 'up';
}
return (
<div className="flex items-center justify-center text-base gap-2">
{priceChangeRef.current !== 'none' && (
<span
className={classNames('flex flex-col justify-center', {
'text-market-green-600 dark:text-market-green':
priceChangeRef.current === 'up',
'text-market-red dark:text-market-red':
priceChangeRef.current === 'down',
})}
>
<VegaIcon
name={
priceChangeRef.current === 'up'
? VegaIconNames.ARROW_UP
: VegaIconNames.ARROW_DOWN
}
/>
</span>
)}
<span
// monospace sizing doesn't quite align with alpha
className="font-mono text-[15px]"
data-testid={`last-traded-${lastTradedPrice}`}
title={t('Last traded price')}
>
{addDecimalsFormatNumber(lastTradedPrice, decimalPlaces)}
</span>
<span>{assetSymbol}</span>
<span
title={t('Spread')}
className="font-mono text-xs text-muted"
data-testid="spread"
>
({addDecimalsFormatNumber(spread, decimalPlaces)})
</span>
</div>
);
};
interface OrderbookProps {
decimalPlaces: number;
positionDecimalPlaces: number;
onClick?: (args: { price?: string; size?: string }) => void;
midPrice?: string;
onClick: (args: { price?: string; size?: string }) => void;
lastTradedPrice: string;
bids: PriceLevelFieldsFragment[];
asks: PriceLevelFieldsFragment[];
assetSymbol: string | undefined;
assetSymbol: string;
}
export const Orderbook = ({
decimalPlaces,
positionDecimalPlaces,
onClick,
midPrice,
lastTradedPrice,
asks,
bids,
assetSymbol,
}: OrderbookProps) => {
const [resolution, setResolution] = useState(1);
const resolutions = new Array(
Math.max(midPrice?.toString().length ?? 0, decimalPlaces + 1)
)
.fill(null)
.map((v, i) => Math.pow(10, i));
const groupedAsks = useMemo(() => {
return compactRows(asks, VolumeType.ask, resolution);
@@ -123,44 +159,11 @@ export const Orderbook = ({
const groupedBids = useMemo(() => {
return compactRows(bids, VolumeType.bid, resolution);
}, [bids, resolution]);
const [isOpen, setOpen] = useState(false);
const previousMidPrice = usePrevious(midPrice);
const priceChangeRef = useRef<'up' | 'down' | 'none'>('none');
if (midPrice && previousMidPrice !== midPrice) {
priceChangeRef.current =
(previousMidPrice || '') > midPrice ? 'down' : 'up';
}
const priceChangeIcon = (
<span
className={classNames(PRICE_CHANGE_CLASS_MAP[priceChangeRef.current])}
>
<VegaIcon name={PRICE_CHANGE_ICON_MAP[priceChangeRef.current]} />
</span>
);
const formatResolution = (r: number) => {
return formatNumberFixed(
Math.log10(r) - decimalPlaces > 0
? Math.pow(10, Math.log10(r) - decimalPlaces)
: 0,
decimalPlaces - Math.log10(r)
);
};
const increaseResolution = () => {
const index = resolutions.indexOf(resolution);
if (index < resolutions.length - 1) {
setResolution(resolutions[index + 1]);
}
};
const decreaseResolution = () => {
const index = resolutions.indexOf(resolution);
if (index > 0) {
setResolution(resolutions[index - 1]);
}
};
// get the best bid/ask, note that we are using the pre aggregated
// values so we can render the most accurate spread in the mid section
const bestAskPrice = asks[0] ? asks[0].price : '0';
const bestBidPrice = bids[0] ? bids[0].price : '0';
return (
<div className="h-full text-xs grid grid-rows-[1fr_min-content]">
@@ -171,21 +174,30 @@ export const Orderbook = ({
1,
Math.floor((height - midHeight) / 2 / (rowHeight + rowGap))
);
const askRows = groupedAsks?.slice(limit * -1) ?? [];
const bidRows = groupedBids?.slice(0, limit) ?? [];
const askRows = groupedAsks.slice(limit * -1);
const bidRows = groupedBids.slice(0, limit);
// this is used for providing a scale to render the volume
// bars based on the visible book
const deepestVisibleAsk = askRows[0];
const deepestVisibleBid = bidRows[bidRows.length - 1];
const maxVol = Math.max(
deepestVisibleAsk?.cumulativeVol || 0,
deepestVisibleBid?.cumulativeVol || 0
);
return (
<div
className="overflow-hidden grid"
data-testid="orderbook-grid-element"
style={{
width: width + 'px',
height: height + 'px',
width,
height,
gridTemplateRows: `1fr ${midHeight}px 1fr`, // cannot use tailwind here as tailwind will not parse a class string with interpolation
}}
>
{askRows.length || bidRows.length ? (
<>
<OrderbookTable
<OrderbookSide
rows={askRows}
type={VolumeType.ask}
resolution={resolution}
@@ -193,22 +205,16 @@ export const Orderbook = ({
positionDecimalPlaces={positionDecimalPlaces}
onClick={onClick}
width={width}
maxVol={maxVol}
/>
<div className="flex items-center justify-center gap-2">
{midPrice && (
<>
<span
className="font-mono text-lg"
data-testid={`middle-mark-price-${midPrice}`}
>
{addDecimalsFormatNumber(midPrice, decimalPlaces)}
</span>
<span className="text-base">{assetSymbol}</span>
{priceChangeIcon}
</>
)}
</div>
<OrderbookTable
<OrderbookMid
lastTradedPrice={lastTradedPrice}
decimalPlaces={decimalPlaces}
assetSymbol={assetSymbol}
bestAskPrice={bestAskPrice}
bestBidPrice={bestBidPrice}
/>
<OrderbookSide
rows={bidRows}
type={VolumeType.bid}
resolution={resolution}
@@ -216,10 +222,11 @@ export const Orderbook = ({
positionDecimalPlaces={positionDecimalPlaces}
onClick={onClick}
width={width}
maxVol={maxVol}
/>
</>
) : (
<div className="inset-0 absolute">
<div className="absolute inset-0">
<Splash>{t('No data')}</Splash>
</div>
)}
@@ -228,59 +235,13 @@ export const Orderbook = ({
}}
</ReactVirtualizedAutoSizer>
</div>
<div className="border-t border-default flex">
<button
onClick={increaseResolution}
disabled={resolutions.indexOf(resolution) >= resolutions.length - 1}
className="flex items-center border-r border-default px-2 cursor-pointer"
data-testid="plus-button"
>
<VegaIcon size={12} name={VegaIconNames.PLUS} />
</button>
<DropdownMenu
open={isOpen}
onOpenChange={(open) => setOpen(open)}
trigger={
<DropdownMenuTrigger
data-testid="resolution"
className="flex justify-between px-1 items-center"
style={{
width: `${
Math.max.apply(
null,
resolutions.map((item) => formatResolution(item).length)
) + 3
}ch`,
}}
>
<VegaIcon
size={12}
name={
isOpen ? VegaIconNames.CHEVRON_UP : VegaIconNames.CHEVRON_DOWN
}
/>
<div className="text-xs text-left">
{formatResolution(resolution)}
</div>
</DropdownMenuTrigger>
}
>
<DropdownMenuContent align="start">
{resolutions.map((r) => (
<DropdownMenuItem key={r} onClick={() => setResolution(r)}>
{formatResolution(r)}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
<button
onClick={decreaseResolution}
disabled={resolutions.indexOf(resolution) <= 0}
className="flex items-center border-x border-default px-2 cursor-pointer"
data-testid="minus-button"
>
<VegaIcon size={12} name={VegaIconNames.MINUS} />
</button>
<div className="border-t border-default">
<OrderbookControls
lastTradedPrice={lastTradedPrice}
resolution={resolution}
decimalPlaces={decimalPlaces}
setResolution={setResolution}
/>
</div>
</div>
);
+6 -4
View File
@@ -3,23 +3,23 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type MarketDataUpdateFieldsFragment = { __typename?: 'ObservableMarketData', marketId: string, auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null };
export type MarketDataUpdateFieldsFragment = { __typename?: 'ObservableMarketData', marketId: string, auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, lastTradedPrice: string, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null };
export type MarketDataUpdateSubscriptionVariables = Types.Exact<{
marketId: Types.Scalars['ID'];
}>;
export type MarketDataUpdateSubscription = { __typename?: 'Subscription', marketsData: Array<{ __typename?: 'ObservableMarketData', marketId: string, auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null }> };
export type MarketDataUpdateSubscription = { __typename?: 'Subscription', marketsData: Array<{ __typename?: 'ObservableMarketData', marketId: string, auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, lastTradedPrice: string, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null }> };
export type MarketDataFieldsFragment = { __typename?: 'MarketData', auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, market: { __typename?: 'Market', id: string }, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null };
export type MarketDataFieldsFragment = { __typename?: 'MarketData', auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, lastTradedPrice: string, market: { __typename?: 'Market', id: string }, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null };
export type MarketDataQueryVariables = Types.Exact<{
marketId: Types.Scalars['ID'];
}>;
export type MarketDataQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', data?: { __typename?: 'MarketData', auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, market: { __typename?: 'Market', id: string }, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null } | null } }> } | null };
export type MarketDataQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', data?: { __typename?: 'MarketData', auctionEnd?: string | null, auctionStart?: string | null, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, indicativePrice: string, indicativeVolume: string, marketState: Types.MarketState, marketTradingMode: Types.MarketTradingMode, marketValueProxy: string, markPrice: string, midPrice: string, openInterest: string, staticMidPrice: string, suppliedStake?: string | null, targetStake?: string | null, trigger: Types.AuctionTrigger, lastTradedPrice: string, market: { __typename?: 'Market', id: string }, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number } }> | null } | null } }> } | null };
export const MarketDataUpdateFieldsFragmentDoc = gql`
fragment MarketDataUpdateFields on ObservableMarketData {
@@ -56,6 +56,7 @@ export const MarketDataUpdateFieldsFragmentDoc = gql`
suppliedStake
targetStake
trigger
lastTradedPrice
}
`;
export const MarketDataFieldsFragmentDoc = gql`
@@ -95,6 +96,7 @@ export const MarketDataFieldsFragmentDoc = gql`
suppliedStake
targetStake
trigger
lastTradedPrice
}
`;
export const MarketDataUpdateDocument = gql`
+2
View File
@@ -32,6 +32,7 @@ fragment MarketDataUpdateFields on ObservableMarketData {
suppliedStake
targetStake
trigger
lastTradedPrice
}
subscription MarketDataUpdate($marketId: ID!) {
@@ -76,6 +77,7 @@ fragment MarketDataFields on MarketData {
suppliedStake
targetStake
trigger
lastTradedPrice
}
query MarketData($marketId: ID!) {
+2
View File
@@ -62,6 +62,7 @@ const marketDataFields: MarketDataFieldsFragment = {
markPrice: '4612690058',
midPrice: '4612690000',
openInterest: '0',
lastTradedPrice: '4612690000',
priceMonitoringBounds: [
{
minValidPrice: '654701',
@@ -99,6 +100,7 @@ const marketDataUpdateFields: MarketDataUpdateFieldsFragment = {
marketValueProxy: '',
markPrice: '4612690058',
midPrice: '0',
lastTradedPrice: '0',
openInterest: '0',
staticMidPrice: '0',
trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_UNSPECIFIED,
@@ -26,36 +26,22 @@ export const ProtocolUpgradeInProgressNotification = () => {
const [nextUpgrade] = useLocalStorageSnapshot(
NEXT_PROTOCOL_UPGRADE_PROPOSAL_SNAPSHOT
);
const { blocksRising, block } = useBlockRising();
const detailsLink = useProtocolUpgradeProposalLink();
let vegaReleaseTag: string | undefined;
let upgradeBlockHeight: string | undefined;
const hasData = data && !error;
const hasStoredData = nextUpgrade && ALLOW_STORED_PROPOSAL_DATA;
if (hasData) {
// gets tag and height from the data api
vegaReleaseTag = data.vegaReleaseTag;
upgradeBlockHeight = data.upgradeBlockHeight;
} else if (hasStoredData) {
// gets tag and height from stored value if data api is unavailable
if (error && !data && nextUpgrade && ALLOW_STORED_PROPOSAL_DATA) {
try {
const stored = JSON.parse(nextUpgrade) as StoredNextProtocolUpgradeData;
vegaReleaseTag = stored.vegaReleaseTag;
upgradeBlockHeight = stored.upgradeBlockHeight;
} catch {
// NOOP - could not parse stored data
// no op
}
}
const hasUpgradeInfo = vegaReleaseTag && upgradeBlockHeight;
const { blocksRising, block } = useBlockRising(
// skips querying blocks if there's no upgrade information available
!hasUpgradeInfo
);
/**
* If upgrade is in progress then none of the nodes should produce blocks,
* same should be with the tendermint block info otherwise it's a network
@@ -64,7 +50,10 @@ export const ProtocolUpgradeInProgressNotification = () => {
* Once the networks is back then the notification disappears.
*/
const upgradeInProgress =
hasUpgradeInfo && !blocksRising && block <= Number(upgradeBlockHeight);
vegaReleaseTag &&
upgradeBlockHeight &&
!blocksRising &&
block <= Number(upgradeBlockHeight);
if (!upgradeInProgress) return null;
@@ -15,33 +15,31 @@ const CHECK_INTERVAL = 5000; // ms
*/
const ALLOW_STALE = 2; // times -> MAX(this, 1) * CHECK_INTERVAL ~> min check time
export const useBlockRising = (skip = false) => {
export const useBlockRising = () => {
const [blocksRising, setBlocksRising] = useState(true);
const [block, setBlock] = useState(0);
const nodes = useEnvironment((state) => state.nodes);
const clients = useMemo(() => {
return nodes.map((n) => {
if (n && n.length > 0) {
const client = createClient({
return nodes.map(
(n) =>
n &&
n.length > 0 &&
createClient({
url: n,
cacheConfig: undefined,
retry: false,
connectToDevTools: false,
connectToHeaderStore: true,
});
return client;
}
return undefined;
});
})
);
}, [nodes]);
const { refetch: fetchBlockInfo } = useBlockInfo();
useEffect(() => {
if (skip) return;
let stale = 0;
let prev = 0;
const check = async () => {
const queries = clients.map((client) =>
const queries = clients.map((client, index) =>
client
? client
.query<BlockStatisticsQuery>({
@@ -49,16 +47,18 @@ export const useBlockRising = (skip = false) => {
fetchPolicy: 'network-only',
errorPolicy: 'ignore',
})
.catch(() => {
// NOOP - could not retrieve statistics for that node (network error)
})
.catch((err) =>
Promise.reject(
`could not retrieve statistics from ${nodes[index]}`
)
)
: undefined
);
const blockInfo = await fetchBlockInfo();
const results = (await Promise.allSettled(compact(queries))).map(
(res) => {
if (res && res.status === 'fulfilled' && res.value) {
if (res && res.status === 'fulfilled') {
return res.value.data.statistics;
} else {
return undefined;
@@ -86,7 +86,7 @@ export const useBlockRising = (skip = false) => {
return () => {
clearInterval(interval);
};
}, [clients, fetchBlockInfo, blocksRising, nodes, skip]);
}, [clients, fetchBlockInfo, blocksRising, nodes]);
return { blocksRising, block };
};
+10 -1
View File
@@ -1,9 +1,14 @@
import { BigNumber } from 'bignumber.js';
import { getUserLocale, formatNumber } from '@vegaprotocol/utils';
import {
getUserLocale,
formatNumber,
getUnlimitedThreshold,
} from '@vegaprotocol/utils';
const INFINITY = '∞';
const DEFAULT_COMPACT_ABOVE = 1_000_000;
const DEFAULT_COMPACT_CAP = new BigNumber(1e24);
/**
* Compacts given number to human readable format.
* @param number
@@ -35,6 +40,10 @@ export const CompactNumber = ({
const decimalPlaces =
(decimals === 'infer' ? number.decimalPlaces() : decimals) || 0;
if (number.isGreaterThan(getUnlimitedThreshold(decimalPlaces))) {
return <span data-testid={testId}>{INFINITY}</span>;
}
if (number.isLessThan(DEFAULT_COMPACT_ABOVE)) {
return (
<span data-testid={testId}>{formatNumber(number, decimalPlaces)}</span>
@@ -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,
+62
View File
@@ -5,7 +5,9 @@ import {
addDecimalsFormatNumberQuantum,
formatNumber,
formatNumberPercentage,
getUnlimitedThreshold,
isNumeric,
quantumDecimalPlaces,
toDecimal,
toNumberParts,
} from './number';
@@ -156,3 +158,63 @@ describe('number utils', () => {
});
});
});
describe('quantumDecimalPlaces', () => {
it.each([
['1', 1, 3],
['10', 1, 2],
['100', 1, 1],
['1000', 1, 0],
['1', 2, 4],
['10', 2, 3],
['100', 2, 2],
['1000', 2, 1],
['1', 3, 5],
['10', 3, 4],
['100', 3, 3],
['1000', 3, 2],
['1', 18, 20],
['1000000000', 18, 11],
['5000000000', 18, 11],
['1000000000000000000', 18, 2],
])(
'converts quantum %s of %d decimal places to %d quant. decimal places',
(quantum, decimals, output) => {
expect(quantumDecimalPlaces(quantum, decimals)).toEqual(output);
}
);
});
describe('getUnlimitedThreshold', () => {
it.each([
[
0,
'9.26336713898529563388567880069503262826159877325124512315660672063305037119488e+76',
],
[
1,
'9.26336713898529563388567880069503262826159877325124512315660672063305037119488e+75',
],
[
2,
'9.26336713898529563388567880069503262826159877325124512315660672063305037119488e+74',
],
[
3,
'9.26336713898529563388567880069503262826159877325124512315660672063305037119488e+73',
],
[
10,
'9.26336713898529563388567880069503262826159877325124512315660672063305037119488e+66',
],
[
18,
'9.26336713898529563388567880069503262826159877325124512315660672063305037119488e+58',
],
])(
'given %d decimal places it returns unlimited threshold %s',
(decimals, output) => {
expect(getUnlimitedThreshold(decimals).toString()).toEqual(output);
}
);
});
+31
View File
@@ -4,6 +4,18 @@ import memoize from 'lodash/memoize';
import { getUserLocale } from '../get-user-locale';
/**
* A raw unformatted value greater than this is considered and displayed
* as UNLIMITED.
*/
export const UNLIMITED_THRESHOLD = new BigNumber(2).pow(256).times(0.8);
/**
* Gets the unlimited threshold value for given decimal places.
* @param decimalPlaces the asset's decimal places
*/
export const getUnlimitedThreshold = (decimalPlaces: number) =>
UNLIMITED_THRESHOLD.dividedBy(Math.pow(10, decimalPlaces));
const MIN_FRACTION_DIGITS = 2;
const MAX_FRACTION_DIGITS = 20;
@@ -90,6 +102,25 @@ export const formatNumberFixed = (
return getFixedNumberFormat(formatDecimals).format(Number(rawValue));
};
export const quantumDecimalPlaces = (
/** Raw asset's quantum value */
rawQuantum: number | string,
/** Asset's decimal places */
decimalPlaces: number
) => {
// if raw quantum value is an empty string then it'll evaluate to 0
// this check ignores NaNs and zeroes
const formatDecimals =
isNaN(Number(rawQuantum)) || Number(rawQuantum) === 0
? decimalPlaces
: Math.max(
0,
Math.log10(100 / Number(addDecimal(rawQuantum, decimalPlaces)))
);
return Math.ceil(formatDecimals);
};
export const addDecimalsFormatNumberQuantum = (
rawValue: string | number,
decimalPlaces: number,
+4 -1
View File
@@ -11,4 +11,7 @@ As a market user I want to see information about orders existing in the market.
- I **Must** see prices sorted descending (<a name="6003-ORDB-007" href="#6003-ORDB-007">6003-ORDB-007</a>)
- I **Must** be able to set a resolution of data (<a name="6003-ORDB-008" href="#6003-ORDB-008">6003-ORDB-008</a>)
- When I click specific price, it **Must** be copied to deal ticket form (<a name="6003-ORDB-009" href="#6003-ORDB-009">6003-ORDB-009</a>)
- Order is removed from orderbook if traded away(<a name="6003-ORDB-010" href="#6003-ORDB-010">6003-ORDB-010</a>)
- Order is removed from orderbook if traded away (<a name="6003-ORDB-010" href="#6003-ORDB-010">6003-ORDB-010</a>)
- Spread (bestAsk - bestOffer) is show in the mid secion (<a name="6003-ORDB-011" href="#6003-ORDB-011">6003-ORDB-011</a>)
- Cumulative volume is displayed visually (volume bars) as a proportion of the entire book volume (<a name="6003-ORDB-012" href="#6003-ORDB-012">6003-ORDB-012</a>)
- Mid section shows the last traded price movement using an arrow (<a name="6003-ORDB-013" href="#6003-ORDB-013">6003-ORDB-013</a>)
+3 -6
View File
@@ -37,14 +37,11 @@ EOT
auth_soft_fail = true
}
docker_service "postgres-1" {
image = "vegaprotocol/timescaledb:2.8.0-pg14"
# Read me for more details: https://github.com/vegaprotocol/docker/tree/main/timescaledb#timescaledb
image = "vegaprotocol/vegacapsule-timescaledb:2.8.0-pg14-v0.0.1"
cmd = "postgres"
args = []
env = {
POSTGRES_USER="vega"
POSTGRES_PASSWORD="vega"
POSTGRES_DBS="vega0,vega1,vega2,vega3,vega4,vega5,vega6,vega7,vega8,vega9,vega10,vega11,vega12,vega13,vega14,vega15,vega16,vega17,vega18,vega19,vega20,vega21,vega22,vega23,vega24,vega25"
}
env = {}
static_port {
value = 5232