Compare commits

..
Author SHA1 Message Date
maciek d9b0a0be02 Merge remote-tracking branch 'origin/develop' into feat/4351-get-started-steps 2023-08-22 18:04:37 +02:00
maciek 9689b5a449 feat: improve get started steps interactions - fixes after feedback 2023-08-22 18:04:27 +02:00
maciek 4f15a0ef06 feat: improve get started steps interactions - fix failing unit test 2023-08-21 14:48:24 +02:00
maciek 72a008a69d feat: improve get started steps interactions - adjust specs 2023-08-21 14:28:52 +02:00
maciek 0c5b5d8e7b Merge remote-tracking branch 'origin/develop' into feat/4351-get-started-steps 2023-08-21 12:46:51 +02:00
maciek 3be1738ecb feat: improve get started steps interactions - fix failing cypress test 2023-08-21 12:46:24 +02:00
maciek 4de59363f4 Merge remote-tracking branch 'origin/develop' into feat/4351-get-started-steps 2023-08-21 10:39:37 +02:00
maciek de6fce0ade feat: improve get started steps interactions - add some more unit tests 2023-08-21 10:39:24 +02:00
maciek 574f73506b feat: improve get started steps interactions - add next unit tests 2023-08-18 17:08:00 +02:00
maciek 759e11cff1 feat: improve get started steps interactions - add a couple of unit tests 2023-08-18 16:03:09 +02:00
maciek 2f08bac47a feat: improve get started steps interactions - add connecting flag to steps logic 2023-08-18 14:32:23 +02:00
maciek 1fdc00fc3b feat: improve get started steps interactions - add connecting flag to steps logic 2023-08-18 13:38:35 +02:00
maciek 4b347c571d Merge remote-tracking branch 'origin/develop' into feat/4351-get-started-steps 2023-08-18 09:09:33 +02:00
maciek 7e5f90cca2 Merge remote-tracking branch 'origin/develop' into feat/4351-get-started-steps 2023-08-17 17:19:57 +02:00
maciek 3090ee88d7 feat: improve get started steps interactions - add checking users records 2023-08-17 17:19:45 +02:00
maciek 35f3d52282 feat: improve get started steps interactions - prevent blinking the banner or dialog 2023-08-16 17:20:36 +02:00
maciek e5b74a141c feat: improve get started steps interactions 2023-08-16 17:03:11 +02:00
339 changed files with 7551 additions and 10406 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Related issues 🔗
Issue: #[Issue number here]
Closes #[Issue number here]
# Description
+1 -1
View File
@@ -125,7 +125,7 @@ jobs:
#----------------------------------------------
- name: Run tests
working-directory: ./console-test
run: poetry run pytest -v -s --numprocesses auto --dist loadfile --durations=20
run: poetry run pytest -s --numprocesses auto
- name: Check files
run: |
ls -al .
-1
View File
@@ -15,7 +15,6 @@ on:
- types
- utils
- i18n
- wallet
jobs:
publish:
+3
View File
@@ -3,3 +3,6 @@
# Lint commit messages to ensure they follow conventional commit standards
yarn commitlint --edit "${1}"
# Lint all staged files
yarn lint-staged
-5
View File
@@ -1,5 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# Lint all staged files
yarn lint-staged
-8
View File
@@ -1,8 +0,0 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# Lint all staged files
yarn nx format:check
# Test all projects with changes
yarn nx affected -t test --exclude trading
@@ -1,26 +0,0 @@
{
"proposalSubmission": {
"rationale": {
"title": "Test new asset proposal",
"description": "E2E test for proposals"
},
"terms": {
"newAsset": {
"changes": {
"name": "USDT Coin",
"symbol": "USDT",
"decimals": "18",
"quantum": "1",
"erc20": {
"contractAddress": "0xb404c51bbc10dcbe948077f18a4b8e553d160084",
"withdrawThreshold": "10",
"lifetimeLimit": "10"
}
}
},
"closingTimestamp": 1724339572,
"enactmentTimestamp": 1724339572,
"validationTimestamp": 1692799617
}
}
}
@@ -1,68 +0,0 @@
context('Oracle page', { tags: '@smoke' }, () => {
describe('Verify elements on page', () => {
before('create market and navigate to oracle page', () => {
cy.createMarket();
cy.visit('/oracles');
});
it('should see oracle data', () => {
cy.getByTestId('oracle-details').should('have.length.at.least', 2);
cy.getByTestId('oracle-details')
.should('exist')
.eq(0)
.within(() => {
cy.get('tr')
.eq(0)
.within(() => {
cy.get('th').should('have.text', 'ID');
cy.get('a').invoke('text').should('have.length', 64);
cy.get('a')
.should('have.attr', 'href')
.and('contain', '/oracles/');
});
cy.get('tr')
.eq(1)
.within(() => {
cy.get('th').should('have.text', 'Type');
cy.get('td').should('have.text', 'External data');
});
cy.get('tr')
.eq(2)
.within(() => {
cy.get('th').should('have.text', 'Signer');
cy.getByTestId('keytype').should('have.text', 'Vega');
cy.get('a').invoke('text').should('have.length', 64);
cy.get('a')
.should('have.attr', 'href')
.and('contain', '/parties/');
});
cy.get('tr')
.eq(3)
.within(() => {
cy.get('th').should('have.text', 'Settlement for');
cy.get('a').invoke('text').should('have.length', 64);
cy.get('a')
.should('have.attr', 'href')
.and('contain', '/markets/');
});
cy.get('tr')
.eq(4)
.within(() => {
cy.get('th').should('have.text', 'Matched data');
cy.get('td').should('have.text', '❌');
});
cy.get('details')
.eq(0)
.within(() => {
cy.contains('Filter').click();
cy.get('.language-json').should('exist');
});
cy.get('details')
.eq(1)
.within(() => {
cy.contains('JSON').click();
cy.get('.language-json').should('exist');
});
});
});
});
});
@@ -1,10 +1,9 @@
import { getNewAssetTxBody } from '../support/governance.functions';
context('Proposal page', { tags: '@smoke' }, function () {
describe('Verify elements on page', function () {
const proposalHeading = 'proposals-heading';
const dateTimeRegex =
/(\d{1,2})\/(\d{1,2})\/(\d{4}), (\d{1,2}):(\d{1,2}):(\d{1,2})/gm;
const proposalTitle = 'Add Lorem Ipsum market';
before('Create market proposal', function () {
cy.visit('/');
@@ -12,8 +11,6 @@ context('Proposal page', { tags: '@smoke' }, function () {
});
it('Able to view proposal', function () {
const proposalTitle = 'Add Lorem Ipsum market';
cy.navigate_to('governanceProposals');
cy.getByTestId(proposalHeading).should('be.visible');
cy.contains(proposalTitle)
@@ -25,9 +22,6 @@ context('Proposal page', { tags: '@smoke' }, function () {
cy.get_element_by_col_id('type').should('have.text', 'NewMarket');
cy.get_element_by_col_id('state').should('have.text', 'Enacted');
cy.getByTestId('vote-progress').should('be.visible');
cy.getByTestId('vote-progress-bar-for')
.invoke('attr', 'style')
.should('eq', 'width: 100%;');
cy.get('[col-id="cDate"]')
.invoke('text')
.should('match', dateTimeRegex);
@@ -41,12 +35,9 @@ context('Proposal page', { tags: '@smoke' }, function () {
});
cy.getByTestId('dialog-title').should('have.text', proposalTitle);
cy.get('.language-json').should('exist');
cy.getByTestId('icon-cross').click();
});
it.skip('Proposal page displayed on mobile', function () {
const proposalTitle = 'Add Lorem Ipsum market';
cy.common_switch_to_mobile_and_click_toggle();
cy.navigate_to('governanceProposals', true);
cy.getByTestId(proposalHeading).should('be.visible');
@@ -54,40 +45,5 @@ context('Proposal page', { tags: '@smoke' }, function () {
cy.get_element_by_col_id('title').should('have.text', proposalTitle);
});
});
it('Able to view new asset proposal', function () {
const proposalTitle = 'Test new asset proposal';
const newAssetProposalBody = getNewAssetTxBody();
cy.VegaWalletSubmitProposal(newAssetProposalBody);
cy.visit('/');
cy.navigate_to('governanceProposals');
cy.contains(proposalTitle)
.parent()
.parent()
.parent()
.within(() => {
cy.get_element_by_col_id('title').should('have.text', proposalTitle);
cy.get_element_by_col_id('type').should('have.text', 'NewAsset');
cy.get_element_by_col_id('state').should(
'have.text',
'Waiting for Node Vote'
);
cy.getByTestId('vote-progress').should('be.visible');
cy.getByTestId('vote-progress-bar-against')
.invoke('attr', 'style')
.should('eq', 'width: 100%;');
cy.get('[col-id="cDate"]')
.invoke('text')
.should('match', dateTimeRegex);
cy.get('[col-id="eDate"]')
.invoke('text')
.should('match', dateTimeRegex);
cy.getByTestId('external-link')
.should('have.attr', 'href')
.and('contains', 'https://governance.fairground.wtf/proposals/');
cy.contains('View terms').should('exist').click();
});
});
});
});
@@ -1,18 +1,8 @@
import { addSeconds, millisecondsToSeconds } from 'date-fns';
export function createSuccessorMarketProposal(parentMarketId) {
cy.VegaWalletSubmitProposal(getSuccessorTxBody(parentMarketId));
}
function getSuccessorTxBody(parentMarketId) {
const MIN_CLOSE_SEC = 500;
const MIN_ENACT_SEC = 700;
const closingDate = addSeconds(new Date(), MIN_CLOSE_SEC);
const enactmentDate = addSeconds(closingDate, MIN_ENACT_SEC);
const closingTimestamp = millisecondsToSeconds(closingDate.getTime());
const enactmentTimestamp = millisecondsToSeconds(enactmentDate.getTime());
return {
proposalSubmission: {
rationale: {
@@ -132,49 +122,8 @@ function getSuccessorTxBody(parentMarketId) {
},
},
},
closingTimestamp,
enactmentTimestamp,
},
},
};
}
export function getNewAssetTxBody() {
const MIN_CLOSE_SEC = 500;
const MIN_ENACT_SEC = 700;
const MIN_VALID_SEC = 60;
const closingDate = addSeconds(new Date(), MIN_CLOSE_SEC);
const enactmentDate = addSeconds(closingDate, MIN_ENACT_SEC);
const validationDate = addSeconds(new Date(), MIN_VALID_SEC);
const closingTimestamp = millisecondsToSeconds(closingDate.getTime());
const enactmentTimestamp = millisecondsToSeconds(enactmentDate.getTime());
const validationTimestamp = millisecondsToSeconds(validationDate.getTime());
return {
proposalSubmission: {
rationale: {
title: 'Test new asset proposal',
description: 'E2E test for proposals',
},
terms: {
newAsset: {
changes: {
name: 'USDT Coin',
symbol: 'USDT',
decimals: '18',
quantum: '1',
erc20: {
contractAddress: '0xb404c51bbc10dcbe948077f18a4b8e553d160084',
withdrawThreshold: '10',
lifetimeLimit: '10',
},
},
},
closingTimestamp,
enactmentTimestamp,
validationTimestamp,
closingTimestamp: 1695666618,
enactmentTimestamp: 1695666618,
},
},
};
+1 -1
View File
@@ -77,7 +77,7 @@
"executor": "nx:run-commands",
"options": {
"commands": [
"npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/main/specs/v0.72.3/blockexplorer.openapi.json --output apps/explorer/src/types/explorer.d.ts --immutable-types"
"npx openapi-typescript https://raw.githubusercontent.com/vegaprotocol/documentation/spec-update-v0.72.0-preview.2/specs/v0.72.0-preview.2/blockexplorer.openapi.json --output apps/explorer/src/types/explorer.d.ts --immutable-types"
]
}
},
@@ -1,25 +0,0 @@
fragment ExplorerStopOrderFields on StopOrder {
id
status
createdAt
trigger {
... on StopOrderPrice {
price
}
... on StopOrderTrailingPercentOffset {
trailingPercentOffset
}
}
createdAt
ocoLinkId
triggerDirection
order {
id
}
}
query ExplorerStopOrder($stopOrderId: ID!) {
stopOrder(id: $stopOrderId) {
...ExplorerStopOrderFields
}
}
@@ -1,70 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerStopOrderFieldsFragment = { __typename?: 'StopOrder', id: string, status: Types.StopOrderStatus, createdAt: any, ocoLinkId?: string | null, triggerDirection: Types.StopOrderTriggerDirection, trigger: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string }, order?: { __typename?: 'Order', id: string } | null };
export type ExplorerStopOrderQueryVariables = Types.Exact<{
stopOrderId: Types.Scalars['ID'];
}>;
export type ExplorerStopOrderQuery = { __typename?: 'Query', stopOrder?: { __typename?: 'StopOrder', id: string, status: Types.StopOrderStatus, createdAt: any, ocoLinkId?: string | null, triggerDirection: Types.StopOrderTriggerDirection, trigger: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string }, order?: { __typename?: 'Order', id: string } | null } | null };
export const ExplorerStopOrderFieldsFragmentDoc = gql`
fragment ExplorerStopOrderFields on StopOrder {
id
status
createdAt
trigger {
... on StopOrderPrice {
price
}
... on StopOrderTrailingPercentOffset {
trailingPercentOffset
}
}
createdAt
ocoLinkId
triggerDirection
order {
id
}
}
`;
export const ExplorerStopOrderDocument = gql`
query ExplorerStopOrder($stopOrderId: ID!) {
stopOrder(id: $stopOrderId) {
...ExplorerStopOrderFields
}
}
${ExplorerStopOrderFieldsFragmentDoc}`;
/**
* __useExplorerStopOrderQuery__
*
* To run a query within a React component, call `useExplorerStopOrderQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerStopOrderQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useExplorerStopOrderQuery({
* variables: {
* stopOrderId: // value for 'stopOrderId'
* },
* });
*/
export function useExplorerStopOrderQuery(baseOptions: Apollo.QueryHookOptions<ExplorerStopOrderQuery, ExplorerStopOrderQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerStopOrderQuery, ExplorerStopOrderQueryVariables>(ExplorerStopOrderDocument, options);
}
export function useExplorerStopOrderLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerStopOrderQuery, ExplorerStopOrderQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerStopOrderQuery, ExplorerStopOrderQueryVariables>(ExplorerStopOrderDocument, options);
}
export type ExplorerStopOrderQueryHookResult = ReturnType<typeof useExplorerStopOrderQuery>;
export type ExplorerStopOrderLazyQueryHookResult = ReturnType<typeof useExplorerStopOrderLazyQuery>;
export type ExplorerStopOrderQueryResult = Apollo.QueryResult<ExplorerStopOrderQuery, ExplorerStopOrderQueryVariables>;
@@ -33,7 +33,7 @@ describe('Order TX Summary component', () => {
expect(res.queryByTestId('order-summary')).not.toBeInTheDocument();
});
it('Renders "Market Price" if the order passed lacks a price', () => {
it('Renders nothing if the order passed lacks a price', () => {
const o: Order = {
marketId: '123',
side: 'SIDE_BUY',
@@ -41,7 +41,7 @@ describe('Order TX Summary component', () => {
size: '10',
};
const res = renderComponent(o);
expect(res.queryByText('Market Price')).toBeInTheDocument();
expect(res.queryByTestId('order-summary')).not.toBeInTheDocument();
});
it('Renders nothing if the order has an unspecified side', () => {
@@ -3,7 +3,6 @@ import type { components } from '../../../types/explorer';
import PriceInMarket from '../price-in-market/price-in-market';
import { sideText } from '../order-details/lib/order-labels';
import SizeInMarket from '../size-in-market/size-in-market';
import { t } from '@vegaprotocol/i18n';
export type OrderSummaryProps = {
order: components['schemas']['v1OrderSubmission'];
@@ -21,6 +20,7 @@ const OrderTxSummary = ({ order }: OrderSummaryProps) => {
if (
!order ||
!order.marketId ||
!order.price ||
!order.side ||
order.side === 'SIDE_UNSPECIFIED'
) {
@@ -36,14 +36,10 @@ const OrderTxSummary = ({ order }: OrderSummaryProps) => {
'-'
)}
&nbsp;<i className="text-xs">@</i>&nbsp;
{order.price ? (
<PriceInMarket
marketId={order.marketId}
price={order.price}
></PriceInMarket>
) : (
t('Market Price')
)}
<PriceInMarket
marketId={order.marketId}
price={order.price}
></PriceInMarket>
</div>
);
};
@@ -1,139 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import type { components } from '../../../../../types/explorer';
import { formatNumberPercentage } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
import PriceInMarket from '../../../price-in-market/price-in-market';
import StopOrderTriggerSummary from './stop-order-trigger';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import fromUnixTime from 'date-fns/fromUnixTime';
const wrapperClasses =
'flex-1 max-w-xs items-center border border-vega-light-200 dark:border-vega-dark-150 rounded-md pv-2 ph-5 mb-5';
export type StopOrderType = 'RisesAbove' | 'FallsBelow' | 'OCO';
type V1OrderSetup = components['schemas']['v1StopOrderSetup'];
interface StopOrderSetupProps extends V1OrderSetup {
type: StopOrderType;
deterministicId: string;
}
export function getExpiryTypeLabel(
expiryStrategy: V1OrderSetup['expiryStrategy']
): string {
switch (expiryStrategy) {
case 'EXPIRY_STRATEGY_CANCELS':
return t('Cancels');
case 'EXPIRY_STRATEGY_SUBMIT':
return t('Submit');
}
return expiryStrategy || t('Unknown');
}
export interface ExpiryTriggerProps {
trailingPercentOffset?: string;
price?: string;
marketId?: string;
}
export function ExpiryTrigger({
trailingPercentOffset,
price,
marketId,
}: ExpiryTriggerProps) {
if (price && marketId) {
return <PriceInMarket price={price} marketId={marketId} />;
}
if (trailingPercentOffset) {
return (
<span>
{formatNumberPercentage(new BigNumber(trailingPercentOffset))}{' '}
(trailing)
</span>
);
}
return null;
}
export function getMovePrefix(
type: StopOrderType,
trailingPercentOffset?: string
): string {
if (type === 'RisesAbove') {
if (trailingPercentOffset) {
return '+';
} else {
return '>';
}
} else {
if (trailingPercentOffset) {
return '-';
} else {
return '<';
}
}
}
export const TypeLabel = {
RisesAbove: t('Rises above ↗'),
FallsBelow: t('Falls below ↘'),
OCO: '',
};
/**
*/
export const StopOrderSetup = ({
type,
price,
orderSubmission,
expiresAt,
expiryStrategy,
trailingPercentOffset,
deterministicId,
}: StopOrderSetupProps) => {
let d = 'Unknown';
try {
d = expiresAt
? fromUnixTime(parseInt(expiresAt) / 1000000000).toLocaleString()
: t('Unknown');
} catch (e) {
d = t('Unknown');
}
return (
<div className={wrapperClasses}>
<div className="mb-12 lg:mb-0">
<div className="bg-slate-100 text-slate-900 px-6 py-2 md:px-6 flex">
<div className="flex-1">
<strong className="font-bold mb-1">{TypeLabel[type]} </strong>
<p className=" font-xs mb-0">
{getMovePrefix(type, trailingPercentOffset)}
<ExpiryTrigger
trailingPercentOffset={trailingPercentOffset}
price={price}
marketId={orderSubmission?.marketId}
/>
</p>
</div>
{expiresAt && expiryStrategy ? (
<div className="flex-1">
<strong className="font-bold mb-1">{t('Expiry Type')}</strong>
<p className=" font-xs mb-0">
<Tooltip description={<span>{d}</span>}>
<span>{getExpiryTypeLabel(expiryStrategy)}</span>
</Tooltip>
</p>
</div>
) : null}
</div>
<StopOrderTriggerSummary
id={deterministicId}
orderSubmission={orderSubmission}
/>
</div>
</div>
);
};
@@ -1,108 +0,0 @@
import { StopOrderStatus } from '@vegaprotocol/types';
import { useExplorerStopOrderQuery } from '../../../order-details/__generated__/StopOrder';
import type { ExplorerStopOrderQuery } from '../../../order-details/__generated__/StopOrder';
import { t } from '@vegaprotocol/i18n';
import { Icon } from '@vegaprotocol/ui-toolkit';
import type { IconName } from '@vegaprotocol/ui-toolkit';
import OrderTxSummary from '../../../order-summary/order-tx-summary';
import type { components } from '../../../../../types/explorer';
import type { ApolloError } from '@apollo/client';
export const StatusLabel: Record<StopOrderStatus, string> = {
[StopOrderStatus.STATUS_CANCELLED]: t('Cancelled'),
[StopOrderStatus.STATUS_EXPIRED]: t('Expired'),
[StopOrderStatus.STATUS_PENDING]: t('Pending'),
[StopOrderStatus.STATUS_REJECTED]: t('Rejected'),
[StopOrderStatus.STATUS_STOPPED]: t('Stopped'),
[StopOrderStatus.STATUS_TRIGGERED]: t('Triggered'),
[StopOrderStatus.STATUS_UNSPECIFIED]: t('Status unknown'),
};
export const StatusIcon: Record<StopOrderStatus, IconName> = {
[StopOrderStatus.STATUS_CANCELLED]: 'disable',
[StopOrderStatus.STATUS_EXPIRED]: 'outdated',
[StopOrderStatus.STATUS_PENDING]: 'circle',
[StopOrderStatus.STATUS_REJECTED]: 'cross',
[StopOrderStatus.STATUS_STOPPED]: 'stop',
[StopOrderStatus.STATUS_TRIGGERED]: 'tick',
[StopOrderStatus.STATUS_UNSPECIFIED]: 'help',
};
export const StatusMidColor: Record<StopOrderStatus, string> = {
[StopOrderStatus.STATUS_CANCELLED]: 'bg-red-100 text-red-900',
[StopOrderStatus.STATUS_EXPIRED]: 'bg-red-100 text-red-900',
[StopOrderStatus.STATUS_PENDING]: 'bg-yellow-100 text-yellow-900',
[StopOrderStatus.STATUS_REJECTED]: 'bg-red-100 text-red-900',
[StopOrderStatus.STATUS_STOPPED]: 'bg-red-100 text-red-900',
[StopOrderStatus.STATUS_TRIGGERED]: 'bg-green-100 text-green-900',
[StopOrderStatus.STATUS_UNSPECIFIED]: 'bg-yellow-100 text-yellow-900',
};
export const StatusBottomColor: Record<StopOrderStatus, string> = {
[StopOrderStatus.STATUS_CANCELLED]: 'bg-red-50 text-red-900 line-through',
[StopOrderStatus.STATUS_EXPIRED]: 'bg-red-50 text-red-900 line-through',
[StopOrderStatus.STATUS_PENDING]: 'bg-yellow-50 text-yellow-900',
[StopOrderStatus.STATUS_REJECTED]: 'bg-red-50 text-red-900 line-through',
[StopOrderStatus.STATUS_STOPPED]: 'bg-red-50 text-red-900 line-through',
[StopOrderStatus.STATUS_TRIGGERED]: 'bg-green-50 text-green-900',
[StopOrderStatus.STATUS_UNSPECIFIED]:
'bg-yellow-50 text-yellow-900 line-through',
};
export function getStopOrderTriggerStatus(
data?: ExplorerStopOrderQuery,
error?: ApolloError
) {
if (data && data.stopOrder) {
return data.stopOrder.status;
}
return StopOrderStatus.STATUS_UNSPECIFIED;
}
export interface StopOrderTriggerSummaryProps {
id: string;
orderSubmission?: components['schemas']['v1OrderSubmission'];
}
/**
*/
const StopOrderTriggerSummary = ({
id,
orderSubmission,
}: StopOrderTriggerSummaryProps) => {
const { data, error } = useExplorerStopOrderQuery({
variables: { stopOrderId: id },
});
const status = getStopOrderTriggerStatus(data, error);
return (
<>
<div
className={`${StatusMidColor[status]} px-3 py-2 md:px-6 flex space-x-4`}
>
<p className="m-0 p-0 align-top">
<Icon
size={6}
name={StatusIcon[status]}
className="inline-block mr-2"
/>
<span className="align-top">{StatusLabel[status]}</span>
</p>
</div>
<div
className={`${StatusBottomColor[status]} px-3 py-2 md:px-6 flex space-x-4`}
>
{orderSubmission && (
<p className="text-vega-grey-400 strike">
<OrderTxSummary order={orderSubmission} />
</p>
)}
</div>
</>
);
};
export default StopOrderTriggerSummary;
@@ -1,12 +1,10 @@
import { t } from '@vegaprotocol/i18n';
import type { components } from '../../../../../types/explorer';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import SizeInMarket from '../../../size-in-market/size-in-market';
export interface TxDetailsOrderIcebergDetailsProps {
iceberg: components['schemas']['v1IcebergOpts'];
size: components['schemas']['v1OrderSubmission']['size'];
marketId?: string;
}
/**
@@ -30,7 +28,6 @@ export interface TxDetailsOrderIcebergDetailsProps {
export const TxOrderIcebergDetails = ({
iceberg,
size,
marketId,
}: TxDetailsOrderIcebergDetailsProps) => {
return (
<div
@@ -39,28 +36,15 @@ export const TxOrderIcebergDetails = ({
>
<Tooltip description={t('Iceberg: Minimum visible size')}>
<span className="align-bottom text-vega-orange-650">
{marketId ? (
<SizeInMarket
size={iceberg.minimumVisibleSize}
marketId={marketId}
/>
) : (
iceberg.minimumVisibleSize
)}
{iceberg.minimumVisibleSize || '-'}
</span>
</Tooltip>
<Tooltip description={t('Iceberg: Total size')}>
<span className="text-sm text-vega-blue-600 mx-3">
{marketId ? <SizeInMarket size={size} marketId={marketId} /> : size}
</span>
<span className="text-sm text-vega-blue-600 mx-3">{size}</span>
</Tooltip>
<Tooltip description={t('Iceberg: Visible peak')}>
<span className="align-top text-vega-yellow-600">
{marketId ? (
<SizeInMarket size={iceberg.peakSize} marketId={marketId} />
) : (
iceberg.peakSize
)}
{iceberg.peakSize || '-'}
</span>
</Tooltip>
</div>
@@ -26,11 +26,6 @@ export const sharedHeaderProps = {
className: 'align-top',
};
const Labels: Record<BlockExplorerTransactionResult['type'], string> = {
'Stop Orders Submission': 'Stop Order',
'Stop Orders Cancellation': 'Cancel Stop Order',
};
/**
* These rows are shown for every transaction type, providing a consistent set of rows for the top
* of a transaction details row. The order is relatively arbitrary but felt right - it might need to
@@ -49,14 +44,12 @@ export const TxDetailsShared = ({
const time: string = blockData?.result.block.header.time || '';
const height: string = blockData?.result.block.header.height || txData.block;
const type = Labels[txData.type] || txData.type;
return (
<>
{hideTypeRow === false ? (
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
<TableCell>{type}</TableCell>
<TableCell>{txData.type}</TableCell>
</TableRow>
) : null}
<TableRow modifier="bordered">
@@ -31,8 +31,6 @@ const AccountType: Record<AccountTypes, string> = {
ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES: 'LP Received Fees',
ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS: 'Market Proposers',
ACCOUNT_TYPE_HOLDING: 'Holding',
ACCOUNT_TYPE_LIQUIDITY_FEES_BONUS_DISTRIBUTION: 'Bonus Distribution',
ACCOUNT_TYPE_LP_LIQUIDITY_FEES: 'LP Liquidity Fees',
};
interface TransferParticipantsProps {
@@ -27,7 +27,6 @@ import { TxDetailsNodeAnnounce } from './tx-node-announce';
import { TxDetailsStateVariable } from './tx-state-variable-proposal';
import { TxProposal } from './tx-proposal';
import { TxDetailsTransfer } from './tx-transfer';
import { TxDetailsStopOrderSubmission } from './tx-stop-order-submission';
interface TxDetailsWrapperProps {
txData: BlockExplorerTransactionResult | undefined;
@@ -117,8 +116,6 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
return TxDetailsUndelegate;
case 'State Variable Proposal':
return TxDetailsStateVariable;
case 'Stop Orders Submission':
return TxDetailsStopOrderSubmission;
case 'Transfer Funds':
return TxDetailsTransfer;
default:
@@ -41,7 +41,7 @@ export const TxDetailsIssueSignatures = ({
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const cmd: Command = txData.command.issueSignatures;
const cmd: Command = txData.command;
const k = cmd.kind ? kind[cmd.kind] : null;
return (
@@ -81,11 +81,7 @@ export const TxDetailsOrder = ({
<TableRow modifier="bordered">
<TableCell>{t('Iceberg details')}</TableCell>
<TableCell>
<TxOrderIcebergDetails
iceberg={iceberg}
size={size}
marketId={marketId}
/>
<TxOrderIcebergDetails iceberg={iceberg} size={size} />
</TableCell>
</TableRow>
) : null}
@@ -1,148 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import { MarketLink } from '../../links/';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import {
getStopOrderIds,
stopOrdersSignatureToDeterministicId,
} from '../lib/deterministic-ids';
import type { components } from '../../../../types/explorer';
import { StopOrderSetup } from './order/stop-order-setup';
type StopOrderSetup = components['schemas']['v1StopOrderSetup'];
interface TxDetailsOrderProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
export function getStopTypeLabel(
risesAbove: StopOrderSetup | undefined,
fallsBelow: StopOrderSetup | undefined
): string {
if (risesAbove && fallsBelow) {
return t('OCO (One Cancels Other)');
} else if (fallsBelow) {
return t('Falls Below ↘');
} else if (risesAbove) {
return t('Rises Above ↗');
} else {
return t('Stop Order');
}
}
export interface StopMarketIdProps {
risesAbove: StopOrderSetup | undefined;
fallsBelow: StopOrderSetup | undefined;
showMarketName?: boolean;
}
export function StopMarketId({
risesAbove,
fallsBelow,
showMarketName = false,
}: StopMarketIdProps) {
const raMarketId = risesAbove?.orderSubmission?.marketId;
const fbMarketId = fallsBelow?.orderSubmission?.marketId;
if (raMarketId && fbMarketId) {
if (raMarketId === fbMarketId) {
return <MarketLink id={raMarketId} showMarketName={showMarketName} />;
} else {
return (
<>
<MarketLink id={raMarketId} showMarketName={showMarketName} />,
<MarketLink id={fbMarketId} showMarketName={showMarketName} />
</>
);
}
} else if (raMarketId) {
return <MarketLink id={raMarketId} showMarketName={showMarketName} />;
} else if (fbMarketId) {
return <MarketLink id={fbMarketId} showMarketName={showMarketName} />;
} else {
return '-';
}
}
/**
*/
export const TxDetailsStopOrderSubmission = ({
txData,
pubKey,
blockData,
}: TxDetailsOrderProps) => {
if (!txData || !txData.command.stopOrdersSubmission) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const tx: components['schemas']['v1StopOrdersSubmission'] =
txData.command.stopOrdersSubmission;
const orderIds = stopOrdersSignatureToDeterministicId(
txData?.signature?.value
);
const { risesAboveId, fallsBelowId } = getStopOrderIds(
orderIds,
tx.risesAbove,
tx.fallsBelow
);
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
blockData={blockData}
/>
<TableRow modifier="bordered">
<TableCell>{t('Market ID')}</TableCell>
<TableCell>
<StopMarketId
risesAbove={tx.risesAbove}
fallsBelow={tx.fallsBelow}
/>
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Market')}</TableCell>
<TableCell>
<StopMarketId
risesAbove={tx.risesAbove}
fallsBelow={tx.fallsBelow}
showMarketName={true}
/>
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Trigger')}</TableCell>
<TableCell>
{getStopTypeLabel(tx.risesAbove, tx.fallsBelow)}
</TableCell>
</TableRow>
</TableWithTbody>
<div className="flex gap-2">
{tx.fallsBelow && fallsBelowId && (
<StopOrderSetup
type={'FallsBelow'}
{...tx.fallsBelow}
deterministicId={fallsBelowId}
/>
)}
{tx.risesAbove && risesAboveId && (
<StopOrderSetup
type={'RisesAbove'}
{...tx.risesAbove}
deterministicId={risesAboveId}
/>
)}
</div>
</>
);
};
@@ -1,8 +1,4 @@
import {
hexToString,
txSignatureToDeterministicId,
stopOrdersSignatureToDeterministicId,
} from './deterministic-ids';
import { hexToString, txSignatureToDeterministicId } from './deterministic-ids';
it('txSignatureToDeterministicId Turns a known signature in to a known deterministic ID', () => {
const signature =
@@ -24,24 +20,3 @@ it('hexToString encodes a known good value as bytes', () => {
const res = hexToString(hex);
expect(res).toEqual([14, 221]);
});
describe('stopOrdersSignatureToDeterministicId', () => {
it('should return empty object if no signature is provided', () => {
const result = stopOrdersSignatureToDeterministicId();
expect(result.length).toEqual(0);
});
it('should return valid deterministic ids if a signature is provided', () => {
const signature = 'deadb33f';
const result = stopOrdersSignatureToDeterministicId(signature);
expect(result.length).toEqual(2);
expect(result[0]).toBe(
'4c45b67a8c08cbf1982883a75beaf309bf172461d04bd427623d6cd3d9ab0e91'
);
expect(result[1]).toBe(
'afe7509ff90d8f26339a0ab81e4d3e1fb6c4d44e94419aa5e13ae7659d894da1'
);
});
});
@@ -1,6 +1,4 @@
import type { components } from '../../../../types/explorer';
import { sha3_256 } from 'js-sha3';
type StopOrderSetup = components['schemas']['v1StopOrderSetup'];
/**
* Encodes a string as bytes
@@ -39,58 +37,3 @@ export function txSignatureToDeterministicId(signature: string): string {
return hash.hex();
}
/**
* Given a stop order signature string, returns the deterministic IDs of both potential
* Stop Orders. A stop order is not an order per se, but a trigger for an order. The order
* created by the stop order will have another ID based on the market event that hit the
* trigger, and as such is not deterministic.
*
* @param signature
* @returns string[]
*/
export function stopOrdersSignatureToDeterministicId(
signature?: string
): string[] {
if (!signature) {
return [];
}
const firstId = txSignatureToDeterministicId(signature);
return [firstId, txSignatureToDeterministicId(firstId)];
}
export type stopSignatures = {
risesAboveId: string | undefined;
fallsBelowId: string | undefined;
};
/**
* In 0.72.10 the way stop order IDs are determined is a little tricky. It will be stabilised
* in a future release.
* @param deterministicIds Output of stopORdersSignatureToDeterministicId
* @param risesAbove Stop order setup
* @param fallsBelow Stop order setup
* @returns Object containing the deterministic IDs of the stop orders
*/
export function getStopOrderIds(
deterministicIds: string[],
risesAbove: StopOrderSetup | undefined,
fallsBelow: StopOrderSetup | undefined
) {
if (risesAbove && fallsBelow) {
return {
risesAboveId: deterministicIds[0],
fallsBelowId: deterministicIds[1],
};
} else if (!fallsBelow && risesAbove) {
return {
risesAboveId: deterministicIds[0],
};
} else {
return {
fallsBelowId: deterministicIds[0] || undefined,
risesAboveId: deterministicIds[1] || undefined,
};
}
}
@@ -34,7 +34,6 @@ export type FilterOption =
| 'Protocol Upgrade'
| 'Register new Node'
| 'State Variable Proposal'
| 'Stop Orders Submission'
| 'Stop Orders Cancellation'
| 'Submit Oracle Data'
| 'Submit Order'
@@ -55,7 +54,6 @@ export const PrimaryFilterOptions: FilterOption[] = [
'Delegate',
'Liquidity Provision Order',
'Proposal',
'Stop Orders Submission',
'Stop Orders Cancellation',
'Submit Oracle Data',
'Submit Order',
@@ -50,25 +50,6 @@ const displayString: StringMap = {
'Stop Orders Cancellation': 'Cancel stop',
};
export function getLabelForStopOrderType(
orderType: string,
command: components['schemas']['v1InputData']
): string {
if (command.stopOrdersSubmission) {
if (
command.stopOrdersSubmission.risesAbove &&
command.stopOrdersSubmission.fallsBelow
) {
return 'Stop ⇅';
} else if (command.stopOrdersSubmission.risesAbove) {
return 'Stop ↗';
} else if (command.stopOrdersSubmission.fallsBelow) {
return 'Stop ↘';
}
}
return 'Stop';
}
export function getLabelForOrderType(
orderType: string,
command: components['schemas']['v1InputData']
@@ -204,9 +185,6 @@ export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
} else if (type === 'Order' && command) {
type = getLabelForOrderType(orderType, command);
colours = 'text-white dark-text-white bg-vega-blue dark:bg-vega-blue';
} else if (type === 'Stop' && command) {
type = getLabelForStopOrderType(orderType, command);
colours = 'text-white dark-text-white bg-vega-blue dark:bg-vega-blue';
}
if (type === 'Vote on Proposal') {
@@ -55,7 +55,7 @@ export const TxsInfiniteList = ({
}
return (
<div>
<div className="overflow-scroll">
<table className={className} data-testid="transactions-list">
<thead>
<tr className="w-full mb-3 text-vega-dark-300 uppercase text-left">
@@ -1,2 +1 @@
export * from './network-parameters';
export * from './structure-network-params';
@@ -1,105 +1,82 @@
import { render, screen } from '@testing-library/react';
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
import { NetworkParametersTable } from './network-parameters';
import { MemoryRouter } from 'react-router-dom';
const renderComponent = (data: NetworkParamsQuery | undefined) => {
return render(
<MemoryRouter>
<NetworkParametersTable data={data} loading={false} />
</MemoryRouter>
);
};
const mockData = {
networkParametersConnection: {
edges: [
{
node: {
key: 'spam.protection.delegation.min.tokens',
value: '3',
},
},
{
node: {
key: 'spam.protection.voting.min.tokens',
value: '1',
},
},
{
node: {
key: 'reward.staking.delegation.minimumValidatorStake',
value: '2',
},
},
{
node: {
key: 'reward.asset',
value:
'fc7fd956078fb1fc9db5c19b88f0874c4299b2a7639ad05a47a28c0aef291b55',
},
},
],
},
};
describe('NetworkParametersTable', () => {
it('renders headers correctly', () => {
renderComponent(mockData);
const allHeadings = screen.getAllByRole('heading');
expect(
allHeadings.map((h) => {
return {
text: h.textContent,
level: h.tagName,
testId: h.getAttribute('data-testid'),
};
})
).toEqual([
{
text: 'Network Parameters',
level: 'H1',
testId: 'network-param-header',
it('renders correctly when it has network params', () => {
const data: NetworkParamsQuery = {
networkParametersConnection: {
edges: [
{
node: {
__typename: 'NetworkParameter',
key: 'market.liquidityProvision.minLpStakeQuantumMultiple',
value: '1',
},
},
{
node: {
__typename: 'NetworkParameter',
key: 'market.fee.factors.infrastructureFee',
value: '0.0005',
},
},
],
},
{ text: 'Spam', level: 'H1', testId: 'spam' },
{ text: 'Protection', level: 'H2', testId: 'spam-protection' },
{ text: 'Delegation', level: 'H3', testId: 'spam-protection-delegation' },
{ text: 'Min', level: 'H4', testId: 'spam-protection-delegation-min' },
{ text: 'Voting', level: 'H3', testId: 'spam-protection-voting' },
{ text: 'Min', level: 'H4', testId: 'spam-protection-voting-min' },
{ text: 'Reward', level: 'H1', testId: 'reward' },
{ text: 'Staking', level: 'H2', testId: 'reward-staking' },
{ text: 'Delegation', level: 'H3', testId: 'reward-staking-delegation' },
]);
};
render(<NetworkParametersTable data={data} loading={false} />);
expect(screen.getByTestId('network-param-header')).toHaveTextContent(
'Network Parameters'
);
const rows = screen.getAllByTestId('key-value-table-row');
expect(rows[0].children[0]).toHaveTextContent(
'market.fee.factors.infrastructureFee'
);
expect(rows[1].children[0]).toHaveTextContent(
'market.liquidityProvision.minLpStakeQuantumMultiple'
);
expect(rows[0].children[1]).toHaveTextContent('0.0005');
expect(rows[1].children[1]).toHaveTextContent('1');
});
it('renders network params correctly', () => {
renderComponent(mockData);
const delegationMinTokensRow = screen.getByTestId(
'spam-protection-delegation-min-tokens'
it('renders the rows in ascending order', () => {
const data: NetworkParamsQuery = {
networkParametersConnection: {
edges: [
{
node: {
__typename: 'NetworkParameter',
key: 'market.fee.factors.infrastructureFee',
value: '0.0005',
},
},
{
node: {
__typename: 'NetworkParameter',
key: 'market.liquidityProvision.minLpStakeQuantumMultiple',
value: '1',
},
},
],
},
};
render(<NetworkParametersTable data={data} loading={false} />);
expect(screen.getByTestId('network-param-header')).toHaveTextContent(
'Network Parameters'
);
expect(delegationMinTokensRow).toHaveTextContent('0.000000000000000003');
const votingMinTokensRow = screen.getByTestId(
'spam-protection-voting-min-tokens'
const rows = screen.getAllByTestId('key-value-table-row');
expect(rows[0].children[0]).toHaveTextContent(
'market.fee.factors.infrastructureFee'
);
expect(votingMinTokensRow).toHaveTextContent('0.000000000000000001');
const minimumValidatorStakeRow = screen.getByTestId(
'reward-staking-delegation-minimumValidatorStake'
);
expect(minimumValidatorStakeRow).toHaveTextContent('2');
const assetRow = screen.getByTestId('reward-asset');
expect(assetRow).toHaveTextContent(
'fc7fd956078fb1fc9db5c19b88f0874c4299b2a7639ad05a47a28c0aef291b55'
expect(rows[1].children[0]).toHaveTextContent(
'market.liquidityProvision.minLpStakeQuantumMultiple'
);
expect(rows[0].children[1]).toHaveTextContent('0.0005');
expect(rows[1].children[1]).toHaveTextContent('1');
});
it('does not render rows when is loading', () => {
renderComponent(undefined);
render(<NetworkParametersTable data={undefined} loading={true} />);
expect(screen.getByTestId('network-param-header')).toHaveTextContent(
'Network Parameters'
);
@@ -1,8 +1,6 @@
import startCase from 'lodash/startCase';
import classNames from 'classnames';
import { Link } from 'react-router-dom';
import {
AsyncRenderer,
KeyValueTable,
KeyValueTableRow,
SyntaxHighlighter,
} from '@vegaprotocol/ui-toolkit';
@@ -14,12 +12,11 @@ import {
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import { RouteTitle } from '../../components/route-title';
import orderBy from 'lodash/orderBy';
import { useNetworkParamsQuery } from '@vegaprotocol/network-parameters';
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
import { useScrollToLocation } from '../../hooks/scroll-to-location';
import { useDocumentTitle } from '../../hooks/use-document-title';
import { structureNetworkParams } from './structure-network-params';
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
import type { GroupedParams } from './structure-network-params';
const PERCENTAGE_PARAMS = [
'governance.proposal.asset.requiredMajority',
@@ -61,54 +58,6 @@ const BIG_NUMBER_PARAMS = [
'governance.proposal.updateAsset.minVoterBalance',
];
export const renderGroupedParams = (
group: GroupedParams,
level: number,
parentKeys: string[] = []
) => {
const Header = `h${level}` as keyof JSX.IntrinsicElements;
const headerStyles = classNames('uppercase font-semibold', {
'pt-6 text-3xl underline': level === 1,
'pt-3 text-2xl': level === 2,
'pt-2 text-lg': level === 3,
'pt-2 text-default': level === 4,
});
return Object.entries(group).map(([key, value]) => {
const fullPath = [...parentKeys, key].join('.');
const isLeafNode = typeof value !== 'object';
const id = parentKeys.concat([key]).join('-');
return (
<div key={key}>
{!isLeafNode && (
<div id={id}>
<Link to={`#${id}`}>
<Header className={headerStyles} data-testid={id}>
{startCase(key)}
</Header>
</Link>
</div>
)}
{isLeafNode ? (
typeof value === 'string' ? (
<div data-testid={id}>
<NetworkParameterRow
key={fullPath}
row={{ key: fullPath, value: value }}
/>
</div>
) : null
) : (
<div className="pb-1">
{renderGroupedParams(value, level + 1, [...parentKeys, key])}
</div>
)}
</div>
);
});
};
export const NetworkParameterRow = ({
row: { key, value },
}: {
@@ -128,9 +77,7 @@ export const NetworkParameterRow = ({
>
{key}
{isSyntaxRow ? (
<div className="pb-2">
<SyntaxHighlighter data={JSON.parse(value)} />
</div>
<SyntaxHighlighter data={JSON.parse(value)} />
) : isNaN(Number(value)) ? (
value
) : BIG_NUMBER_PARAMS.includes(key) ? (
@@ -166,12 +113,17 @@ export const NetworkParametersTable = ({
loading={loading}
error={error}
render={(data) => {
const flatParams = removePaginationWrapper(
data.networkParametersConnection.edges
const ascParams = orderBy(
removePaginationWrapper(data.networkParametersConnection.edges),
(param) => param.key,
'asc'
);
const groupedParams = structureNetworkParams(flatParams);
return (
<div className="-mt-6">{renderGroupedParams(groupedParams, 1)}</div>
<KeyValueTable data-testid="parameters">
{(ascParams || []).map((row) => (
<NetworkParameterRow key={row.key} row={row} />
))}
</KeyValueTable>
);
}}
/>
@@ -1,109 +0,0 @@
import type { GroupedParams } from './structure-network-params';
import {
structureParams,
sortGroupedParams,
structureNetworkParams,
} from './structure-network-params';
describe('structureParams', () => {
it('should correctly structure params', () => {
const input = [
{ key: 'spam.protection.delegation.min.tokens', value: '10' },
{ key: 'spam.protection.voting.min.tokens', value: '5' },
];
const output: GroupedParams = {
spam: {
protection: {
delegation: {
min: {
tokens: '10',
},
},
voting: {
min: {
tokens: '5',
},
},
},
},
};
expect(structureParams(input)).toEqual(output);
});
it('should handle top-level keys correctly', () => {
const input = [{ key: 'levelOne', value: '10' }];
const output = {
levelOne: '10',
};
expect(structureParams(input)).toEqual(output);
});
});
describe('sortGroupedParams', () => {
it('should correctly sort grouped params', () => {
const input: GroupedParams = {
spam: {
protection: {
delegation: {
min: {
tokens: '10',
},
},
},
},
reward: '50',
};
const output: GroupedParams = {
reward: '50',
spam: {
protection: {
delegation: {
min: {
tokens: '10',
},
},
},
},
};
expect(sortGroupedParams(input)).toEqual(output);
});
it('should handle already sorted keys', () => {
const input = {
a: '10',
b: {
c: '5',
d: '6',
},
};
expect(sortGroupedParams(input)).toEqual(input);
});
});
describe('structureNetworkParams', () => {
it('should structure and sort network params correctly', () => {
const input = [
{ key: 'spam.protection.delegation.min.tokens', value: '10' },
{ key: 'reward.asset', value: '50' },
];
const output: GroupedParams = {
reward: {
asset: '50',
},
spam: {
protection: {
delegation: {
min: {
tokens: '10',
},
},
},
},
};
expect(structureNetworkParams(input)).toEqual(output);
});
it('should return an empty object if no params are provided', () => {
expect(structureNetworkParams([])).toEqual({});
});
});
@@ -1,107 +0,0 @@
/**
* Categorizes and sorts an array of key-value pairs of network params into a nested object structure.
*
* The function takes an array of network params where keys are dot-delimited
* strings representing nested categories (e.g., 'spam.protection.delegation.min.tokens').
*
* Why this is necessary:
* A flat key-value structure wouldn't provide the hierarchical information needed.
* Organizing network parameters like this allows the rendering of nested headers
* and their corresponding network params.
*
* It also ensures that items with the minimum amount of nesting are ordered first. This
* allows us to render these items first, before more deeply nested items are rendered with
* subheaders. This creates a more intuitive UI.
*
* For example, given the input:
* [
* { key: 'spam.protection.delegation.min.tokens', value: '10' },
* { key: 'spam.protection.voting.min.tokens', value: '5' },
* { key: 'reward.staking.delegation.minimumValidatorStake', value: '2' }
* { key: 'reward.asset', value: 'fc7fd956078fb1fc9db5c19b88f0874c4299b2a7639ad05a47a28c0aef291b55' }
* ]
*
* The output will be:
* {
* spam: {
* protection: {
* delegation: {
* min: {
* tokens: '10'
* }
* },
* voting: {
* min: {
* tokens: '5'
* }
* }
* }
* },
* reward: {
* asset: 'fc7fd956078fb1fc9db5c19b88f0874c4299b2a7639ad05a47a28c0aef291b55',
* staking: {
* delegation: {
* minimumValidatorStake: '2'
* }
* }
* }
* }
*
* @param {Array} params - An array of key-value pairs to categorize and sort.
* @returns {GroupedParams} - A nested object that groups the key-value pairs.
*/
export type GroupedParams = {
[key: string]: string | GroupedParams;
};
export const structureParams = (
params: { key: string; value: string }[]
): GroupedParams => {
const grouped: GroupedParams = {};
params.forEach(({ key, value }) => {
const parts = key.split('.');
let node: GroupedParams = grouped;
parts.forEach((part, i) => {
if (typeof node[part] === 'undefined') {
node[part] = i === parts.length - 1 ? value : {};
}
if (typeof node[part] === 'object') {
node = node[part] as GroupedParams;
}
});
});
return grouped;
};
export const sortGroupedParams = (
groupedParams: GroupedParams
): GroupedParams => {
const sorted: GroupedParams = {};
// Sort top-level keys first
Object.entries(groupedParams).forEach(([key, value]) => {
if (typeof value === 'string') {
sorted[key] = value;
}
});
Object.entries(groupedParams).forEach(([key, value]) => {
if (typeof value === 'object') {
sorted[key] = sortGroupedParams(value);
}
});
return sorted;
};
export const structureNetworkParams = (
params: { key: string; value: string }[]
) => {
const grouped = structureParams(params);
return sortGroupedParams(grouped);
};
@@ -1,34 +1,4 @@
fragment ExplorerOracleDataConnection on OracleSpec {
dataConnection {
edges {
node {
externalData {
data {
signers {
signer {
... on ETHAddress {
address
}
... on PubKey {
key
}
}
}
data {
name
value
}
matchedSpecIds
broadcastAt
}
}
}
}
}
}
fragment ExplorerOracleDataSource on OracleSpec {
...ExplorerOracleDataConnection
dataSourceSpec {
spec {
id
@@ -64,7 +34,6 @@ fragment ExplorerOracleDataSource on OracleSpec {
key {
name
type
numberDecimalPlaces
}
conditions {
value
@@ -80,6 +49,38 @@ fragment ExplorerOracleDataSource on OracleSpec {
}
}
fragment ExplorerOracleDataConnection on OracleSpec {
dataConnection {
pageInfo {
hasNextPage
}
edges {
node {
externalData {
data {
signers {
signer {
... on ETHAddress {
address
}
... on PubKey {
key
}
}
}
data {
name
value
}
matchedSpecIds
broadcastAt
}
}
}
}
}
}
query ExplorerOracleSpecs {
oracleSpecsConnection(pagination: { first: 50 }) {
pageInfo {
@@ -96,5 +97,6 @@ query ExplorerOracleSpecs {
query ExplorerOracleSpecById($id: ID!) {
oracleSpec(oracleSpecId: $id) {
...ExplorerOracleDataSource
...ExplorerOracleDataConnection
}
}
+41 -38
View File
@@ -3,55 +3,24 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerOracleDataConnectionFragment = { __typename?: 'OracleSpec', dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array<string> | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } };
export type ExplorerOracleDataSourceFragment = { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } } } } } };
export type ExplorerOracleDataSourceFragment = { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } } } } }, dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array<string> | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } };
export type ExplorerOracleDataConnectionFragment = { __typename?: 'OracleSpec', dataConnection: { __typename?: 'OracleDataConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean }, edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array<string> | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } };
export type ExplorerOracleSpecsQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type ExplorerOracleSpecsQuery = { __typename?: 'Query', oracleSpecsConnection?: { __typename?: 'OracleSpecsConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean }, edges?: Array<{ __typename?: 'OracleSpecEdge', node: { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } } } } }, dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array<string> | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } } } | null> | null } | null };
export type ExplorerOracleSpecsQuery = { __typename?: 'Query', oracleSpecsConnection?: { __typename?: 'OracleSpecsConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean }, edges?: Array<{ __typename?: 'OracleSpecEdge', node: { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } } } } } } } | null> | null } | null };
export type ExplorerOracleSpecByIdQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
export type ExplorerOracleSpecByIdQuery = { __typename?: 'Query', oracleSpec?: { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType, numberDecimalPlaces?: number | null }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } } } } }, dataConnection: { __typename?: 'OracleDataConnection', edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array<string> | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } } | null };
export type ExplorerOracleSpecByIdQuery = { __typename?: 'Query', oracleSpec?: { __typename?: 'OracleSpec', dataSourceSpec: { __typename?: 'ExternalDataSourceSpec', spec: { __typename?: 'DataSourceSpec', id: string, createdAt: any, updatedAt?: any | null, status: Types.DataSourceSpecStatus, data: { __typename?: 'DataSourceDefinition', sourceType: { __typename?: 'DataSourceDefinitionExternal', sourceType: { __typename?: 'DataSourceSpecConfiguration', signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, filters?: Array<{ __typename?: 'Filter', key: { __typename?: 'PropertyKey', name?: string | null, type: Types.PropertyKeyType }, conditions?: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator }> | null }> | null } } | { __typename?: 'DataSourceDefinitionInternal', sourceType: { __typename?: 'DataSourceSpecConfigurationTime', conditions: Array<{ __typename?: 'Condition', value?: string | null, operator: Types.ConditionOperator } | null> } } } } }, dataConnection: { __typename?: 'OracleDataConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean }, edges?: Array<{ __typename?: 'OracleDataEdge', node: { __typename?: 'OracleData', externalData: { __typename?: 'ExternalData', data: { __typename?: 'Data', matchedSpecIds?: Array<string> | null, broadcastAt: any, signers?: Array<{ __typename?: 'Signer', signer: { __typename?: 'ETHAddress', address?: string | null } | { __typename?: 'PubKey', key?: string | null } }> | null, data?: Array<{ __typename?: 'Property', name: string, value: string }> | null } } } } | null> | null } } | null };
export const ExplorerOracleDataConnectionFragmentDoc = gql`
fragment ExplorerOracleDataConnection on OracleSpec {
dataConnection {
edges {
node {
externalData {
data {
signers {
signer {
... on ETHAddress {
address
}
... on PubKey {
key
}
}
}
data {
name
value
}
matchedSpecIds
broadcastAt
}
}
}
}
}
}
`;
export const ExplorerOracleDataSourceFragmentDoc = gql`
fragment ExplorerOracleDataSource on OracleSpec {
...ExplorerOracleDataConnection
dataSourceSpec {
spec {
id
@@ -87,7 +56,6 @@ export const ExplorerOracleDataSourceFragmentDoc = gql`
key {
name
type
numberDecimalPlaces
}
conditions {
value
@@ -102,7 +70,40 @@ export const ExplorerOracleDataSourceFragmentDoc = gql`
}
}
}
${ExplorerOracleDataConnectionFragmentDoc}`;
`;
export const ExplorerOracleDataConnectionFragmentDoc = gql`
fragment ExplorerOracleDataConnection on OracleSpec {
dataConnection {
pageInfo {
hasNextPage
}
edges {
node {
externalData {
data {
signers {
signer {
... on ETHAddress {
address
}
... on PubKey {
key
}
}
}
data {
name
value
}
matchedSpecIds
broadcastAt
}
}
}
}
}
}
`;
export const ExplorerOracleSpecsDocument = gql`
query ExplorerOracleSpecs {
oracleSpecsConnection(pagination: {first: 50}) {
@@ -148,9 +149,11 @@ export const ExplorerOracleSpecByIdDocument = gql`
query ExplorerOracleSpecById($id: ID!) {
oracleSpec(oracleSpecId: $id) {
...ExplorerOracleDataSource
...ExplorerOracleDataConnection
}
}
${ExplorerOracleDataSourceFragmentDoc}`;
${ExplorerOracleDataSourceFragmentDoc}
${ExplorerOracleDataConnectionFragmentDoc}`;
/**
* __useExplorerOracleSpecByIdQuery__
@@ -1,23 +1,23 @@
import { render } from '@testing-library/react';
import { OracleData } from './oracle-data';
import { type ExplorerOracleDataConnectionFragment } from '../__generated__/Oracles';
import type { ExplorerOracleDataConnectionFragment } from '../__generated__/Oracles';
type DataConnection = ExplorerOracleDataConnectionFragment['dataConnection'];
function renderComponent(
data: ExplorerOracleDataConnectionFragment['dataConnection']
) {
function renderComponent(data: ExplorerOracleDataConnectionFragment) {
return <OracleData data={data} />;
}
describe('Oracle Data view', () => {
it('Renders nothing when data is null', () => {
const res = render(renderComponent(null as unknown as DataConnection));
const res = render(
renderComponent(null as unknown as ExplorerOracleDataConnectionFragment)
);
expect(res.container).toBeEmptyDOMElement();
});
it('Renders nothing when dataConnection is empty', () => {
const res = render(renderComponent({} as DataConnection));
const res = render(
renderComponent({} as ExplorerOracleDataConnectionFragment)
);
expect(res.container).toBeEmptyDOMElement();
});
it('Renders nothing when dataConnection has no edges', () => {
@@ -26,7 +26,7 @@ describe('Oracle Data view', () => {
dataConnection: {
edges: null,
},
} as DataConnection)
} as ExplorerOracleDataConnectionFragment)
);
expect(res.container).toBeEmptyDOMElement();
});
@@ -37,7 +37,7 @@ describe('Oracle Data view', () => {
dataConnection: {
edges: [],
},
} as unknown as DataConnection)
} as unknown as ExplorerOracleDataConnectionFragment)
);
expect(res.container).toBeEmptyDOMElement();
});
@@ -47,18 +47,20 @@ describe('Oracle Data view', () => {
it('Renders details component when there is data', () => {
const res = render(
renderComponent({
edges: [
{
node: {
externalData: {
data: {
broadcastAt: '2022-01-01',
dataConnection: {
edges: [
{
node: {
externalData: {
data: {
broadcastAt: '2022-01-01',
},
},
},
},
},
],
} as DataConnection)
],
},
} as ExplorerOracleDataConnectionFragment)
);
expect(res.getByText('Broadcast data')).toBeInTheDocument();
});
@@ -4,7 +4,7 @@ import filter from 'recursive-key-filter';
import type { ExplorerOracleDataConnectionFragment } from '../__generated__/Oracles';
interface OracleDataTypeProps {
data: ExplorerOracleDataConnectionFragment['dataConnection'];
data: ExplorerOracleDataConnectionFragment;
}
/**
@@ -14,7 +14,12 @@ interface OracleDataTypeProps {
* that Does The Job, rather than because it's good.
*/
export function OracleData({ data }: OracleDataTypeProps) {
if (!data || !data.edges?.length || data.edges.length > 1) {
if (
!data ||
!data.dataConnection ||
!data.dataConnection.edges?.length ||
data.dataConnection.edges.length > 1
) {
return null;
}
@@ -22,7 +27,7 @@ export function OracleData({ data }: OracleDataTypeProps) {
<details data-testid="oracle-data">
<summary>{t('Broadcast data')}</summary>
<ul>
{data.edges.map((d) => {
{data.dataConnection.edges.map((d) => {
if (!d) {
return null;
}
@@ -27,8 +27,8 @@ export function OracleMarkets({ id }: OracleMarketsProps) {
const m = markets.find((m) => {
const p = m.tradableInstrument.instrument.product;
if (
p?.dataSourceSpecForSettlementData?.id === id ||
p?.dataSourceSpecForTradingTermination?.id === id
p.dataSourceSpecForSettlementData.id === id ||
p.dataSourceSpecForTradingTermination.id === id
) {
return true;
}
@@ -36,9 +36,15 @@ export function OracleMarkets({ id }: OracleMarketsProps) {
});
if (m && m.id) {
const type =
id ===
m.tradableInstrument.instrument.product.dataSourceSpecForSettlementData
.id
? 'Settlement for'
: 'Termination for';
return (
<TableRow modifier="bordered">
<TableHeader scope="row">{getLabel(id, m)}</TableHeader>
<TableHeader scope="row">{type}</TableHeader>
<TableCell modifier="bordered" data-testid={`m-${m.id}`}>
<MarketLink id={m.id} />
</TableCell>
@@ -55,14 +61,3 @@ export function OracleMarkets({ id }: OracleMarketsProps) {
</TableRow>
);
}
export function getLabel(
id: string,
m: ExplorerOracleForMarketsMarketFragment | null
): string {
const settlementId =
m?.tradableInstrument?.instrument?.product?.dataSourceSpecForSettlementData
?.id || null;
return id === settlementId ? 'Settlement for' : 'Termination for';
}
@@ -22,7 +22,7 @@ export type SourceType =
interface OracleDetailsProps {
id: string;
dataSource: ExplorerOracleDataSourceFragment;
dataConnection: ExplorerOracleDataConnectionFragment['dataConnection'];
dataConnection?: ExplorerOracleDataConnectionFragment;
// Defaults to false. Hides the count of 'broadcasts' this oracle has seen
showBroadcasts?: boolean;
}
@@ -41,7 +41,8 @@ export const OracleDetails = ({
showBroadcasts = false,
}: OracleDetailsProps) => {
const sourceType = dataSource.dataSourceSpec.spec.data.sourceType;
const reportsCount: number = dataConnection.edges?.length || 0;
const reportsCount: number =
dataConnection?.dataConnection.edges?.length || 0;
return (
<div>
@@ -34,20 +34,11 @@ const Oracles = () => {
if (!id) {
return null;
}
const dataConnection = o?.node.dataConnection;
return (
<div
id={id}
key={id}
className="mb-10"
data-testid="oracle-details"
>
<div id={id} key={id} className="mb-10">
<OracleDetails
id={id}
dataSource={o?.node}
dataConnection={dataConnection}
showBroadcasts={false}
/>
<details>
@@ -39,7 +39,7 @@ export const Oracle = () => {
<OracleDetails
id={id || ''}
dataSource={data?.oracleSpec}
dataConnection={data?.oracleSpec.dataConnection}
dataConnection={data?.oracleSpec}
showBroadcasts={true}
/>
<details>
+1 -5
View File
@@ -920,8 +920,6 @@ export interface components {
* - ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES: Per asset reward account for fees received by liquidity providers
* - ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS: Per asset reward account for market proposers when the market goes above some trading threshold
* - ACCOUNT_TYPE_HOLDING: Per asset account for holding in-flight unfilled orders' funds
* - ACCOUNT_TYPE_LP_LIQUIDITY_FEES: Network controlled liquidity provider's account, per market, to hold accrued liquidity fees.
* - ACCOUNT_TYPE_LIQUIDITY_FEES_BONUS_DISTRIBUTION: Network controlled liquidity fees bonus distribution account, per market.
* @default ACCOUNT_TYPE_UNSPECIFIED
* @enum {string}
*/
@@ -943,9 +941,7 @@ export interface components {
| 'ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES'
| 'ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES'
| 'ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS'
| 'ACCOUNT_TYPE_HOLDING'
| 'ACCOUNT_TYPE_LP_LIQUIDITY_FEES'
| 'ACCOUNT_TYPE_LIQUIDITY_FEES_BONUS_DISTRIBUTION';
| 'ACCOUNT_TYPE_HOLDING';
/** Vega representation of an external asset */
readonly vegaAssetDetails: {
/** @description Vega built-in asset. */
-4
View File
@@ -20,10 +20,6 @@ NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_VEGA_REST_URL=http://localhost:3008/api/v2/
NX_SUCCESSOR_MARKETS=true
NX_VEGA_EXPLORER_URL=https://explorer.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
#Test configuration variables
CYPRESS_FAIRGROUND=false
CYPRESS_VEGA_URL=http://localhost:3008/graphql
@@ -11,9 +11,6 @@ import {
getDateFormatForSpecifiedDays,
getProposalFromTitle,
getProposalInformationFromTable,
goToMakeNewProposal,
governanceProposalType,
longProposalDescription,
proposalChangeType,
submitUniqueRawProposal,
validateProposalDetailsDiff,
@@ -46,6 +43,7 @@ const proposalDetailsTitle = 'proposal-title';
const proposalDetailsDescription = 'proposal-description';
const openProposals = 'open-proposals';
const viewProposalButton = 'view-proposal-btn';
const proposalDescriptionToggle = 'proposal-description-toggle';
const voteBreakdownToggle = 'vote-breakdown-toggle';
const proposalTermsToggle = 'proposal-json-toggle';
const marketDataToggle = 'proposal-market-data-toggle';
@@ -73,13 +71,10 @@ describe(
// 3001-VOTE-050 3001-VOTE-054 3001-VOTE-055 3002-PROP-019
it('Newly created raw proposal details - shows proposal title and full description', function () {
const proposalDetails = longProposalDescription;
const proposalDescription =
'I propose that everyone evaluate the following IPFS document and vote Yes if they agree. bafybeigwwctpv37xdcwacqxvekr6e4kaemqsrv34em6glkbiceo3fcy4si';
goToMakeNewProposal(governanceProposalType.RAW);
submitUniqueRawProposal({
proposalTitle: 'raw proposal with long description',
proposalDescription: proposalDetails,
});
createRawProposal();
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
cy.getByTestId(openProposals).within(() => {
getProposalFromTitle(rawProposal.rationale.title).within(() => {
@@ -90,17 +85,12 @@ describe(
'contain.text',
rawProposal.rationale.title
);
cy.getByTestId(proposalDescriptionToggle).click();
cy.getByTestId('proposal-description-toggle');
cy.getByTestId(proposalDetailsDescription)
.find('p')
.should('have.text', proposalDescription);
});
cy.getByTestId(proposalDetailsDescription).within(() => {
cy.get('p').should('not.have.text', 'Hyperlink text');
cy.getByTestId('show-more-btn').click();
cy.get('p')
.invoke('text')
.should('have.have.length', 2194) // Full description is displayed
.and('contain', 'Hyperlink text');
cy.get('a').should('have.attr', 'href');
});
// 3001-VOTE-008
getProposalInformationFromTable('ID')
.invoke('text')
@@ -371,6 +361,34 @@ describe(
stakingPageDisassociateAllTokens();
});
it('Error message should be displayed if error returned from wallet when voting', function () {
const errorMsg =
'Application error: party has already submitted the maximum number of transactions of this type per epoch (3)';
createRawProposal();
cy.get<testFreeformProposal>('@rawProposal').then((rawProposal) => {
getProposalFromTitle(rawProposal.rationale.title).within(() =>
cy.getByTestId(viewProposalButton).click()
);
cy.intercept('POST', '/api/v2/requests', {
jsonrpc: '2.0',
error: {
code: 2001,
message: 'Application error',
data: 'party has already submitted the maximum number of transactions of this type per epoch (3)',
},
id: '-PK5EGmErnjLhAmzMeclC',
});
cy.contains('Vote breakdown').should('be.visible', { timeout: 10000 });
cy.getByTestId('vote-buttons').contains('for').click();
cy.getByTestId('dialog-title').should(
'have.text',
'Transaction failed'
);
cy.getByTestId('Error').should('have.text', errorMsg);
});
});
it('Able to see successor market details with new and updated values', function () {
cy.createMarket();
cy.reload();
@@ -407,8 +425,7 @@ describe(
// 3003-PMAN-011 3003-PMAN-012
cy.getByTestId(marketDataToggle).click();
cy.getByTestId('proposal-market-data').within(() => {
// Assert that all toggles are removed
cy.getByTestId('accordion-toggle').should('not.exist');
cy.contains('Key details').click();
validateProposalDetailsDiff(
'Name',
proposalChangeType.UPDATED,
@@ -432,6 +449,7 @@ describe(
'Opening auction'
);
cy.contains('Instrument').click();
validateProposalDetailsDiff(
'Market Name',
proposalChangeType.UPDATED,
@@ -439,6 +457,7 @@ describe(
'Test market 1'
);
cy.contains('Metadata').click();
validateProposalDetailsDiff(
'Sector',
proposalChangeType.UPDATED,
@@ -274,6 +274,3 @@ export enum proposalChangeType {
UPDATED = 'Updated',
ADDED = 'Added',
}
export const longProposalDescription =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam bibendum orci augue, vel imperdiet augue ultrices sed. In hac habitasse platea dictumst. Sed eget elit vitae nisl tincidunt faucibus. Donec pellentesque mauris nec viverra blandit. Aenean eros diam, tempor eu luctus nec, rhoncus non massa. Mauris libero diam, mattis et enim ut, lobortis pharetra elit. Phasellus vel metus accumsan, rhoncus tellus finibus, blandit mi. In sollicitudin ex ac tortor ornare interdum. Sed est ipsum, vestibulum eget dolor vel, porta luctus elit. Fusce justo nibh, placerat eget sollicitudin eleifend, rhoncus id lorem. Fusce vitae magna vel urna faucibus accumsan quis id purus.\nPraesent convallis dolor sed ante ultricies tempor. Proin sed risus ut libero euismod semper. Duis quis quam sed lacus viverra blandit vel scelerisque diam. Donec interdum, ipsum eget imperdiet ornare, risus augue faucibus lectus, ullamcorper scelerisque erat sapien in purus. Nunc molestie tincidunt felis dignissim vestibulum. Quisque quis ornare enim, non dignissim lectus. Mauris mollis, massa ut maximus consectetur, sem mi lobortis quam, vel malesuada eros tortor nec ex. Cras ac nunc sed erat malesuada varius a quis nulla. Curabitur cursus nec sem sit amet aliquet. Ut tristique tortor neque, a dignissim lectus dictum vel. Praesent sollicitudin bibendum vulputate.\nAenean bibendum tristique diam laoreet posuere. Curabitur ornare lectus ut diam ultricies, ut sodales eros lacinia. Maecenas mauris turpis, gravida non arcu ac, interdum auctor sapien. Vestibulum sed tortor quam. Interdum et malesuada fames ac ante ipsum primis in faucibus. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec finibus pulvinar magna, non laoreet lectus molestie nec. Vestibulum tempus mattis vehicula. Praesent in orci lectus. In commodo sollicitudin lacus, et lobortis eros placerat vitae. Proin mi libero, feugiat id pretium posuere, rhoncus ut augue. Cras massa tortor, rutrum sed ex vitae, posuere pretium augue. Donec pellentesque suscipit dignissim. Vivamus convallis a odio vitae sodales. Nullam non eleifend mauris, sed iaculis lectus. Cras facilisis justo at ante.\n[Hyperlink text](https://dweb.link/ipfs/bafybeigwwctpv37xdcwacqxvekr6e4kaemqsrv34em6glkbiceo3fcy4si)';
-3
View File
@@ -22,8 +22,6 @@ NX_VEGA_REST_URL=https://api.n00.stagnet1.vega.xyz/api/v2/
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/firefox/addon/vega-wallet-fairground
#Test configuration variables
CYPRESS_FAIRGROUND=false
@@ -31,4 +29,3 @@ LC_ALL="en_US.UTF-8"
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
+1 -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/firefox/addon/vega-wallet-fairground
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
@@ -30,4 +30,3 @@ CYPRESS_FAIRGROUND=false
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_METAMASK_SNAPS=false
+1 -2
View File
@@ -15,11 +15,10 @@ 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/firefox/addon/vega-wallet-fairground
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
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
+1 -2
View File
@@ -15,11 +15,10 @@ 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/firefox/addon/vega-wallet-mainnet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_METAMASK_SNAPS=false
+1 -2
View File
@@ -14,11 +14,10 @@ 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/firefox/addon/vega-wallet-mainnet
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_TENDERMINT_URL=https://be.mainnet-mirror.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_METAMASK_SNAPS=false
+1 -2
View File
@@ -11,11 +11,10 @@ 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/firefox/addon/vega-wallet-fairground
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
+1 -2
View File
@@ -16,11 +16,10 @@ 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/firefox/addon/vega-wallet-fairground
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
+1 -2
View File
@@ -13,11 +13,10 @@ 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/firefox/addon/vega-wallet-fairground
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_TENDERMINT_URL=https://tm.be.validators-testnet.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=false
NX_METAMASK_SNAPS=false
+4 -37
View File
@@ -41,7 +41,6 @@ import {
AppFailure,
NodeSwitcherDialog,
useNodeSwitcherStore,
DocsLinks,
} from '@vegaprotocol/environment';
import { ENV } from './config';
import type { InMemoryCacheConfig } from '@apollo/client';
@@ -110,17 +109,8 @@ const Web3Container = ({
store.connectors,
store.initialize,
]);
const {
ETHEREUM_PROVIDER_URL,
ETH_LOCAL_PROVIDER_URL,
ETH_WALLET_MNEMONIC,
VEGA_ENV,
VEGA_URL,
VEGA_EXPLORER_URL,
CHROME_EXTENSION_URL,
MOZILLA_EXTENSION_URL,
VEGA_WALLET_URL,
} = useEnvironment();
const { ETHEREUM_PROVIDER_URL, ETH_LOCAL_PROVIDER_URL, ETH_WALLET_MNEMONIC } =
useEnvironment();
useEffect(() => {
if (chainId) {
return initializeConnectors(
@@ -149,33 +139,10 @@ const Web3Container = ({
return <SplashLoader />;
}
if (
!VEGA_URL ||
!VEGA_WALLET_URL ||
!VEGA_EXPLORER_URL ||
!DocsLinks ||
!CHROME_EXTENSION_URL ||
!MOZILLA_EXTENSION_URL
) {
return null;
}
return (
<Web3Provider connectors={connectors}>
<Web3Connector connectors={connectors} chainId={Number(chainId)}>
<VegaWalletProvider
config={{
network: VEGA_ENV,
vegaUrl: VEGA_URL,
vegaWalletServiceUrl: VEGA_WALLET_URL,
links: {
explorer: VEGA_EXPLORER_URL,
concepts: DocsLinks?.VEGA_WALLET_CONCEPTS_URL,
chromeExtensionUrl: CHROME_EXTENSION_URL,
mozillaExtensionUrl: MOZILLA_EXTENSION_URL,
},
}}
>
<VegaWalletProvider>
<ContractsProvider>
<AppLoader>
<BalanceManager>
@@ -308,7 +275,7 @@ const AppContainer = () => {
<Router>
<ScrollToTop />
<AppStateProvider>
<div className="min-h-full text-white grid">
<div className="grid min-h-full text-white">
<NodeGuard
skeleton={<div>{t('Loading')}</div>}
failure={
@@ -54,7 +54,7 @@ export const WalletCardRow = ({
}) => {
const ref = React.useRef<HTMLDivElement | null>(null);
useAnimateValue(ref, value);
const [integers, decimalsPlaces, separator] = useNumberParts(value, decimals);
const [integers, decimalsPlaces] = useNumberParts(value, decimals);
return (
<div
@@ -75,10 +75,7 @@ export const WalletCardRow = ({
className="font-mono flex-1 text-right"
data-testid="associated-amount"
>
<span>
{integers}
{separator}
</span>
<span>{integers}.</span>
<span>{decimalsPlaces}</span>
</span>
)}
@@ -113,10 +110,7 @@ export const WalletCardAsset = ({
border,
subheading,
}: WalletCardAssetProps) => {
const [integers, decimalsPlaces, separator] = useNumberParts(
balance,
decimals
);
const [integers, decimalsPlaces] = useNumberParts(balance, decimals);
return (
<div className="flex flex-nowrap mt-2 mb-4">
@@ -138,10 +132,7 @@ export const WalletCardAsset = ({
</div>
</div>
<div className="px-2 basis-full font-mono" data-testid="currency-value">
<span>
{integers}
{separator}
</span>
<span>{integers}.</span>
<span className="text-neutral-400">{decimalsPlaces}</span>
</div>
</div>
+2 -14
View File
@@ -201,8 +201,6 @@
"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",
@@ -211,23 +209,13 @@
"against": "Against",
"majorityRequired": "Majority Required",
"participation": "Participation",
"majorityThreshold": "majority threshold",
"participationThreshold": "participation threshold",
"met": "met",
"notMet": "not met",
"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",
+1 -9
View File
@@ -1,25 +1,17 @@
import { FLAGS } from '@vegaprotocol/environment';
import {
JsonRpcConnector,
ViewConnector,
InjectedConnector,
SnapConnector,
DEFAULT_SNAP_ID,
} from '@vegaprotocol/wallet';
const urlParams = new URLSearchParams(window.location.search);
export const jsonRpc = new JsonRpcConnector();
export const injected = new InjectedConnector();
export const jsonRpc = new JsonRpcConnector();
export const view = new ViewConnector(urlParams.get('address'));
export const snap = FLAGS.METAMASK_SNAPS
? new SnapConnector(DEFAULT_SNAP_ID)
: undefined;
export const Connectors = {
injected,
jsonRpc,
view,
snap,
};
+50 -23
View File
@@ -31,6 +31,10 @@ 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;
@@ -43,34 +47,57 @@ 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 (
<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>
<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>
<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} />
))}
</ul>
{proposals.map((proposal) => (
<ProposalsListItem
key={proposal.id}
proposal={proposal}
networkParams={networkParams}
/>
))}
</ul>
<div className="mt-6">
<Link to={`${Routes.PROPOSALS}`}>
<Button size="md">{t('homeProposalsButtonText')}</Button>
</Link>
</div>
</section>
<div className="mt-6">
<Link to={`${Routes.PROPOSALS}`}>
<Button size="md">{t('homeProposalsButtonText')}</Button>
</Link>
</div>
</section>
</AsyncRenderer>
);
};
@@ -1,27 +1,44 @@
import ReactMarkdown from 'react-markdown';
import { RoundedWrapper, ShowMore } from '@vegaprotocol/ui-toolkit';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
export const ProposalDescription = ({
description,
}: {
description: string;
}) => (
<section data-testid="proposal-description">
<RoundedWrapper paddingBottom={true} marginBottomLarge={true}>
<div className="p-2">
<ShowMore>
<ReactMarkdown
className="react-markdown-container"
/* Prevents HTML embedded in the description from rendering */
skipHtml={true}
/* Stops users embedding images which could be used for tracking */
disallowedElements={['img']}
linkTarget="_blank"
>
{description}
</ReactMarkdown>
</ShowMore>
</div>
</RoundedWrapper>
</section>
);
}) => {
const { t } = useTranslation();
const [showDescription, setShowDescription] = useState(false);
return (
<section data-testid="proposal-description">
<CollapsibleToggle
toggleState={showDescription}
setToggleState={setShowDescription}
dataTestId={'proposal-description-toggle'}
>
<SubHeading title={t('proposalDescription')} />
</CollapsibleToggle>
{showDescription && (
<RoundedWrapper paddingBottom={true} marginBottomLarge={true}>
<div className="p-2">
<ReactMarkdown
className="react-markdown-container"
/* Prevents HTML embedded in the description from rendering */
skipHtml={true}
/* Stops users embedding images which could be used for tracking */
disallowedElements={['img']}
linkTarget="_blank"
>
{description}
</ReactMarkdown>
</div>
</RoundedWrapper>
)}
</section>
);
};
@@ -16,6 +16,7 @@ import { ProposalHeader } from './proposal-header';
import {
lastWeek,
nextWeek,
mockNetworkParams,
mockWalletContext,
createUserVoteQueryMock,
} from '../../test-helpers/mocks';
@@ -47,6 +48,7 @@ const renderComponent = (
<ProposalHeader
proposal={proposal}
isListItem={isListItem}
networkParams={mockNetworkParams}
voteState={voteState}
/>
</VegaWalletContext.Provider>
@@ -8,19 +8,22 @@ 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;
}) => {
@@ -143,7 +146,7 @@ export const ProposalHeader = ({
className="flex items-center gap-2"
data-testid={`user-voted-${voteState.toLowerCase()}`}
>
<div data-testid="you-voted-icon">
<div className="text-vega-green" data-testid="you-voted-icon">
<VegaIcon name={VegaIconNames.VOTE} size={24} />
</div>
<div>
@@ -182,7 +185,7 @@ export const ProposalHeader = ({
</div>
)}
<VoteBreakdown proposal={proposal} />
<ProposalVotingStatus proposal={proposal} networkParams={networkParams} />
</>
);
};
@@ -15,6 +15,8 @@ import {
SettlementAssetInfoPanel,
} from '@vegaprotocol/markets';
import {
Accordion,
AccordionItem,
Button,
CopyWithTooltip,
Dialog,
@@ -41,9 +43,6 @@ export const useMarketDataDialogStore = create<MarketDataDialogState>(
})
);
const marketDataHeaderStyles =
'font-alpha calt text-base border-b border-vega-dark-200 mt-2 py-2';
export const ProposalMarketData = ({
marketData,
parentMarketData,
@@ -77,14 +76,6 @@ export const ProposalMarketData = ({
parentTerminationData !== undefined &&
isEqual(terminationData, parentTerminationData);
const showParentPriceMonitoringBounds =
parentMarketData?.priceMonitoringSettings?.parameters?.triggers !==
undefined &&
!isEqual(
marketData.priceMonitoringSettings?.parameters?.triggers,
parentMarketData?.priceMonitoringSettings?.parameters?.triggers
);
const getSigners = (data: DataSourceDefinition) => {
if (data.sourceType.__typename === 'DataSourceDefinitionExternal') {
const signers = data.sourceType.sourceType.signers || [];
@@ -117,141 +108,164 @@ export const ProposalMarketData = ({
</Button>
</div>
<div className="mb-10">
<h2 className={marketDataHeaderStyles}>{t('Key details')}</h2>
<KeyDetailsInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
<h2 className={marketDataHeaderStyles}>{t('Instrument')}</h2>
<InstrumentInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
{isEqual(
getSigners(settlementData),
getSigners(terminationData)
) ? (
<>
<h2 className={marketDataHeaderStyles}>{t('Oracle')}</h2>
<OracleInfoPanel
market={marketData}
type="settlementData"
parentMarket={
isParentSettlementDataEqual ? undefined : parentMarketData
<Accordion>
<AccordionItem
itemId="key-details"
title={t('Key details')}
content={
<KeyDetailsInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
<AccordionItem
itemId="instrument"
title={t('Instrument')}
content={
<InstrumentInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
{isEqual(
getSigners(settlementData),
getSigners(terminationData)
) ? (
<AccordionItem
itemId="oracles"
title={t('Oracle')}
content={
<OracleInfoPanel
market={marketData}
type="settlementData"
parentMarket={
isParentSettlementDataEqual
? undefined
: parentMarketData
}
/>
}
/>
</>
) : (
<>
<h2 className={marketDataHeaderStyles}>
{t('Settlement Oracle')}
</h2>
<OracleInfoPanel
market={marketData}
type="settlementData"
parentMarket={
isParentSettlementDataEqual ? undefined : parentMarketData
}
/>
<h2 className={marketDataHeaderStyles}>
{t('Termination Oracle')}
</h2>
<OracleInfoPanel
market={marketData}
type="termination"
parentMarket={
isParentTerminationDataEqual ? undefined : parentMarketData
}
/>
</>
)}
{/*Note: successor markets will not differ in their settlement*/}
{/*assets, so no need to pass in parent market data for comparison.*/}
<h2 className={marketDataHeaderStyles}>{t('Settlement assets')}</h2>
<SettlementAssetInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
<h2 className={marketDataHeaderStyles}>{t('Metadata')}</h2>
<MetadataInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
<h2 className={marketDataHeaderStyles}>{t('Risk model')}</h2>
<RiskModelInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
<h2 className={marketDataHeaderStyles}>{t('Risk parameters')}</h2>
<RiskParametersInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
<h2 className={marketDataHeaderStyles}>{t('Risk factors')}</h2>
<RiskFactorsInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
{showParentPriceMonitoringBounds &&
(
parentMarketData?.priceMonitoringSettings?.parameters
?.triggers || []
).map((_, triggerIndex) => (
) : (
<>
<h2 className={marketDataHeaderStyles}>
{t(`Parent price monitoring bounds ${triggerIndex + 1}`)}
</h2>
<AccordionItem
itemId="settlement-oracle"
title={t('Settlement Oracle')}
content={
<OracleInfoPanel
market={marketData}
type="settlementData"
parentMarket={
isParentSettlementDataEqual
? undefined
: parentMarketData
}
/>
}
/>
<div className="text-vega-dark-300 line-through">
<AccordionItem
itemId="termination-oracle"
title={t('Termination Oracle')}
content={
<OracleInfoPanel
market={marketData}
type="termination"
parentMarket={
isParentTerminationDataEqual
? undefined
: parentMarketData
}
/>
}
/>
</>
)}
{/*Note: successor markets will not differ in their settlement*/}
{/*assets, so no need to pass in parent market data for comparison.*/}
<AccordionItem
itemId="settlement-asset"
title={t('Settlement asset')}
content={<SettlementAssetInfoPanel market={marketData} />}
/>
<AccordionItem
itemId="metadata"
title={t('Metadata')}
content={
<MetadataInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
<AccordionItem
itemId="risk-model"
title={t('Risk model')}
content={
<RiskModelInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
<AccordionItem
itemId="risk-parameters"
title={t('Risk parameters')}
content={
<RiskParametersInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
<AccordionItem
itemId="risk-factors"
title={t('Risk factors')}
content={
<RiskFactorsInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
{(
marketData.priceMonitoringSettings?.parameters?.triggers || []
).map((_, triggerIndex) => (
<AccordionItem
itemId={`trigger-${triggerIndex}`}
title={t(`Price monitoring bounds ${triggerIndex + 1}`)}
content={
<PriceMonitoringBoundsInfoPanel
market={parentMarketData}
market={marketData}
parentMarket={parentMarketData}
triggerIndex={triggerIndex}
/>
</div>
</>
))}
{(
marketData.priceMonitoringSettings?.parameters?.triggers || []
).map((_, triggerIndex) => (
<>
<h2 className={marketDataHeaderStyles}>
{t(`Price monitoring bounds ${triggerIndex + 1}`)}
</h2>
<PriceMonitoringBoundsInfoPanel
market={marketData}
triggerIndex={triggerIndex}
}
/>
</>
))}
<h2 className={marketDataHeaderStyles}>
{t('Liquidity monitoring parameters')}
</h2>
<LiquidityMonitoringParametersInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
<h2 className={marketDataHeaderStyles}>
{t('Liquidity price range')}
</h2>
<LiquidityPriceRangeInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
))}
<AccordionItem
itemId="liqudity-monitoring-parameters"
title={t('Liquidity monitoring parameters')}
content={
<LiquidityMonitoringParametersInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
<AccordionItem
itemId="liquidity-price-range"
title={t('Liquidity price range')}
content={
<LiquidityPriceRangeInfoPanel
market={marketData}
parentMarket={parentMarketData}
/>
}
/>
</Accordion>
</div>
</>
)}
@@ -0,0 +1 @@
export { ProposalVotesTable } from './proposal-votes-table';
@@ -0,0 +1,119 @@
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();
});
});
@@ -0,0 +1,177 @@
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>
)}
</>
);
};
@@ -0,0 +1 @@
export * from './proposal-voting-status';
@@ -0,0 +1,153 @@
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();
});
});
@@ -0,0 +1,174 @@
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>
);
};
@@ -1,7 +1,6 @@
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
import { VegaWalletProvider } from '@vegaprotocol/wallet';
import type { VegaWalletConfig } from '@vegaprotocol/wallet';
import { render, screen } from '@testing-library/react';
import { generateProposal } from '../../test-helpers/generate-proposals';
import { Proposal } from './proposal';
@@ -34,27 +33,21 @@ 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>,
}));
const vegaWalletConfig: VegaWalletConfig = {
network: 'TESTNET',
vegaUrl: 'https://vega.xyz',
vegaWalletServiceUrl: 'https://wallet.vega.xyz',
links: {
explorer: 'explorer',
concepts: 'concepts',
chromeExtensionUrl: 'chrome',
mozillaExtensionUrl: 'mozilla',
},
};
const renderComponent = (proposal: ProposalQuery['proposal']) => {
render(
<MemoryRouter>
<MockedProvider>
<VegaWalletProvider config={vegaWalletConfig}>
<VegaWalletProvider>
<Proposal
restData={{}}
proposal={proposal as ProposalQuery['proposal']}
@@ -98,6 +91,8 @@ 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,8 +5,9 @@ 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 { UserVote } from '../vote-details';
import { VoteDetails } from '../vote-details';
import { ListAsset } from '../list-asset';
import Routes from '../../../routes';
import { ProposalMarketData } from '../proposal-market-data';
@@ -21,6 +22,14 @@ 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>;
@@ -71,32 +80,39 @@ 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;
}
}
@@ -124,81 +140,91 @@ export const Proposal = ({
<ProposalHeader
proposal={proposal}
isListItem={false}
networkParams={networkParams}
voteState={voteState}
/>
<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 id="details">
<div className="my-10">
<ProposalChangeTable proposal={proposal} />
</div>
)}
{proposal.terms.change.__typename === 'UpdateMarket' && (
<div className="mb-4">
<ProposalMarketChanges
originalProposal={
originalMarketProposalRestData?.data?.proposal?.terms?.newMarket
?.changes || {}
}
latestEnactedProposal={
mostRecentlyEnactedAssociatedMarketProposal?.node?.proposal?.terms
?.updateMarket?.changes || {}
}
updatedProposal={
restData?.data?.proposal?.terms?.updateMarket?.changes || {}
}
{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}
/>
</div>
)}
) : null}
{(proposal.terms.change.__typename === 'NewAsset' ||
proposal.terms.change.__typename === 'UpdateAsset') &&
asset && (
<div className="mb-4">
<ProposalDescription description={proposal.rationale.description} />
</div>
{newMarketData && (
<div className="mb-4">
<ProposalAssetDetails asset={asset} />
<ProposalMarketData
marketData={newMarketData}
parentMarketData={parentMarketData ? parentMarketData : undefined}
/>
</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>
{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>
<div className="mb-6">
<ProposalJson proposal={restData?.data?.proposal} />
<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>
<div className="mb-4">
<ProposalVotesTable proposal={proposal} proposalType={proposalType} />
</div>
</div>
</section>
);
@@ -6,7 +6,11 @@ import { MockedProvider } from '@apollo/client/testing';
import { render, screen } from '@testing-library/react';
import { format } from 'date-fns';
import { ProposalRejectionReason, ProposalState } from '@vegaprotocol/types';
import { generateProposal } from '../../test-helpers/generate-proposals';
import {
generateNoVotes,
generateProposal,
generateYesVotes,
} from '../../test-helpers/generate-proposals';
import { ProposalsListItemDetails } from './proposals-list-item-details';
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
import {
@@ -89,6 +93,84 @@ 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({
@@ -131,6 +213,43 @@ 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,6 +11,7 @@ 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,
@@ -19,10 +20,18 @@ 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;
@@ -69,11 +78,31 @@ export const ProposalsListItemDetails = ({
}
case ProposalState.STATE_OPEN: {
voteDetails = (
<span className={nowToEnactmentInHours < 6 ? 'text-vega-orange' : ''}>
<span className={nowToEnactmentInHours < 6 ? 'text-vega-pink' : ''}>
{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,19 +4,28 @@ 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 }: ProposalsListItemProps) => {
export const ProposalsListItem = ({
proposal,
networkParams,
}: ProposalsListItemProps) => {
const { voteState } = useUserVote(proposal?.id);
if (!proposal || !proposal.id) return null;
if (!proposal || !proposal.id || !networkParams) return null;
return (
<li id={proposal.id} data-testid="proposals-list-item">
<RoundedWrapper paddingBottom={true} heightFull={true}>
<ProposalHeader proposal={proposal} voteState={voteState} />
<ProposalHeader
proposal={proposal}
networkParams={networkParams}
voteState={voteState}
/>
<ProposalsListItemDetails proposal={proposal} />
</RoundedWrapper>
</li>
@@ -8,6 +8,7 @@ import { ProtocolUpgradeProposalsListItem } from '../protocol-upgrade-proposals-
import { ProposalsListFilter } from '../proposals-list-filter';
import Routes from '../../../routes';
import {
AsyncRenderer,
Button,
Toggle,
VegaIcon,
@@ -19,6 +20,10 @@ 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']>;
@@ -70,6 +75,20 @@ 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>(
@@ -134,166 +153,181 @@ export const ProposalsList = ({
p?.party?.id?.toString().includes(filterString);
return (
<div data-testid="proposals-list">
<div className="grid xs:grid-cols-2 items-center">
<Heading
centerContent={false}
marginBottom={false}
title={t('pageTitleProposals')}
/>
<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')}
/>
{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>
{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
);
}
}}
/>
)}
</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>
<section className="-mx-4 p-4 mb-8 bg-vega-dark-100">
<SubHeading title={t('openProposals')} />
{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.length > 0 ||
sortedProtocolUpgradeProposals.open.length > 0 ? (
<ul data-testid="open-proposals">
{filterString.length < 1 &&
sortedProtocolUpgradeProposals.open.map((proposal) => (
<ProtocolUpgradeProposalsListItem
key={proposal.upgradeBlockHeight}
proposal={proposal}
/>
))}
<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}
{sortedProposals.open.filter(filterPredicate).map((proposal) => (
<ProposalsListItem
key={proposal?.id}
proposal={proposal}
networkParams={networkParams}
/>
))}
</ul>
) : (
<p className="mb-0" data-testid="no-open-proposals">
{t('noOpenProposals')}
</p>
)}
</section>
{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
)
}
/>
<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>
</div>
)
}
)
}
<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}
<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>
)}
</ul>
</>
) : (
<p className="mb-0" data-testid="no-closed-proposals">
{t('noClosedProposals')}
</p>
)}
</section>
</div>
)}
<Link className="underline" to={Routes.PROPOSALS_REJECTED}>
{t('seeRejectedProposals')}
</Link>
</div>
{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>
);
};
@@ -1,10 +1,15 @@
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>;
@@ -12,6 +17,19 @@ 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 = (
@@ -21,7 +39,11 @@ 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}
@@ -31,7 +53,11 @@ export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
{proposals.length > 0 ? (
<ul data-testid="rejected-proposals">
{proposals.filter(filterPredicate).map((proposal) => (
<ProposalsListItem key={proposal?.id} proposal={proposal} />
<ProposalsListItem
key={proposal?.id}
proposal={proposal}
networkParams={networkParams}
/>
))}
</ul>
) : (
@@ -40,6 +66,6 @@ export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
</p>
)}
</section>
</>
</AsyncRenderer>
);
};
@@ -1 +0,0 @@
export * from './vote-breakdown';
@@ -1,348 +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,
} 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}%`);
});
});
@@ -1,378 +0,0 @@
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 { UserVote } from './user-vote';
export { VoteDetails } from './vote-details';
@@ -1,78 +0,0 @@
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>
);
};
@@ -4,7 +4,6 @@ import { VoteButtons } from './vote-buttons';
import { VoteState } from './use-user-vote';
import { ProposalState } from '@vegaprotocol/types';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { mockWalletContext } from '../../test-helpers/mocks';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { MockedProvider } from '@apollo/react-testing';
@@ -68,7 +67,7 @@ describe('Vote buttons', () => {
disconnect: jest.fn(),
selectPubKey: jest.fn(),
connector: null,
} as unknown as VegaWalletContextShape;
};
render(
<AppStateProvider>
@@ -190,15 +190,14 @@ export const VoteButtons = ({
(voteState === VoteState.Yes || voteState === VoteState.No) && (
<p data-testid="you-voted">
<span>{t('youVoted')}:</span>{' '}
<span className="mx-1 text-white font-bold uppercase">
<span className="text-white font-bold">
{t(`voteState_${voteState}`)}
</span>{' '}
{voteDatetime ? (
<span>on {format(voteDatetime, DATE_FORMAT_LONG)}. </span>
<span>{format(voteDatetime, DATE_FORMAT_LONG)}. </span>
) : null}
{proposalVotable ? (
<ButtonLink
className="text-white"
data-testid="change-vote-button"
onClick={() => {
setChangeVote(true);
@@ -0,0 +1,255 @@
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>
</>
);
};
@@ -114,7 +114,6 @@ describe('Raw proposal form', () => {
{
pubKey,
sendTx: mockSendTx,
links: { explorer: 'explorer' },
} as unknown as VegaWalletContextShape
}
>
@@ -1,7 +1,7 @@
import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
import type { MockedResponse } from '@apollo/client/testing';
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
import type { PubKey, VegaWalletContextShape } from '@vegaprotocol/wallet';
import type { PubKey } from '@vegaprotocol/wallet';
import type { VoteValue } from '@vegaprotocol/types';
import type { UserVoteQuery } from '../components/vote-details/__generated__/Vote';
import { UserVoteDocument } from '../components/vote-details/__generated__/Vote';
@@ -21,7 +21,7 @@ export const mockWalletContext = {
disconnect: jest.fn(),
selectPubKey: jest.fn(),
connector: null,
} as unknown as VegaWalletContextShape;
};
const mockEthereumConfig = {
network_id: '3',
@@ -0,0 +1,469 @@
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-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');
});
});
@@ -0,0 +1,212 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import type { MarketsQuery } from '@vegaprotocol/markets';
import * as Schema from '@vegaprotocol/types';
const rowSelector =
'[data-testid="tab-open-markets"] .ag-center-cols-container .ag-row';
const colInstrumentCode =
'[col-id="tradableInstrument.instrument.code"] [data-testid="market-code"]';
describe('markets all table', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.clearLocalStorage().then(() => {
cy.setOnBoardingViewed();
cy.mockTradingPage(
Schema.MarketState.STATE_ACTIVE,
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY_TARGET_NOT_MET
);
cy.mockSubscription();
cy.visit('/#/markets/all');
});
});
it('can see table headers', () => {
cy.wait('@Markets');
cy.wait('@MarketsData');
const headers = [
'Market',
'Description',
'Trading mode',
'Status',
'Successor market',
'Best bid',
'Best offer',
'Mark price',
'Settlement asset',
'',
];
cy.getByTestId('tab-open-markets').within(($headers) => {
cy.wrap($headers)
.get('.ag-header-cell-text')
.each(($header, i) => {
cy.wrap($header).should('have.text', headers[i]);
});
});
});
it('markets tab should be rendered properly', () => {
cy.get('[data-testid="Open markets"]').should(
'have.attr',
'data-state',
'active'
);
cy.get('[data-testid="Proposed markets"]').should(
'have.attr',
'data-state',
'inactive'
);
cy.get('[data-testid="Closed markets"]').should(
'have.attr',
'data-state',
'inactive'
);
});
it('renders markets correctly', () => {
// 6001-MARK-035
cy.get(rowSelector)
.first()
.find(colInstrumentCode)
.should('have.text', 'SOLUSD');
// 6001-MARK-036
cy.get(rowSelector)
.first()
.find('[col-id="tradableInstrument.instrument.name"]')
.should('have.text', 'SUSPENDED MARKET');
// 6001-MARK-037
cy.get(rowSelector)
.first()
.find('[col-id="tradingMode"]')
.should('have.text', 'Continuous');
// 6001-MARK-038
cy.get(rowSelector)
.first()
.find('[col-id="state"]')
.should('have.text', 'Active');
// 6001-MARK-039
cy.get(rowSelector)
.first()
.find('[col-id="data.bestBidPrice"]')
.should('have.text', '0.00');
// 6001-MARK-040
cy.get(rowSelector)
.first()
.find('[col-id="data.bestOfferPrice"]')
.should('have.text', '0.00');
// 6001-MARK-041
cy.get(rowSelector)
.first()
.find('[col-id="data.markPrice"]')
.should('have.text', '84.41');
// 6001-MARK-042
cy.get(rowSelector)
.first()
.find(
'[col-id="tradableInstrument.instrument.product.settlementAsset.symbol"]'
)
.should('have.text', 'XYZalpha');
// 6001-MARK-043
cy.get(rowSelector)
.first()
.find(
'[col-id="tradableInstrument.instrument.product.settlementAsset.symbol"] button'
)
.click();
cy.getByTestId('dialog-title').should('have.text', 'Asset details - tEURO');
cy.getByTestId('close-asset-details-dialog').click();
});
it('can open row actions', () => {
// 6001-MARK-044
cy.get('.ag-pinned-right-cols-container')
.find('[col-id="market-actions"]')
.first()
.find('button')
.click();
// 6001-MARK-045
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');
// 6001-MARK-046
cy.get(dropdownContent)
.find(dropdownContentItem)
.eq(1)
.find('a')
.then(($el) => {
const href = $el.attr('href');
expect(/\/markets\/market-1/.test(href || '')).to.equal(true);
})
.should('have.text', 'View on Explorer');
// 6001-MARK-047
cy.get(dropdownContent)
.find(dropdownContentItem)
.eq(2)
.should('have.text', 'View settlement asset details');
cy.getByTestId('market-actions-content').click();
});
it('able to open and sort full market list - market page', () => {
// 6001-MARK-064
const ExpectedSortedMarkets = [
'AAPL.MF21',
'BTCUSD.MF21',
'ETHBTC.QM21',
'SOLUSD',
];
cy.get('[data-testid="Open markets"]').click({ force: true });
cy.url().should('eq', Cypress.config('baseUrl') + '/#/markets/all');
cy.contains('AAPL.MF21').should('be.visible');
cy.get('.ag-header-cell-label').contains('Market').click(); // sort by market name
for (let i = 0; i < ExpectedSortedMarkets.length; i++) {
cy.get(`[row-index=${i}]`)
.find(colInstrumentCode)
.should('have.text', ExpectedSortedMarkets[i]);
}
});
it('can drag and drop columns', () => {
// 6001-MARK-065
cy.get('.ag-overlay-loading-wrapper').should('not.be.visible');
cy.get(colInstrumentCode)
.realMouseDown()
.realMouseMove(700, 15)
.realMouseUp();
cy.get(colInstrumentCode).should(($element) => {
const attributeValue = $element.attr('aria-colindex');
expect(attributeValue).not.to.equal('1');
});
});
});
describe('no open markets', { tags: '@smoke', testIsolation: true }, () => {
before(() => {
cy.mockTradingPage();
const markets: MarketsQuery = {};
cy.mockGQL((req) => {
aliasGQLQuery(req, 'Markets', markets);
});
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/all');
});
it.skip('can see no markets message', () => {
// 6001-MARK-048
cy.getByTestId('tab-open-markets').should('contain.text', 'No markets');
});
});
@@ -133,7 +133,11 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
validateMarketDataRow(4, 'Decimals', '5');
validateMarketDataRow(5, 'Quantum', '1');
validateMarketDataRow(6, 'Status', 'Enabled');
validateMarketDataRow(7, 'Contract address', '0x0158…78a4');
validateMarketDataRow(
7,
'Contract address',
'0x0158031158Bb4dF2AD02eAA31e8963E84EA978a4'
);
validateMarketDataRow(8, 'Withdrawal threshold', '0.0005');
validateMarketDataRow(9, 'Lifetime limit', '1,230');
validateMarketDataRow(10, 'Infrastructure fee account balance', '0.00001');
@@ -0,0 +1,233 @@
import { aliasGQLQuery, checkSorting } from '@vegaprotocol/cypress';
import type { ProposalsListQuery } from '@vegaprotocol/proposals';
const rowSelector =
'[data-testid="tab-proposed-markets"] .ag-center-cols-container .ag-row';
const colMarketId = '[col-id="market"] [data-testid="market-code"]';
describe('markets proposed table', { tags: '@smoke' }, () => {
before(() => {
cy.mockTradingPage();
cy.mockSubscription();
cy.setOnBoardingViewed();
cy.visit('/#/markets/all');
cy.get('[data-testid="Proposed markets"]').click();
});
it('can see table headers', () => {
const headers = [
'Market',
'Description',
'Settlement asset',
'State',
'Parent market',
'Voting',
'Closing date',
'Enactment date',
'',
];
cy.getByTestId('tab-proposed-markets').within(($headers) => {
cy.wrap($headers)
.get('.ag-header-cell-text')
.each(($header, i) => {
cy.wrap($header).should('have.text', headers[i]);
});
});
});
it('renders markets correctly', () => {
// 6001-MARK-049
cy.get(rowSelector).first().find(colMarketId).should('have.text', 'ETHUSD');
// 6001-MARK-050
cy.get(rowSelector)
.first()
.find('[col-id="description"]')
.should('have.text', 'ETHUSD');
// 6001-MARK-051
cy.get(rowSelector)
.first()
.find('[col-id="asset"]')
.should('have.text', 'tDAI TEST');
// 6001-MARK-052
// 6001-MARK-053
cy.get(rowSelector)
.first()
.find('[col-id="state"]')
.should('have.text', 'Open');
// 6001-MARK-054
cy.get(rowSelector)
.first()
.find('[col-id="voting"]')
.should('have.text', '');
// 6001-MARK-056
cy.get(rowSelector)
.first()
.find('[col-id="closing-date"]')
.should('not.be.empty');
// 6001-MARK-057
cy.get(rowSelector)
.first()
.find('[col-id="enactment-date"]')
.should('not.be.empty');
});
it('can open row actions', () => {
// 6001-MARK-058
cy.get('.ag-pinned-right-cols-container')
.find('[col-id="proposal-actions"]')
.first()
.find('button')
.click();
const dropdownContent = '[data-testid="proposal-actions-content"]';
const dropdownContentItem = '[role="menuitem"]';
// 6001-MARK-059
cy.get(dropdownContent)
.find(dropdownContentItem)
.eq(0)
.find('a')
.should('have.text', 'View proposal')
.and(
'have.attr',
'href',
`${Cypress.env(
'VEGA_TOKEN_URL'
)}/proposals/e9ec6d5c46a7e7bcabf9ba7a893fa5a5eeeec08b731f06f7a6eb7bf0e605b829`
);
});
// 6001-MARK-060
it('can see proposed market link', () => {
cy.getByTestId('tab-proposed-markets')
.find('[data-testid="external-link"]')
.should('have.length', 11)
.last()
.should('have.text', 'Propose a new market')
.and(
'have.attr',
'href',
`${Cypress.env('VEGA_TOKEN_URL')}/proposals/propose/new-market`
);
});
it('proposed markets tab should be sorted properly', () => {
// 6001-MARK-062
cy.get('[data-testid="Proposed markets"]').click({ force: true });
const marketColDefault = [
'ETHUSD',
'LINKUSD',
'ETHUSD',
'ETHDAI.MF21',
'AAPL.MF21',
'BTCUSD.MF21',
'TSLA.QM21',
'AAVEDAI.MF21',
'ETHBTC.QM21',
'UNIDAI.MF21',
];
const marketColAsc = [
'AAPL.MF21',
'AAVEDAI.MF21',
'BTCUSD.MF21',
'ETHBTC.QM21',
'ETHDAI.MF21',
'ETHUSD',
'ETHUSD',
'LINKUSD',
'TSLA.QM21',
'UNIDAI.MF21',
];
const marketColDesc = [
'UNIDAI.MF21',
'TSLA.QM21',
'LINKUSD',
'ETHUSD',
'ETHUSD',
'ETHDAI.MF21',
'ETHBTC.QM21',
'BTCUSD.MF21',
'AAVEDAI.MF21',
'AAPL.MF21',
];
checkSorting(
'market',
marketColDefault,
marketColAsc,
marketColDesc,
' [data-testid="market-code"]'
);
const stateColDefault = [
'Open',
'Passed',
'Waiting for Node Vote',
'Open',
'Passed',
'Open',
'Passed',
'Open',
'Waiting for Node Vote',
'Open',
];
const stateColAsc = [
'Open',
'Open',
'Open',
'Open',
'Open',
'Passed',
'Passed',
'Passed',
'Waiting for Node Vote',
'Waiting for Node Vote',
];
const stateColDesc = [
'Waiting for Node Vote',
'Waiting for Node Vote',
'Passed',
'Passed',
'Passed',
'Open',
'Open',
'Open',
'Open',
'Open',
];
checkSorting('state', stateColDefault, stateColAsc, stateColDesc);
});
it('can drag and drop columns', () => {
// 6001-MARK-063
cy.get(colMarketId).realMouseDown().realMouseMove(700, 15).realMouseUp();
cy.get(colMarketId).should(($element) => {
const attributeValue = $element.attr('aria-colindex');
expect(attributeValue).not.to.equal('1');
});
});
});
describe('no markets proposed', { tags: '@smoke', testIsolation: true }, () => {
before(() => {
cy.mockTradingPage();
const proposal: ProposalsListQuery = {};
cy.mockGQL((req) => {
aliasGQLQuery(req, 'ProposalsList', proposal);
});
cy.mockSubscription();
cy.setOnBoardingViewed();
});
it.skip('can see no markets message', () => {
cy.visit('/#/markets/all');
cy.get('[data-testid="Proposed markets"]').click();
// 6001-MARK-061
cy.getByTestId('tab-proposed-markets').should('contain.text', 'No markets');
});
});
@@ -0,0 +1,117 @@
const orderbookTab = 'Orderbook';
const orderbookTable = 'tab-orderbook';
const askPrice = 'price-9894185';
const bidPrice = 'price-9889001';
const askVolume = 'ask-vol-9894185';
const bidVolume = 'bid-vol-9889001';
const askCumulative = 'cumulative-vol-9894185';
const bidCumulative = 'cumulative-vol-9889001';
const midPrice = 'middle-mark-price-4612690000';
const priceResolution = 'resolution';
const dealTicketPrice = 'order-price';
const dealTicketSize = 'order-size';
const resPrice = 'price-990';
describe('order book', { tags: '@smoke' }, () => {
before(() => {
cy.setOnBoardingViewed();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
beforeEach(() => {
cy.mockTradingPage();
});
it('show order book', () => {
// 6003-ORDB-001
// 6003-ORDB-002
cy.getByTestId(orderbookTab).click();
cy.getByTestId(orderbookTable).should('be.visible');
cy.getByTestId(orderbookTable).should('not.be.empty');
});
it('show orders prices', () => {
// 6003-ORDB-003
cy.getByTestId(askPrice).should('have.text', '98.94185');
cy.getByTestId(bidPrice).should('have.text', '98.89001');
});
it('show prices volumes', () => {
// 6003-ORDB-004
cy.getByTestId(askVolume).should('have.text', '1');
cy.getByTestId(bidVolume).should('have.text', '1');
});
it('show prices cumulative volumes', () => {
// 6003-ORDB-005
cy.getByTestId(askCumulative).should('have.text', '38');
cy.getByTestId(bidCumulative).should('have.text', '7');
});
it('show mid price', () => {
// 6003-ORDB-006
cy.getByTestId(midPrice).should('have.text', '46,126.90');
});
it('sort prices descending', () => {
// 6003-ORDB-007
const prices: number[] = [];
cy.getByTestId(orderbookTable).within(() => {
cy.get('[data-testid*=price]')
.each(($el) => {
prices.push(Number($el.text()));
})
.then(() => {
expect(prices).to.deep.equal(prices.sort((a, b) => b - a));
});
});
});
it('copy price to deal ticket form', () => {
// 6003-ORDB-009
cy.getByTestId(askPrice).click();
cy.getByTestId(dealTicketPrice).should('have.value', '98.94185');
});
it('copy size to deal ticket form', () => {
// 6003-ORDB-009
cy.getByTestId(bidCumulative).click();
cy.getByTestId(dealTicketSize).should('have.value', '7');
});
it('copy size to deal ticket form', () => {
// 6003-ORDB-009
cy.getByTestId(bidVolume).click();
cy.getByTestId(dealTicketSize).should('have.value', '1');
});
it('change price resolution', () => {
// 6003-ORDB-008
const resolutions = [
'0.00000',
'0.0000',
'0.000',
'0.00',
'0.0',
'0',
'10',
'100',
'1,000',
'10,000',
];
cy.getByTestId(priceResolution).click();
cy.get('[role="menu"]')
.find('[role="menuitem"]')
.each(($el, index) => {
expect($el.text()).to.equal(resolutions[index]);
});
cy.get('[role="menuitem"]').eq(4).click();
cy.getByTestId(resPrice).should('have.text', '99.0');
cy.getByTestId(askPrice).should('not.exist');
cy.getByTestId(bidPrice).should('not.exist');
});
});
@@ -0,0 +1,110 @@
import * as Schema from '@vegaprotocol/types';
import { testOrderSubmission } from '../support/order-validation';
import type { OrderSubmission } from '@vegaprotocol/wallet';
import { createOrder } from '../support/create-order';
describe.skip('must submit order', { tags: '@smoke' }, () => {
// 7002-SORD-039
before(() => {
cy.setVegaWallet();
cy.mockTradingPage();
cy.mockSubscription();
cy.visit('/#/markets/market-0');
cy.wait('@Markets');
});
beforeEach(() => {
cy.setVegaWallet();
});
it('successfully places market buy order', () => {
// 7002-SORD-010
// 0003-WTXN-012
// 0003-WTXN-003
cy.mockVegaWalletTransaction();
const order: OrderSubmission = {
marketId: 'market-0',
type: Schema.OrderType.TYPE_MARKET,
side: Schema.Side.SIDE_BUY,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
postOnly: false,
reduceOnly: false,
size: '100',
};
createOrder(order);
testOrderSubmission(order);
});
it('successfully places market sell order', () => {
cy.mockVegaWalletTransaction();
const order: OrderSubmission = {
marketId: 'market-0',
type: Schema.OrderType.TYPE_MARKET,
side: Schema.Side.SIDE_SELL,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
size: '100',
postOnly: false,
reduceOnly: false,
};
createOrder(order);
testOrderSubmission(order);
});
it('successfully places limit buy order', () => {
// 7002-SORD-017
cy.mockVegaWalletTransaction();
const order: OrderSubmission = {
marketId: 'market-0',
type: Schema.OrderType.TYPE_LIMIT,
side: Schema.Side.SIDE_BUY,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTC,
size: '100',
postOnly: false,
reduceOnly: false,
price: '200',
};
createOrder(order);
testOrderSubmission(order, { price: '20000000' });
});
it('successfully places limit sell order', () => {
cy.mockVegaWalletTransaction();
const order: OrderSubmission = {
marketId: 'market-0',
type: Schema.OrderType.TYPE_LIMIT,
side: Schema.Side.SIDE_SELL,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GFN,
size: '100',
postOnly: false,
reduceOnly: false,
price: '50000',
};
createOrder(order);
testOrderSubmission(order, { price: '5000000000' });
});
it('successfully places GTT limit buy order', () => {
cy.mockVegaWalletTransaction();
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
const order: OrderSubmission = {
marketId: 'market-0',
type: Schema.OrderType.TYPE_LIMIT,
side: Schema.Side.SIDE_SELL,
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_GTT,
size: '100',
price: '1.00',
expiresAt: expiresAt.toISOString().substring(0, 16),
postOnly: false,
reduceOnly: false,
};
createOrder(order);
testOrderSubmission(order, {
price: '100000',
expiresAt:
new Date(order.expiresAt as string).getTime().toString() + '000000',
postOnly: false,
reduceOnly: false,
});
});
});
@@ -54,15 +54,6 @@ describe('deal ticket basics', { tags: '@smoke' }, () => {
cy.getByTestId(toggleLimit).next('input').should('be.checked');
cy.getByTestId(orderPriceField).should('have.value', '101');
});
it('sidebar should be open after reload', () => {
cy.mockTradingPage();
cy.getByTestId('deal-ticket-form').should('be.visible');
cy.getByTestId('Order').click();
cy.getByTestId('deal-ticket-form').should('not.exist');
cy.reload();
cy.getByTestId('deal-ticket-form').should('be.visible');
});
});
describe(
@@ -24,9 +24,6 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.mockTradingPage();
cy.getByTestId('deal-ticket-fee-margin-required').within(() => {
cy.get('button').click();
});
});
describe('limit order', () => {
@@ -36,7 +33,9 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
it('must see the price unit', function () {
// 7002-SORD-018
cy.getByTestId(orderPriceField).next().should('have.text', 'DAI');
cy.getByTestId(orderPriceField)
.siblings('label')
.should('have.text', 'Price (DAI)');
});
it('must see warning when placing an order with expiry date in past', () => {
@@ -65,7 +64,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(orderPriceField).clear().type('1.123456');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('deal-ticket-error-message-price').should(
cy.getByTestId('deal-ticket-error-message-price-limit').should(
'have.text',
'Price accepts up to 5 decimal places'
);
@@ -88,7 +87,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
cy.getByTestId(orderSizeField).clear().type('1.234');
// 7002-SORD-060
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId('deal-ticket-error-message-size').should(
cy.getByTestId('deal-ticket-error-message-size-market').should(
'have.text',
'Size must be whole numbers for this market'
);
@@ -97,7 +96,7 @@ describe('deal ticker order validation', { tags: '@smoke' }, () => {
it('must warn if order size is set to 0', function () {
cy.getByTestId(orderSizeField).clear().type('0');
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId('deal-ticket-error-message-size').should(
cy.getByTestId('deal-ticket-error-message-size-market').should(
'have.text',
'Size cannot be lower than 1'
);
@@ -222,17 +222,12 @@ describe('subscribe orders', { tags: '@smoke' }, () => {
it('must see a filled order', () => {
// 7002-SORD-046
// 7003-MORD-020
// NOT COVERED: Must be able to see/link to all trades that were created from this order
updateOrder({
id: orderId,
status: Schema.OrderStatus.STATUS_FILLED,
});
cy.getByTestId(`order-status-${orderId}`).should('have.text', 'Filled');
cy.get('[col-id="market.tradableInstrument.instrument.code"]').contains(
'[title="Future"]',
'Futr'
);
});
it('must see a rejected order', () => {
@@ -1,25 +1,59 @@
import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { partyAssetsQuery } from '@vegaprotocol/mock';
import { ledgerEntriesQuery } from '@vegaprotocol/mock';
describe('Portfolio page', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.mockGQL((req) => {
aliasGQLQuery(req, 'PartyAssets', partyAssetsQuery());
});
cy.mockTradingPage();
cy.mockGQL((req) => {
aliasGQLQuery(req, 'LedgerEntries', ledgerEntriesQuery());
});
cy.mockSubscription();
cy.setVegaWallet();
});
describe('Ledger entries', () => {
it('Download form should be properly rendered', () => {
// 7007-LEEN-001
it('List should be properly rendered', () => {
cy.visit('/#/portfolio');
cy.getByTestId('"Ledger entries"').click();
const headers = [
'Sender',
'Account type',
'Market',
'Receiver',
'Account type',
'Market',
'Transfer type',
'Quantity',
'Asset',
'Sender account balance',
'Receiver account balance',
'Vega time',
];
cy.getByTestId('tab-ledger-entries').within(($headers) => {
cy.wrap($headers)
.getByTestId('ledger-download-button')
.should('be.visible');
.get('.ag-header-cell-text')
.each(($header, i) => {
cy.wrap($header).should('have.text', headers[i]);
});
});
cy.get(
'[data-testid="tab-ledger-entries"] .ag-center-cols-container .ag-row'
).should('have.length', ledgerEntriesQuery().ledgerEntries.edges.length);
});
it('account filters should be callable', () => {
cy.visit('/#/portfolio');
cy.getByTestId('"Ledger entries"').click();
cy.get('[role="columnheader"][col-id="fromAccountType"]').realHover();
cy.get(
'[role="columnheader"][col-id="fromAccountType"] .ag-header-cell-menu-button'
).click();
cy.get('fieldset.ag-simple-filter-body-wrapper')
.should('be.visible')
.within((fields) => {
cy.wrap(fields).find('label').should('have.length', 18);
});
cy.getByTestId('"Ledger entries"').click();
cy.get('fieldset.ag-simple-filter-body-wrapper').should('not.exist');
});
});
});
@@ -43,10 +43,11 @@ 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', '0xEe7D…d94F');
cy.getByTestId('ethereum-address').should('have.text', ethWalletAddress);
cy.getByTestId('disconnect-ethereum-wallet')
.should('have.text', 'Disconnect')
.click();
-3
View File
@@ -12,8 +12,6 @@ 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/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/firefox/addon/vega-wallet-fairground
# Cosmic elevator flags
@@ -21,7 +19,6 @@ NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_TENDERMINT_URL=https://tm.n01.stagnet1.vega.rocks
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.n01.stagnet1.vega.xyz/websocket
+1 -2
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/firefox/addon/vega-wallet-fairground
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
NX_ETH_LOCAL_PROVIDER_URL=http://localhost:8545/
NX_ETH_WALLET_MNEMONIC="ozone access unlock valid olympic save include omit supply green clown session"
@@ -23,7 +23,6 @@ NX_SUCCESSOR_MARKETS=false
NX_STOP_ORDERS=false
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=false
NX_TENDERMINT_URL=http://localhost:26617
NX_TENDERMINT_WEBSOCKET_URL=wss://localhost:26617/websocket
+1 -2
View File
@@ -14,14 +14,13 @@ 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/firefox/addon/vega-wallet-fairground
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/pl/firefox/addon/vega-wallet
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
# NX_ICEBERG_ORDERS
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket

Some files were not shown because too many files have changed in this diff Show More