Compare commits

..
Author SHA1 Message Date
Dariusz Majcherczyk cb4aeb6d8d test: added check if the market not exist 2023-02-17 10:05:01 +01:00
Dariusz Majcherczyk e5bcc1dc96 test: wait for hash 2023-02-17 09:44:37 +01:00
Dariusz Majcherczyk 2a8094ed6d test: added wait for market 2023-02-17 09:16:41 +01:00
157 changed files with 2301 additions and 6091 deletions
+12 -3
View File
@@ -4,6 +4,15 @@ context('Asset page', { tags: '@regression' }, () => {
describe('Verify elements on page', () => {
before('Navigate to assets page', () => {
cy.visit('/assets');
// Check we have enough enough assets
cy.getAssets().then((assets) => {
assert.isAtLeast(
Object.keys(assets).length,
5,
'Ensuring we have at least 5 assets to test'
);
});
});
it('should be able to see full assets list', () => {
@@ -31,7 +40,7 @@ context('Asset page', { tags: '@regression' }, () => {
});
});
it('should open details page when clicked on "View details"', () => {
it('should open details dialog when clicked on "View details"', () => {
cy.getAssets().then((assets) => {
Object.values(assets).forEach((asset) => {
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`)
@@ -40,8 +49,8 @@ context('Asset page', { tags: '@regression' }, () => {
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`)
.eq(0)
.click();
cy.getByTestId('asset-header').should('have.text', asset.name);
cy.go('back');
cy.getByTestId('dialog-content').should('be.visible');
cy.getByTestId('dialog-close').click();
});
});
});
@@ -1,35 +1,22 @@
import { render, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { assetsList } from '../../mocks/assets';
import { AssetsTable } from './assets-table';
describe('AssetsTable', () => {
it('shows loading message on first render', async () => {
const res = render(
<MemoryRouter>
<AssetsTable data={null} />
</MemoryRouter>
);
const res = render(<AssetsTable data={null} />);
expect(await res.findByText('Loading...')).toBeInTheDocument();
});
it('shows no data message if no assets found', async () => {
const res = render(
<MemoryRouter>
<AssetsTable data={[]} />
</MemoryRouter>
);
const res = render(<AssetsTable data={[]} />);
expect(
await res.findByText('This chain has no assets')
).toBeInTheDocument();
});
it('shows a table/list with all the assets', async () => {
const res = render(
<MemoryRouter>
<AssetsTable data={assetsList} />
</MemoryRouter>
);
const res = render(<AssetsTable data={assetsList} />);
await waitFor(() => {
const rowA1 = res.container.querySelector('[row-id="123"]');
expect(rowA1).toBeInTheDocument();
@@ -1,4 +1,5 @@
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { AssetTypeMapping, AssetStatusMapping } from '@vegaprotocol/assets';
import { t } from '@vegaprotocol/react-helpers';
import type { VegaICellRendererParams } from '@vegaprotocol/ui-toolkit';
@@ -8,14 +9,15 @@ import { AgGridColumn } from 'ag-grid-react';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import { useRef, useLayoutEffect } from 'react';
import { BREAKPOINT_MD } from '../../config/breakpoints';
import { useNavigate } from 'react-router-dom';
import type { RowClickedEvent } from 'ag-grid-community';
type AssetsTableProps = {
data: AssetFieldsFragment[] | null;
};
export const AssetsTable = ({ data }: AssetsTableProps) => {
const navigate = useNavigate();
const openAssetDetailsDialog = useAssetDetailsDialogStore(
(state) => state.open
);
const ref = useRef<AgGridReact>(null);
const showColumnsOnDesktop = () => {
ref.current?.columnApi.setColumnsVisible(
@@ -47,23 +49,17 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
autoHeight: true,
}}
suppressCellFocus={true}
onRowClicked={({ data }: RowClickedEvent) => {
navigate(data.id);
onGridReady={() => {
showColumnsOnDesktop();
}}
>
<AgGridColumn headerName={t('Symbol')} field="symbol" />
<AgGridColumn headerName={t('Name')} field="name" />
<AgGridColumn
flex="2"
headerName={t('ID')}
field="id"
hide={window.innerWidth < BREAKPOINT_MD}
/>
<AgGridColumn flex="2" headerName={t('ID')} field="id" />
<AgGridColumn
colId="type"
headerName={t('Type')}
field="source.__typename"
hide={window.innerWidth < BREAKPOINT_MD}
valueFormatter={({ value }: { value?: string }) =>
value && AssetTypeMapping[value].value
}
@@ -71,7 +67,6 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
<AgGridColumn
headerName={t('Status')}
field="status"
hide={window.innerWidth < BREAKPOINT_MD}
valueFormatter={({ value }: { value?: string }) =>
value && AssetStatusMapping[value].value
}
@@ -88,13 +83,28 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
value,
}: VegaICellRendererParams<AssetFieldsFragment, 'id'>) =>
value ? (
<ButtonLink
onClick={(e) => {
navigate(value);
}}
>
{t('View details')}
</ButtonLink>
<div className="pb-1">
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(value, e.target as HTMLElement);
}}
>
{t('View details')}
</ButtonLink>{' '}
<span className="max-md:hidden">
<ButtonLink
onClick={(e) => {
openAssetDetailsDialog(
value,
e.target as HTMLElement,
true
);
}}
>
{t('View JSON')}
</ButtonLink>
</span>
</div>
) : (
''
)
@@ -1,50 +0,0 @@
query ExplorerEpoch($id: ID!) {
epoch(id: $id) {
id
timestamps {
start
end
firstBlock
lastBlock
}
}
}
query ExplorerFutureEpoch {
networkParameter(key: "validators.epoch.length") {
value
}
epoch {
id
timestamps {
start
}
}
}
# query ExplorerEpoch($id: ID!) {
#
##### This could be useful for calculating roughly when a future epoch will
##### occur, but epoch not exist results in a total error
# networkParameter(key: "validators.epoch.length") {
# value
# }
#
##### This could be useful for relating where we are in time, but as above
##### the total failure caused by epoch(id) not existing
##### means this is useful
# currentEpoch: epoch {
# id
# }
#
# epoch(id: $id) {
# id
# timestamps {
# start
# end
# firstBlock
# lastBlock
# }
# }
#}
@@ -1,99 +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 ExplorerEpochQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
export type ExplorerEpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null, firstBlock: string, lastBlock?: string | null } } };
export type ExplorerFutureEpochQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type ExplorerFutureEpochQuery = { __typename?: 'Query', networkParameter?: { __typename?: 'NetworkParameter', value: string } | null, epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null } } };
export const ExplorerEpochDocument = gql`
query ExplorerEpoch($id: ID!) {
epoch(id: $id) {
id
timestamps {
start
end
firstBlock
lastBlock
}
}
}
`;
/**
* __useExplorerEpochQuery__
*
* To run a query within a React component, call `useExplorerEpochQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerEpochQuery` 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 } = useExplorerEpochQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useExplorerEpochQuery(baseOptions: Apollo.QueryHookOptions<ExplorerEpochQuery, ExplorerEpochQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerEpochQuery, ExplorerEpochQueryVariables>(ExplorerEpochDocument, options);
}
export function useExplorerEpochLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerEpochQuery, ExplorerEpochQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerEpochQuery, ExplorerEpochQueryVariables>(ExplorerEpochDocument, options);
}
export type ExplorerEpochQueryHookResult = ReturnType<typeof useExplorerEpochQuery>;
export type ExplorerEpochLazyQueryHookResult = ReturnType<typeof useExplorerEpochLazyQuery>;
export type ExplorerEpochQueryResult = Apollo.QueryResult<ExplorerEpochQuery, ExplorerEpochQueryVariables>;
export const ExplorerFutureEpochDocument = gql`
query ExplorerFutureEpoch {
networkParameter(key: "validators.epoch.length") {
value
}
epoch {
id
timestamps {
start
}
}
}
`;
/**
* __useExplorerFutureEpochQuery__
*
* To run a query within a React component, call `useExplorerFutureEpochQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerFutureEpochQuery` 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 } = useExplorerFutureEpochQuery({
* variables: {
* },
* });
*/
export function useExplorerFutureEpochQuery(baseOptions?: Apollo.QueryHookOptions<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>(ExplorerFutureEpochDocument, options);
}
export function useExplorerFutureEpochLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>(ExplorerFutureEpochDocument, options);
}
export type ExplorerFutureEpochQueryHookResult = ReturnType<typeof useExplorerFutureEpochQuery>;
export type ExplorerFutureEpochLazyQueryHookResult = ReturnType<typeof useExplorerFutureEpochLazyQuery>;
export type ExplorerFutureEpochQueryResult = Apollo.QueryResult<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>;
@@ -1,56 +0,0 @@
import { render } from '@testing-library/react';
import { IconForEpoch } from './epoch';
const THE_PAST = 'Monday, 17 February 2022 11:44:09';
const THE_FUTURE = 'Monday, 17 February 3023 11:44:09';
describe('IconForEpoch', () => {
it('Handles malformed dates', () => {
const start = 'This is n0t a d4te';
const end = '📅';
const screen = render(<IconForEpoch start={start} end={end} />);
expect(screen.getByRole('img')).toHaveAttribute(
'aria-label',
'calendar icon'
);
});
it('defaults to a calendar icon', () => {
const start = null as unknown as string;
const end = null as unknown as string;
const screen = render(<IconForEpoch start={start} end={end} />);
expect(screen.getByRole('img')).toHaveAttribute(
'aria-label',
'calendar icon'
);
});
it('if start and end are both in the future, stick with calendar', () => {
const screen = render(<IconForEpoch start={THE_FUTURE} end={THE_FUTURE} />);
expect(screen.getByRole('img')).toHaveAttribute(
'aria-label',
'calendar icon'
);
});
it('if start is in the past and end is in the future, this is currently active', () => {
const screen = render(<IconForEpoch start={THE_PAST} end={THE_FUTURE} />);
expect(screen.getByRole('img')).toHaveAttribute(
'aria-label',
'circle icon'
);
});
it('if start and end are in the paste, this is done', () => {
const screen = render(<IconForEpoch start={THE_PAST} end={THE_PAST} />);
expect(screen.getByRole('img')).toHaveAttribute(
'aria-label',
'tick-circle icon'
);
});
});
@@ -1,125 +0,0 @@
import { MockedProvider } from '@apollo/client/testing';
import { render } from '@testing-library/react';
import EpochMissingOverview, { calculateEpochData } from './epoch-missing';
import { getSecondsFromInterval } from '@vegaprotocol/react-helpers';
const START_DATE_PAST = 'Monday, 17 February 2022 11:44:09';
describe('getSecondsFromInterval', () => {
it('returns 0 for bad data', () => {
expect(getSecondsFromInterval(null as unknown as string)).toEqual(0);
expect(getSecondsFromInterval('')).toEqual(0);
expect(getSecondsFromInterval('🧙')).toEqual(0);
expect(getSecondsFromInterval(2 as unknown as string)).toEqual(0);
});
it('parses out months from a capital M', () => {
expect(getSecondsFromInterval('2M')).toEqual(5184000);
});
it('parses out days from a capital D', () => {
expect(getSecondsFromInterval('1D')).toEqual(86400);
});
it('parses out hours from a lower case h', () => {
expect(getSecondsFromInterval('11h')).toEqual(39600);
});
it('parses out minutes from a lower case m', () => {
expect(getSecondsFromInterval('10m')).toEqual(600);
});
it('parses out seconds from a lower case s', () => {
expect(getSecondsFromInterval('99s')).toEqual(99);
});
it('parses complex examples', () => {
expect(getSecondsFromInterval('24h')).toEqual(86400);
expect(getSecondsFromInterval('1h30m')).toEqual(5400);
expect(getSecondsFromInterval('1D1h30m1s')).toEqual(91801);
});
});
describe('calculateEpochData', () => {
it('Handles bad data', () => {
const currentEpochId = null as unknown as string;
const missingEpochId = null as unknown as string;
const epochStart = null as unknown as string;
const epochLength = null as unknown as string;
const res = calculateEpochData(
currentEpochId,
missingEpochId,
epochStart,
epochLength
);
expect(res).toHaveProperty('label', 'Missing data');
expect(res).toHaveProperty('isInFuture', false);
});
it('Calculates that a bigger epoch number is in the future from basic data', () => {
const currentEpochId = '10';
const missingEpochId = '20';
const epochStart = '';
const epochLength = '';
const res = calculateEpochData(
currentEpochId,
missingEpochId,
epochStart,
epochLength
);
expect(res).toHaveProperty('isInFuture', true);
});
it('If it has an epoch length and a start time, it provides an estimate', () => {
const currentEpochId = '10';
const missingEpochId = '20';
const epochStart = START_DATE_PAST;
const epochLength = '1s';
const res = calculateEpochData(
currentEpochId,
missingEpochId,
epochStart,
epochLength
);
// 'Estimate: 17/02/2022, 11:44:19 - in less than a minute')
expect(res).toHaveProperty('label');
expect(res.label).toMatch(/^Estimate/);
expect(res.label).toMatch(/in less than a minute$/);
});
it('Provide decent string for past', () => {
const currentEpochId = '20';
const missingEpochId = '10';
const epochStart = START_DATE_PAST;
const epochLength = '1s';
const res = calculateEpochData(
currentEpochId,
missingEpochId,
epochStart,
epochLength
);
// 'Estimate: 17/02/2022, 11:44:19 - in less than a minute')
expect(res).toHaveProperty('label');
expect(res.label).toMatch(/^Estimate/);
expect(res.label).toMatch(/less than a minute ago$/);
});
});
describe('EpochMissingOverview', () => {
function renderComponent(missingEpochId: string) {
return render(
<MockedProvider>
<EpochMissingOverview missingEpochId={missingEpochId} />
</MockedProvider>
);
}
it('renders a - if no id is provided', () => {
const n = null as unknown as string;
const screen = renderComponent(n);
expect(screen.getByTestId('empty')).toBeInTheDocument();
});
});
@@ -1,111 +0,0 @@
import { useExplorerFutureEpochQuery } from './__generated__/Epoch';
import addSeconds from 'date-fns/addSeconds';
import formatDistance from 'date-fns/formatDistance';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import isFuture from 'date-fns/isFuture';
import { isValidDate } from '@vegaprotocol/react-helpers';
import { getSecondsFromInterval } from '@vegaprotocol/react-helpers';
export type EpochMissingOverviewProps = {
missingEpochId?: string;
};
/**
* Renders a set of details for an epoch that has no representation in the
* data node. This is primarily for one of two reasons:
*
* 1. The epoch hasn't happened yet
* 2. The epoch happened before a snapshot, and thus the details don't exist
*
* This component is used when the API has responded with no data for an epoch
* by ID, so we already know that we can't display start time/block etc.
*
* We can detect 1 if the epoch is a higher number than the current epoch
* We can detect 2 if the epoch is in the past, but we still get no response.
*/
const EpochMissingOverview = ({
missingEpochId,
}: EpochMissingOverviewProps) => {
const { data, error, loading } = useExplorerFutureEpochQuery();
// This should not happen, but it's easily handled
if (!missingEpochId) {
return <span data-testid="empty">-</span>;
}
// No data should also not happen - we've requested the current epoch. This
// could happen at chain restart, but shouldn't. If it does, fallback.
if (!data || loading || error) {
return <span data-testid="empty">{missingEpochId}</span>;
}
// If we have enough information to predict a future or past block time, let's do it
if (
!missingEpochId ||
!data.epoch.id ||
!data.epoch.timestamps.start ||
!data?.networkParameter?.value
) {
return <span data-testid="empty">{missingEpochId}</span>;
}
const { label, isInFuture } = calculateEpochData(
data.epoch.id,
missingEpochId,
data.epoch.timestamps.start,
data.networkParameter.value
);
return (
<Tooltip description={<p className="text-xs m-2">{label}</p>}>
<p>
{isInFuture ? (
<Icon name="calendar" className="mr-1" />
) : (
<Icon name="outdated" className="mr-1" />
)}
{missingEpochId}
</p>
</Tooltip>
);
};
export function calculateEpochData(
currentEpochId: string,
missingEpochId: string,
epochStart: string,
epochLength: string
) {
// Blank string will be return 0 seconds from getSecondsFromInterval
const epochLengthInSeconds = getSecondsFromInterval(epochLength);
if (!epochStart || !epochLength) {
// Let's just take a guess
return {
label: 'Missing data',
isInFuture: parseInt(missingEpochId) > parseInt(currentEpochId),
};
}
const startFrom = new Date(epochStart);
const diff = parseInt(missingEpochId) - parseInt(currentEpochId);
const futureDate = addSeconds(startFrom, diff * epochLengthInSeconds);
const label =
isValidDate(futureDate) && isValidDate(startFrom)
? `Estimate: ${futureDate.toLocaleString()} - ${formatDistance(
futureDate,
startFrom,
{ addSuffix: true }
)}`
: 'Missing data';
return {
label,
isInFuture: isFuture(futureDate),
};
}
export default EpochMissingOverview;
@@ -1,127 +0,0 @@
import { useExplorerEpochQuery } from './__generated__/Epoch';
import { t } from '@vegaprotocol/react-helpers';
import { BlockLink } from '../links';
import { Time } from '../time';
import { TimeAgo } from '../time-ago';
import EpochMissingOverview from './epoch-missing';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import type { IconProps } from '@vegaprotocol/ui-toolkit';
import isPast from 'date-fns/isPast';
const borderClass =
'border-solid border-2 border-vega-dark-200 border-collapse';
export type EpochOverviewProps = {
id?: string;
};
/**
* Displays detailed information about an epoch, given an ID. This
* works for past epochs and current epochs - future epochs, and a
* few other situations (see epoch-missing) will not return us
* enough information to render this.
*
* The details are hidden in a tooltip, behind the epoch number
*/
const EpochOverview = ({ id }: EpochOverviewProps) => {
const { data, error, loading } = useExplorerEpochQuery({
variables: { id: id || '' },
});
const ti = data?.epoch.timestamps;
if (
error?.message &&
error.message.includes('no resource corresponding to this id')
) {
return <EpochMissingOverview missingEpochId={id} />;
}
if (!ti || loading || error) {
return <span>{id}</span>;
}
const description = (
<table className="text-xs m-2">
<thead>
<tr>
<th></th>
<th className={`text-center ${borderClass}`}>{t('Block')}</th>
<th className={`text-center ${borderClass}`}>{t('Time')}</th>
</tr>
</thead>
<tbody>
<tr>
<th className={`px-2 ${borderClass}`}>{t('Start')}</th>
<td className={`px-2 ${borderClass}`}>
{ti.firstBlock ? <BlockLink height={ti.firstBlock} /> : '-'}
</td>
<td className={`px-2 ${borderClass}`}>
<Time date={ti.start} />
<br />
<TimeAgo date={ti.start} />
</td>
</tr>
<tr>
<th className={`px-2 ${borderClass}`}>{t('End')}</th>
<td className={`px-2 ${borderClass}`}>
{ti.lastBlock ? (
<BlockLink height={ti.lastBlock} />
) : (
t('In progress')
)}
</td>
<td className={`px-2 ${borderClass}`}>
{ti.end ? (
<>
<Time date={ti.end} />
<br />
<TimeAgo date={ti.end} />
</>
) : (
<span>{t('-')}</span>
)}
</td>
</tr>
</tbody>
</table>
);
return (
<Tooltip description={description}>
<p>
<IconForEpoch start={ti.start} end={ti.end} />
{id}
</p>
</Tooltip>
);
};
export type IconForEpochProps = {
start: string;
end: string;
};
/**
* Chooses an icon to display next to the epoch number, representing
* when the epoch is relative to now (i.e. not yet started, started,
* finished)
*/
export function IconForEpoch({ start, end }: IconForEpochProps) {
const startHasPassed = start ? isPast(new Date(start)) : false;
const endHasPassed = end ? isPast(new Date(end)) : false;
let i: IconProps['name'] = 'calendar';
if (!startHasPassed && !endHasPassed) {
i = 'calendar';
} else if (startHasPassed && !endHasPassed) {
i = 'circle';
} else if (startHasPassed && endHasPassed) {
i = 'tick-circle';
}
return <Icon name={i} className="mr-2" />;
}
export default EpochOverview;
@@ -1,24 +1,17 @@
import { NodeSwitcherDialog, useEnvironment } from '@vegaprotocol/environment';
import { t, useScreenDimensions } from '@vegaprotocol/react-helpers';
import { ExternalLink, Link } from '@vegaprotocol/ui-toolkit';
import { useMemo, useState } from 'react';
import { ENV } from '../../config/env';
import { t } from '@vegaprotocol/react-helpers';
import { Link } from '@vegaprotocol/ui-toolkit';
import { useState } from 'react';
export const Footer = () => {
const { VEGA_URL, GIT_COMMIT_HASH, GIT_ORIGIN_URL } = useEnvironment();
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useState(false);
const { screenSize } = useScreenDimensions();
const showFullFeedbackLabel = useMemo(
() => ['lg', 'xl'].includes(screenSize),
[screenSize]
);
return (
<>
<footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-xs md:text-md md:flex md:col-span-2 px-4 py-2 gap-4 border-t border-neutral-700 dark:border-neutral-300">
<div className="flex justify-between gap-2 align-middle">
{GIT_COMMIT_HASH && (
<div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4">
<div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4">
{GIT_COMMIT_HASH && (
<p data-testid="git-commit-hash">
{t('Version')}:{' '}
<Link
@@ -32,21 +25,15 @@ export const Footer = () => {
{GIT_COMMIT_HASH}
</Link>
</p>
</div>
)}
)}
</div>
<div className="content-center flex pl-2 md:border-r border-neutral-700 dark:border-neutral-300 pr-4">
<div className="flex pl-2 content-center">
{VEGA_URL && <NodeUrl url={VEGA_URL} />}
<Link className="ml-2" onClick={() => setNodeSwitcherOpen(true)}>
{t('Change')}
</Link>
</div>
<div className="flex pl-2 content-center">
<ExternalLink href={ENV.addresses.feedback}>
{showFullFeedbackLabel ? t('Share your feedback') : t('Feedback')}
</ExternalLink>
</div>
</div>
</footer>
<NodeSwitcherDialog
@@ -11,14 +11,14 @@ export const InfoBlock = ({ title, subtitle, tooltipInfo }: InfoBlockProps) => {
return (
<div className="flex flex-col text-center ">
<h3 className="text-4xl">{title}</h3>
<p className="text-vega-dark-100 dark:text-vega-light-200">
<p className="text-zinc-800 dark:text-zinc-300">
{subtitle}
{tooltipInfo ? (
<Tooltip description={tooltipInfo} align="center">
<span>
<Icon
name="info-sign"
className="ml-2 text-vega-light-300 dark:text-vega-dark-300"
className="ml-2 text-zinc-400 dark:text-zinc-600"
/>
</span>
</Tooltip>
@@ -44,12 +44,12 @@ export const InfoPanel = ({
text={id}
startChars={visibleChars}
endChars={visibleChars}
className="text-vega-dark-100 dark:text-vega-light-200"
className="text-black dark:text-zinc-200"
/>
) : (
<p
title={id}
className="text-vega-dark-100 dark:text-vega-light-200 truncate ..."
className="text-black dark:text-zinc-200 truncate ..."
>
{id}
</p>
@@ -70,7 +70,7 @@ export const InfoPanel = ({
</div>
{copy && (
<CopyWithTooltip text={id}>
<button className="bg-vega-light-100 dark:bg-vega-dark-100 rounded-sm py-2 px-3">
<button className="bg-zinc-100 dark:bg-zinc-900 rounded-sm py-2 px-3">
<Icon name="duplicate" />
</button>
</CopyWithTooltip>
@@ -5,13 +5,9 @@ import {
useAssetDataProvider,
useAssetDetailsDialogStore,
} from '@vegaprotocol/assets';
import { useNavigate } from 'react-router-dom';
import { Routes } from '../../../routes/route-names';
export type AssetLinkProps = Partial<ComponentProps<typeof ButtonLink>> & {
assetId: string;
asDialog?: boolean;
showAssetSymbol?: boolean;
};
/**
@@ -19,32 +15,17 @@ export type AssetLinkProps = Partial<ComponentProps<typeof ButtonLink>> & {
* with a link to the assets modal. If the name does not come back
* it will use the ID instead.
*/
export const AssetLink = ({
assetId,
asDialog,
showAssetSymbol = false,
...props
}: AssetLinkProps) => {
export const AssetLink = ({ assetId, ...props }: AssetLinkProps) => {
const { data: asset } = useAssetDataProvider(assetId);
const open = useAssetDetailsDialogStore((state) => state.open);
const navigate = useNavigate();
const label = asset
? showAssetSymbol
? asset?.symbol
: asset?.name
: assetId;
const label = asset?.name ? asset.name : assetId;
return (
<ButtonLink
data-testid="asset-link"
disabled={!asset}
onClick={(e) => {
if (asDialog) {
open(assetId, e.target as HTMLElement);
} else {
navigate(`${Routes.ASSETS}/${asset?.id}`);
}
open(assetId, e.target as HTMLElement);
}}
{...props}
>
@@ -1,39 +0,0 @@
import { render } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import PartyLink from './party-link';
describe('PartyLink', () => {
it('renders Network for 000.000 party', () => {
const zeroes =
'0000000000000000000000000000000000000000000000000000000000000000';
const screen = render(<PartyLink id={zeroes} />);
expect(screen.getByText('Network')).toBeInTheDocument();
});
it('renders Network for network party', () => {
const screen = render(<PartyLink id="network" />);
expect(screen.getByText('Network')).toBeInTheDocument();
});
it('renders ID with no link for invalid party', () => {
const screen = render(<PartyLink id="this-party-is-not-valid" />);
expect(screen.getByTestId('invalid-party')).toBeInTheDocument();
});
it('links a valid party to the party page', () => {
const aValidParty =
'13464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6e';
const screen = render(
<MemoryRouter>
<PartyLink id={aValidParty} />
</MemoryRouter>
);
const el = screen.getByText(aValidParty);
expect(el).toBeInTheDocument();
// The text should be a link that points to the party's page
expect(el.parentElement?.tagName).toEqual('A');
expect(el.parentElement?.getAttribute('href')).toContain(aValidParty);
});
});
@@ -3,47 +3,19 @@ import { Link } from 'react-router-dom';
import type { ComponentProps } from 'react';
import Hash from '../hash';
import { t } from '@vegaprotocol/react-helpers';
import { isValidPartyId } from '../../../routes/parties/id/components/party-id-error';
import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
export const SPECIAL_CASE_NETWORK_ID =
'0000000000000000000000000000000000000000000000000000000000000000';
export const SPECIAL_CASE_NETWORK = 'network';
export type PartyLinkProps = Partial<ComponentProps<typeof Link>> & {
id: string;
truncate?: boolean;
};
const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
// Some transactions will involve the 'network' party, which is alias for '000...000'
// The party page does not handle this nicely, so in this case we render the word 'Network'
if (id === SPECIAL_CASE_NETWORK || id === SPECIAL_CASE_NETWORK_ID) {
return (
<span className="font-mono" data-testid="network">
{t('Network')}
</span>
);
}
// If the party doesn't look correct, there's no point in linking to id. Just render
// the ID as it was given to us
if (!isValidPartyId(id)) {
return (
<span className="font-mono" data-testid="invalid-party">
{id}
</span>
);
}
const PartyLink = ({ id, ...props }: PartyLinkProps) => {
return (
<Link
className="underline font-mono"
{...props}
to={`/${Routes.PARTIES}/${id}`}
>
<Hash text={truncate ? truncateMiddle(id) : id} />
<Hash text={id} />
</Link>
);
};
@@ -13,7 +13,7 @@ export interface DeterministicOrderDetailsProps {
}
export const wrapperClasses =
'grid lg:grid-cols-1 flex items-center max-w-xl border border-vega-light-200 dark:border-vega-dark-150 rounded-md pv-2 ph-5 mb-5';
'grid lg:grid-cols-1 flex items-center max-w-xl border border-zinc-200 dark:border-zinc-800 rounded-md pv-2 ph-5 mb-5';
/**
* This component renders the *current* details for an order
@@ -42,7 +42,7 @@ const DeterministicOrderDetails = ({
<h2 className="text-3xl font-bold mb-4 display-5">
{t('Order not found')}
</h2>
<p className="text-vega-light-400 mb-12">
<p className="text-gray-500 mb-12">
{t('No order created from this transaction')}
</p>
</div>
@@ -38,7 +38,7 @@ export const PageHeader = ({
</h2>
{copy && (
<CopyWithTooltip data-testid="copy-to-clipboard" text={title}>
<button className="bg-vega-light-100 dark:bg-vega-dark-100 rounded-sm py-2 px-3">
<button className="bg-zinc-100 dark:bg-zinc-900 rounded-sm py-2 px-3">
<Icon name="duplicate" className="" />
</button>
</CopyWithTooltip>
@@ -8,7 +8,7 @@ interface PanelProps {
export const Panel = ({ children, className }: PanelProps) => (
<div
className={classNames(
'border border-vega-light-150 dark:border-vega-dark-150 rounded-md p-5 mb-5',
'border border-zinc-200 dark:border-zinc-800 rounded-md p-5 mb-5',
className
)}
>
@@ -1,227 +0,0 @@
import type { ProposalListFieldsFragment } from '@vegaprotocol/governance';
import { VoteProgress } from '@vegaprotocol/governance';
import type { AgGridReact } from 'ag-grid-react';
import { AgGridColumn } from 'ag-grid-react';
import type {
VegaICellRendererParams,
VegaValueFormatterParams,
} from '@vegaprotocol/ui-toolkit';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
import type { RowClickedEvent } from 'ag-grid-community';
import {
getDateTimeFormat,
NetworkParams,
t,
useNetworkParams,
} from '@vegaprotocol/react-helpers';
import { ProposalStateMapping } from '@vegaprotocol/types';
import BigNumber from 'bignumber.js';
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
import { BREAKPOINT_MD } from '../../config/breakpoints';
import { JsonViewerDialog } from '../dialogs/json-viewer-dialog';
type ProposalTermsDialog = {
open: boolean;
title: string;
content: unknown;
};
type ProposalsTableProps = {
data: ProposalListFieldsFragment[] | null;
};
export const ProposalsTable = ({ data }: ProposalsTableProps) => {
const { params } = useNetworkParams([
NetworkParams.governance_proposal_market_requiredMajority,
]);
const tokenLink = useLinks(DApp.Token);
const requiredMajorityPercentage = useMemo(() => {
const requiredMajority =
params?.governance_proposal_market_requiredMajority ?? 1;
return new BigNumber(requiredMajority).times(100);
}, [params?.governance_proposal_market_requiredMajority]);
const gridRef = useRef<AgGridReact>(null);
useLayoutEffect(() => {
const showColumnsOnDesktop = () => {
gridRef.current?.columnApi.setColumnsVisible(
['voting', 'cDate', 'eDate', 'type'],
window.innerWidth > BREAKPOINT_MD
);
gridRef.current?.columnApi.setColumnWidth(
'actions',
window.innerWidth > BREAKPOINT_MD ? 221 : 80
);
};
window.addEventListener('resize', showColumnsOnDesktop);
return () => {
window.removeEventListener('resize', showColumnsOnDesktop);
};
}, []);
const [dialog, setDialog] = useState<ProposalTermsDialog>({
open: false,
title: '',
content: null,
});
return (
<>
<AgGrid
ref={gridRef}
rowData={data}
getRowId={({ data }: { data: ProposalListFieldsFragment }) =>
data.id || data.rationale.title
}
overlayNoRowsTemplate={t('This chain has no markets')}
domLayout="autoHeight"
defaultColDef={{
flex: 1,
resizable: true,
sortable: true,
filter: true,
filterParams: { buttons: ['reset'] },
autoHeight: true,
}}
suppressCellFocus={true}
onRowClicked={({ data, event }: RowClickedEvent) => {
if (
(event?.target as HTMLElement).tagName.toUpperCase() !== 'BUTTON'
) {
const proposalPage = tokenLink(
TOKEN_PROPOSAL.replace(':id', data.id)
);
window.open(proposalPage, '_blank');
}
}}
>
<AgGridColumn
colId="title"
headerName={t('Title')}
field="rationale.title"
flex={2}
wrapText={true}
/>
<AgGridColumn
colId="type"
maxWidth={180}
hide={window.innerWidth <= BREAKPOINT_MD}
headerName={t('Type')}
field="terms.change.__typename"
/>
<AgGridColumn
maxWidth={100}
headerName={t('State')}
field="state"
valueFormatter={({
value,
}: VegaValueFormatterParams<ProposalListFieldsFragment, 'state'>) => {
return value ? ProposalStateMapping[value] : '-';
}}
/>
<AgGridColumn
colId="voting"
maxWidth={100}
hide={window.innerWidth <= BREAKPOINT_MD}
headerName={t('Voting')}
cellRenderer={({
data,
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
if (data) {
const yesTokens = new BigNumber(data.votes.yes.totalTokens);
const noTokens = new BigNumber(data.votes.no.totalTokens);
const totalTokensVoted = yesTokens.plus(noTokens);
const yesPercentage = totalTokensVoted.isZero()
? new BigNumber(0)
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
return (
<div className="uppercase flex h-full items-center justify-center pt-2">
<VoteProgress
threshold={requiredMajorityPercentage}
progress={yesPercentage}
/>
</div>
);
}
return '-';
}}
/>
<AgGridColumn
colId="cDate"
maxWidth={150}
hide={window.innerWidth <= BREAKPOINT_MD}
headerName={t('Closing date')}
field="terms.closingDatetime"
valueFormatter={({
value,
}: VegaValueFormatterParams<
ProposalListFieldsFragment,
'terms.closingDatetime'
>) => {
return value ? getDateTimeFormat().format(new Date(value)) : '-';
}}
/>
<AgGridColumn
colId="eDate"
maxWidth={150}
hide={window.innerWidth <= BREAKPOINT_MD}
headerName={t('Enactment date')}
field="terms.enactmentDatetime"
valueFormatter={({
value,
}: VegaValueFormatterParams<
ProposalListFieldsFragment,
'terms.enactmentDatetime'
>) => {
return value ? getDateTimeFormat().format(new Date(value)) : '-';
}}
/>
<AgGridColumn
colId="actions"
minWidth={window.innerWidth > BREAKPOINT_MD ? 221 : 80}
maxWidth={221}
sortable={false}
filter={false}
resizable={false}
cellRenderer={({
data,
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
const proposalPage = tokenLink(
TOKEN_PROPOSAL.replace(':id', data?.id || '')
);
const openDialog = () => {
if (!data) return;
setDialog({
open: true,
title: data.rationale.title,
content: data.terms,
});
};
return (
<div className="pb-1">
<button
className="underline max-md:hidden"
onClick={openDialog}
>
{t('View terms')}
</button>{' '}
<ExternalLink className="max-md:hidden" href={proposalPage}>
{t('Open in Governance')}
</ExternalLink>
<ExternalLink className="md:hidden" href={proposalPage}>
{t('Open')}
</ExternalLink>
</div>
);
}}
/>
</AgGrid>
<JsonViewerDialog
open={dialog.open}
onChange={(isOpen) => setDialog({ ...dialog, open: isOpen })}
title={dialog.title}
content={dialog.content}
/>
</>
);
};
@@ -1,43 +0,0 @@
import { useAssetDataProvider } from '@vegaprotocol/assets';
import { addDecimalsFormatNumber } from '@vegaprotocol/react-helpers';
import { AssetLink } from '../links';
export type DecimalSource = 'ASSET';
export type SizeInAssetProps = {
assetId: string;
size?: string | number;
decimalSource?: DecimalSource;
};
/**
* Given a market ID and an order size it will fetch the market
* order size, and format the size accordingly
*/
const SizeInAsset = ({
assetId,
size,
decimalSource = 'ASSET',
}: SizeInAssetProps) => {
const { data } = useAssetDataProvider(assetId);
if (!size) {
return <span>-</span>;
}
let label = size;
if (data) {
if (decimalSource === 'ASSET' && data.decimals) {
label = addDecimalsFormatNumber(size, data.decimals);
}
}
return (
<p>
<span>{label}</span>&nbsp;
<AssetLink assetId={assetId} showAssetSymbol={true} asDialog={true} />
</p>
);
};
export default SizeInAsset;
@@ -3,7 +3,7 @@ import { useExplorerMarketQuery } from '../links/market-link/__generated__/Marke
export type DecimalSource = 'MARKET';
export type SizeInMarketProps = {
export type PriceInMarketProps = {
marketId: string;
size?: string | number;
decimalSource?: DecimalSource;
@@ -17,7 +17,7 @@ const SizeInMarket = ({
marketId,
size,
decimalSource = 'MARKET',
}: SizeInMarketProps) => {
}: PriceInMarketProps) => {
const { data } = useExplorerMarketQuery({
variables: { id: marketId },
fetchPolicy: 'cache-first',
@@ -10,7 +10,7 @@ import { MemoryRouter } from 'react-router-dom';
type Deposit = components['schemas']['vegaBuiltinAssetDeposit'];
const fullMock: Deposit = {
partyId: '0000000000000000000000000000000000000000000000000000000000000001',
partyId: 'party123',
vegaAssetId: 'asset123',
amount: 'amount123',
};
@@ -10,7 +10,7 @@ import { MemoryRouter } from 'react-router-dom';
type Withdrawal = components['schemas']['vegaBuiltinAssetWithdrawal'];
const fullMock: Withdrawal = {
partyId: '0000000000000000000000000000000000000000000000000000000000000001',
partyId: 'party123',
vegaAssetId: 'asset123',
amount: 'amount123',
};
@@ -67,12 +67,11 @@ describe('Chain Event: Builtin asset withdrawal', () => {
expect(screen.getByText(`${fullMock.amount}`)).toBeInTheDocument();
expect(screen.getByText(t('Recipient'))).toBeInTheDocument();
const partyLink = screen.getByText(`${fullMock.partyId}`);
expect(partyLink).toBeInTheDocument();
if (!partyLink.parentElement) {
throw new Error('Party link does not exist');
}
expect(partyLink.parentElement.tagName).toEqual('A');
expect(partyLink.parentElement.getAttribute('href')).toEqual(
`/parties/${fullMock.partyId}`
@@ -10,12 +10,10 @@ import { TxDetailsChainEventDeposit } from './tx-erc20-deposit';
type Deposit = components['schemas']['vegaERC20Deposit'];
const fullMock: Deposit = {
vegaAssetId:
'0000000000000000000000000000000000000000000000000000000000000002',
vegaAssetId: 'asset123',
amount: 'amount123',
sourceEthereumAddress: 'eth123',
targetPartyId:
'0000000000000000000000000000000000000000000000000000000000000001',
targetPartyId: 'vega123',
};
describe('Chain Event: ERC20 asset deposit', () => {
@@ -13,8 +13,7 @@ const fullMock: Deposit = {
amount: 'amount123',
blockTime: 'block123',
ethereumAddress: 'eth123',
vegaPublicKey:
'0000000000000000000000000000000000000000000000000000000000000001',
vegaPublicKey: 'vega123',
};
describe('Chain Event: Stake deposit', () => {
@@ -13,8 +13,7 @@ const fullMock: Remove = {
amount: 'amount123',
blockTime: 'block123',
ethereumAddress: 'eth123',
vegaPublicKey:
'0000000000000000000000000000000000000000000000000000000000000001',
vegaPublicKey: 'vega123',
};
describe('Chain Event: Stake remove', () => {
@@ -36,7 +36,7 @@ export const ChainResponseCode = ({
error && error.length > 100 ? error.replace(/,/g, ',\r\n') : error;
return (
<div title={`Response code: ${code} - ${label}`} className="inline-block">
<div title={`Response code: ${code} - ${label}`}>
<span
className="mr-2"
aria-label={isSuccess ? 'Success' : 'Warning'}
@@ -14,14 +14,10 @@ interface TxDetailsSharedProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
// A transitional property used in some complex TX types to display more detailed type information
// than the shared component can derive
hideTypeRow?: boolean;
}
// Applied to all header cells
export const sharedHeaderProps = {
const sharedHeaderProps = {
// Ensures that multi line contents still have the header aligned to the first line
className: 'align-top',
};
@@ -35,7 +31,6 @@ export const TxDetailsShared = ({
txData,
pubKey,
blockData,
hideTypeRow = false,
}: TxDetailsSharedProps) => {
if (!txData) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
@@ -46,12 +41,10 @@ export const TxDetailsShared = ({
return (
<>
{hideTypeRow === false ? (
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
<TableCell>{txData.type}</TableCell>
</TableRow>
) : null}
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
<TableCell>{txData.type}</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Hash')}</TableCell>
<TableCell>
@@ -1,138 +0,0 @@
import { t } from '@vegaprotocol/react-helpers';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import { PartyLink } from '../../../../links';
import {
SPECIAL_CASE_NETWORK,
SPECIAL_CASE_NETWORK_ID,
} from '../../../../links/party-link/party-link';
import SizeInAsset from '../../../../size-in-asset/size-in-asset';
import { AccountTypeMapping } from '@vegaprotocol/types';
import { AccountType } from '@vegaprotocol/types';
import { headerClasses, wrapperClasses } from '../transfer-details';
import type { Transfer } from '../transfer-details';
interface TransferParticipantsProps {
transfer: Transfer;
from: string;
}
/**
* Renders a box containing the To, From and amount of a
* transfer. This is shown for all transfers, including
* recurring and reward transfers.
*
* @param transfer A recurring transfer object
* @param from The sender is not in the transaction, but comes from the Transaction submitter
*/
export function TransferParticipants({
transfer,
from,
}: TransferParticipantsProps) {
// This mapping is required as the global account types require a type to be set, while
// the underlying protobufs allow for every field to be undefined.
const fromAcct =
transfer.fromAccountType &&
transfer.fromAccountType !== 'ACCOUNT_TYPE_UNSPECIFIED'
? AccountType[transfer.fromAccountType]
: AccountType.ACCOUNT_TYPE_GENERAL;
const fromAccountTypeLabel = transfer.fromAccountType
? AccountTypeMapping[fromAcct]
: 'Unknown';
const toAcct =
transfer.toAccountType &&
transfer.toAccountType !== 'ACCOUNT_TYPE_UNSPECIFIED'
? AccountType[transfer.toAccountType]
: AccountType.ACCOUNT_TYPE_GENERAL;
const toAccountTypeLabel = transfer.fromAccountType
? AccountTypeMapping[toAcct]
: 'Unknown';
return (
<div className={wrapperClasses}>
<h2 className={headerClasses}>{t('Transfer')}</h2>
<div className="relative block rounded-lg py-6 text-center">
<PartyLink id={from} truncate={true} />
<Tooltip
description={
<p>{`${t('From account')}: ${fromAccountTypeLabel}`}</p>
}
>
<span>
<Icon className="ml-3" name={'bank-account'} />
</span>
</Tooltip>
<br />
{/* This block of divs is used to render the inset arrow containing the transfer amount */}
<div className="bg-vega-light-200 dark:vega-dark-200 flex items-center justify-center my-4 relative">
<div className="bg-vega-light-200 dark:bg-vega-dark-200 border w-full pt-5 pb-3 px-3 border-vega-light-200 dark:border-vega-dark-150 relative">
<div className="text-xs z-20 relative leading-none">
{transfer.asset ? (
<SizeInAsset assetId={transfer.asset} size={transfer.amount} />
) : null}
</div>
{/* Empty divs for the top arrow and the bottom arrow of the transfer inset */}
<div className="z-10 absolute top-[-1px] left-1/2 w-4 h-4">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 9"
className="fill-vega-light-100 dark:fill-black"
>
<path d="M0,0L8,9l8,-9Z" />
</svg>
</div>
<div className="z-10 absolute bottom-[-16px] left-1/2 w-4 h-4">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 9"
className="fill-vega-light-100 dark:fill-vega-dark-200"
>
<path d="M0,0L8,9l8,-9Z" />
</svg>
</div>
{/*
<div className="z-10 absolute top-0 left-1/2 transform -translate-x-1/2 -translate-y-1/2 rotate-45 w-4 h-4 dark:border-vega-dark-200 border-vega-light-200 bg-white dark:bg-black border-r border-b"></div>
<div className="z-10 absolute bottom-0 left-1/2 transform -translate-x-1/2 translate-y-1/2 rotate-45 w-4 h-4 border-vega-light-200 dark:border-vega-dark-200 bg-vega-light-200 dark:bg-vega-dark-200 border-r border-b"></div>
*/}
</div>
</div>
<TransferRecurringRecipient to={transfer.to} />
<Tooltip
description={<p>{`${t('To account')}: ${toAccountTypeLabel}`}</p>}
>
<span>
<Icon className="ml-3" name={'bank-account'} />
</span>
</Tooltip>
<br />
</div>
</div>
);
}
interface TransferRecurringRecipientProps {
to?: string;
}
/**
* If the transfer is to 000...000, then this is a transfer to the
* Rewards Pool rather than the network. This component saves this
* logic from complicating the To section of the participants block
*
* @param markets String[] IDs of markets for this dispatch strategy
*/
export function TransferRecurringRecipient({
to,
}: TransferRecurringRecipientProps) {
if (to === SPECIAL_CASE_NETWORK || to === SPECIAL_CASE_NETWORK_ID) {
return <span>{t('Rewards pool')}</span>;
} else if (to) {
return <PartyLink id={to} truncate={true} />;
}
// Fallback should not happen
return null;
}
@@ -1,86 +0,0 @@
import { t } from '@vegaprotocol/react-helpers';
import { Icon } from '@vegaprotocol/ui-toolkit';
import EpochOverview from '../../../../epoch-overview/epoch';
import { useExplorerFutureEpochQuery } from '../../../../epoch-overview/__generated__/Epoch';
import { headerClasses, wrapperClasses } from '../transfer-details';
import type { IconProps } from '@vegaprotocol/ui-toolkit';
import type { Recurring } from '../transfer-details';
interface TransferRepeatProps {
recurring: Recurring;
}
/**
* Renderer for a transfer. These can vary quite
* widely, essentially every field can be null.
*
* @param transfer A recurring transfer object
*/
export function TransferRepeat({ recurring }: TransferRepeatProps) {
const { data } = useExplorerFutureEpochQuery();
if (!recurring) {
return null;
}
return (
<div className={wrapperClasses}>
<h2 className={headerClasses}>{t('Active epochs')}</h2>
<div className="relative block rounded-lg py-6 text-center p-6">
<p>
<EpochOverview id={recurring.startEpoch} />
</p>
<p className="leading-10 my-2">
<IconForEpoch
start={recurring.startEpoch}
end={recurring.endEpoch}
current={data?.epoch.id}
/>
</p>
<p>
{recurring.endEpoch ? (
<EpochOverview id={recurring.endEpoch} />
) : (
<span>{t('Forever')}</span>
)}
</p>
</div>
</div>
);
}
export type IconForTransferProps = {
current?: string;
start?: string;
end?: string;
};
/**
* Pick an icon rto represent the state of the repetition for this recurring
* transfer. It can be unstarted, in progress, or complete.
*
* @param start The epoch in which the transfer first occurs
* @param end The last epoch in which the transfer occurs
* @param current The current epoch
*/
function IconForEpoch({ start, end, current }: IconForTransferProps) {
let i: IconProps['name'] = 'repeat';
if (current && start && end) {
const startEpoch = parseInt(start);
const endEpoch = parseInt(end);
const currentEpoch = parseInt(current);
if (currentEpoch > endEpoch) {
// If we've finished
i = 'updated';
} else if (startEpoch > currentEpoch) {
// If we haven't yet started
i = 'time';
} else if (startEpoch < currentEpoch && endEpoch > currentEpoch) {
i = 'repeat';
}
}
return <Icon name={i} className="mr-2" />;
}
@@ -1,97 +0,0 @@
import { t } from '@vegaprotocol/react-helpers';
import { AssetLink, MarketLink } from '../../../../links';
import { headerClasses, wrapperClasses } from '../transfer-details';
import type { components } from '../../../../../../types/explorer';
import type { Recurring } from '../transfer-details';
import { DispatchMetricLabels } from '@vegaprotocol/types';
export type Metric = components['schemas']['vegaDispatchMetric'];
export type Strategy = components['schemas']['vegaDispatchStrategy'];
const metricLabels = {
DISPATCH_METRIC_UNSPECIFIED: 'Unknown metric',
...DispatchMetricLabels,
};
interface TransferRewardsProps {
recurring: Recurring;
}
/**
* Renderer for a transfer. These can vary quite
* widely, essentially every field can be null.
*
* @param transfer A recurring transfer object
*/
export function TransferRewards({ recurring }: TransferRewardsProps) {
const metric =
recurring?.dispatchStrategy?.metric || 'DISPATCH_METRIC_UNSPECIFIED';
if (!recurring || !recurring.dispatchStrategy) {
return null;
}
return (
<div className={wrapperClasses}>
<h2 className={headerClasses}>{t('Reward metrics')}</h2>
<ul className="relative block rounded-lg py-6 text-center p-6">
{recurring.dispatchStrategy.assetForMetric ? (
<li>
<strong>{t('Asset')}</strong>:{' '}
<AssetLink assetId={recurring.dispatchStrategy.assetForMetric} />
</li>
) : null}
<li>
<strong>{t('Metric')}</strong>: {metricLabels[metric]}
</li>
{recurring.dispatchStrategy.markets &&
recurring.dispatchStrategy.markets.length > 0 ? (
<li>
<strong>{t('Markets in scope')}</strong>:
<ul>
{recurring.dispatchStrategy.markets.map((m) => (
<li key={m}>
<MarketLink id={m} />
</li>
))}
</ul>
</li>
) : null}
<li>
<strong>{t('Factor')}</strong>: {recurring.factor}
</li>
</ul>
</div>
);
}
interface TransferRecurringStrategyProps {
strategy: Strategy;
}
/**
* Simple renderer for a dispatch strategy in a recurring transfer
*
* @param strategy Dispatch strategy object
*/
export function TransferRecurringStrategy({
strategy,
}: TransferRecurringStrategyProps) {
if (!strategy) {
return null;
}
return (
<>
{strategy.assetForMetric ? (
<li>
<strong>{t('Asset for metric')}</strong>:{' '}
<AssetLink assetId={strategy.assetForMetric} />
</li>
) : null}
<li>
<strong>{t('Metric')}</strong>: {strategy.metric}
</li>
</>
);
}
@@ -1,39 +0,0 @@
import type { components } from '../../../../../types/explorer';
import { TransferRepeat } from './blocks/transfer-repeat';
import { TransferRewards } from './blocks/transfer-rewards';
import { TransferParticipants } from './blocks/transfer-participants';
export type Recurring = components['schemas']['v1RecurringTransfer'];
export type Metric = components['schemas']['vegaDispatchMetric'];
export const wrapperClasses =
'border border-vega-light-150 dark:border-vega-dark-200 rounded-md pv-2 mb-5 w-full sm:w-1/4 min-w-[200px] ';
export const headerClasses =
'bg-solid bg-vega-light-150 dark:bg-vega-dark-150 border-vega-light-150 text-center text-xl py-2 font-alpha';
export type Transfer = components['schemas']['commandsv1Transfer'];
interface TransferDetailsProps {
transfer: Transfer;
from: string;
}
/**
* Renderer for a transfer. These can vary quite
* widely, essentially every field can be null.
*
* @param transfer A recurring transfer object
*/
export function TransferDetails({ transfer, from }: TransferDetailsProps) {
const recurring = transfer.recurring;
return (
<div className="flex gap-5 flex-wrap">
<TransferParticipants from={from} transfer={transfer} />
{recurring ? <TransferRepeat recurring={transfer.recurring} /> : null}
{recurring && recurring.dispatchStrategy ? (
<TransferRewards recurring={transfer.recurring} />
) : null}
</div>
);
}
@@ -23,7 +23,6 @@ import { TxDetailsProtocolUpgrade } from './tx-details-protocol-upgrade';
import { TxDetailsIssueSignatures } from './tx-issue-signatures';
import { TxDetailsNodeAnnounce } from './tx-node-announce';
import { TxDetailsStateVariable } from './tx-state-variable-proposal';
import { TxDetailsTransfer } from './tx-transfer';
interface TxDetailsWrapperProps {
txData: BlockExplorerTransactionResult | undefined;
@@ -109,8 +108,6 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
return TxDetailsUndelegate;
case 'State Variable Proposal':
return TxDetailsStateVariable;
case 'Transfer Funds':
return TxDetailsTransfer;
default:
return TxDetailsGeneric;
}
@@ -1,125 +0,0 @@
import { t } from '@vegaprotocol/react-helpers';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { sharedHeaderProps, TxDetailsShared } from './shared/tx-details-shared';
import { TableRow, TableCell, TableWithTbody } from '../../table';
import type { components } from '../../../../types/explorer';
import { PartyLink } from '../../links';
import SizeInAsset from '../../size-in-asset/size-in-asset';
import { TransferDetails } from './transfer/transfer-details';
import {
SPECIAL_CASE_NETWORK,
SPECIAL_CASE_NETWORK_ID,
} from '../../links/party-link/party-link';
type Transfer = components['schemas']['commandsv1Transfer'];
interface TxDetailsNodeAnnounceProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* Displays the details of a transfer. Broadly there are three distinct
* types of transfer, listed below in order of complexity:
*
* - A one off transfer
* - A recurring transfer
* - A recurring rewards pool transfer
*
* One off transfers are simple, really the important data is the amount
* and who sent it to whom. This is rendered as one distinct box.
*
* A recurring transfer has two components - the same as above, and an
* additional box that shows details about how it repeats. This is defined
* as a start epoch and and end epoch. The Epoch/MissingEpoch components
* render slightly differently depending on if the epoch is in the past,
* current or in the future.
*
* Finally rewards pool transfers get the two boxes above, and an additional
* one that describes how the reward is distributed.
*
* The information is split up in to three boxes to allow for the reuse across
* all the types of transfer above.
*/
export const TxDetailsTransfer = ({
txData,
pubKey,
blockData,
}: TxDetailsNodeAnnounceProps) => {
if (!txData) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const transfer: Transfer = txData.command.transfer;
if (!transfer) {
return <>{t('Transfer data missing')}</>;
}
const from = txData.submitter;
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
<TableCell>{getTypeLabelForTransfer(transfer)}</TableCell>
</TableRow>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
blockData={blockData}
hideTypeRow={true}
/>
{from ? (
<TableRow modifier="bordered">
<TableCell>{t('From')}</TableCell>
<TableCell>
<PartyLink id={from} />
</TableCell>
</TableRow>
) : null}
{transfer.to ? (
<TableRow modifier="bordered">
<TableCell>{t('To')}</TableCell>
<TableCell>
<PartyLink id={transfer.to} />
</TableCell>
</TableRow>
) : null}
{transfer.asset && transfer.amount ? (
<TableRow modifier="bordered">
<TableCell>{t('Amount')}</TableCell>
<TableCell>
<SizeInAsset assetId={transfer.asset} size={transfer.amount} />
</TableCell>
</TableRow>
) : null}
</TableWithTbody>
<TransferDetails from={from} transfer={transfer} />
</>
);
};
/**
* Gets a string description of this transfer
* @param txData A full transfer
* @returns string Transfer label
*/
export function getTypeLabelForTransfer(tx: Transfer) {
if (tx.to === SPECIAL_CASE_NETWORK || tx.to === SPECIAL_CASE_NETWORK_ID) {
if (tx.recurring && tx.recurring.dispatchStrategy) {
return 'Reward top up transfer';
}
// Else: we don't know that it's a reward transfer, so let's not guess
} else if (tx.recurring) {
return 'Recurring transfer';
} else if (tx.oneOff) {
// Currently redundant, but could be used to indicate something more specific
return 'Transfer';
}
return 'Transfer';
}
@@ -14,17 +14,14 @@ interface StringMap {
// Using https://github.com/vegaprotocol/protos/blob/e0f646ce39aab1fc66a9200ceec0262306d3beb3/commands/transaction.go#L93 as a reference
const displayString: StringMap = {
OrderSubmission: 'Order Submission',
'Submit Order': 'Order',
OrderCancellation: 'Order Cancellation',
OrderAmendment: 'Order Amendment',
VoteSubmission: 'Vote Submission',
WithdrawSubmission: 'Withdraw Submission',
Withdraw: 'Withdraw Request',
LiquidityProvisionSubmission: 'LP order',
'Liquidity Provision Order': 'LP order',
LiquidityProvisionCancellation: 'LP cancel',
LiquidityProvisionAmendment: 'LP update',
'Amend LiquidityProvision Order': 'Amend LP',
LiquidityProvisionSubmission: 'Liquidity Provision',
LiquidityProvisionCancellation: 'Liquidity Cancellation',
LiquidityProvisionAmendment: 'Liquidity Amendment',
ProposalSubmission: 'Governance Proposal',
AnnounceNode: 'Node Announcement',
NodeVote: 'Node Vote',
@@ -34,11 +31,10 @@ const displayString: StringMap = {
DelegateSubmission: 'Delegation',
UndelegateSubmission: 'Undelegation',
KeyRotateSubmission: 'Key Rotation',
StateVariableProposal: 'State Variable',
StateVariableProposal: 'State Variable Proposal',
Transfer: 'Transfer',
CancelTransfer: 'Cancel Transfer',
ValidatorHeartbeat: 'Heartbeat',
'Batch Market Instructions': 'Batch',
ValidatorHeartbeat: 'Validator Heartbeat',
};
/**
@@ -136,8 +132,7 @@ export function getLabelForChainEvent(
export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
let type = displayString[orderType] || orderType;
let colours =
'text-white dark:text-white bg-vega-dark-150 dark:bg-vega-dark-150';
let colours = 'text-white dark:text-white bg-zinc-800 dark:bg-zinc-800';
// This will get unwieldy and should probably produce a different colour of tag
if (type === 'Chain Event' && !!command?.chainEvent) {
@@ -1,58 +0,0 @@
import { getTypeLabelForTransfer } from './details/tx-transfer';
import type { components } from '../../../types/explorer';
type Transfer = components['schemas']['commandsv1Transfer'];
describe('TX: Transfer: getLabelForTransfer', () => {
it('renders reward top up label if the TO party is 000', () => {
const mock: Transfer = {
to: '0000000000000000000000000000000000000000000000000000000000000000',
recurring: {
dispatchStrategy: {},
},
};
expect(getTypeLabelForTransfer(mock)).toEqual('Reward top up transfer');
});
it('renders reward top up label if the TO party is network', () => {
const mock = {
to: 'network',
recurring: {
dispatchStrategy: {},
},
};
expect(getTypeLabelForTransfer(mock)).toEqual('Reward top up transfer');
});
it('renders recurring label if the tx has a recurring property', () => {
const mock: Transfer = {
to: '0000000000000000000000000000000000000000000000000000000000000001',
recurring: {
startEpoch: '0',
},
};
expect(getTypeLabelForTransfer(mock)).toEqual('Recurring transfer');
});
it('renders one off label if the tx has a oneOff property', () => {
const mock: Transfer = {
to: '0000000000000000000000000000000000000000000000000000000000000001',
oneOff: {
deliverOn: '0',
},
};
expect(getTypeLabelForTransfer(mock)).toEqual('Transfer');
});
it('renders one off label otherwise', () => {
const mock: Transfer = {
to: '0000000000000000000000000000000000000000000000000000000000000001',
};
expect(getTypeLabelForTransfer(mock)).toEqual('Transfer');
});
});
@@ -37,9 +37,7 @@ export const TxsInfiniteListItem = ({
className="text-sm col-span-10 xl:col-span-3 leading-none"
data-testid="tx-hash"
>
<span className="xl:hidden uppercase text-vega-dark-300">
ID:&nbsp;
</span>
<span className="xl:hidden uppercase text-zinc-500">ID:&nbsp;</span>
<TruncatedLink
to={`/${Routes.TX}/${toHex(hash)}`}
text={hash}
@@ -51,9 +49,7 @@ export const TxsInfiniteListItem = ({
className="text-sm col-span-10 xl:col-span-3 leading-none"
data-testid="pub-key"
>
<span className="xl:hidden uppercase text-vega-dark-300">
By:&nbsp;
</span>
<span className="xl:hidden uppercase text-zinc-500">By:&nbsp;</span>
<TruncatedLink
to={`/${Routes.PARTIES}/${submitter}`}
text={submitter}
@@ -68,9 +64,7 @@ export const TxsInfiniteListItem = ({
className="text-sm col-span-3 xl:col-span-1 leading-none flex items-center"
data-testid="tx-block"
>
<span className="xl:hidden uppercase text-vega-dark-300">
Block:&nbsp;
</span>
<span className="xl:hidden uppercase text-zinc-500">Block:&nbsp;</span>
<TruncatedLink
to={`/${Routes.BLOCKS}/${block}`}
text={block}
@@ -82,7 +76,7 @@ export const TxsInfiniteListItem = ({
className="text-sm col-span-2 xl:col-span-1 leading-none flex items-center"
data-testid="tx-success"
>
<span className="xl:hidden uppercase text-vega-dark-300">
<span className="xl:hidden uppercase text-zinc-500">
Success:&nbsp;
</span>
{isNumber(code) ? (
@@ -94,7 +94,7 @@ export const TxsInfiniteList = ({
return (
<div className={className} data-testid="transactions-list">
<div className="xl:grid grid-cols-10 w-full mb-3 hidden text-vega-dark-300 uppercase">
<div className="xl:grid grid-cols-10 w-full mb-3 hidden text-zinc-500 uppercase">
<div className="col-span-3">
<span className="hidden xl:inline">Transaction &nbsp;</span>
<span>ID</span>
-3
View File
@@ -33,7 +33,4 @@ export const ENV = {
parties: truthy.includes(windowOrDefault('NX_EXPLORER_PARTIES')),
validators: truthy.includes(windowOrDefault('NX_EXPLORER_VALIDATORS')),
},
addresses: {
feedback: windowOrDefault('NX_GITHUB_FEEDBACK_URL'),
},
};
@@ -1,50 +0,0 @@
import { t } from '@vegaprotocol/react-helpers';
import { RouteTitle } from '../../components/route-title';
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
import { useScrollToLocation } from '../../hooks/scroll-to-location';
import { useDocumentTitle } from '../../hooks/use-document-title';
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets';
import { useParams } from 'react-router-dom';
import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog';
import { useState } from 'react';
export const AssetPage = () => {
useDocumentTitle(['Assets']);
useScrollToLocation();
const { assetId } = useParams<{ assetId: string }>();
const { data, loading, error } = useAssetDataProvider(assetId || '');
const title = data ? data.name : error ? t('Asset not found') : '';
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
return (
<>
<section className="relative">
<RouteTitle data-testid="asset-header">{title}</RouteTitle>
<AsyncRenderer
noDataMessage={t('Asset not found')}
data={data}
loading={loading}
error={error}
>
<div className="absolute top-0 right-0">
<Button size="xs" onClick={() => setDialogOpen(true)}>
{t('View JSON')}
</Button>
</div>
<div className="h-full relative">
<AssetDetailsTable asset={data as AssetFieldsFragment} />
</div>
</AsyncRenderer>
</section>
<JsonViewerDialog
open={dialogOpen}
onChange={(isOpen) => setDialogOpen(isOpen)}
title={data?.name || ''}
content={data}
/>
</>
);
};
@@ -6,7 +6,7 @@ import { useDocumentTitle } from '../../hooks/use-document-title';
import { useAssetsDataProvider } from '@vegaprotocol/assets';
import { AssetsTable } from '../../components/assets/assets-table';
export const AssetsPage = () => {
export const Assets = () => {
useDocumentTitle(['Assets']);
useScrollToLocation();
@@ -1,2 +1 @@
export * from './assets-page';
export * from './asset-page';
export * from './assets';
@@ -0,0 +1,82 @@
query ExplorerProposals {
proposalsConnection {
edges {
node {
id
rationale {
title
description
}
reference
state
datetime
rejectionReason
party {
id
}
terms {
closingDatetime
enactmentDatetime
change {
... on NewMarket {
instrument {
name
}
}
... on UpdateMarket {
marketId
}
... on NewAsset {
__typename
symbol
source {
... on BuiltinAsset {
maxFaucetAmountMint
}
... on ERC20 {
contractAddress
}
}
}
... on UpdateNetworkParameter {
networkParameter {
key
value
}
}
}
}
votes {
yes {
totalTokens
totalNumber
votes {
value
party {
id
stakingSummary {
currentStakeAvailable
}
}
datetime
}
}
no {
totalTokens
totalNumber
votes {
value
party {
id
stakingSummary {
currentStakeAvailable
}
}
datetime
}
}
}
}
}
}
}
@@ -0,0 +1,122 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerProposalsQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type ExplorerProposalsQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename: 'NewAsset', symbol: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string } } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, votes?: Array<{ __typename?: 'Vote', value: Types.VoteValue, datetime: any, party: { __typename?: 'Party', id: string, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string } } }> | null }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, votes?: Array<{ __typename?: 'Vote', value: Types.VoteValue, datetime: any, party: { __typename?: 'Party', id: string, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string } } }> | null } } } } | null> | null } | null };
export const ExplorerProposalsDocument = gql`
query ExplorerProposals {
proposalsConnection {
edges {
node {
id
rationale {
title
description
}
reference
state
datetime
rejectionReason
party {
id
}
terms {
closingDatetime
enactmentDatetime
change {
... on NewMarket {
instrument {
name
}
}
... on UpdateMarket {
marketId
}
... on NewAsset {
__typename
symbol
source {
... on BuiltinAsset {
maxFaucetAmountMint
}
... on ERC20 {
contractAddress
}
}
}
... on UpdateNetworkParameter {
networkParameter {
key
value
}
}
}
}
votes {
yes {
totalTokens
totalNumber
votes {
value
party {
id
stakingSummary {
currentStakeAvailable
}
}
datetime
}
}
no {
totalTokens
totalNumber
votes {
value
party {
id
stakingSummary {
currentStakeAvailable
}
}
datetime
}
}
}
}
}
}
}
`;
/**
* __useExplorerProposalsQuery__
*
* To run a query within a React component, call `useExplorerProposalsQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerProposalsQuery` 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 } = useExplorerProposalsQuery({
* variables: {
* },
* });
*/
export function useExplorerProposalsQuery(baseOptions?: Apollo.QueryHookOptions<ExplorerProposalsQuery, ExplorerProposalsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerProposalsQuery, ExplorerProposalsQueryVariables>(ExplorerProposalsDocument, options);
}
export function useExplorerProposalsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerProposalsQuery, ExplorerProposalsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerProposalsQuery, ExplorerProposalsQueryVariables>(ExplorerProposalsDocument, options);
}
export type ExplorerProposalsQueryHookResult = ReturnType<typeof useExplorerProposalsQuery>;
export type ExplorerProposalsLazyQueryHookResult = ReturnType<typeof useExplorerProposalsLazyQuery>;
export type ExplorerProposalsQueryResult = Apollo.QueryResult<ExplorerProposalsQuery, ExplorerProposalsQueryVariables>;
@@ -1 +1,63 @@
export * from './proposals-page';
import { t } from '@vegaprotocol/react-helpers';
import React from 'react';
import { RouteTitle } from '../../components/route-title';
import { SubHeading } from '../../components/sub-heading';
import { Loader, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { useExplorerProposalsQuery } from './__generated__/Proposals';
import { useDocumentTitle } from '../../hooks/use-document-title';
import EmptyList from '../../components/empty-list/empty-list';
const Governance = () => {
const { data, loading } = useExplorerProposalsQuery({
errorPolicy: 'ignore',
});
useDocumentTitle();
if (!data || !data.proposalsConnection || !data.proposalsConnection.edges) {
if (!loading) {
return (
<section>
<RouteTitle data-testid="governance-header">
{t('Governance Proposals')}
</RouteTitle>
<EmptyList
heading={t('This chain has no proposals')}
label={t('0 proposals')}
/>
</section>
);
} else {
return <Loader />;
}
}
const proposals = data?.proposalsConnection?.edges.map((e) => {
return e?.node;
});
return (
<section>
<RouteTitle data-testid="governance-header">
{t('Governance Proposals')}
</RouteTitle>
{proposals.map((p) => {
if (!p || !p.id) {
return null;
}
return (
<React.Fragment key={p.id}>
<SubHeading>
{p.rationale.title || p.rationale.description}
</SubHeading>
<SyntaxHighlighter data={p} />
</React.Fragment>
);
})}
</section>
);
};
export default Governance;
@@ -1,33 +0,0 @@
import { proposalsDataProvider } from '@vegaprotocol/governance';
import { t, useDataProvider } from '@vegaprotocol/react-helpers';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { ProposalsTable } from '../../components/proposals/proposals-table';
import { RouteTitle } from '../../components/route-title';
import { useScrollToLocation } from '../../hooks/scroll-to-location';
import { useDocumentTitle } from '../../hooks/use-document-title';
export const Proposals = () => {
useScrollToLocation();
const { data, loading, error } = useDataProvider({
dataProvider: proposalsDataProvider,
});
useDocumentTitle([t('Governance Proposals')]);
return (
<section>
<RouteTitle data-testid="proposals-heading">
{t('Governance proposals')}
</RouteTitle>
<AsyncRenderer
noDataMessage={t('This chain has no proposals')}
data={data}
loading={loading}
error={error}
>
<ProposalsTable data={data} />
</AsyncRenderer>
</section>
);
};
@@ -68,7 +68,7 @@ const Party = () => {
return (
<section>
<h1
className="font-alpha uppercase font-xl mb-4 text-vega-dark-100 dark:text-vega-light-100"
className="font-alpha uppercase font-xl mb-4 text-zinc-800 dark:text-zinc-200"
data-testid="parties-header"
>
{t('Public key')}
+4 -13
View File
@@ -1,6 +1,6 @@
import { AssetPage, AssetsPage } from './assets';
import { Assets } from './assets';
import BlockPage from './blocks';
import { Proposals } from './governance';
import Governance from './governance';
import Home from './home';
import OraclePage from './oracles';
import Oracles from './oracles/home';
@@ -53,16 +53,7 @@ const assetsRoutes: Route[] = flags.assets
path: Routes.ASSETS,
text: t('Assets'),
name: 'Assets',
children: [
{
index: true,
element: <AssetsPage />,
},
{
path: ':assetId',
element: <AssetPage />,
},
],
element: <Assets />,
},
]
: [];
@@ -84,7 +75,7 @@ const governanceRoutes: Route[] = flags.governance
path: Routes.GOVERNANCE,
name: 'Governance proposals',
text: t('Governance Proposals'),
element: <Proposals />,
element: <Governance />,
},
]
: [];
+21 -62
View File
@@ -15,7 +15,6 @@ type OneOf<T extends any[]> = T extends [infer Only]
? OneOf<[XOR<A, B>, ...Rest]>
: never;
/* eslint-enable @typescript-eslint/no-explicit-any */
export interface paths {
'/info': {
/**
@@ -41,6 +40,8 @@ export interface paths {
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
/**
@@ -65,7 +66,7 @@ export interface components {
| 'OPERATOR_LESS_THAN'
| 'OPERATOR_LESS_THAN_OR_EQUAL';
/**
* The supported oracle sources
* The supported Oracle sources
* @description - ORACLE_SOURCE_UNSPECIFIED: The default value
* - ORACLE_SOURCE_OPEN_ORACLE: Specifies that the payload will be base64 encoded JSON conforming to the Open Oracle standard
* - ORACLE_SOURCE_JSON: Specifies that the payload will be base64 encoded JSON, but does not specify the shape of the data
@@ -173,7 +174,7 @@ export interface components {
readonly '@type'?: string;
[key: string]: unknown | undefined;
};
/** Used to announce a node as a new pending validator */
/** Used announce a node as a new pending validator */
readonly v1AnnounceNode: {
/** AvatarURL of the validator */
readonly avatarUrl?: string;
@@ -268,13 +269,13 @@ export interface components {
readonly v1ETHAddress: {
readonly address?: string;
};
/** A transaction to allow a validator to rotate their ethereum keys */
/** A transaction to allow validator to rotate their ethereum keys */
readonly v1EthereumKeyRotateSubmission: {
/** Currently used public address */
readonly currentAddress?: string;
/** Signature that can be verified using the new ethereum address */
readonly ethereumSignature?: components['schemas']['v1Signature'];
/** The new address to rotate to */
/** The new adress to rotate to */
readonly newAddress?: string;
/** Ethereum public key to use as a submitter to allow automatic signature generation */
readonly submitterAddress?: string;
@@ -308,9 +309,7 @@ export interface components {
readonly version?: string;
};
readonly v1InputData: {
/** A command used by a node operator to announce its node as a pending validator */
readonly announceNode?: components['schemas']['v1AnnounceNode'];
/** A command to submit a batch of order instructions to a market */
readonly batchMarketInstructions?: components['schemas']['v1BatchMarketInstructions'];
/**
* Format: uint64
@@ -324,35 +323,17 @@ export interface components {
* `block_height` prevents replay attacks in conjunction with `nonce` (see above).
*/
readonly blockHeight?: string;
/** A command to request cancelling a recurring transfer */
readonly cancelTransfer?: components['schemas']['v1CancelTransfer'];
/**
* Command used by a validator to submit an event forwarded to the Vega network to provide information
* on events happening on other networks, to be used by a foreign chain
* to recognise a decision taken by the Vega network
*/
readonly chainEvent?: components['schemas']['v1ChainEvent'];
/** Command to delegate tokens to a validator */
readonly delegateSubmission?: components['schemas']['v1DelegateSubmission'];
/** Command used by a validator to allow given validator to rotate their Ethereum keys */
readonly ethereumKeyRotateSubmission?: components['schemas']['v1EthereumKeyRotateSubmission'];
/** Command used by a validator to submit signatures to a smart contract */
readonly issueSignatures?: components['schemas']['v1IssueSignatures'];
/** Command used by a validator to allow given validator to rotate their Vega keys */
readonly keyRotateSubmission?: components['schemas']['v1KeyRotateSubmission'];
/** Command to request amending a liquidity commitment */
readonly liquidityProvisionAmendment?: components['schemas']['v1LiquidityProvisionAmendment'];
/** Command to request cancelling a liquidity commitment */
readonly liquidityProvisionCancellation?: components['schemas']['v1LiquidityProvisionCancellation'];
/** Command to submit a liquidity commitment */
readonly liquidityProvisionSubmission?: components['schemas']['v1LiquidityProvisionSubmission'];
/** Command used by a validator to submit a signature, to be used by a foreign chain to recognise a decision taken by the Vega network */
readonly nodeSignature?: components['schemas']['v1NodeSignature'];
/**
* Validator commands
* Command used by a validator when a node votes for validating that a given resource exists or is valid,
* for example, an ERC20 deposit is valid and exists on ethereum
*/
/** Validator commands */
readonly nodeVote?: components['schemas']['v1NodeVote'];
/**
* Format: uint64
@@ -368,50 +349,30 @@ export interface components {
* slightly differently, causing a different hash.
*/
readonly nonce?: string;
/**
* Oracles
* Command to submit new oracle data from third party providers
*/
/** Oracles */
readonly oracleDataSubmission?: components['schemas']['v1OracleDataSubmission'];
/** Command to amend an order */
readonly orderAmendment?: components['schemas']['v1OrderAmendment'];
/**
* User commands
* Command to cancel an order
*/
readonly orderCancellation?: components['schemas']['v1OrderCancellation'];
/** A command for submitting an order */
/** User commands */
readonly orderSubmission?: components['schemas']['v1OrderSubmission'];
/** Command to submit a governance proposal */
readonly proposalSubmission?: components['schemas']['v1ProposalSubmission'];
/** Command used by a validator to propose a protocol upgrade */
readonly protocolUpgradeProposal?: components['schemas']['v1ProtocolUpgradeProposal'];
/** Command used by a validator to submit a floating point value */
readonly stateVariableProposal?: components['schemas']['v1StateVariableProposal'];
/** Command to submit a transfer */
readonly transfer?: components['schemas']['commandsv1Transfer'];
/** Command to remove tokens delegated to a validator */
readonly undelegateSubmission?: components['schemas']['v1UndelegateSubmission'];
/**
* Command used by a validator to signal they are still online and validating blocks
* or ready to validate blocks when they are still a pending validator
*/
readonly validatorHeartbeat?: components['schemas']['v1ValidatorHeartbeat'];
/** Command to submit a vote on a governance proposal */
readonly voteSubmission?: components['schemas']['v1VoteSubmission'];
/** Command to submit a withdrawal */
readonly withdrawSubmission?: components['schemas']['v1WithdrawSubmission'];
};
/** A transaction for a validator to submit signatures to a smart contract */
readonly v1IssueSignatures: {
/** The kind of signatures to generate, namely for whether a signer is being added or removed */
readonly kind?: components['schemas']['v1NodeSignatureKind'];
/** The ethereum address which will submit the signatures to the smart contract */
/** The ethereum address which will submit the signatures to the smart-contract */
readonly submitter?: string;
/** The ID of the node that will be signed in or out of the smart contract */
/** The ID of the node that will be signed in or out of the smartcontract */
readonly validatorNodeId?: string;
};
/** A transaction to allow a validator to rotate their Vega keys */
/** A transaction to allow validator to rotate their Vega keys */
readonly v1KeyRotateSubmission: {
/** Hash of currently used public key */
readonly currentPubKeyHash?: string;
@@ -535,8 +496,8 @@ export interface components {
/** Specific details for a one off transfer */
readonly v1OneOffTransfer: {
/**
* A unix timestamp in seconds. Time at which the
* transfer should be delivered into the To account
* A unix timestamp in second. Time at which the
* transfer should be delivered in the to account
* Format: int64
*/
readonly deliverOn?: string;
@@ -551,7 +512,7 @@ export interface components {
readonly payload?: string;
/**
* @description The source from which the data is coming from. Must be base64 encoded.
* Oracle data is a type of external data source data.
* Oracle data a type of external data source data.
*/
readonly source?: components['schemas']['OracleDataSubmissionOracleSource'];
};
@@ -642,7 +603,7 @@ export interface components {
/** Type for the order, required field - See `Order.Type` */
readonly type?: components['schemas']['vegaOrderType'];
};
/** @description PropertyKey describes the property key contained in data source data. */
/** @description PropertyKey describes the property key contained in an data source data. */
readonly v1PropertyKey: {
/** @description name is the name of the property. */
readonly name?: string;
@@ -689,7 +650,6 @@ export interface components {
/** Proposal configuration and the actual change that is meant to be executed when proposal is enacted */
readonly terms?: components['schemas']['vegaProposalTerms'];
};
/** A transaction for a validator to suggest a protocol upgrade */
readonly v1ProtocolUpgradeProposal: {
/**
* The block height at which to perform the upgrade
@@ -748,7 +708,6 @@ export interface components {
*/
readonly pubKey?: components['schemas']['v1PubKey'];
};
/** A transaction for a validator to submit a floating point value */
readonly v1StateVariableProposal: {
/** The state value proposal details */
readonly proposal?: components['schemas']['vegaStateValueProposal'];
@@ -1010,7 +969,7 @@ export interface components {
readonly sourceEthereumAddress?: string;
/** The Vega network internal identifier of the asset */
readonly vegaAssetId?: string;
/** The updated withdrawal threshold */
/** The updated withdraw threshold */
readonly withdrawThreshold?: string;
};
/** An asset allow-listing for an ERC20 token */
@@ -1105,7 +1064,7 @@ export interface components {
/** The ethereum address of the old signer */
readonly oldSigner?: string;
};
/** The threshold has been updated on the multisig control */
/** The threshold have been updated on the multisigcontrol */
readonly vegaERC20ThresholdSet: {
/**
* Format: int64
@@ -1119,13 +1078,13 @@ export interface components {
* Format: int64
*/
readonly newThreshold?: number;
/** The nonce created by the Vega network */
/** The nonce create by the vega network */
readonly nonce?: string;
};
readonly vegaERC20Update: {
/**
* The lifetime limits deposit per address.
* This will be interpreted against the asset decimals.
* This is will be interpreted against the asset decimals.
* note: this is a temporary measure that can be changed by governance
*/
readonly lifetimeLimit?: string;
@@ -1278,7 +1237,7 @@ export interface components {
* price levels over which automated liquidity provision orders will be deployed
*/
readonly lpPriceRange?: string;
/** Optional new market metadata, tags */
/** Optional new market meta data, tags */
readonly metadata?: readonly string[];
/**
* Decimal places for order sizes, sets what size the smallest order / position on the market can be
+64 -322
View File
@@ -2761,7 +2761,7 @@
"tranche_end": "2023-12-05T00:00:00.000Z",
"total_added": "86666.297",
"total_removed": "0",
"locked_amount": "68085.0879931921036624142",
"locked_amount": "69212.7941588870528164928",
"deposits": [
{
"amount": "86666.297",
@@ -2827,7 +2827,7 @@
"tranche_end": "2023-06-01T00:00:00.000Z",
"total_added": "2500",
"total_removed": "0",
"locked_amount": "1370.11249745624725",
"locked_amount": "1435.351483007733",
"deposits": [
{
"amount": "2500",
@@ -3214,8 +3214,8 @@
"tranche_start": "2023-02-01T00:00:00.000Z",
"tranche_end": "2023-08-01T00:00:00.000Z",
"total_added": "37500",
"total_removed": "3009.936598125",
"locked_amount": "33303.35424340085625",
"total_removed": "2836.415649675",
"locked_amount": "34287.34557243708",
"deposits": [
{
"amount": "7500",
@@ -3244,11 +3244,6 @@
"user": "0x0B4e6fcE839B01ef43DA6F890FAC7B1Afb004600",
"tx": "0x78fd0cf1ca8de93d15eee1424146bcd70b84cc168b4c1ea1ac50020b8d67bcc1"
},
{
"amount": "173.52094845",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0x6f125819654942fd6c08c0929e7f9e9629eb65c2cfa92acae0f8db9685a43537"
},
{
"amount": "183.137181525",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
@@ -3284,12 +3279,6 @@
"tranche_id": 34,
"tx": "0x5bfef799be13741d89872cc604df1f35b41069397e681577fc15c039866d8707"
},
{
"amount": "173.52094845",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tranche_id": 34,
"tx": "0x6f125819654942fd6c08c0929e7f9e9629eb65c2cfa92acae0f8db9685a43537"
},
{
"amount": "183.137181525",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
@@ -3304,8 +3293,8 @@
}
],
"total_tokens": "7500",
"withdrawn_tokens": "734.823415425",
"remaining_tokens": "6765.176584575"
"withdrawn_tokens": "561.302466975",
"remaining_tokens": "6938.697533025"
},
{
"address": "0x0B4e6fcE839B01ef43DA6F890FAC7B1Afb004600",
@@ -3337,7 +3326,7 @@
"tranche_end": "2023-12-05T00:00:00.000Z",
"total_added": "129999.45",
"total_removed": "0",
"locked_amount": "68022.9689171925635253",
"locked_amount": "69149.646192904764493335",
"deposits": [
{
"amount": "129999.45",
@@ -3403,7 +3392,7 @@
"tranche_end": "2023-09-03T00:00:00.000Z",
"total_added": "62600",
"total_removed": "0",
"locked_amount": "33228.45556823947058",
"locked_amount": "34043.00988077117846",
"deposits": [
{
"amount": "10000",
@@ -3596,7 +3585,7 @@
"tranche_end": "2023-09-17T00:00:00.000Z",
"total_added": "5000",
"total_removed": "0",
"locked_amount": "2845.810819381025",
"locked_amount": "2910.871067985794",
"deposits": [
{
"amount": "5000",
@@ -3807,7 +3796,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "97499.58",
"total_removed": "0",
"locked_amount": "9946.3975044981425824698",
"locked_amount": "11051.5629572361427967976",
"deposits": [
{
"amount": "97499.58",
@@ -3840,7 +3829,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "135173.4239508",
"total_removed": "98230.390980249184455396",
"locked_amount": "13595.008214756311272005455032",
"locked_amount": "15105.57858979342307160273426",
"deposits": [
{
"amount": "135173.4239508",
@@ -3886,7 +3875,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "32499.86",
"total_removed": "0",
"locked_amount": "4184.277665044903355192",
"locked_amount": "4649.201685825244862006",
"deposits": [
{
"amount": "32499.86",
@@ -3919,7 +3908,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "10833.29",
"total_removed": "0",
"locked_amount": "1361.9417767027505250727",
"locked_amount": "1513.2700339508446292286",
"deposits": [
{
"amount": "10833.29",
@@ -3952,7 +3941,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "22749.93",
"total_removed": "4720.860935375",
"locked_amount": "5091.2425455170145949404",
"locked_amount": "5656.9413696662315119734",
"deposits": [
{
"amount": "6500",
@@ -4103,8 +4092,8 @@
"tranche_start": "2022-11-01T00:00:00.000Z",
"tranche_end": "2023-05-01T00:00:00.000Z",
"total_added": "22500",
"total_removed": "4546.97235645",
"locked_amount": "8545.548457642726575",
"total_removed": "4373.451408",
"locked_amount": "9135.943255064457",
"deposits": [
{
"amount": "7500",
@@ -4128,11 +4117,6 @@
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0x4a7c7462e3bb6abe2d33dfe5a1bf1e4211e52e5e03239c02554988f7d3635523"
},
{
"amount": "173.52094845",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0xb40f1c9a538e1998da74acd7fceea8a718e97b2af8e903277e16a4b729628a52"
},
{
"amount": "167.6680479",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
@@ -4228,12 +4212,6 @@
"tranche_id": 33,
"tx": "0x4a7c7462e3bb6abe2d33dfe5a1bf1e4211e52e5e03239c02554988f7d3635523"
},
{
"amount": "173.52094845",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tranche_id": 33,
"tx": "0xb40f1c9a538e1998da74acd7fceea8a718e97b2af8e903277e16a4b729628a52"
},
{
"amount": "167.6680479",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
@@ -4320,8 +4298,8 @@
}
],
"total_tokens": "7500",
"withdrawn_tokens": "4546.97235645",
"remaining_tokens": "2953.02764355"
"withdrawn_tokens": "4373.451408",
"remaining_tokens": "3126.548592"
},
{
"address": "0x2539b51EbDE65a75672aBcfE9439a706a99D18D1",
@@ -4346,7 +4324,7 @@
"tranche_end": "2023-06-02T00:00:00.000Z",
"total_added": "1939928.38",
"total_removed": "928642.9598472029154",
"locked_amount": "535442.5012105923529199294",
"locked_amount": "560684.945746241749007341",
"deposits": [
{
"amount": "1852091.69",
@@ -7320,15 +7298,10 @@
"tranche_id": 10,
"tranche_start": "2021-07-15T23:37:11.000Z",
"tranche_end": "2021-07-15T23:37:11.000Z",
"total_added": "6259302.299000000000000001",
"total_removed": "6213483.280000000000000001",
"total_added": "6159302.299000000000000001",
"total_removed": "6113483.280000000000000001",
"locked_amount": "0",
"deposits": [
{
"amount": "100000",
"user": "0xb2d6DEC77558Cf8EdB7c428d23E70Eab0688544f",
"tx": "0xa4417547c32fb4936e0dd6ee338ba31b2fd622388fe8a3906d9e7020d5e8371b"
},
{
"amount": "100000",
"user": "0xb2d6DEC77558Cf8EdB7c428d23E70Eab0688544f",
@@ -7851,11 +7824,6 @@
}
],
"withdrawals": [
{
"amount": "100000",
"user": "0xb2d6DEC77558Cf8EdB7c428d23E70Eab0688544f",
"tx": "0xa4417547c32fb4936e0dd6ee338ba31b2fd622388fe8a3906d9e7020d5e8371b"
},
{
"amount": "100000",
"user": "0xb2d6DEC77558Cf8EdB7c428d23E70Eab0688544f",
@@ -8321,12 +8289,6 @@
{
"address": "0xb2d6DEC77558Cf8EdB7c428d23E70Eab0688544f",
"deposits": [
{
"amount": "100000",
"user": "0xb2d6DEC77558Cf8EdB7c428d23E70Eab0688544f",
"tranche_id": 10,
"tx": "0xa4417547c32fb4936e0dd6ee338ba31b2fd622388fe8a3906d9e7020d5e8371b"
},
{
"amount": "100000",
"user": "0xb2d6DEC77558Cf8EdB7c428d23E70Eab0688544f",
@@ -8509,12 +8471,6 @@
}
],
"withdrawals": [
{
"amount": "100000",
"user": "0xb2d6DEC77558Cf8EdB7c428d23E70Eab0688544f",
"tranche_id": 10,
"tx": "0xa4417547c32fb4936e0dd6ee338ba31b2fd622388fe8a3906d9e7020d5e8371b"
},
{
"amount": "100000",
"user": "0xb2d6DEC77558Cf8EdB7c428d23E70Eab0688544f",
@@ -8690,8 +8646,8 @@
"tx": "0xac16a4ce688d40a482a59914d68c3a676592f8804ee8f0781b66a4ba5ccfbdfc"
}
],
"total_tokens": "3056651",
"withdrawn_tokens": "3056651",
"total_tokens": "2956651",
"withdrawn_tokens": "2956651",
"remaining_tokens": "0"
},
{
@@ -36637,8 +36593,8 @@
"tranche_start": "2022-03-05T00:00:00.000Z",
"tranche_end": "2023-06-05T00:00:00.000Z",
"total_added": "3732368.4671",
"total_removed": "609657.626547646980493",
"locked_amount": "847290.027811054165469569256",
"total_removed": "592998.0503546212334",
"locked_amount": "886078.876576593193858643006",
"deposits": [
{
"amount": "1998.95815",
@@ -36807,11 +36763,6 @@
}
],
"withdrawals": [
{
"amount": "16659.576193025747093",
"user": "0xB523235B6c7C74DDB26b10E78bFb2d0Cb63Ae289",
"tx": "0x2c5cfeee95fba21323fdbfde219501aeafe8d7f236d1283b4f8f7c7760e4401c"
},
{
"amount": "144779.049152",
"user": "0x1da69E9C22d77Ef8Ccbf5a1F2d83eDBc5Dcc20fA",
@@ -37911,17 +37862,10 @@
"tx": "0x8cc5159f3d665dd33a2bdac6e990cf1b65dc18b6b5e37a07b37dd54495a8d33c"
}
],
"withdrawals": [
{
"amount": "16659.576193025747093",
"user": "0xB523235B6c7C74DDB26b10E78bFb2d0Cb63Ae289",
"tranche_id": 1,
"tx": "0x2c5cfeee95fba21323fdbfde219501aeafe8d7f236d1283b4f8f7c7760e4401c"
}
],
"withdrawals": [],
"total_tokens": "21666.5743",
"withdrawn_tokens": "16659.576193025747093",
"remaining_tokens": "5006.998106974252907"
"withdrawn_tokens": "0",
"remaining_tokens": "21666.5743"
},
{
"address": "0x7227e17101E6C70F4dAfC7DDB77BB7D83DdfC1C8",
@@ -37975,8 +37919,8 @@
"tranche_start": "2022-06-05T00:00:00.000Z",
"tranche_end": "2023-12-05T00:00:00.000Z",
"total_added": "15870102.715470999700000001",
"total_removed": "572826.70366554650274952",
"locked_amount": "8304123.623039459863359272608269620829954",
"total_removed": "567626.84057226362274952",
"locked_amount": "8441666.3902799341022910153716735157718303",
"deposits": [
{
"amount": "16249.93",
@@ -38510,31 +38454,6 @@
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
"tx": "0x4870598253a2a664d9c96af37904f08ac772ee9620b5975560ef7e7fb51c9a7f"
},
{
"amount": "2820.55961",
"user": "0xF4c75FdbAe821C6B5DBB9f001bB23cBA58D1bA37",
"tx": "0x039c7d1eb6b18a13c3fb91589403d43710e12b319e7d40936280a576502f80fe"
},
{
"amount": "1073.5102073953235",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0x75d6ba37c5621a2bfd39153fdeb79265a5fd36872774966fff58a7a0c476b99b"
},
{
"amount": "404.073696023421875",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0xa0d7984104142e0bfbd90d11fbb3006941b04976943427b1e248514e0d6dbcce"
},
{
"amount": "574.414613957721625",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0x702ab149d41128eefe14cffbca4297575ef4b88a30a15c864af13bf9621e000f"
},
{
"amount": "327.304965906413",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tx": "0xe5fb2af901c286e061280cff77fcf8056e52dffb23b9fc7bba3dfb15abee8abd"
},
{
"amount": "856.08784586478614",
"user": "0xe3eB4CF43C072658401996b71D43eE26E573562D",
@@ -40243,30 +40162,6 @@
"tranche_id": 2,
"tx": "0x307e498402679f0f8ee11dcd3dfc684db5558203469f07ce58e98a4f87f0fa62"
},
{
"amount": "1073.5102073953235",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tranche_id": 2,
"tx": "0x75d6ba37c5621a2bfd39153fdeb79265a5fd36872774966fff58a7a0c476b99b"
},
{
"amount": "404.073696023421875",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tranche_id": 2,
"tx": "0xa0d7984104142e0bfbd90d11fbb3006941b04976943427b1e248514e0d6dbcce"
},
{
"amount": "574.414613957721625",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tranche_id": 2,
"tx": "0x702ab149d41128eefe14cffbca4297575ef4b88a30a15c864af13bf9621e000f"
},
{
"amount": "327.304965906413",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
"tranche_id": 2,
"tx": "0xe5fb2af901c286e061280cff77fcf8056e52dffb23b9fc7bba3dfb15abee8abd"
},
{
"amount": "966.75883976995675",
"user": "0x20CD77B9FC2f1fEDfb6F184E25f7127BFE991C8b",
@@ -41409,8 +41304,8 @@
}
],
"total_tokens": "259998.8875",
"withdrawn_tokens": "123657.945723010546375",
"remaining_tokens": "136340.941776989453625"
"withdrawn_tokens": "121278.642239727666375",
"remaining_tokens": "138720.245260272333625"
},
{
"address": "0x89051CAb67Bc7F8CC44F7e270c6EDaf1EC57676c",
@@ -42070,12 +41965,6 @@
}
],
"withdrawals": [
{
"amount": "2820.55961",
"user": "0xF4c75FdbAe821C6B5DBB9f001bB23cBA58D1bA37",
"tranche_id": 2,
"tx": "0x039c7d1eb6b18a13c3fb91589403d43710e12b319e7d40936280a576502f80fe"
},
{
"amount": "1412.763584",
"user": "0xF4c75FdbAe821C6B5DBB9f001bB23cBA58D1bA37",
@@ -42276,8 +42165,8 @@
}
],
"total_tokens": "200000",
"withdrawn_tokens": "93634.576066",
"remaining_tokens": "106365.423934"
"withdrawn_tokens": "90814.016456",
"remaining_tokens": "109185.983544"
},
{
"address": "0x1b956E6c00E238194B331eddEFF72Cb5f28A8d01",
@@ -43629,8 +43518,8 @@
"tranche_start": "2021-11-05T00:00:00.000Z",
"tranche_end": "2023-05-05T00:00:00.000Z",
"total_added": "14597706.0446472999",
"total_removed": "3930266.448252707129704282",
"locked_amount": "1944868.680169767119412467869710978",
"total_removed": "3926967.723059737571037032",
"locked_amount": "2071847.284667310624156645942280506",
"deposits": [
{
"amount": "129284.449",
@@ -43864,26 +43753,6 @@
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tx": "0x532c270b8a50b8e2923a96b038129a203e01eba1b84faf238dd9b48aeceb1c91"
},
{
"amount": "1488.49150551791738425",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tx": "0x6bacc9b91153b8977b9cabc510975f9d249be098bf8dc841ca72408513d9fbb1"
},
{
"amount": "560.9020402316959745",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tx": "0x6dfd5e6e697a6db058ed45cd16d19ee2bb68b5e3dfecdc52b7829d85e9adbaa0"
},
{
"amount": "797.50404161762011275",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tx": "0xa7b60b86ca0234d8bcfeac24131315f65b0f041aebd74593e73a86c75ea2cce4"
},
{
"amount": "451.82760560232519575",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tx": "0x5fcd1783bbd5f8b519d94782580777ff37d84df742f617baf632fc693615f39a"
},
{
"amount": "1333.9237119810715295",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
@@ -46780,30 +46649,6 @@
"tranche_id": 3,
"tx": "0x532c270b8a50b8e2923a96b038129a203e01eba1b84faf238dd9b48aeceb1c91"
},
{
"amount": "1488.49150551791738425",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tranche_id": 3,
"tx": "0x6bacc9b91153b8977b9cabc510975f9d249be098bf8dc841ca72408513d9fbb1"
},
{
"amount": "560.9020402316959745",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tranche_id": 3,
"tx": "0x6dfd5e6e697a6db058ed45cd16d19ee2bb68b5e3dfecdc52b7829d85e9adbaa0"
},
{
"amount": "797.50404161762011275",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tranche_id": 3,
"tx": "0xa7b60b86ca0234d8bcfeac24131315f65b0f041aebd74593e73a86c75ea2cce4"
},
{
"amount": "451.82760560232519575",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
"tranche_id": 3,
"tx": "0x5fcd1783bbd5f8b519d94782580777ff37d84df742f617baf632fc693615f39a"
},
{
"amount": "1333.9237119810715295",
"user": "0x4Aa3c35F6CC2d507E5C18205ee57099A4C80B19b",
@@ -49248,8 +49093,8 @@
}
],
"total_tokens": "359123.469575",
"withdrawn_tokens": "310868.25548215842910375",
"remaining_tokens": "48255.21409284157089625"
"withdrawn_tokens": "307569.5302891888704365",
"remaining_tokens": "51553.9392858111295635"
},
{
"address": "0xBdd412797c1B78535Afc5F71503b91fAbD0160fB",
@@ -50587,7 +50432,7 @@
"tranche_end": "2023-04-05T00:00:00.000Z",
"total_added": "5778205.3912159303",
"total_removed": "2749067.463242913023296295",
"locked_amount": "451525.974462742142445752680292053",
"locked_amount": "501695.989059927736982047096440314",
"deposits": [
{
"amount": "552496.6455",
@@ -52613,8 +52458,8 @@
"tranche_start": "2022-06-05T00:00:00.000Z",
"tranche_end": "2023-06-05T00:00:00.000Z",
"total_added": "472355.6199999996",
"total_removed": "34093.0379154332685",
"locked_amount": "134257.94822294382169397518924404",
"total_removed": "33643.6376997992685",
"locked_amount": "140404.26303635577567951130086252",
"deposits": [
{
"amount": "3000",
@@ -59298,41 +59143,6 @@
"user": "0x4F4A140E9B4a8403792970C6b5535ed0AA32c8A4",
"tx": "0x181a9afe9b3592427f1ee10f205e01f34ae7be383294e8d93e397766092bf6df"
},
{
"amount": "18.399238968",
"user": "0xD3ec605d078326B0a636ca90d496Ebb5Eb457a27",
"tx": "0xa82e451753cbced76b064af62868d3170dff7afca557680e657c27ebba5c7beb"
},
{
"amount": "141.230663368",
"user": "0xb7df334217A208B5B35a3824D1CDdBCa85BD352B",
"tx": "0x41e9fa9b5003c1d6edf1e9423bd6ae7ce6e4e48fcbce0b47ba17968a67da4d48"
},
{
"amount": "12.107001524",
"user": "0x4eD2b3c68BB4fda084ce1591a210F4aC8b71234A",
"tx": "0xa2e745ec65b1caad05b18aa7c730d0550a263df2208ba10cecee5a30f815447b"
},
{
"amount": "24.023287672",
"user": "0xcB555C5602cC0b2434A27d277e14Cb5C7bF4Ead1",
"tx": "0xbb25fa75431a6463909c9efde31d110e1aa5c945d0363fcc4bd5c46f869cabac"
},
{
"amount": "94.45296804",
"user": "0x7345AaA0D0e4C4A46d921fA9323973956F2b7392",
"tx": "0x10a149356dc25da53994746b06fc8922faa23be9fb781541e91357d7d4f4d36a"
},
{
"amount": "142.126249364",
"user": "0x289047e767D23561A2035cd3fdc60c94ed1bBA3B",
"tx": "0x61b2f8e5f4efb5049e418ede94fd54dfc1ed8d0f1165ff31517f4f748ef14899"
},
{
"amount": "17.060806698",
"user": "0x28FC83947F02f59Cb36b40f97f2D32BBC5D00585",
"tx": "0x9d1ced8d04e8d58af92b1434bcb269487a3f6ddf4305c0739ab0f158f5401ec6"
},
{
"amount": "13.1116203702",
"user": "0x4cBC0C88d8FE503f62823B42f30a4900292C13bE",
@@ -63948,17 +63758,10 @@
"tx": "0x1c2c084e3efafb2ffa09c0b259bd229cce0d6da04dd092d911c7e6136a7ac8a9"
}
],
"withdrawals": [
{
"amount": "141.230663368",
"user": "0xb7df334217A208B5B35a3824D1CDdBCa85BD352B",
"tranche_id": 5,
"tx": "0x41e9fa9b5003c1d6edf1e9423bd6ae7ce6e4e48fcbce0b47ba17968a67da4d48"
}
],
"withdrawals": [],
"total_tokens": "200",
"withdrawn_tokens": "141.230663368",
"remaining_tokens": "58.769336632"
"withdrawn_tokens": "0",
"remaining_tokens": "200"
},
{
"address": "0x287a34EB03418ad55c2e93d0E4FB1e18DCE607ED",
@@ -65971,17 +65774,10 @@
"tx": "0x75de6ca47e0da361181d14aceb5d08e572754861465caa67a12b528886d55307"
}
],
"withdrawals": [
{
"amount": "142.126249364",
"user": "0x289047e767D23561A2035cd3fdc60c94ed1bBA3B",
"tranche_id": 5,
"tx": "0x61b2f8e5f4efb5049e418ede94fd54dfc1ed8d0f1165ff31517f4f748ef14899"
}
],
"withdrawals": [],
"total_tokens": "200",
"withdrawn_tokens": "142.126249364",
"remaining_tokens": "57.873750636"
"withdrawn_tokens": "0",
"remaining_tokens": "200"
},
{
"address": "0x45bAfA40E9e8099cCd9Ed0e3112A11E7a1d37beb",
@@ -67505,12 +67301,6 @@
}
],
"withdrawals": [
{
"amount": "18.399238968",
"user": "0xD3ec605d078326B0a636ca90d496Ebb5Eb457a27",
"tranche_id": 5,
"tx": "0xa82e451753cbced76b064af62868d3170dff7afca557680e657c27ebba5c7beb"
},
{
"amount": "262.76406646",
"user": "0xD3ec605d078326B0a636ca90d496Ebb5Eb457a27",
@@ -67519,8 +67309,8 @@
}
],
"total_tokens": "400",
"withdrawn_tokens": "281.163305428",
"remaining_tokens": "118.836694572"
"withdrawn_tokens": "262.76406646",
"remaining_tokens": "137.23593354"
},
{
"address": "0x7f6aba7563Cb5d31980D440337D3d1A6e3dB58F3",
@@ -72767,12 +72557,6 @@
}
],
"withdrawals": [
{
"amount": "12.107001524",
"user": "0x4eD2b3c68BB4fda084ce1591a210F4aC8b71234A",
"tranche_id": 5,
"tx": "0xa2e745ec65b1caad05b18aa7c730d0550a263df2208ba10cecee5a30f815447b"
},
{
"amount": "15.80745814",
"user": "0x4eD2b3c68BB4fda084ce1591a210F4aC8b71234A",
@@ -72853,8 +72637,8 @@
}
],
"total_tokens": "400",
"withdrawn_tokens": "282.749759004",
"remaining_tokens": "117.250240996"
"withdrawn_tokens": "270.64275748",
"remaining_tokens": "129.35724252"
},
{
"address": "0x5c90765F50629570738fEe7b7FA82ae118f81Ed1",
@@ -77124,12 +76908,6 @@
}
],
"withdrawals": [
{
"amount": "94.45296804",
"user": "0x7345AaA0D0e4C4A46d921fA9323973956F2b7392",
"tranche_id": 5,
"tx": "0x10a149356dc25da53994746b06fc8922faa23be9fb781541e91357d7d4f4d36a"
},
{
"amount": "119.5173516",
"user": "0x7345AaA0D0e4C4A46d921fA9323973956F2b7392",
@@ -77174,8 +76952,8 @@
}
],
"total_tokens": "1200",
"withdrawn_tokens": "850.0164003",
"remaining_tokens": "349.9835997"
"withdrawn_tokens": "755.56343226",
"remaining_tokens": "444.43656774"
},
{
"address": "0x47181cf36aDFb0b5dB7EfDB123f403D890607841",
@@ -77367,12 +77145,6 @@
}
],
"withdrawals": [
{
"amount": "24.023287672",
"user": "0xcB555C5602cC0b2434A27d277e14Cb5C7bF4Ead1",
"tranche_id": 5,
"tx": "0xbb25fa75431a6463909c9efde31d110e1aa5c945d0363fcc4bd5c46f869cabac"
},
{
"amount": "37.881582952",
"user": "0xcB555C5602cC0b2434A27d277e14Cb5C7bF4Ead1",
@@ -77393,8 +77165,8 @@
}
],
"total_tokens": "400",
"withdrawn_tokens": "283.113077116",
"remaining_tokens": "116.886922884"
"withdrawn_tokens": "259.089789444",
"remaining_tokens": "140.910210556"
},
{
"address": "0x3738bec36216eA2F11B954891C65AAd1Bc852156",
@@ -80218,12 +79990,6 @@
}
],
"withdrawals": [
{
"amount": "17.060806698",
"user": "0x28FC83947F02f59Cb36b40f97f2D32BBC5D00585",
"tranche_id": 5,
"tx": "0x9d1ced8d04e8d58af92b1434bcb269487a3f6ddf4305c0739ab0f158f5401ec6"
},
{
"amount": "15.872983256",
"user": "0x28FC83947F02f59Cb36b40f97f2D32BBC5D00585",
@@ -80250,8 +80016,8 @@
}
],
"total_tokens": "200",
"withdrawn_tokens": "142.641089548",
"remaining_tokens": "57.358910452"
"withdrawn_tokens": "125.58028285",
"remaining_tokens": "74.41971715"
},
{
"address": "0xBef628af8547cE5264835F32487055a7e3dF393D",
@@ -81452,7 +81218,7 @@
"tranche_start": "2021-12-05T00:00:00.000Z",
"tranche_end": "2022-06-05T00:00:00.000Z",
"total_added": "171288.42",
"total_removed": "66601.1049690697989",
"total_removed": "66301.1049690697989",
"locked_amount": "0",
"deposits": [
{
@@ -85697,16 +85463,6 @@
"user": "0x217254578EFd323CF93b1E0FC900A2f4C97A2e6e",
"tx": "0xbed9954657a0b981080f8b66187ee3ce27ba8b18dc7419f430624cda2d4e435a"
},
{
"amount": "50",
"user": "0xa40284F5359954E1a0c024132940375FD480C640",
"tx": "0x2d1ecfd5ecefe61fa80d8c7d20b9e4afac178554d364c8796eec0d7fdc8f0942"
},
{
"amount": "250",
"user": "0xc0A603F3B555ba1622B80CF54EAFE6fb717f3Cd7",
"tx": "0xcb9db64f3df7fb6f1addc0073e2a8a5e285618b07d955d4435ff674327cec8e5"
},
{
"amount": "30",
"user": "0x4A13d4dC5e06ACdA81C011D55a7DaAc332bC5Dbf",
@@ -95681,17 +95437,10 @@
"tx": "0xadc25e73736cd85e84fee16f2d7ee41f16f7deb83a52ceb82b327974063964f7"
}
],
"withdrawals": [
{
"amount": "50",
"user": "0xa40284F5359954E1a0c024132940375FD480C640",
"tranche_id": 6,
"tx": "0x2d1ecfd5ecefe61fa80d8c7d20b9e4afac178554d364c8796eec0d7fdc8f0942"
}
],
"withdrawals": [],
"total_tokens": "50",
"withdrawn_tokens": "50",
"remaining_tokens": "0"
"withdrawn_tokens": "0",
"remaining_tokens": "50"
},
{
"address": "0x9603B92De110066121E9D7218482A672bd8afbD9",
@@ -100429,17 +100178,10 @@
"tx": "0x3a0390dffa4280c286016566b29c62e4cec36c6504f7ac3d1de80d22b2903008"
}
],
"withdrawals": [
{
"amount": "250",
"user": "0xc0A603F3B555ba1622B80CF54EAFE6fb717f3Cd7",
"tranche_id": 6,
"tx": "0xcb9db64f3df7fb6f1addc0073e2a8a5e285618b07d955d4435ff674327cec8e5"
}
],
"withdrawals": [],
"total_tokens": "250",
"withdrawn_tokens": "250",
"remaining_tokens": "0"
"withdrawn_tokens": "0",
"remaining_tokens": "250"
},
{
"address": "0xE9e7C70a1e2A5F4A612349871aE18eDCEbB253d9",
@@ -1,5 +1,4 @@
/// <reference types="cypress" />
import { associateTokenStartOfTests } from '../../support/governance.functions';
import {
createUpdateNetworkProposalTxBody,
@@ -22,7 +21,6 @@ context(
before('Connect wallets and set approval', function () {
cy.visit('/');
cy.vega_wallet_set_specified_approval_amount('1000');
associateTokenStartOfTests();
cy.connectVegaWallet();
cy.ethereum_wallet_connect();
cy.ensure_specified_unstaked_tokens_are_associated(1);
@@ -0,0 +1,849 @@
/// <reference types="cypress" />
const vegaWalletUnstakedBalance =
'[data-testid="vega-wallet-balance-unstaked"]';
const vegaWalletStakedBalances =
'[data-testid="vega-wallet-balance-staked-validators"]';
const vegaWalletAssociatedBalance = '[data-testid="currency-value"]';
const vegaWalletNameElement = '[data-testid="wallet-name"]';
const vegaWallet = '[data-testid="vega-wallet"]';
const connectToVegaWalletButton = '[data-testid="connect-to-vega-wallet-btn"]';
const newProposalSubmitButton = '[data-testid="proposal-submit"]';
const dialogCloseButton = '[data-testid="dialog-close"]';
const viewProposalButton = '[data-testid="view-proposal-btn"]';
const openProposals = '[data-testid="open-proposals"]';
const proposalVoteProgressForPercentage =
'[data-testid="vote-progress-indicator-percentage-for"]';
const proposalVoteProgressAgainstPercentage =
'[data-testid="vote-progress-indicator-percentage-against"]';
const proposalVoteProgressForTokens =
'[data-testid="vote-progress-indicator-tokens-for"]';
const proposalVoteProgressAgainstTokens =
'[data-testid="vote-progress-indicator-tokens-against"]';
const changeVoteButton = '[data-testid="change-vote-button"]';
const proposalDetailsTitle = '[data-testid="proposal-title"]';
const proposalDetailsDescription = '[data-testid="proposal-description"]';
const rawProposalData = '[data-testid="proposal-data"]';
const minVoteButton = '[data-testid="min-vote"]';
const maxVoteButton = '[data-testid="max-vote"]';
const voteButtons = '[data-testid="vote-buttons"]';
const votingDate = '[data-testid="voting-date"]';
const voteTwoMinExtraNote = '[data-testid="voting-2-mins-extra"]';
const voteStatus = '[data-testid="vote-status"]';
const rejectProposalsLink = '[href="/proposals/rejected"]';
const feedbackError = '[data-testid="Error"]';
const noOpenProposals = '[data-testid="no-open-proposals"]';
const noClosedProposals = '[data-testid="no-closed-proposals"]';
const txTimeout = Cypress.env('txTimeout');
const epochTimeout = Cypress.env('epochTimeout');
const proposalTimeout = { timeout: 14000 };
const minCloseDays = 2;
const maxCloseDays = 3;
const requiredParticipation = 0.001;
const governanceProposalType = {
NETWORK_PARAMETER: 'Network parameter',
NEW_MARKET: 'New market',
UPDATE_MARKET: 'Update market',
NEW_ASSET: 'New asset',
FREEFORM: 'Freeform',
RAW: 'raw proposal',
};
context(
'Governance flow - with eth and vega wallets connected',
{ tags: '@slow' },
function () {
before('connect wallets and set approval limit', function () {
cy.visit('/');
cy.get_network_parameters().then((network_parameters) => {
cy.wrap(
network_parameters['spam.protection.proposal.min.tokens'] /
1000000000000000000
).as('minProposerBalance');
cy.wrap(
network_parameters['spam.protection.voting.min.tokens'] /
1000000000000000000
).as('minVoterBalance');
cy.wrap(
network_parameters['governance.proposal.freeform.requiredMajority'] *
100
).as('requiredMajority');
cy.wrap(
network_parameters['governance.proposal.freeform.minClose'].split(
'h'
)[0]
).as('minCloseHours');
cy.wrap(
network_parameters['governance.proposal.freeform.maxClose'].split(
'h'
)[0]
).as('maxCloseHours');
});
cy.vega_wallet_set_specified_approval_amount('1000');
});
describe('Eth wallet - contains VEGA tokens', function () {
before(
'checking network parameters (therefore environment) is fit for test',
function () {
assert.isAtLeast(
parseInt(this.minProposerBalance),
0.00001,
'Asserting that value is at least 0.00001 for network parameter minProposerBalance'
);
assert.isAtLeast(
parseInt(this.minVoterBalance),
0.00001,
'Asserting that value is at least 0.00001 for network parameter minVoterBalance'
);
// workaround for first eth tx hanging
associateTokenStartOfTests();
}
);
beforeEach('visit governance tab', function () {
cy.reload();
cy.wait_for_spinner();
cy.connectVegaWallet();
cy.ethereum_wallet_connect();
cy.navigate_to('proposals');
});
it('Should be able to see that no proposals exist', function () {
// 3001-VOTE-003
cy.get(noOpenProposals)
.should('be.visible')
.and('have.text', 'There are no open or yet to enact proposals');
cy.get(noClosedProposals)
.should('be.visible')
.and('have.text', 'There are no enacted or rejected proposals');
});
// 3002-PROP-002
// 3002-PROP-003
it('Submit a proposal form - shows how many vega tokens are required to make a proposal', function () {
// 3002-PROP-005
cy.go_to_make_new_proposal(governanceProposalType.NEW_MARKET);
cy.contains(
`You must have at least ${this.minProposerBalance} VEGA associated to make a proposal`
).should('be.visible');
});
// 3002-PROP-011
it('Able to submit a valid freeform proposal - with minimum required tokens associated', function () {
cy.ensure_specified_unstaked_tokens_are_associated(
this.minProposerBalance
);
cy.go_to_make_new_proposal(governanceProposalType.FREEFORM);
cy.get(minVoteButton).should('be.visible'); // 3002-PROP-008
cy.get(maxVoteButton).should('be.visible');
cy.get(votingDate).should('not.be.empty');
cy.get(voteTwoMinExtraNote).should(
'contain.text',
'we add 2 minutes of extra time'
);
cy.enter_unique_freeform_proposal_body('50', generateProposalTitle());
cy.get(newProposalSubmitButton).should('be.visible').click();
// 3002-PROP-012
// 3002-PROP-016
cy.wait_for_proposal_submitted();
});
it('Newly created proposals list - proposals closest to closing date appear higher in list', function () {
// 3001-VOTE-005
cy.ensure_specified_unstaked_tokens_are_associated(
this.minProposerBalance
);
let proposalDays = [
minCloseDays + 1,
maxCloseDays,
minCloseDays + 3,
minCloseDays + 2,
];
for (var index = 0; index < proposalDays.length; index++) {
cy.go_to_make_new_proposal(governanceProposalType.RAW);
cy.create_ten_digit_unix_timestamp_for_specified_days(
proposalDays[index]
).then((closingDateTimestamp) => {
cy.enter_raw_proposal_body(closingDateTimestamp);
});
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.contains('Awaiting network confirmation', epochTimeout).should(
'be.visible'
);
cy.contains('Proposal submitted', proposalTimeout).should(
'be.visible'
);
cy.get(dialogCloseButton).click();
cy.wait_for_proposal_sync();
}
let arrayOfProposals = [];
cy.navigate_to('proposals');
cy.get(proposalDetailsTitle)
.each((proposalTitleElement) => {
arrayOfProposals.push(proposalTitleElement.text());
})
.then(() => {
cy.get_sort_order_of_supplied_array(arrayOfProposals).should(
'equal',
'descending'
);
});
});
it('Able to submit a valid freeform proposal - with minimum required tokens associated - but also staked', function () {
cy.ensure_specified_unstaked_tokens_are_associated('2');
cy.navigate_to_page_if_not_already_loaded('proposals');
cy.get(vegaWalletUnstakedBalance, txTimeout).should('contain', '2');
cy.navigate_to('validators');
cy.click_on_validator_from_list(0);
cy.staking_validator_page_add_stake('2');
cy.close_staking_dialog();
cy.get(vegaWalletStakedBalances, txTimeout).should('contain', '2');
cy.navigate_to('proposals');
cy.go_to_make_new_proposal(governanceProposalType.FREEFORM);
cy.enter_unique_freeform_proposal_body('50', generateProposalTitle());
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.wait_for_proposal_submitted();
});
it('Newly created proposals list - able to filter by proposerID to show it in list', function () {
const proposerId = Cypress.env('vegaWalletPublicKey');
const proposalTitle = generateProposalTitle();
createFreeformProposal(this.minProposerBalance, proposalTitle);
cy.get_proposal_id_from_list(proposalTitle);
cy.get('@proposalIdText').then((proposalId) => {
cy.get('[data-testid="set-proposals-filter-visible"]').click();
cy.get('[data-testid="filter-input"]').type(proposerId);
cy.get(`#${proposalId}`).should('contain', proposalId);
});
});
it('Newly created proposals list - shows title and portion of summary', function () {
createRawProposal(this.minProposerBalance); // 3001-VOTE-052
cy.get('@rawProposal').then((rawProposal) => {
cy.get_proposal_id_from_list(rawProposal.rationale.title);
cy.get('@proposalIdText').then((proposalId) => {
cy.get(openProposals).within(() => {
// 3001-VOTE-008
// 3001-VOTE-034
cy.get(`#${proposalId}`)
// 3001-VOTE-097
.should('contain', rawProposal.rationale.title)
.and('be.visible');
cy.get(`#${proposalId}`)
.should(
'contain',
rawProposal.rationale.description.substring(0, 59)
)
.and('be.visible');
});
});
});
});
it('Newly created proposals list - shows open proposals in an open state', function () {
// 3001-VOTE-004
// 3001-VOTE-035
createRawProposal(this.minProposerBalance);
cy.get('@rawProposal').then((rawProposal) => {
cy.get_submitted_proposal_from_proposal_list(
rawProposal.rationale.title
).within(() => {
cy.get(viewProposalButton).should('be.visible').click();
});
cy.get('@proposalIdText').then((proposalId) => {
cy.get_proposal_information_from_table('ID')
.contains(proposalId)
.and('be.visible');
});
cy.get_proposal_information_from_table('State')
.contains('Open')
.and('be.visible');
cy.get_proposal_information_from_table('Type')
.contains('Freeform')
.and('be.visible');
});
});
// 3001-VOTE-071
it('Newly created freeform proposals list - shows proposal participation - both met and not', function () {
const proposalTitle = generateProposalTitle();
createFreeformProposal(this.minProposerBalance, proposalTitle);
cy.get_submitted_proposal_from_proposal_list(proposalTitle)
.as('submittedProposal')
.within(() => {
// 3001-VOTE-039
cy.get(voteStatus).should('have.text', 'Participation not reached');
cy.get(viewProposalButton).click();
});
cy.vote_for_proposal('for');
cy.get_proposal_information_from_table('Total Supply')
.invoke('text')
.then((totalSupply) => {
let tokensRequiredToAchieveResult = parseFloat(
(totalSupply.replace(/,/g, '') * requiredParticipation) / 100
).toFixed(2);
cy.ensure_specified_unstaked_tokens_are_associated(
tokensRequiredToAchieveResult
);
cy.navigate_to_page_if_not_already_loaded('proposals');
cy.get('@submittedProposal').within(() =>
cy.get(viewProposalButton).click()
);
cy.get_proposal_information_from_table('Token participation met')
.contains('👍')
.should('be.visible');
cy.navigate_to('proposals');
cy.get('@submittedProposal').within(() =>
cy.get(voteStatus).should('have.text', 'Set to pass')
);
});
});
// 3001-VOTE-055
it('Newly created raw proposal details - shows proposal title and full description', function () {
createRawProposal(this.minProposerBalance);
cy.get('@rawProposal').then((rawProposal) => {
cy.get_proposal_id_from_list(rawProposal.rationale.title);
cy.get('@proposalIdText').then((proposalId) => {
cy.get(openProposals).within(() => {
cy.get(`#${proposalId}`).within(() => {
cy.get(viewProposalButton).should('be.visible').click();
});
});
});
cy.get(proposalDetailsTitle)
.should('contain', rawProposal.rationale.title)
.and('be.visible');
cy.get(proposalDetailsDescription)
.should('contain', rawProposal.rationale.description)
.and('be.visible');
});
// 3001-VOTE-052
cy.get('code.language-json')
.should('exist')
.within(() => {
cy.get('.hljs-string').eq(0).should('have.text', '"ProposalTerms"');
});
});
// 3001-VOTE-043
it('Newly created freeform proposal details - shows proposed and closing dates', function () {
const closingVoteHrs = '72';
const proposalTitle = generateProposalTitle();
cy.ensure_specified_unstaked_tokens_are_associated(
this.minProposerBalance
);
cy.go_to_make_new_proposal(governanceProposalType.FREEFORM);
cy.create_ten_digit_unix_timestamp_for_specified_days('3').then(
(closingDateTimestamp) => {
cy.enter_unique_freeform_proposal_body(
closingVoteHrs,
proposalTitle
);
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.wait_for_proposal_submitted();
cy.wait_for_proposal_sync();
cy.navigate_to('proposals');
cy.get_submitted_proposal_from_proposal_list(proposalTitle).within(
() => cy.get(viewProposalButton).click()
);
cy.convert_unix_timestamp_to_governance_data_table_date_format(
closingDateTimestamp
).then((closingDate) => {
cy.get_proposal_information_from_table('Closes on')
.contains(closingDate)
.should('be.visible');
});
}
);
cy.get_governance_proposal_date_format_for_specified_days('0').then(
(proposalDate) => {
cy.get_proposal_information_from_table('Proposed on')
.contains(proposalDate)
.should('be.visible');
}
);
});
it('Newly created proposal details - shows default status set to fail', function () {
// 3001-VOTE-037
// 3001-VOTE-040
// 3001-VOTE-067
createRawProposal(this.minProposerBalance);
cy.get('@rawProposal').then((rawProposal) => {
cy.get_submitted_proposal_from_proposal_list(
rawProposal.rationale.title
).within(() => cy.get(viewProposalButton).click());
});
cy.contains('Participation: Not Met 0.00 0.00%(0.00% Required)').should(
'be.visible'
);
cy.get_proposal_information_from_table('Expected to pass')
.contains('👎')
.should('be.visible');
// 3001-VOTE-062
// 3001-VOTE-040
// 3001-VOTE-070
cy.get_proposal_information_from_table('Token majority met')
.contains('👎')
.should('be.visible');
// 3001-VOTE-068
cy.get_proposal_information_from_table('Token participation met')
.contains('👎')
.should('be.visible');
});
// 3001-VOTE-080 3001-VOTE-090 3001-VOTE-069 3001-VOTE-072 3001-VOTE-073
it('Newly created proposal details - ability to vote for and against proposal - with minimum required tokens associated', function () {
createRawProposal(this.minProposerBalance);
cy.get('@rawProposal').then((rawProposal) => {
cy.get_submitted_proposal_from_proposal_list(
rawProposal.rationale.title
).within(() => cy.get(viewProposalButton).click());
});
// 3001-VOTE-080
cy.get(voteButtons).contains('against').should('be.visible');
cy.get(voteButtons).contains('for').should('be.visible');
cy.vote_for_proposal('for');
cy.get_governance_proposal_date_format_for_specified_days(
'0',
'shortMonth'
).then((votedDate) => {
// 3001-VOTE-051
// 3001-VOTE-093
cy.contains('You voted:')
.siblings()
.contains('For')
.siblings()
.contains(votedDate)
.should('be.visible');
});
cy.get(proposalVoteProgressForPercentage) // 3001-VOTE-072
.contains('100.00%')
.and('be.visible');
cy.get(proposalVoteProgressAgainstPercentage)
.contains('0.00%')
.and('be.visible');
cy.get(proposalVoteProgressForTokens)
.contains('1.00')
.and('be.visible');
cy.get(proposalVoteProgressAgainstTokens)
.contains('0.00')
.and('be.visible');
cy.get_proposal_information_from_table('Tokens for proposal')
.should('have.text', parseFloat(this.minProposerBalance).toFixed(2))
.and('be.visible');
cy.get_proposal_information_from_table('Tokens against proposal')
.should('have.text', '0.00')
.and('be.visible');
// 3001-VOTE-061
cy.get_proposal_information_from_table('Participation required')
.contains(`${requiredParticipation}%`)
.should('be.visible');
// 3001-VOTE-066
cy.get_proposal_information_from_table('Majority Required') // 3001-VOTE-073
.contains(`${parseFloat(this.requiredMajority).toFixed(2)}%`)
.should('be.visible');
cy.get_proposal_information_from_table('Number of voting parties')
.should('have.text', '1')
.and('be.visible');
cy.get(changeVoteButton).should('be.visible').click();
cy.vote_for_proposal('for');
// 3001-VOTE-064
cy.get_proposal_information_from_table('Tokens for proposal')
.should('have.text', parseFloat(this.minProposerBalance).toFixed(2))
.and('be.visible');
cy.get(changeVoteButton).should('be.visible').click();
cy.vote_for_proposal('against');
cy.get(proposalVoteProgressAgainstPercentage)
.contains('100.00%')
.and('be.visible');
cy.get_proposal_information_from_table('Tokens against proposal')
.should('have.text', parseFloat(this.minProposerBalance).toFixed(2))
.and('be.visible');
cy.get_proposal_information_from_table('Number of voting parties')
.should('have.text', '1')
.and('be.visible');
});
// 3001-VOTE-042, 3001-VOTE-057, 3001-VOTE-058, 3001-VOTE-059, 3001-VOTE-060
it('Newly created proposal details - ability to increase associated tokens - by voting again after association', function () {
createRawProposal(this.minProposerBalance);
cy.get('@rawProposal').then((rawProposal) => {
cy.get_submitted_proposal_from_proposal_list(
rawProposal.rationale.title
)
.as('submittedProposal')
.within(() => cy.get(viewProposalButton).click());
});
cy.vote_for_proposal('for');
// 3001-VOTE-079
cy.contains('You voted: For').should('be.visible');
cy.get(proposalVoteProgressForTokens).contains('1').and('be.visible');
cy.get_proposal_information_from_table('Total Supply')
.invoke('text')
.then((totalSupply) => {
let tokensRequiredToAchieveResult = parseFloat(
(totalSupply.replace(/,/g, '') * requiredParticipation) / 100
).toFixed(2);
cy.ensure_specified_unstaked_tokens_are_associated(
tokensRequiredToAchieveResult
);
cy.navigate_to_page_if_not_already_loaded('proposals');
cy.get('@submittedProposal').within(() =>
cy.get(viewProposalButton).click()
);
cy.get(proposalVoteProgressForPercentage)
.contains('100.00%')
.and('be.visible');
cy.get(proposalVoteProgressAgainstPercentage)
.contains('0.00%')
.and('be.visible');
// 3001-VOTE-065
cy.get(changeVoteButton).should('be.visible').click();
cy.vote_for_proposal('for');
cy.get(proposalVoteProgressForTokens)
.contains(tokensRequiredToAchieveResult)
.and('be.visible');
cy.get(proposalVoteProgressAgainstTokens)
.contains('0.00')
.and('be.visible');
cy.get_proposal_information_from_table(
'Total tokens voted percentage'
)
.should('have.text', '0.00%')
.and('be.visible');
cy.get_proposal_information_from_table('Tokens for proposal')
.should('have.text', tokensRequiredToAchieveResult)
.and('be.visible');
cy.get_proposal_information_from_table('Tokens against proposal')
.should('have.text', '0.00')
.and('be.visible');
cy.get_proposal_information_from_table('Number of voting parties')
.should('have.text', '1')
.and('be.visible');
cy.get_proposal_information_from_table('Expected to pass')
.contains('👍')
.should('be.visible');
// 3001-VOTE-062
cy.get_proposal_information_from_table('Token majority met')
.contains('👍')
.should('be.visible');
cy.get_proposal_information_from_table('Token participation met')
.contains('👍')
.should('be.visible');
cy.get_proposal_information_from_table('Tokens for proposal')
.contains(tokensRequiredToAchieveResult)
.and('be.visible');
});
});
it('Creating a proposal - proposal rejected - when closing time sooner than system default', function () {
cy.ensure_specified_unstaked_tokens_are_associated(
this.minProposerBalance
);
cy.go_to_make_new_proposal(governanceProposalType.FREEFORM);
cy.enter_unique_freeform_proposal_body('40', generateProposalTitle());
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.contains('Awaiting network confirmation', epochTimeout).should(
'not.exist'
);
});
it('Creating a proposal - proposal rejected - when closing time later than system default', function () {
cy.ensure_specified_unstaked_tokens_are_associated(
this.minProposerBalance
);
cy.go_to_make_new_proposal(governanceProposalType.FREEFORM);
cy.enter_unique_freeform_proposal_body(
'100000',
generateProposalTitle()
);
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.contains('Awaiting network confirmation', epochTimeout).should(
'not.exist'
);
});
// 3001-VOTE-006
it('Creating a proposal - proposal rejected - able to access rejected proposals', function () {
cy.ensure_specified_unstaked_tokens_are_associated(
this.minProposerBalance
);
cy.go_to_make_new_proposal(governanceProposalType.RAW);
cy.create_ten_digit_unix_timestamp_for_specified_days('1000').then(
(closingDateTimestamp) => {
cy.enter_raw_proposal_body(closingDateTimestamp).as('rawProposal');
}
);
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.contains('Awaiting network confirmation', epochTimeout).should(
'be.visible'
);
cy.contains('Proposal rejected', proposalTimeout).should('be.visible');
cy.get(dialogCloseButton).click();
cy.wait_for_proposal_sync();
cy.navigate_to('proposals');
cy.get(rejectProposalsLink).click();
cy.get('@rawProposal').then((rawProposal) => {
cy.get_submitted_proposal_from_proposal_list(
rawProposal.rationale.title
).within(() => {
cy.contains('Rejected').should('be.visible');
cy.contains('Close time too late').should('be.visible');
cy.get(viewProposalButton).click();
});
});
cy.get_proposal_information_from_table('State')
.contains('Rejected')
.and('be.visible');
cy.get_proposal_information_from_table('Rejection reason')
.contains('PROPOSAL_ERROR_CLOSE_TIME_TOO_LATE')
.and('be.visible');
cy.get_proposal_information_from_table('Error details')
.contains('proposal closing time too late')
.and('be.visible');
});
// 0005-ETXN-004
it('Unable to create a proposal - when no tokens are associated', function () {
cy.vega_wallet_teardown();
cy.get(vegaWalletAssociatedBalance, txTimeout).contains(
'0.00',
txTimeout
);
cy.go_to_make_new_proposal(governanceProposalType.RAW);
cy.create_ten_digit_unix_timestamp_for_specified_days('8').then(
(closingDateTimestamp) => {
cy.enter_raw_proposal_body(closingDateTimestamp).as;
}
);
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.contains('Transaction failed', proposalTimeout).should('be.visible');
cy.get(feedbackError).should(
'have.text',
'Network error: the network blocked the transaction through the spam protection'
);
cy.get(dialogCloseButton).click();
});
// 3002-PROP-009
it('Unable to create a proposal - when some but not enough tokens are associated', function () {
cy.ensure_specified_unstaked_tokens_are_associated(
this.minProposerBalance - 0.000001
);
cy.go_to_make_new_proposal(governanceProposalType.RAW);
cy.create_ten_digit_unix_timestamp_for_specified_days('8').then(
(closingDateTimestamp) => {
cy.enter_raw_proposal_body(closingDateTimestamp);
}
);
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.contains('Transaction failed', proposalTimeout).should('be.visible');
cy.get(feedbackError).should(
'have.text',
'Network error: the network blocked the transaction through the spam protection'
);
cy.get(dialogCloseButton).click();
});
it('Unable to create a freeform proposal - when json parent section contains unexpected field', function () {
// 3001-VOTE-038 3002-PROP-013 3002-PROP-014
cy.ensure_specified_unstaked_tokens_are_associated(
this.minProposerBalance
);
cy.go_to_make_new_proposal(governanceProposalType.RAW);
cy.create_ten_digit_unix_timestamp_for_specified_days('8').then(
(closingDateTimestamp) => {
cy.fixture('/proposals/raw.json').then((freeformProposal) => {
freeformProposal.terms.closingTimestamp = closingDateTimestamp;
freeformProposal.unexpected = `i shouldn't be here`;
let proposalPayload = JSON.stringify(freeformProposal);
cy.get(rawProposalData).type(proposalPayload, {
parseSpecialCharSequences: false,
delay: 2,
});
});
}
);
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.contains('Transaction failed', proposalTimeout).should('be.visible');
cy.get(feedbackError).should(
'have.text',
'Invalid params: the transaction is malformed'
);
cy.get(dialogCloseButton).click();
cy.get(rawProposalData)
.invoke('val')
.should('contain', "i shouldn't be here");
});
it('Unable to create a freeform proposal - when json terms section contains unexpected field', function () {
// 3001-VOTE-038
cy.ensure_specified_unstaked_tokens_are_associated(
this.minProposerBalance
);
cy.go_to_make_new_proposal(governanceProposalType.RAW);
cy.create_ten_digit_unix_timestamp_for_specified_days('8').then(
(closingDateTimestamp) => {
cy.fixture('/proposals/raw.json').then((rawProposal) => {
rawProposal.terms.closingTimestamp = closingDateTimestamp;
rawProposal.terms.unexpectedField = `i shouldn't be here`;
let proposalPayload = JSON.stringify(rawProposal);
cy.get(rawProposalData).type(proposalPayload, {
parseSpecialCharSequences: false,
delay: 2,
});
});
}
);
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.contains('Transaction failed', proposalTimeout).should('be.visible');
cy.get(feedbackError).should(
'have.text',
'Invalid params: the transaction is malformed'
);
cy.get(dialogCloseButton).click();
});
// 1005-PROP-009
it.skip(
'Unable to vote on a freeform proposal - when some but not enough vega associated',
{ tags: '@smoke' },
function () {
const proposalTitle = generateProposalTitle();
cy.vega_wallet_teardown();
cy.ensure_specified_unstaked_tokens_are_associated(
this.minProposerBalance
);
cy.go_to_make_new_proposal(governanceProposalType.FREEFORM);
cy.enter_unique_freeform_proposal_body('50', proposalTitle);
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.wait_for_proposal_submitted();
cy.staking_page_disassociate_tokens('0.0001');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
'contain',
'0.9999'
);
});
cy.navigate_to('proposals');
cy.get_submitted_proposal_from_proposal_list(proposalTitle).within(
() => cy.get(viewProposalButton).click()
);
cy.contains('Vote breakdown').should('be.visible', {
timeout: 10000,
});
cy.get(voteButtons).should('not.exist');
cy.getByTestId('min-proposal-requirements').should(
'have.text',
`You must have at least ${this.minVoterBalance} VEGA associated to vote on this proposal`
);
}
);
it('Unable to vote on a proposal - when vega wallet disconnected - option to connect from within', function () {
createRawProposal(this.minProposerBalance);
cy.get('[data-testid="manage-vega-wallet"]').click();
cy.get('[data-testid="disconnect"]').click();
cy.get('@rawProposal').then((rawProposal) => {
cy.get_submitted_proposal_from_proposal_list(
rawProposal.rationale.title
).within(() => cy.get(viewProposalButton).click());
});
// 3001-VOTE-075
// 3001-VOTE-076
cy.get(connectToVegaWalletButton)
.should('be.visible')
.and('have.text', 'Connect Vega wallet')
.click();
cy.getByTestId('connector-jsonRpc').click();
cy.get(vegaWalletNameElement).should('be.visible');
cy.get(connectToVegaWalletButton).should('not.exist');
// 3001-VOTE-100
cy.get(vegaWalletAssociatedBalance, txTimeout).contains(
'1.00',
txTimeout
);
cy.vote_for_proposal('against');
// 3001-VOTE-079
cy.contains('You voted: Against').should('be.visible');
});
function createRawProposal(proposerBalance) {
if (proposerBalance)
cy.ensure_specified_unstaked_tokens_are_associated(proposerBalance);
cy.go_to_make_new_proposal(governanceProposalType.RAW);
cy.create_ten_digit_unix_timestamp_for_specified_days('8').then(
(closingDateTimestamp) => {
cy.enter_raw_proposal_body(closingDateTimestamp).as('rawProposal');
}
);
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.wait_for_proposal_submitted();
cy.wait_for_proposal_sync();
cy.navigate_to('proposals');
}
function createFreeformProposal(proposerBalance, proposalTitle) {
cy.ensure_specified_unstaked_tokens_are_associated(proposerBalance);
cy.go_to_make_new_proposal(governanceProposalType.FREEFORM);
cy.enter_unique_freeform_proposal_body('50', proposalTitle);
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.wait_for_proposal_submitted();
cy.wait_for_proposal_sync();
cy.get(proposalDetailsTitle).invoke('text').as('proposalTitle');
cy.navigate_to('proposals');
}
function generateProposalTitle() {
const randomNum = Math.floor(Math.random() * 1000) + 1;
return randomNum + ': Freeform e2e proposal';
}
// This is a workaround function to begin tests with associating tokens without failing
// Should be removed when eth transaction bug is fixed
function associateTokenStartOfTests() {
cy.highlight(`Associating tokens for first time`);
cy.ethereum_wallet_connect();
cy.connectVegaWallet();
cy.get('[href="/token/associate"]').first().click();
cy.getByTestId('associate-radio-wallet', { timeout: 30000 }).click();
cy.getByTestId('token-amount-input', epochTimeout).type('1');
cy.getByTestId('token-input-submit-button', txTimeout)
.should('be.enabled')
.click();
cy.contains(
`Associating with Vega key. Waiting for ${Cypress.env(
'blockConfirmations'
)} more confirmations..`,
txTimeout
).should('be.visible');
cy.getByTestId('associated-amount', txTimeout).should(
'contain.text',
'1'
);
// Wait is needed to allow time for transaction to complete
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000);
cy.vega_wallet_teardown();
cy.clearLocalStorage();
}
});
}
);
@@ -54,11 +54,11 @@ context(
cy.wait_for_spinner();
cy.connectVegaWallet();
cy.ethereum_wallet_connect();
cy.ensure_specified_unstaked_tokens_are_associated('1');
cy.navigate_to('proposals');
});
it('Able to submit valid update network parameter proposal', function () {
cy.ensure_specified_unstaked_tokens_are_associated('1');
cy.go_to_make_new_proposal(governanceProposalType.NETWORK_PARAMETER);
// 3002-PROP-006
cy.get(newProposalTitle).type('Test update network parameter proposal');
@@ -1,273 +0,0 @@
import { associateTokenStartOfTests } from '../../support/common.functions';
import {
createRawProposal,
generateFreeFormProposalTitle,
governanceProposalType,
} from '../../support/governance.functions';
const proposalVoteProgressForPercentage =
'[data-testid="vote-progress-indicator-percentage-for"]';
const proposalVoteProgressAgainstPercentage =
'[data-testid="vote-progress-indicator-percentage-against"]';
const proposalVoteProgressForTokens =
'[data-testid="vote-progress-indicator-tokens-for"]';
const proposalVoteProgressAgainstTokens =
'[data-testid="vote-progress-indicator-tokens-against"]';
const changeVoteButton = '[data-testid="change-vote-button"]';
const proposalDetailsTitle = '[data-testid="proposal-title"]';
const proposalDetailsDescription = '[data-testid="proposal-description"]';
const openProposals = '[data-testid="open-proposals"]';
const viewProposalButton = '[data-testid="view-proposal-btn"]';
describe(
'Governance flow for proposal details',
{ tags: '@slow' },
function () {
before('connect wallets and set approval limit', function () {
cy.visit('/');
cy.vega_wallet_set_specified_approval_amount('1000');
associateTokenStartOfTests();
});
beforeEach('visit proposals tab', function () {
cy.reload();
cy.wait_for_spinner();
cy.connectVegaWallet();
cy.ethereum_wallet_connect();
cy.ensure_specified_unstaked_tokens_are_associated(1);
cy.navigate_to_page_if_not_already_loaded('proposals');
});
// 3001-VOTE-055
it('Newly created raw proposal details - shows proposal title and full description', function () {
createRawProposal();
cy.get('@rawProposal').then((rawProposal) => {
cy.get_proposal_id_from_list(rawProposal.rationale.title);
cy.get('@proposalIdText').then((proposalId) => {
cy.get(openProposals).within(() => {
cy.get(`#${proposalId}`).within(() => {
cy.get(viewProposalButton).should('be.visible').click();
});
});
});
cy.get(proposalDetailsTitle)
.should('contain', rawProposal.rationale.title)
.and('be.visible');
cy.get(proposalDetailsDescription)
.should('contain', rawProposal.rationale.description)
.and('be.visible');
});
// 3001-VOTE-052
cy.get('code.language-json')
.should('exist')
.within(() => {
cy.get('.hljs-string').eq(0).should('have.text', '"ProposalTerms"');
});
});
// 3001-VOTE-043
it('Newly created freeform proposal details - shows proposed and closing dates', function () {
const closingVoteHrs = '72';
const proposalTitle = generateFreeFormProposalTitle();
cy.go_to_make_new_proposal(governanceProposalType.FREEFORM);
cy.create_ten_digit_unix_timestamp_for_specified_days('3').then(
(closingDateTimestamp) => {
cy.enter_unique_freeform_proposal_body(closingVoteHrs, proposalTitle);
cy.wait_for_proposal_submitted();
cy.wait_for_proposal_sync();
cy.navigate_to('proposals');
cy.get_submitted_proposal_from_proposal_list(proposalTitle).within(
() => cy.get(viewProposalButton).click()
);
cy.convert_unix_timestamp_to_governance_data_table_date_format(
closingDateTimestamp
).then((closingDate) => {
cy.get_proposal_information_from_table('Closes on')
.contains(closingDate)
.should('be.visible');
});
}
);
cy.get_governance_proposal_date_format_for_specified_days('0').then(
(proposalDate) => {
cy.get_proposal_information_from_table('Proposed on')
.contains(proposalDate)
.should('be.visible');
}
);
});
it('Newly created proposal details - shows default status set to fail', function () {
// 3001-VOTE-037
// 3001-VOTE-040
// 3001-VOTE-067
createRawProposal();
cy.get('@rawProposal').then((rawProposal) => {
cy.get_submitted_proposal_from_proposal_list(
rawProposal.rationale.title
).within(() => cy.get(viewProposalButton).click());
});
cy.contains('Participation: Not Met 0.00 0.00%(0.00% Required)').should(
'be.visible'
);
cy.get_proposal_information_from_table('Expected to pass')
.contains('👎')
.should('be.visible');
// 3001-VOTE-062
// 3001-VOTE-040
// 3001-VOTE-070
cy.get_proposal_information_from_table('Token majority met')
.contains('👎')
.should('be.visible');
// 3001-VOTE-068
cy.get_proposal_information_from_table('Token participation met')
.contains('👎')
.should('be.visible');
});
// 3001-VOTE-080 3001-VOTE-090 3001-VOTE-069 3001-VOTE-072 3001-VOTE-073
it('Newly created proposal details - ability to vote for and against proposal - with minimum required tokens associated', function () {
createRawProposal();
cy.get('@rawProposal').then((rawProposal) => {
cy.get_submitted_proposal_from_proposal_list(
rawProposal.rationale.title
).within(() => cy.get(viewProposalButton).click());
});
// 3001-VOTE-080
cy.getByTestId('vote-buttons').contains('against').should('be.visible');
cy.getByTestId('vote-buttons').contains('for').should('be.visible');
cy.vote_for_proposal('for');
cy.get_governance_proposal_date_format_for_specified_days(
'0',
'shortMonth'
).then((votedDate) => {
// 3001-VOTE-051
// 3001-VOTE-093
cy.contains('You voted:')
.siblings()
.contains('For')
.siblings()
.contains(votedDate)
.should('be.visible');
});
cy.get(proposalVoteProgressForPercentage) // 3001-VOTE-072
.contains('100.00%')
.and('be.visible');
cy.get(proposalVoteProgressAgainstPercentage)
.contains('0.00%')
.and('be.visible');
cy.get(proposalVoteProgressForTokens).contains('1.00').and('be.visible');
cy.get(proposalVoteProgressAgainstTokens)
.contains('0.00')
.and('be.visible');
cy.get_proposal_information_from_table('Tokens for proposal')
.should('have.text', parseFloat(1).toFixed(2))
.and('be.visible');
cy.get_proposal_information_from_table('Tokens against proposal')
.should('have.text', '0.00')
.and('be.visible');
// 3001-VOTE-061
cy.get_proposal_information_from_table('Participation required')
.contains(0.001)
.should('be.visible');
// 3001-VOTE-066
cy.get_proposal_information_from_table('Majority Required') // 3001-VOTE-073
.contains(`${parseFloat(100).toFixed(2)}%`)
.should('be.visible');
cy.get_proposal_information_from_table('Number of voting parties')
.should('have.text', '1')
.and('be.visible');
cy.get(changeVoteButton).should('be.visible').click();
cy.vote_for_proposal('for');
// 3001-VOTE-064
cy.get_proposal_information_from_table('Tokens for proposal')
.should('have.text', parseFloat(1).toFixed(2))
.and('be.visible');
cy.get(changeVoteButton).should('be.visible').click();
cy.vote_for_proposal('against');
cy.get(proposalVoteProgressAgainstPercentage)
.contains('100.00%')
.and('be.visible');
cy.get_proposal_information_from_table('Tokens against proposal')
.should('have.text', parseFloat(1).toFixed(2))
.and('be.visible');
cy.get_proposal_information_from_table('Number of voting parties')
.should('have.text', '1')
.and('be.visible');
});
// 3001-VOTE-042, 3001-VOTE-057, 3001-VOTE-058, 3001-VOTE-059, 3001-VOTE-060
it('Newly created proposal details - ability to increase associated tokens - by voting again after association', function () {
createRawProposal();
cy.get('@rawProposal').then((rawProposal) => {
cy.get_submitted_proposal_from_proposal_list(
rawProposal.rationale.title
)
.as('submittedProposal')
.within(() => cy.get(viewProposalButton).click());
});
cy.vote_for_proposal('for');
// 3001-VOTE-079
cy.contains('You voted: For').should('be.visible');
cy.get(proposalVoteProgressForTokens).contains('1').and('be.visible');
cy.get_proposal_information_from_table('Total Supply')
.invoke('text')
.then((totalSupply) => {
let tokensRequiredToAchieveResult = parseFloat(
(totalSupply.replace(/,/g, '') * 0.001) / 100
).toFixed(2);
cy.ensure_specified_unstaked_tokens_are_associated(
tokensRequiredToAchieveResult
);
cy.navigate_to_page_if_not_already_loaded('proposals');
cy.get('@submittedProposal').within(() =>
cy.get(viewProposalButton).click()
);
cy.get(proposalVoteProgressForPercentage)
.contains('100.00%')
.and('be.visible');
cy.get(proposalVoteProgressAgainstPercentage)
.contains('0.00%')
.and('be.visible');
// 3001-VOTE-065
cy.get(changeVoteButton).should('be.visible').click();
cy.vote_for_proposal('for');
cy.get(proposalVoteProgressForTokens)
.contains(tokensRequiredToAchieveResult)
.and('be.visible');
cy.get(proposalVoteProgressAgainstTokens)
.contains('0.00')
.and('be.visible');
cy.get_proposal_information_from_table(
'Total tokens voted percentage'
)
.should('have.text', '0.00%')
.and('be.visible');
cy.get_proposal_information_from_table('Tokens for proposal')
.should('have.text', tokensRequiredToAchieveResult)
.and('be.visible');
cy.get_proposal_information_from_table('Tokens against proposal')
.should('have.text', '0.00')
.and('be.visible');
cy.get_proposal_information_from_table('Number of voting parties')
.should('have.text', '1')
.and('be.visible');
cy.get_proposal_information_from_table('Expected to pass')
.contains('👍')
.should('be.visible');
// 3001-VOTE-062
cy.get_proposal_information_from_table('Token majority met')
.contains('👍')
.should('be.visible');
cy.get_proposal_information_from_table('Token participation met')
.contains('👍')
.should('be.visible');
cy.get_proposal_information_from_table('Tokens for proposal')
.contains(tokensRequiredToAchieveResult)
.and('be.visible');
});
});
}
);
@@ -1,347 +0,0 @@
/// <reference types="cypress" />
import {
createRawProposal,
generateFreeFormProposalTitle,
governanceProposalType,
} from '../../support/governance.functions';
import { associateTokenStartOfTests } from '../../support/common.functions';
const vegaWalletUnstakedBalance =
'[data-testid="vega-wallet-balance-unstaked"]';
const vegaWalletStakedBalances =
'[data-testid="vega-wallet-balance-staked-validators"]';
const vegaWalletAssociatedBalance = '[data-testid="currency-value"]';
const vegaWalletNameElement = '[data-testid="wallet-name"]';
const vegaWallet = '[data-testid="vega-wallet"]';
const connectToVegaWalletButton = '[data-testid="connect-to-vega-wallet-btn"]';
const newProposalSubmitButton = '[data-testid="proposal-submit"]';
const dialogCloseButton = '[data-testid="dialog-close"]';
const viewProposalButton = '[data-testid="view-proposal-btn"]';
const rawProposalData = '[data-testid="proposal-data"]';
const minVoteButton = '[data-testid="min-vote"]';
const maxVoteButton = '[data-testid="max-vote"]';
const voteButtons = '[data-testid="vote-buttons"]';
const votingDate = '[data-testid="voting-date"]';
const voteTwoMinExtraNote = '[data-testid="voting-2-mins-extra"]';
const rejectProposalsLink = '[href="/proposals/rejected"]';
const feedbackError = '[data-testid="Error"]';
const noOpenProposals = '[data-testid="no-open-proposals"]';
const noClosedProposals = '[data-testid="no-closed-proposals"]';
const txTimeout = Cypress.env('txTimeout');
const epochTimeout = Cypress.env('epochTimeout');
const proposalTimeout = { timeout: 14000 };
context(
'Governance flow - with eth and vega wallets connected',
{ tags: '@slow' },
function () {
before('connect wallets and set approval limit', function () {
cy.visit('/');
cy.get_network_parameters().then((network_parameters) => {
cy.wrap(
network_parameters['spam.protection.proposal.min.tokens'] /
1000000000000000000
).as('minProposerBalance');
cy.wrap(
network_parameters['spam.protection.voting.min.tokens'] /
1000000000000000000
).as('minVoterBalance');
cy.wrap(
network_parameters['governance.proposal.freeform.requiredMajority'] *
100
).as('requiredMajority');
});
cy.vega_wallet_set_specified_approval_amount('1000');
associateTokenStartOfTests();
});
beforeEach('visit governance tab', function () {
cy.reload();
cy.wait_for_spinner();
cy.connectVegaWallet();
cy.ethereum_wallet_connect();
cy.ensure_specified_unstaked_tokens_are_associated(1);
cy.navigate_to_page_if_not_already_loaded('proposals');
});
it('Should be able to see that no proposals exist', function () {
// 3001-VOTE-003
cy.get(noOpenProposals)
.should('be.visible')
.and('have.text', 'There are no open or yet to enact proposals');
cy.get(noClosedProposals)
.should('be.visible')
.and('have.text', 'There are no enacted or rejected proposals');
});
// 3002-PROP-002
// 3002-PROP-003
it('Submit a proposal form - shows how many vega tokens are required to make a proposal', function () {
// 3002-PROP-005
cy.go_to_make_new_proposal(governanceProposalType.NEW_MARKET);
cy.contains(
`You must have at least 1 VEGA associated to make a proposal`
).should('be.visible');
});
// 3002-PROP-011
it('Able to submit a valid freeform proposal - with minimum required tokens associated', function () {
cy.go_to_make_new_proposal(governanceProposalType.FREEFORM);
cy.get(minVoteButton).should('be.visible'); // 3002-PROP-008
cy.get(maxVoteButton).should('be.visible');
cy.get(votingDate).should('not.be.empty');
cy.get(voteTwoMinExtraNote).should(
'contain.text',
'we add 2 minutes of extra time'
);
cy.enter_unique_freeform_proposal_body(
'50',
generateFreeFormProposalTitle()
);
// 3002-PROP-012
// 3002-PROP-016
cy.wait_for_proposal_submitted();
});
it('Able to submit a valid freeform proposal - with minimum required tokens associated - but also staked', function () {
cy.ensure_specified_unstaked_tokens_are_associated('2');
cy.get(vegaWalletUnstakedBalance, txTimeout).should('contain', '2');
cy.navigate_to('validators');
cy.click_on_validator_from_list(0);
cy.staking_validator_page_add_stake('2');
cy.close_staking_dialog();
cy.get(vegaWalletStakedBalances, txTimeout).should('contain', '2');
cy.navigate_to('proposals');
cy.go_to_make_new_proposal(governanceProposalType.FREEFORM);
cy.enter_unique_freeform_proposal_body(
'50',
generateFreeFormProposalTitle()
);
cy.wait_for_proposal_submitted();
});
it('Creating a proposal - proposal rejected - when closing time sooner than system default', function () {
cy.go_to_make_new_proposal(governanceProposalType.FREEFORM);
cy.enter_unique_freeform_proposal_body(
'0.1',
generateFreeFormProposalTitle()
);
cy.contains('Awaiting network confirmation', epochTimeout).should(
'not.exist'
);
cy.get('input:invalid')
.invoke('prop', 'validationMessage')
.should('equal', 'Value must be greater than or equal to 1.');
});
it('Creating a proposal - proposal rejected - when closing time later than system default', function () {
cy.go_to_make_new_proposal(governanceProposalType.FREEFORM);
cy.enter_unique_freeform_proposal_body(
'100000',
generateFreeFormProposalTitle()
);
cy.contains('Awaiting network confirmation', epochTimeout).should(
'not.exist'
);
cy.get('input:invalid')
.invoke('prop', 'validationMessage')
.should('equal', 'Value must be less than or equal to 8760.');
});
// 3001-VOTE-006
it('Creating a proposal - proposal rejected - able to access rejected proposals', function () {
cy.go_to_make_new_proposal(governanceProposalType.RAW);
cy.create_ten_digit_unix_timestamp_for_specified_days('1000').then(
(closingDateTimestamp) => {
cy.enter_raw_proposal_body(closingDateTimestamp).as('rawProposal');
}
);
cy.contains('Awaiting network confirmation', epochTimeout).should(
'be.visible'
);
cy.contains('Proposal rejected', proposalTimeout).should('be.visible');
cy.get(dialogCloseButton).click();
cy.wait_for_proposal_sync();
cy.navigate_to('proposals');
cy.get(rejectProposalsLink).click();
cy.get('@rawProposal').then((rawProposal) => {
cy.get_submitted_proposal_from_proposal_list(
rawProposal.rationale.title
).within(() => {
cy.contains('Rejected').should('be.visible');
cy.contains('Close time too late').should('be.visible');
cy.get(viewProposalButton).click();
});
});
cy.get_proposal_information_from_table('State')
.contains('Rejected')
.and('be.visible');
cy.get_proposal_information_from_table('Rejection reason')
.contains('PROPOSAL_ERROR_CLOSE_TIME_TOO_LATE')
.and('be.visible');
cy.get_proposal_information_from_table('Error details')
.contains('proposal closing time too late')
.and('be.visible');
});
// 0005-ETXN-004
it('Unable to create a proposal - when no tokens are associated', function () {
cy.vega_wallet_teardown();
cy.get(vegaWalletAssociatedBalance, txTimeout).contains(
'0.00',
txTimeout
);
cy.go_to_make_new_proposal(governanceProposalType.RAW);
cy.create_ten_digit_unix_timestamp_for_specified_days('8').then(
(closingDateTimestamp) => {
cy.enter_raw_proposal_body(closingDateTimestamp).as;
}
);
cy.contains('Transaction failed', proposalTimeout).should('be.visible');
cy.get(feedbackError).should(
'have.text',
'Network error: the network blocked the transaction through the spam protection'
);
cy.get(dialogCloseButton).click();
});
// 3002-PROP-009
it('Unable to create a proposal - when some but not enough tokens are associated', function () {
cy.ensure_specified_unstaked_tokens_are_associated(0.000001);
cy.go_to_make_new_proposal(governanceProposalType.RAW);
cy.create_ten_digit_unix_timestamp_for_specified_days('8').then(
(closingDateTimestamp) => {
cy.enter_raw_proposal_body(closingDateTimestamp);
}
);
cy.contains('Transaction failed', proposalTimeout).should('be.visible');
cy.get(feedbackError).should(
'have.text',
'Network error: the network blocked the transaction through the spam protection'
);
cy.get(dialogCloseButton).click();
});
it('Unable to create a freeform proposal - when json parent section contains unexpected field', function () {
// 3001-VOTE-038 3002-PROP-013 3002-PROP-014
cy.go_to_make_new_proposal(governanceProposalType.RAW);
cy.create_ten_digit_unix_timestamp_for_specified_days('8').then(
(closingDateTimestamp) => {
cy.fixture('/proposals/raw.json').then((freeformProposal) => {
freeformProposal.terms.closingTimestamp = closingDateTimestamp;
freeformProposal.unexpected = `i shouldn't be here`;
let proposalPayload = JSON.stringify(freeformProposal);
cy.get(rawProposalData).type(proposalPayload, {
parseSpecialCharSequences: false,
delay: 2,
});
});
}
);
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.contains('Transaction failed', proposalTimeout).should('be.visible');
cy.get(feedbackError).should(
'have.text',
'Invalid params: the transaction is malformed'
);
cy.get(dialogCloseButton).click();
cy.get(rawProposalData)
.invoke('val')
.should('contain', "i shouldn't be here");
});
it('Unable to create a freeform proposal - when json terms section contains unexpected field', function () {
// 3001-VOTE-038
cy.go_to_make_new_proposal(governanceProposalType.RAW);
cy.create_ten_digit_unix_timestamp_for_specified_days('8').then(
(closingDateTimestamp) => {
cy.fixture('/proposals/raw.json').then((rawProposal) => {
rawProposal.terms.closingTimestamp = closingDateTimestamp;
rawProposal.terms.unexpectedField = `i shouldn't be here`;
let proposalPayload = JSON.stringify(rawProposal);
cy.get(rawProposalData).type(proposalPayload, {
parseSpecialCharSequences: false,
delay: 2,
});
});
}
);
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.contains('Transaction failed', proposalTimeout).should('be.visible');
cy.get(feedbackError).should(
'have.text',
'Invalid params: the transaction is malformed'
);
cy.get(dialogCloseButton).click();
});
// 1005-PROP-009
it.skip(
'Unable to vote on a freeform proposal - when some but not enough vega associated',
{ tags: '@smoke' },
function () {
const proposalTitle = generateFreeFormProposalTitle();
cy.ensure_specified_unstaked_tokens_are_associated(1);
cy.go_to_make_new_proposal(governanceProposalType.FREEFORM);
cy.enter_unique_freeform_proposal_body('50', proposalTitle);
cy.wait_for_proposal_submitted();
cy.staking_page_disassociate_tokens('0.0001');
cy.get(vegaWallet).within(() => {
cy.get(vegaWalletAssociatedBalance).should('have.length', 1);
cy.get(vegaWalletAssociatedBalance, txTimeout).should(
'contain',
'0.9999'
);
});
cy.navigate_to('proposals');
cy.get_submitted_proposal_from_proposal_list(proposalTitle).within(() =>
cy.get(viewProposalButton).click()
);
cy.contains('Vote breakdown').should('be.visible', {
timeout: 10000,
});
cy.get(voteButtons).should('not.exist');
cy.getByTestId('min-proposal-requirements').should(
'have.text',
`You must have at least ${this.minVoterBalance} VEGA associated to vote on this proposal`
);
}
);
it('Unable to vote on a proposal - when vega wallet disconnected - option to connect from within', function () {
createRawProposal();
cy.get('[data-testid="manage-vega-wallet"]').click();
cy.get('[data-testid="disconnect"]').click();
cy.get('@rawProposal').then((rawProposal) => {
cy.get_submitted_proposal_from_proposal_list(
rawProposal.rationale.title
).within(() => cy.get(viewProposalButton).click());
});
// 3001-VOTE-075
// 3001-VOTE-076
cy.get(connectToVegaWalletButton)
.should('be.visible')
.and('have.text', 'Connect Vega wallet')
.click();
cy.getByTestId('connector-jsonRpc').click();
cy.get(vegaWalletNameElement).should('be.visible');
cy.get(connectToVegaWalletButton).should('not.exist');
// 3001-VOTE-100
cy.get(vegaWalletAssociatedBalance, txTimeout).contains(
'1.00',
txTimeout
);
cy.vote_for_proposal('against');
// 3001-VOTE-079
cy.contains('You voted: Against').should('be.visible');
});
}
);
@@ -1,162 +0,0 @@
import {
createFreeformProposal,
createRawProposal,
generateFreeFormProposalTitle,
governanceProposalType,
} from '../../support/governance.functions';
const proposalDetailsTitle = '[data-testid="proposal-title"]';
const openProposals = '[data-testid="open-proposals"]';
const voteStatus = '[data-testid="vote-status"]';
const viewProposalButton = '[data-testid="view-proposal-btn"]';
describe('Governance flow for proposal list', { tags: '@slow' }, function () {
before('connect wallets and set approval limit', function () {
cy.vega_wallet_set_specified_approval_amount('1000');
cy.visit('/');
});
beforeEach('visit proposals tab', function () {
cy.reload();
cy.wait_for_spinner();
cy.connectVegaWallet();
cy.ethereum_wallet_connect();
cy.ensure_specified_unstaked_tokens_are_associated(1);
cy.navigate_to_page_if_not_already_loaded('proposals');
});
it('Newly created proposals list - proposals closest to closing date appear higher in list', function () {
const minCloseDays = 2;
const maxCloseDays = 3;
// 3001-VOTE-005
let proposalDays = [
minCloseDays + 1,
maxCloseDays,
minCloseDays + 3,
minCloseDays + 2,
];
for (var index = 0; index < proposalDays.length; index++) {
cy.go_to_make_new_proposal(governanceProposalType.RAW);
cy.create_ten_digit_unix_timestamp_for_specified_days(
proposalDays[index]
).then((closingDateTimestamp) => {
cy.enter_raw_proposal_body(closingDateTimestamp);
});
cy.wait_for_proposal_submitted();
cy.wait_for_proposal_sync();
}
let arrayOfProposals = [];
cy.navigate_to('proposals');
cy.get(proposalDetailsTitle)
.each((proposalTitleElement) => {
arrayOfProposals.push(proposalTitleElement.text());
})
.then(() => {
cy.get_sort_order_of_supplied_array(arrayOfProposals).should(
'equal',
'descending'
);
});
});
it('Newly created proposals list - able to filter by proposerID to show it in list', function () {
const proposerId = Cypress.env('vegaWalletPublicKey');
const proposalTitle = generateFreeFormProposalTitle();
createFreeformProposal(proposalTitle);
cy.get_proposal_id_from_list(proposalTitle);
cy.get('@proposalIdText').then((proposalId) => {
cy.get('[data-testid="set-proposals-filter-visible"]').click();
cy.get('[data-testid="filter-input"]').type(proposerId);
cy.get(`#${proposalId}`).should('contain', proposalId);
});
});
it('Newly created proposals list - shows title and portion of summary', function () {
createRawProposal(this.minProposerBalance); // 3001-VOTE-052
cy.get('@rawProposal').then((rawProposal) => {
cy.get_proposal_id_from_list(rawProposal.rationale.title);
cy.get('@proposalIdText').then((proposalId) => {
cy.get(openProposals).within(() => {
// 3001-VOTE-008
// 3001-VOTE-034
cy.get(`#${proposalId}`)
// 3001-VOTE-097
.should('contain', rawProposal.rationale.title)
.and('be.visible');
cy.get(`#${proposalId}`)
.should(
'contain',
rawProposal.rationale.description.substring(0, 59)
)
.and('be.visible');
});
});
});
});
it('Newly created proposals list - shows open proposals in an open state', function () {
// 3001-VOTE-004
// 3001-VOTE-035
createRawProposal(this.minProposerBalance);
cy.get('@rawProposal').then((rawProposal) => {
cy.get_submitted_proposal_from_proposal_list(
rawProposal.rationale.title
).within(() => {
cy.get(viewProposalButton).should('be.visible').click();
});
cy.get('@proposalIdText').then((proposalId) => {
cy.get_proposal_information_from_table('ID')
.contains(proposalId)
.and('be.visible');
});
cy.get_proposal_information_from_table('State')
.contains('Open')
.and('be.visible');
cy.get_proposal_information_from_table('Type')
.contains('Freeform')
.and('be.visible');
});
});
// 3001-VOTE-071
it('Newly created freeform proposals list - shows proposal participation - both met and not', function () {
const proposalTitle = generateFreeFormProposalTitle();
const requiredParticipation = 0.001;
createFreeformProposal(proposalTitle);
cy.get_submitted_proposal_from_proposal_list(proposalTitle)
.as('submittedProposal')
.within(() => {
// 3001-VOTE-039
cy.get(voteStatus).should('have.text', 'Participation not reached');
cy.get(viewProposalButton).click();
});
cy.vote_for_proposal('for');
cy.get_proposal_information_from_table('Total Supply')
.invoke('text')
.then((totalSupply) => {
let tokensRequiredToAchieveResult = parseFloat(
(totalSupply.replace(/,/g, '') * requiredParticipation) / 100
).toFixed(2);
cy.ensure_specified_unstaked_tokens_are_associated(
tokensRequiredToAchieveResult
);
cy.navigate_to_page_if_not_already_loaded('proposals');
cy.get('@submittedProposal').within(() =>
cy.get(viewProposalButton).click()
);
cy.get_proposal_information_from_table('Token participation met')
.contains('👍')
.should('be.visible');
cy.navigate_to('proposals');
cy.get('@submittedProposal').within(() =>
cy.get(voteStatus).should('have.text', 'Set to pass')
);
});
});
});
@@ -63,39 +63,6 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
.and('have.text', 'Browse, vote, and propose');
});
});
it('should show open or enacted proposals with proposal summary', function () {
cy.get('body').then(($body) => {
if (!$body.find('[data-testid="proposals-list-item"]').length) {
cy.createMarket();
cy.reload();
cy.wait_for_spinner();
}
});
cy.getByTestId('proposals-list-item')
.should('have.length.at.least', 1)
.first()
.within(() => {
cy.getByTestId('proposal-title')
.invoke('text')
.should('not.be.empty');
cy.getByTestId('proposal-type')
.invoke('text')
.should('not.be.empty');
cy.getByTestId('proposal-description')
.invoke('text')
.should('not.be.empty');
cy.getByTestId('proposal-details')
.invoke('text')
.should('not.be.empty');
cy.getByTestId('proposal-status')
.invoke('text')
.should('not.be.empty');
cy.getByTestId('vote-details')
.invoke('text')
.should('not.be.empty');
cy.getByTestId('view-proposal-btn').should('be.visible');
});
});
it('should have external link for governance', function () {
cy.getByTestId('home-proposals').within(() => {
cy.getByTestId('external-link')
@@ -38,6 +38,7 @@ context('View functionality with public key', { tags: '@smoke' }, function () {
cy.navigate_to('proposals');
cy.go_to_make_new_proposal('Freeform');
cy.enter_unique_freeform_proposal_body('50', 'pub key proposal test');
cy.getByTestId('proposal-submit').should('be.visible').click();
cy.getByTestId('dialog-content').within(() => {
cy.get('h1').should('have.text', 'Transaction failed');
cy.getByTestId('Error').should('have.text', expectedErrorTxt);
@@ -30,11 +30,11 @@ context('Staking Page - verify elements on page', function () {
describe('with wallets disconnected', { tags: '@smoke' }, function () {
describe('description section', function () {
it('Should have validators tab highlighted', function () {
it('Should have staking tab highlighted', function () {
cy.verify_tab_highlighted('validators');
});
it('Should have validators ON VEGA header visible', function () {
it('Should have STAKING ON VEGA header visible', function () {
cy.verify_page_header('Validators');
});
@@ -338,8 +338,8 @@ context(
.contains(currency.id)
.parent()
.siblings()
.invoke('text')
.should('not.be.empty');
.within(() => cy.contains_exactly('10.00').should('be.visible'));
cy.get(vegaWalletCurrencyTitle)
.contains(currency.id)
.parent()
@@ -1,6 +1,3 @@
const epochTimeout = Cypress.env('epochTimeout');
const txTimeout = Cypress.env('txTimeout');
Cypress.Commands.add(
'convert_token_value_to_number',
{ prevSubject: true },
@@ -52,29 +49,3 @@ Cypress.Commands.add('wait_for_spinner', () => {
cy.get(navigation.pageSpinner, Cypress.env('epochTimeout')).should('exist');
cy.get(navigation.pageSpinner, { timeout: 20000 }).should('not.exist');
});
// This is a workaround function to begin tests with associating tokens without failing
// Should be removed when eth transaction bug is fixed
export function associateTokenStartOfTests() {
cy.highlight(`Associating tokens for first time`);
cy.ethereum_wallet_connect();
cy.connectVegaWallet();
cy.get('[href="/token/associate"]').first().click();
cy.getByTestId('associate-radio-wallet', { timeout: 30000 }).click();
cy.getByTestId('token-amount-input', epochTimeout).type('1');
cy.getByTestId('token-input-submit-button', txTimeout)
.should('be.enabled')
.click();
cy.contains(
`Associating with Vega key. Waiting for ${Cypress.env(
'blockConfirmations'
)} more confirmations..`,
txTimeout
).should('be.visible');
cy.getByTestId('associated-amount', txTimeout).should('contain.text', '1');
// Wait is needed to allow time for transaction to complete
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(10000);
cy.vega_wallet_teardown();
cy.clearLocalStorage();
}
@@ -8,7 +8,6 @@ const rawProposalData = '[data-testid="proposal-data"]';
const voteButtons = '[data-testid="vote-buttons"]';
const dialogTitle = '[data-testid="dialog-title"]';
const proposalVoteDeadline = '[data-testid="proposal-vote-deadline"]';
const newProposalSubmitButton = '[data-testid="proposal-submit"]';
const dialogCloseButton = '[data-testid="dialog-close"]';
const epochTimeout = Cypress.env('epochTimeout');
const proposalTimeout = { timeout: 14000 };
@@ -62,7 +61,6 @@ Cypress.Commands.add('enter_raw_proposal_body', (timestamp) => {
parseSpecialCharSequences: false,
delay: 2,
});
cy.get(newProposalSubmitButton).should('be.visible').click();
cy.wrap(rawProposal);
});
});
@@ -75,7 +73,6 @@ Cypress.Commands.add(
'this is a e2e freeform proposal description'
);
cy.get(proposalVoteDeadline).clear().click().type(timestamp);
cy.getByTestId('proposal-submit').should('be.visible').click();
}
);
@@ -188,40 +185,3 @@ Cypress.Commands.add('wait_for_proposal_submitted', () => {
cy.contains('Proposal submitted', proposalTimeout).should('be.visible');
cy.get(dialogCloseButton).click();
});
export function createRawProposal(proposerBalance) {
if (proposerBalance)
cy.ensure_specified_unstaked_tokens_are_associated(proposerBalance);
cy.go_to_make_new_proposal('raw proposal');
cy.create_ten_digit_unix_timestamp_for_specified_days('8').then(
(closingDateTimestamp) => {
cy.enter_raw_proposal_body(closingDateTimestamp).as('rawProposal');
}
);
cy.wait_for_proposal_submitted();
cy.wait_for_proposal_sync();
cy.navigate_to('proposals');
}
export function generateFreeFormProposalTitle() {
const randomNum = Math.floor(Math.random() * 1000) + 1;
return randomNum + ': Freeform e2e proposal';
}
export function createFreeformProposal(proposalTitle) {
cy.go_to_make_new_proposal(governanceProposalType.FREEFORM);
cy.enter_unique_freeform_proposal_body('50', proposalTitle);
cy.wait_for_proposal_submitted();
cy.wait_for_proposal_sync();
cy.getByTestId('proposal-title').invoke('text').as('proposalTitle');
cy.navigate_to('proposals');
}
export const governanceProposalType = {
NETWORK_PARAMETER: 'Network parameter',
NEW_MARKET: 'New market',
UPDATE_MARKET: 'Update market',
NEW_ASSET: 'New asset',
FREEFORM: 'Freeform',
RAW: 'raw proposal',
};
@@ -18,7 +18,7 @@ export const NavDropDown = ({ navbarTheme }: { navbarTheme: NavbarTheme }) => {
<AppNavLink
name={
<NavDropdownMenuTrigger
className="w-auto flex items-center -m-3 p-3 cursor-pointer"
className="w-auto flex items-center"
data-testid="state-trigger"
onClick={() => setOpen(!isOpen)}
>
@@ -1,4 +1,4 @@
import { act } from '@testing-library/react';
import { act } from '@testing-library/react-hooks';
import { usePendingBalancesStore } from './use-pending-balances-manager';
import type { Event } from 'ethers';
@@ -1,6 +1,7 @@
import uniqBy from 'lodash/uniqBy';
import { create } from 'zustand';
import type { Event } from 'ethers';
import uniqBy from 'lodash/uniqBy';
import create from 'zustand';
export type PendingTxsStore = {
pendingBalances: Event[];
@@ -432,7 +432,6 @@
"associatedWithVegaKeys": "Associated with Vega keys",
"thisEpoch": "This Epoch",
"nextEpoch": "Next epoch",
"toSeeYourRewardsConnectYourWallet": "TO SEE YOUR REWARDS, CONNECT YOUR WALLET",
"rewardsIntro": "Earn rewards and infrastructure fees for trading and maintaining the network.",
"rewardsCallout": "Rewards are credited {{duration}} after the epoch ends.",
"rewardsCalloutDetail": "This delay is set by a network parameter",
@@ -458,7 +457,6 @@
"rewardsColMarketCreationHeader": "MARKET CREATION",
"rewardsColMarketCreationTooltip": "Market creation rewards are paid out to the creator of any market that exceeds a set threshold of cumulative volume in a given epoch, currently [rewards.marketCreationQuantumMultiple]",
"rewardsColTotalHeader": "TOTAL",
"ofTotalDistributed": "of total distributed",
"checkBackSoon": "Check back soon",
"yourStake": "Your stake",
"reward": "Reward",
@@ -137,24 +137,6 @@ query Proposal($proposalId: ID!) {
}
}
}
priceMonitoringParameters {
triggers {
horizonSecs
probability
auctionExtensionSecs
}
}
liquidityMonitoringParameters {
triggeringRatio
targetStakeParameters {
timeWindow
scalingFactor
}
}
positionDecimalPlaces
lpPriceRange
linearSlippageFactor
quadraticSlippageFactor
}
... on UpdateMarket {
marketId
File diff suppressed because one or more lines are too long
@@ -1,25 +0,0 @@
import { render } from '@testing-library/react';
import { AppStateProvider } from '../../contexts/app-state/app-state-provider';
import { ConnectToSeeRewards } from './connect-to-see-rewards';
describe('ConnectToSeeRewards', () => {
it('should render button correctly', () => {
const { getByTestId } = render(
<AppStateProvider>
<ConnectToSeeRewards />
</AppStateProvider>
);
expect(getByTestId('connect-to-vega-wallet-btn')).toBeInTheDocument();
});
it('should render the correct text', () => {
const { getByText } = render(
<AppStateProvider>
<ConnectToSeeRewards />
</AppStateProvider>
);
expect(
getByText('TO SEE YOUR REWARDS, CONNECT YOUR WALLET')
).toBeInTheDocument();
});
});
@@ -1,40 +0,0 @@
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { Button } from '@vegaprotocol/ui-toolkit';
import {
AppStateActionType,
useAppState,
} from '../../contexts/app-state/app-state-context';
import { SubHeading } from '../../components/heading';
export const ConnectToSeeRewards = () => {
const { appDispatch } = useAppState();
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
}));
const { t } = useTranslation();
const classes = classNames(
'flex flex-col items-center justify-center h-[300px] w-full',
'border border-vega-dark-200'
);
return (
<div className={classes}>
<SubHeading title={t('toSeeYourRewardsConnectYourWallet')} />
<Button
data-testid="connect-to-vega-wallet-btn"
onClick={() => {
appDispatch({
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY,
isOpen: true,
});
openVegaWalletDialog();
}}
>
{t('connectVegaWallet')}
</Button>
</div>
);
};
@@ -5,10 +5,9 @@ import { removePaginationWrapper } from '@vegaprotocol/react-helpers';
import { useRewardsQuery } from '../home/__generated__/Rewards';
import { ENV } from '../../../config';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { EpochIndividualRewardsTable } from './epoch-individual-rewards-table';
import { generateEpochIndividualRewardsList } from './generate-epoch-individual-rewards-list';
import { RewardTable } from './reward-table';
export const EpochIndividualRewards = () => {
export const RewardInfo = () => {
const { t } = useTranslation();
const { pubKey } = useVegaWallet();
const { delegationsPagination } = ENV;
@@ -31,10 +30,13 @@ export const EpochIndividualRewards = () => {
return removePaginationWrapper(data.party.rewardsConnection.edges);
}, [data]);
const epochIndividualRewardSummaries = useMemo(() => {
if (!data?.party) return [];
return generateEpochIndividualRewardsList(rewards);
}, [data?.party, rewards]);
const delegations = useMemo(() => {
if (!data?.party || !data.party.delegationsConnection?.edges?.length) {
return [];
}
return removePaginationWrapper(data.party.delegationsConnection.edges);
}, [data]);
return (
<AsyncRenderer
@@ -43,18 +45,20 @@ export const EpochIndividualRewards = () => {
data={data}
render={() => (
<div>
<p className="mb-10">
{t('Connected Vega key')}:{' '}
<span className="text-white">{pubKey}</span>
<p>
{t('Connected Vega key')}: {pubKey}
</p>
{epochIndividualRewardSummaries.length ? (
epochIndividualRewardSummaries.map(
(epochIndividualRewardSummary) => (
<EpochIndividualRewardsTable
data={epochIndividualRewardSummary}
{rewards.length ? (
rewards.map((reward, i) => {
if (!reward) return null;
return (
<RewardTable
key={i}
reward={reward}
delegations={delegations || []}
/>
)
)
);
})
) : (
<p>{t('noRewards')}</p>
)}
@@ -0,0 +1,79 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useAppState } from '../../../contexts/app-state/app-state-context';
import { formatNumber, toBigNum } from '@vegaprotocol/react-helpers';
import { BigNumber } from '../../../lib/bignumber';
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
import { format } from 'date-fns';
import { DATE_FORMAT_DETAILED } from '../../../lib/date-formats';
import type {
DelegationFieldsFragment,
RewardFieldsFragment,
} from '../home/__generated__/Rewards';
interface RewardTableProps {
reward: RewardFieldsFragment;
delegations: DelegationFieldsFragment[] | [];
}
export const RewardTable = ({ reward, delegations }: RewardTableProps) => {
const { t } = useTranslation();
const {
appState: { decimals },
} = useAppState();
// Get your stake for epoch in which you have rewards
const stakeForEpoch = useMemo(() => {
if (!delegations.length) return '0';
const delegationsForEpoch = delegations
.filter((d) => d.epoch.toString() === reward.epoch.id)
.map((d) => toBigNum(d.amount, decimals));
if (delegationsForEpoch.length) {
return BigNumber.sum.apply(null, [
new BigNumber(0),
...delegationsForEpoch,
]);
}
return new BigNumber(0);
}, [decimals, delegations, reward.epoch.id]);
return (
<div className="mb-24">
<h3 className="text-lg text-white mb-4">
{t('Epoch')} {reward.epoch.id}
</h3>
<KeyValueTable>
<KeyValueTableRow>
{t('rewardType')}
<span>{reward.rewardType}</span>
</KeyValueTableRow>
<KeyValueTableRow>
{t('yourStake')}
<span>{stakeForEpoch.toString()}</span>
</KeyValueTableRow>
<KeyValueTableRow>
{t('reward')}
<span>
{formatNumber(toBigNum(reward.amount, decimals))}{' '}
{reward.asset.symbol}
</span>
</KeyValueTableRow>
<KeyValueTableRow>
{t('shareOfReward')}
<span>
{new BigNumber(reward.percentageOfTotal).dp(2).toString()}%
</span>
</KeyValueTableRow>
<KeyValueTableRow>
{t('received')}
<span>
{format(new Date(reward.receivedAt), DATE_FORMAT_DETAILED)}
</span>
</KeyValueTableRow>
</KeyValueTable>
</div>
);
};
@@ -1,63 +0,0 @@
import { render } from '@testing-library/react';
import { AppStateProvider } from '../../../contexts/app-state/app-state-provider';
import { EpochIndividualRewardsTable } from './epoch-individual-rewards-table';
const mockData = {
epoch: '4441',
rewards: [
{
asset: 'tDAI',
totalAmount: '5',
rewardTypes: {
ACCOUNT_TYPE_GLOBAL_REWARD: {
amount: '0',
percentageOfTotal: '0',
},
ACCOUNT_TYPE_FEES_INFRASTRUCTURE: {
amount: '5',
percentageOfTotal: '0.00305237260923',
},
ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES: {
amount: '0',
percentageOfTotal: '0',
},
ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES: {
amount: '0',
percentageOfTotal: '0',
},
ACCOUNT_TYPE_FEES_LIQUIDITY: {
amount: '0',
percentageOfTotal: '0',
},
ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS: {
amount: '0',
percentageOfTotal: '0',
},
},
},
],
};
describe('EpochIndividualRewardsTable', () => {
it('should render correctly', () => {
const { getByTestId } = render(
<AppStateProvider>
<EpochIndividualRewardsTable data={mockData} />
</AppStateProvider>
);
expect(getByTestId('epoch-individual-rewards-table')).toBeInTheDocument();
expect(getByTestId('individual-rewards-asset')).toBeInTheDocument();
expect(getByTestId('ACCOUNT_TYPE_GLOBAL_REWARD')).toBeInTheDocument();
expect(getByTestId('ACCOUNT_TYPE_FEES_INFRASTRUCTURE')).toBeInTheDocument();
expect(
getByTestId('ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES')
).toBeInTheDocument();
expect(
getByTestId('ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES')
).toBeInTheDocument();
expect(getByTestId('ACCOUNT_TYPE_FEES_LIQUIDITY')).toBeInTheDocument();
expect(
getByTestId('ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS')
).toBeInTheDocument();
});
});
@@ -1,111 +0,0 @@
import { formatNumber, toBigNum } from '@vegaprotocol/react-helpers';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import { useAppState } from '../../../contexts/app-state/app-state-context';
import {
rowGridItemStyles,
RewardsTable,
} from '../shared-rewards-table-assets/shared-rewards-table-assets';
import type { EpochIndividualReward } from './generate-epoch-individual-rewards-list';
import { useTranslation } from 'react-i18next';
interface EpochIndividualRewardsGridProps {
data: EpochIndividualReward;
}
interface RewardItemProps {
value: string;
percentageOfTotal?: string;
dataTestId: string;
last?: boolean;
}
const DisplayReward = ({
reward,
percentageOfTotal,
}: {
reward: string;
percentageOfTotal?: string;
}) => {
const { t } = useTranslation();
const {
appState: { decimals },
} = useAppState();
if (Number(reward) === 0) {
return <span className="text-vega-dark-300">-</span>;
}
return (
<Tooltip
description={
<div className="flex flex-col items-start">
<span>{formatNumber(toBigNum(reward, decimals), decimals)}</span>
{percentageOfTotal && (
<span className="text-vega-dark-300">
({percentageOfTotal}% {t('ofTotalDistributed')})
</span>
)}
</div>
}
>
<button>
<div className="flex flex-col items-start">
<span>{formatNumber(toBigNum(reward, decimals))}</span>
{percentageOfTotal && (
<span className="text-vega-dark-300">
({formatNumber(toBigNum(percentageOfTotal, 4)).toString()}%)
</span>
)}
</div>
</button>
</Tooltip>
);
};
const RewardItem = ({
value,
percentageOfTotal,
dataTestId,
last,
}: RewardItemProps) => (
<div data-testid={dataTestId} className={rowGridItemStyles(last)}>
<div className="h-full w-5 absolute right-0 top-0 bg-gradient-to-r from-transparent to-black pointer-events-none" />
<div className="overflow-auto p-5">
<DisplayReward reward={value} percentageOfTotal={percentageOfTotal} />
</div>
<div className="h-full w-5 absolute left-0 top-0 bg-gradient-to-l from-transparent to-black pointer-events-none" />
</div>
);
export const EpochIndividualRewardsTable = ({
data,
}: EpochIndividualRewardsGridProps) => {
return (
<RewardsTable
dataTestId="epoch-individual-rewards-table"
epoch={Number(data.epoch)}
>
{data.rewards.map(({ asset, rewardTypes, totalAmount }, i) => (
<div className="contents" key={i}>
<div
data-testid="individual-rewards-asset"
className={`${rowGridItemStyles()} p-5`}
>
{asset}
</div>
{Object.entries(rewardTypes).map(
([key, { amount, percentageOfTotal }]) => (
<RewardItem
key={key}
value={amount}
percentageOfTotal={percentageOfTotal}
dataTestId={key}
/>
)
)}
<RewardItem dataTestId="total" value={totalAmount} last={true} />
</div>
))}
</RewardsTable>
);
};
@@ -1,235 +0,0 @@
import { generateEpochIndividualRewardsList } from './generate-epoch-individual-rewards-list';
import { AccountType } from '@vegaprotocol/types';
import type { RewardFieldsFragment } from '../home/__generated__/Rewards';
describe('generateEpochIndividualRewardsList', () => {
const reward1: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '100',
percentageOfTotal: '0.1',
receivedAt: new Date(),
asset: { id: 'usd', symbol: 'USD' },
party: { id: 'blah' },
epoch: { id: '1' },
};
const reward2: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '50',
percentageOfTotal: '0.05',
receivedAt: new Date(),
asset: { id: 'eur', symbol: 'EUR' },
party: { id: 'blah' },
epoch: { id: '2' },
};
const reward3: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '200',
percentageOfTotal: '0.2',
receivedAt: new Date(),
asset: { id: 'gbp', symbol: 'GBP' },
party: { id: 'blah' },
epoch: { id: '2' },
};
const reward4: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '100',
percentageOfTotal: '0.1',
receivedAt: new Date(),
asset: { id: 'usd', symbol: 'USD' },
party: { id: 'blah' },
epoch: { id: '1' },
};
const rewardWrongType: RewardFieldsFragment = {
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE,
amount: '50',
percentageOfTotal: '0.05',
receivedAt: new Date(),
asset: { id: 'eur', symbol: 'EUR' },
party: { id: 'blah' },
epoch: { id: '2' },
};
it('should return an empty array if no rewards are provided', () => {
expect(generateEpochIndividualRewardsList([])).toEqual([]);
});
it('should filter out any rewards of the wrong type', () => {
const result = generateEpochIndividualRewardsList([rewardWrongType]);
expect(result).toEqual([]);
});
it('should return reward in the correct format', () => {
const result = generateEpochIndividualRewardsList([reward1]);
expect(result[0]).toEqual({
epoch: '1',
rewards: [
{
asset: 'USD',
totalAmount: '100',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '100',
percentageOfTotal: '0.1',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
],
});
});
it('should return an array sorted by epoch descending', () => {
const rewards = [reward1, reward2, reward3, reward4];
const result1 = generateEpochIndividualRewardsList(rewards);
expect(result1[0].epoch).toEqual('2');
expect(result1[1].epoch).toEqual('1');
const reorderedRewards = [reward4, reward3, reward2, reward1];
const result2 = generateEpochIndividualRewardsList(reorderedRewards);
expect(result2[0].epoch).toEqual('2');
expect(result2[1].epoch).toEqual('1');
});
it('correctly calculates the total value of rewards for an asset', () => {
const rewards = [reward1, reward4];
const result = generateEpochIndividualRewardsList(rewards);
expect(result[0].rewards[0].totalAmount).toEqual('200');
});
it('returns data in the expected shape', () => {
// Just sanity checking the whole structure here
const rewards = [reward1, reward2, reward3, reward4];
const result = generateEpochIndividualRewardsList(rewards);
expect(result).toEqual([
{
epoch: '2',
rewards: [
{
asset: 'EUR',
totalAmount: '50',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '50',
percentageOfTotal: '0.05',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
{
asset: 'GBP',
totalAmount: '200',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
amount: '200',
percentageOfTotal: '0.2',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
],
},
{
epoch: '1',
rewards: [
{
asset: 'USD',
totalAmount: '200',
rewardTypes: {
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
amount: '100',
percentageOfTotal: '0.1',
},
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
amount: '100',
percentageOfTotal: '0.1',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
amount: '0',
percentageOfTotal: '0',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
amount: '0',
percentageOfTotal: '0',
},
},
},
],
},
]);
});
});
@@ -1,79 +0,0 @@
import { BigNumber } from '../../../lib/bignumber';
import { RowAccountTypes } from '../shared-rewards-table-assets/shared-rewards-table-assets';
import type { RewardFieldsFragment } from '../home/__generated__/Rewards';
import type { AccountType } from '@vegaprotocol/types';
export interface EpochIndividualReward {
epoch: string;
rewards: {
asset: string;
totalAmount: string;
rewardTypes: {
[key in AccountType]?: {
amount: string;
percentageOfTotal: string;
};
};
}[];
}
const accountTypes = Object.keys(RowAccountTypes);
const emptyRowAccountTypes = accountTypes.map((type) => [
type,
{
amount: '0',
percentageOfTotal: '0',
},
]);
export const generateEpochIndividualRewardsList = (
rewards: RewardFieldsFragment[]
) => {
// We take the rewards and aggregate them by epoch and asset.
const epochIndividualRewards = rewards.reduce((map, reward) => {
const epochId = reward.epoch.id;
const assetName = reward.asset.symbol;
const rewardType = reward.rewardType;
const amount = reward.amount;
const percentageOfTotal = reward.percentageOfTotal;
// if the rewardType is not of a type we display in the table, we skip it.
if (!accountTypes.includes(rewardType)) {
return map;
}
if (!map.has(epochId)) {
map.set(epochId, { epoch: epochId, rewards: [] });
}
const epoch = map.get(epochId);
let asset = epoch?.rewards.find((r) => r.asset === assetName);
if (!asset) {
asset = {
asset: assetName,
totalAmount: '0',
rewardTypes: Object.fromEntries(emptyRowAccountTypes),
};
epoch?.rewards.push(asset);
}
asset.rewardTypes[rewardType] = { amount, percentageOfTotal };
// totalAmount is the sum of all rewardTypes amounts
asset.totalAmount = Object.values(asset.rewardTypes).reduce(
(sum, rewardType) => {
return new BigNumber(sum).plus(rewardType.amount).toString();
},
'0'
);
return map;
}, new Map<string, EpochIndividualReward>());
return Array.from(epochIndividualRewards.values()).sort(
(a, b) => Number(b.epoch) - Number(a.epoch)
);
};
@@ -5,7 +5,7 @@ import { generateEpochTotalRewardsList } from './generate-epoch-total-rewards-li
import { NoRewards } from '../no-rewards';
import { EpochTotalRewardsTable } from './epoch-total-rewards-table';
export const EpochTotalRewards = () => {
export const EpochRewards = () => {
const { data, loading, error, refetch } = useEpochAssetsRewardsQuery({
variables: {
epochRewardSummariesPagination: {
@@ -15,7 +15,7 @@ export const EpochTotalRewards = () => {
});
useRefreshAfterEpoch(data?.epoch.timestamps.expiry, refetch);
const epochTotalRewardSummaries = generateEpochTotalRewardsList(data) || [];
const epochRewardSummaries = generateEpochTotalRewardsList(data) || [];
return (
<AsyncRenderer
@@ -27,12 +27,12 @@ export const EpochTotalRewards = () => {
className="max-w-full overflow-auto"
data-testid="epoch-rewards-total"
>
{epochTotalRewardSummaries.length === 0 ? (
{epochRewardSummaries.length === 0 ? (
<NoRewards />
) : (
<>
{epochTotalRewardSummaries.map((epochTotalSummary, index) => (
<EpochTotalRewardsTable data={epochTotalSummary} key={index} />
{epochRewardSummaries.map((aggregatedEpochSummary) => (
<EpochTotalRewardsTable data={aggregatedEpochSummary} />
))}
</>
)}
@@ -1,5 +1,4 @@
import { render } from '@testing-library/react';
import { AppStateProvider } from '../../../contexts/app-state/app-state-provider';
import { EpochTotalRewardsTable } from './epoch-total-rewards-table';
import { AccountType } from '@vegaprotocol/types';
@@ -11,30 +10,10 @@ const mockData = {
'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
name: 'tDAI TEST',
rewards: [
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '295',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
totalAmount: '295',
},
@@ -43,25 +22,15 @@ const mockData = {
describe('EpochTotalRewardsTable', () => {
it('should render correctly', () => {
const { getByTestId } = render(
<AppStateProvider>
<EpochTotalRewardsTable data={mockData} />
</AppStateProvider>
);
const { getByTestId } = render(<EpochTotalRewardsTable data={mockData} />);
expect(getByTestId('epoch-total-rewards-table')).toBeInTheDocument();
expect(getByTestId('asset')).toBeInTheDocument();
expect(getByTestId('ACCOUNT_TYPE_GLOBAL_REWARD')).toBeInTheDocument();
expect(getByTestId('ACCOUNT_TYPE_FEES_INFRASTRUCTURE')).toBeInTheDocument();
expect(
getByTestId('ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES')
).toBeInTheDocument();
expect(
getByTestId('ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES')
).toBeInTheDocument();
expect(getByTestId('ACCOUNT_TYPE_FEES_LIQUIDITY')).toBeInTheDocument();
expect(
getByTestId('ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS')
).toBeInTheDocument();
expect(getByTestId('global')).toBeInTheDocument();
expect(getByTestId('infra')).toBeInTheDocument();
expect(getByTestId('taker')).toBeInTheDocument();
expect(getByTestId('maker')).toBeInTheDocument();
expect(getByTestId('liquidity')).toBeInTheDocument();
expect(getByTestId('market-maker')).toBeInTheDocument();
expect(getByTestId('total')).toBeInTheDocument();
});
});
@@ -1,14 +1,19 @@
import { formatNumber, toBigNum } from '@vegaprotocol/react-helpers';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import { useAppState } from '../../../contexts/app-state/app-state-context';
import {
rowGridItemStyles,
RewardsTable,
} from '../shared-rewards-table-assets/shared-rewards-table-assets';
import type { EpochTotalSummary } from './generate-epoch-total-rewards-list';
import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { formatNumber } from '@vegaprotocol/react-helpers';
import { Tooltip, Icon } from '@vegaprotocol/ui-toolkit';
import { AccountType } from '@vegaprotocol/types';
import { SubHeading } from '../../../components/heading';
import type { AggregatedEpochSummary } from './generate-epoch-total-rewards-list';
interface EpochTotalRewardsGridProps {
data: EpochTotalSummary;
data: AggregatedEpochSummary;
}
interface ColumnHeaderProps {
title: string;
tooltipContent?: string;
className?: string;
}
interface RewardItemProps {
@@ -17,28 +22,61 @@ interface RewardItemProps {
last?: boolean;
}
const DisplayReward = ({ reward }: { reward: string }) => {
const {
appState: { decimals },
} = useAppState();
const displayReward = (reward: string) => {
if (Number(reward) === 0) {
return <span className="text-vega-dark-300">-</span>;
return <span className="text-vega-dark-300">0</span>;
}
return (
<Tooltip description={formatNumber(toBigNum(reward, decimals), decimals)}>
<button>{formatNumber(toBigNum(reward, decimals))}</button>
</Tooltip>
);
if (reward.split('.')[1] && reward.split('.')[1].length > 4) {
return (
<Tooltip description={formatNumber(reward)}>
<button>{formatNumber(Number(reward).toFixed(4))}</button>
</Tooltip>
);
}
return <span>{formatNumber(reward)}</span>;
};
const gridStyles = classNames(
'grid grid-cols-[repeat(8,minmax(100px,auto))] max-w-full overflow-auto',
`border-t border-vega-dark-200`,
'text-sm'
);
const headerGridItemStyles = (last = false) =>
classNames('border-r border-b border-b-vega-dark-200', 'py-3 px-5', {
'border-r-vega-dark-150': !last,
'border-r-0': last,
});
const rowGridItemStyles = (last = false) =>
classNames('relative', 'border-r border-b border-b-vega-dark-150', {
'border-r-vega-dark-150': !last,
'border-r-0': last,
});
const ColumnHeader = ({
title,
tooltipContent,
className,
}: ColumnHeaderProps) => (
<div className={className}>
<h2 className="mb-1 text-sm text-vega-dark-300">{title}</h2>
{tooltipContent && (
<Tooltip description={tooltipContent}>
<button>
<Icon name={'info-sign'} className="text-vega-dark-200" />
</button>
</Tooltip>
)}
</div>
);
const RewardItem = ({ value, dataTestId, last }: RewardItemProps) => (
<div data-testid={dataTestId} className={rowGridItemStyles(last)}>
<div className="h-full w-5 absolute right-0 top-0 bg-gradient-to-r from-transparent to-black pointer-events-none" />
<div className="overflow-auto p-5">
<DisplayReward reward={value} />
</div>
<div className="overflow-auto p-5">{displayReward(value)}</div>
<div className="h-full w-5 absolute left-0 top-0 bg-gradient-to-l from-transparent to-black pointer-events-none" />
</div>
);
@@ -46,19 +84,129 @@ const RewardItem = ({ value, dataTestId, last }: RewardItemProps) => (
export const EpochTotalRewardsTable = ({
data,
}: EpochTotalRewardsGridProps) => {
const { t } = useTranslation();
const rowData = data.assetRewards.map(({ name, rewards, totalAmount }) => ({
name,
ACCOUNT_TYPE_GLOBAL_REWARD:
rewards
.filter((r) => r.rewardType === AccountType.ACCOUNT_TYPE_GLOBAL_REWARD)
.map((r) => r.amount)[0] || '0',
ACCOUNT_TYPE_FEES_INFRASTRUCTURE:
rewards
.filter(
(r) => r.rewardType === AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE
)
.map((r) => r.amount)[0] || '0',
ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES:
rewards
.filter(
(r) =>
r.rewardType === AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES
)
.map((r) => r.amount)[0] || '0',
ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES:
rewards
.filter(
(r) =>
r.rewardType === AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES
)
.map((r) => r.amount)[0] || '0',
ACCOUNT_TYPE_FEES_LIQUIDITY:
rewards
.filter((r) => r.rewardType === AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY)
.map((r) => r.amount)[0] || '0',
ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS:
rewards
.filter(
(r) =>
r.rewardType === AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS
)
.map((r) => r.amount)[0] || '0',
totalAmount: totalAmount,
}));
return (
<RewardsTable dataTestId="epoch-total-rewards-table" epoch={data.epoch}>
{data.assetRewards.map(({ name, rewards, totalAmount }, i) => (
<div className="contents" key={i}>
<div data-testid="asset" className={`${rowGridItemStyles()} p-5`}>
{name}
<div data-testid="epoch-total-rewards-table" className="mb-12">
<SubHeading title={`EPOCH ${data.epoch}`} />
<div className={gridStyles}>
<ColumnHeader
title={t('rewardsColAssetHeader')}
className={headerGridItemStyles()}
/>
<ColumnHeader
title={t('rewardsColStakingHeader')}
tooltipContent={t('rewardsColStakingTooltip')}
className={headerGridItemStyles()}
/>
<ColumnHeader
title={t('rewardsColInfraHeader')}
tooltipContent={t('rewardsColInfraTooltip')}
className={headerGridItemStyles()}
/>
<ColumnHeader
title={t('rewardsColPriceTakingHeader')}
tooltipContent={t('rewardsColPriceTakingTooltip')}
className={headerGridItemStyles()}
/>
<ColumnHeader
title={t('rewardsColPriceMakingHeader')}
tooltipContent={t('rewardsColPriceMakingTooltip')}
className={headerGridItemStyles()}
/>
<ColumnHeader
title={t('rewardsColLiquidityProvisionHeader')}
tooltipContent={t('rewardsColLiquidityProvisionTooltip')}
className={headerGridItemStyles()}
/>
<ColumnHeader
title={t('rewardsColMarketCreationHeader')}
tooltipContent={t('rewardsColMarketCreationTooltip')}
className={headerGridItemStyles()}
/>
<ColumnHeader
title={t('rewardsColTotalHeader')}
className={headerGridItemStyles(true)}
/>
{rowData.map((row, i) => (
<div className="contents" key={i}>
<div data-testid="asset" className={`${rowGridItemStyles()} p-5`}>
{row.name}
</div>
<RewardItem
dataTestId="global"
value={row.ACCOUNT_TYPE_GLOBAL_REWARD}
/>
<RewardItem
dataTestId="infra"
value={row.ACCOUNT_TYPE_FEES_INFRASTRUCTURE}
/>
<RewardItem
dataTestId="taker"
value={row.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES}
/>
<RewardItem
dataTestId="maker"
value={row.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES}
/>
<RewardItem
dataTestId="liquidity"
value={row.ACCOUNT_TYPE_FEES_LIQUIDITY}
/>
<RewardItem
dataTestId="market-maker"
value={row.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS}
/>
<RewardItem
dataTestId="total"
value={row.totalAmount}
last={true}
/>
</div>
{rewards.map(({ rewardType, amount }, i) => (
<RewardItem key={i} dataTestId={rewardType} value={amount} />
))}
<RewardItem dataTestId="total" value={totalAmount} last={true} />
</div>
))}
</RewardsTable>
))}
</div>
</div>
);
};
@@ -61,7 +61,7 @@ describe('generateEpochAssetRewardsList', () => {
expect(result).toEqual([]);
});
it('should return an array of unnamed assets if no asset names are provided (should not happen)', () => {
it('should return an array of unnamed assets if no assets are provided (should not happen)', () => {
const epochData = {
assetsConnection: {
edges: [],
@@ -72,10 +72,18 @@ describe('generateEpochAssetRewardsList', () => {
node: {
epoch: 1,
assetId: '1',
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE,
amount: '123',
},
},
{
node: {
epoch: 2,
assetId: '1',
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '5',
},
},
],
},
epoch: {
@@ -96,34 +104,30 @@ describe('generateEpochAssetRewardsList', () => {
name: '',
rewards: [
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE,
amount: '123',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
totalAmount: '123',
},
],
},
{
epoch: 2,
assetRewards: [
{
assetId: '1',
name: '',
rewards: [
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '5',
},
],
totalAmount: '5',
},
],
},
]);
});
@@ -151,7 +155,7 @@ describe('generateEpochAssetRewardsList', () => {
node: {
epoch: 1,
assetId: '1',
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE,
amount: '123',
},
},
@@ -163,6 +167,22 @@ describe('generateEpochAssetRewardsList', () => {
amount: '100',
},
},
{
node: {
epoch: 1,
assetId: '2',
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '17.9873',
},
},
{
node: {
epoch: 1,
assetId: '2',
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '1',
},
},
{
node: {
epoch: 2,
@@ -191,31 +211,30 @@ describe('generateEpochAssetRewardsList', () => {
name: 'Asset 1',
rewards: [
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE,
amount: '123',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '100',
},
],
totalAmount: '223',
},
{
assetId: '2',
name: 'Asset 2',
rewards: [
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '123',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '17.9873',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
amount: '1',
},
],
totalAmount: '223',
totalAmount: '18.9873',
},
],
},
@@ -226,30 +245,10 @@ describe('generateEpochAssetRewardsList', () => {
assetId: '1',
name: 'Asset 1',
rewards: [
{
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
amount: '0',
},
{
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
amount: '5',
},
{
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
amount: '0',
},
],
totalAmount: '5',
},
@@ -3,8 +3,6 @@ import type {
EpochRewardSummaryFieldsFragment,
} from '../home/__generated__/Rewards';
import { removePaginationWrapper } from '@vegaprotocol/react-helpers';
import { RowAccountTypes } from '../shared-rewards-table-assets/shared-rewards-table-assets';
import type { AccountType } from '@vegaprotocol/types';
interface EpochSummaryWithNamedReward extends EpochRewardSummaryFieldsFragment {
name: string;
@@ -20,16 +18,11 @@ export interface AggregatedEpochRewardSummary {
totalAmount: string;
}
export interface EpochTotalSummary {
export interface AggregatedEpochSummary {
epoch: EpochRewardSummaryFieldsFragment['epoch'];
assetRewards: AggregatedEpochRewardSummary[];
}
const emptyRowAccountTypes = Object.keys(RowAccountTypes).map((type) => ({
rewardType: type as AccountType,
amount: '0',
}));
export const generateEpochTotalRewardsList = (
epochData: EpochAssetsRewardsQuery | undefined
) => {
@@ -65,7 +58,7 @@ export const generateEpochTotalRewardsList = (
}, [] as EpochSummaryWithNamedReward[][]);
// Now aggregate the array of arrays of epoch summaries by asset rewards.
const epochTotalRewards: EpochTotalSummary[] =
const aggregatedEpochSummaries: AggregatedEpochSummary[] =
aggregatedEpochSummariesByEpochNumber.map((epochSummaries) => {
const assetRewards = epochSummaries.reduce((acc, epochSummary) => {
const assetRewardIndex = acc.findIndex(
@@ -79,36 +72,18 @@ export const generateEpochTotalRewardsList = (
assetId: epochSummary.assetId,
name: epochSummary.name,
rewards: [
...emptyRowAccountTypes.map((emptyRowAccountType) => {
if (
emptyRowAccountType.rewardType === epochSummary.rewardType
) {
return {
rewardType: epochSummary.rewardType,
amount: epochSummary.amount,
};
} else {
return emptyRowAccountType;
}
}),
{
rewardType: epochSummary.rewardType,
amount: epochSummary.amount,
},
],
totalAmount: epochSummary.amount,
});
} else {
acc[assetRewardIndex].rewards = acc[assetRewardIndex].rewards.map(
(reward) => {
if (reward.rewardType === epochSummary.rewardType) {
return {
rewardType: epochSummary.rewardType,
amount: (
Number(reward.amount) + Number(epochSummary.amount)
).toString(),
};
} else {
return reward;
}
}
);
acc[assetRewardIndex].rewards.push({
rewardType: epochSummary.rewardType,
amount: epochSummary.amount,
});
acc[assetRewardIndex].totalAmount = (
Number(acc[assetRewardIndex].totalAmount) +
Number(epochSummary.amount)
@@ -124,5 +99,5 @@ export const generateEpochTotalRewardsList = (
};
});
return epochTotalRewards;
return aggregatedEpochSummaries;
};
@@ -1,30 +1,34 @@
// @ts-ignore No types available for duration-js
import Duration from 'duration-js';
import { useMemo, useState } from 'react';
import React, { useMemo, useState } from 'react';
import { formatDistance } from 'date-fns';
import { useTranslation } from 'react-i18next';
import {
Button,
Callout,
Intent,
AsyncRenderer,
Toggle,
ExternalLink,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import {
useNetworkParams,
NetworkParams,
createDocsLinks,
} from '@vegaprotocol/react-helpers';
import {
AppStateActionType,
useAppState,
} from '../../../contexts/app-state/app-state-context';
import { useEpochQuery } from './__generated__/Rewards';
import { EpochCountdown } from '../../../components/epoch-countdown';
import { Heading, SubHeading } from '../../../components/heading';
import { EpochIndividualRewards } from '../epoch-individual-rewards/epoch-individual-rewards';
import { RewardInfo } from '../epoch-individual-awards/reward-info';
import { EpochRewards } from '../epoch-total-rewards/epoch-rewards';
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
import { useEnvironment } from '@vegaprotocol/environment';
import { ConnectToSeeRewards } from '../connect-to-see-rewards';
import { EpochTotalRewards } from '../epoch-total-rewards/epoch-total-rewards';
type RewardsView = 'total' | 'individual';
@@ -35,14 +39,10 @@ export const RewardsPage = () => {
const [toggleRewardsView, setToggleRewardsView] =
useState<RewardsView>('total');
const {
data: epochData,
loading: epochLoading,
error: epochError,
refetch,
} = useEpochQuery();
useRefreshAfterEpoch(epochData?.epoch.timestamps.expiry, refetch);
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
}));
const { appDispatch } = useAppState();
const {
params,
@@ -50,6 +50,14 @@ export const RewardsPage = () => {
error: paramsError,
} = useNetworkParams([NetworkParams.reward_staking_delegation_payoutDelay]);
const {
data: epochData,
loading: epochLoading,
error: epochError,
refetch,
} = useEpochQuery();
useRefreshAfterEpoch(epochData?.epoch.timestamps.expiry, refetch);
const payoutDuration = useMemo(() => {
if (!params) {
return 0;
@@ -95,8 +103,8 @@ export const RewardsPage = () => {
</div>
) : null}
{epochData?.epoch &&
epochData.epoch?.id &&
{epochData &&
epochData.epoch.id &&
epochData.epoch.timestamps.start &&
epochData.epoch.timestamps.expiry && (
<section className="mb-16">
@@ -140,13 +148,26 @@ export const RewardsPage = () => {
</section>
{toggleRewardsView === 'total' ? (
<EpochTotalRewards />
<EpochRewards />
) : (
<section>
{pubKey && pubKeys?.length ? (
<EpochIndividualRewards />
<RewardInfo />
) : (
<ConnectToSeeRewards />
<div>
<Button
data-testid="connect-to-vega-wallet-btn"
onClick={() => {
appDispatch({
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY,
isOpen: true,
});
openVegaWalletDialog();
}}
>
{t('connectVegaWallet')}
</Button>
</div>
)}
</section>
)}
@@ -1,125 +0,0 @@
import classNames from 'classnames';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import { useTranslation } from 'react-i18next';
import type { ReactNode } from 'react';
import { SubHeading } from '../../../components/heading';
import { AccountType } from '@vegaprotocol/types';
// This is the data structure that matters for defining which Account types
// are displayed in the rewards tables. It sets column titles and tooltips,
// and is used to filter the data that is passed to functions to generate
// the table rows. It's important to preserve the order.
export const RowAccountTypes = {
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
columnTitle: 'rewardsColStakingHeader',
description: 'rewardsColStakingTooltip',
},
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
columnTitle: 'rewardsColInfraHeader',
description: 'rewardsColInfraTooltip',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
columnTitle: 'rewardsColPriceTakingHeader',
description: 'rewardsColPriceTakingTooltip',
},
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
columnTitle: 'rewardsColPriceMakingHeader',
description: 'rewardsColPriceMakingTooltip',
},
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
columnTitle: 'rewardsColLiquidityProvisionHeader',
description: 'rewardsColLiquidityProvisionTooltip',
},
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
columnTitle: 'rewardsColMarketCreationHeader',
description: 'rewardsColMarketCreationTooltip',
},
};
interface ColumnHeaderProps {
title: string;
tooltipContent?: string;
className?: string;
}
const headerGridItemStyles = (last = false) =>
classNames('border-r border-b border-b-vega-dark-200', 'py-3 px-5', {
'border-r-vega-dark-150': !last,
'border-r-0': last,
});
export const rowGridItemStyles = (last = false) =>
classNames('relative', 'border-r border-b border-b-vega-dark-150', {
'border-r-vega-dark-150': !last,
'border-r-0': last,
});
const gridStyles = classNames(
'grid grid-cols-[repeat(8,minmax(100px,auto))] max-w-full overflow-auto',
`border-t border-vega-dark-200`,
'text-sm'
);
const ColumnHeader = ({
title,
tooltipContent,
className,
}: ColumnHeaderProps) => (
<div className={className}>
<h2 className="mb-1 text-sm text-vega-dark-300">{title}</h2>
{tooltipContent && (
<Tooltip description={tooltipContent}>
<button>
<Icon name={'info-sign'} className="text-vega-dark-200" />
</button>
</Tooltip>
)}
</div>
);
const ColumnHeaders = () => {
const { t } = useTranslation();
return (
<div className="contents">
<ColumnHeader
title={t('rewardsColAssetHeader')}
className={headerGridItemStyles()}
/>
{Object.values(RowAccountTypes).map(({ columnTitle, description }) => (
<ColumnHeader
key={columnTitle}
title={t(columnTitle)}
tooltipContent={t(description)}
className={headerGridItemStyles()}
/>
))}
<ColumnHeader
title={t('rewardsColTotalHeader')}
className={headerGridItemStyles(true)}
/>
</div>
);
};
export interface RewardTableProps {
dataTestId: string;
epoch: number;
children: ReactNode;
}
// Rewards table children will be the row items. Make sure they contain
// the same number of columns and map to the data of the ColumnHeaders component.
export const RewardsTable = ({
dataTestId,
epoch,
children,
}: RewardTableProps) => (
<div data-testid={dataTestId} className="mb-12">
<SubHeading title={`EPOCH ${epoch}`} />
<div className={gridStyles}>
<ColumnHeaders />
{children}
</div>
</div>
);
@@ -50,117 +50,101 @@ export const StandbyPendingValidatorsTable = ({
const nodes = useMemo(() => {
if (!data) return [];
return data
.sort((a, b) => {
const aVotingPower = new BigNumber(a.rankingScore.votingPower);
const bVotingPower = new BigNumber(b.rankingScore.votingPower);
return bVotingPower.minus(aVotingPower).toNumber();
})
.map((node, index) => {
const votingPowerRanking = index + 1;
return data.map(
({
id,
name,
avatarUrl,
stakedByDelegates,
stakedByOperator,
stakedTotal,
rankingScore: { stakeScore },
pendingStake,
}) => {
const { rawValidatorScore, performanceScore } =
getLastEpochScoreAndPerformance(previousEpochData, id);
const overstakedAmount = getOverstakedAmount(
rawValidatorScore,
stakedTotal,
totalStake
);
let individualStakeNeededForPromotion,
individualStakeNeededForPromotionDescription;
if (stakeNeededForPromotion && performanceScore) {
const stakedTotalBigNum = new BigNumber(stakedTotal);
const stakeNeededBigNum = new BigNumber(stakeNeededForPromotion);
const performanceScoreBigNum = new BigNumber(performanceScore);
const calc = stakeNeededBigNum
.dividedBy(performanceScoreBigNum)
.minus(stakedTotalBigNum);
if (calc.isGreaterThan(0)) {
individualStakeNeededForPromotion = calc.toString();
individualStakeNeededForPromotionDescription = t(
stakeNeededForPromotionDescription,
{
prefix: formatNumber(calc, 2).toString(),
}
);
} else {
individualStakeNeededForPromotion = '0';
individualStakeNeededForPromotionDescription = t(
stakeNeededForPromotionDescription,
{
prefix: formatNumber(0, 2).toString(),
}
);
}
}
return {
...node,
votingPowerRanking,
};
})
.map(
({
id,
name,
avatarUrl,
stakedByDelegates,
stakedByOperator,
stakedTotal,
rankingScore: { stakeScore },
pendingStake,
votingPowerRanking,
}) => {
const { rawValidatorScore, performanceScore } =
getLastEpochScoreAndPerformance(previousEpochData, id);
const overstakedAmount = getOverstakedAmount(
[ValidatorFields.VALIDATOR]: {
avatarUrl,
name,
},
[ValidatorFields.STAKE]: formatNumber(
toBigNum(stakedTotal, decimals),
2
),
[ValidatorFields.STAKE_NEEDED_FOR_PROMOTION]:
individualStakeNeededForPromotion || null,
[ValidatorFields.STAKE_NEEDED_FOR_PROMOTION_DESCRIPTION]:
individualStakeNeededForPromotionDescription || t('n/a'),
[ValidatorFields.STAKE_SHARE]: stakedTotalPercentage(stakeScore),
[ValidatorFields.STAKED_BY_DELEGATES]: formatNumber(
toBigNum(stakedByDelegates, decimals),
2
),
[ValidatorFields.STAKED_BY_OPERATOR]: formatNumber(
toBigNum(stakedByOperator, decimals),
2
),
[ValidatorFields.PERFORMANCE_SCORE]:
getFormattedPerformanceScore(performanceScore).toString(),
[ValidatorFields.PERFORMANCE_PENALTY]:
getPerformancePenalty(performanceScore),
[ValidatorFields.OVERSTAKED_AMOUNT]: overstakedAmount.toString(),
[ValidatorFields.OVERSTAKING_PENALTY]: getOverstakingPenalty(
overstakedAmount,
totalStake
),
[ValidatorFields.TOTAL_PENALTIES]: getTotalPenalties(
rawValidatorScore,
performanceScore,
stakedTotal,
totalStake
);
let individualStakeNeededForPromotion,
individualStakeNeededForPromotionDescription;
if (stakeNeededForPromotion && performanceScore) {
const stakedTotalBigNum = new BigNumber(stakedTotal);
const stakeNeededBigNum = new BigNumber(stakeNeededForPromotion);
const performanceScoreBigNum = new BigNumber(performanceScore);
const calc = stakeNeededBigNum
.dividedBy(performanceScoreBigNum)
.minus(stakedTotalBigNum);
if (calc.isGreaterThan(0)) {
individualStakeNeededForPromotion = calc.toString();
individualStakeNeededForPromotionDescription = t(
stakeNeededForPromotionDescription,
{
prefix: formatNumber(calc, 2).toString(),
}
);
} else {
individualStakeNeededForPromotion = '0';
individualStakeNeededForPromotionDescription = t(
stakeNeededForPromotionDescription,
{
prefix: formatNumber(0, 2).toString(),
}
);
}
}
return {
id,
[ValidatorFields.RANKING_INDEX]: votingPowerRanking,
[ValidatorFields.VALIDATOR]: {
avatarUrl,
name,
},
[ValidatorFields.STAKE]: formatNumber(
toBigNum(stakedTotal, decimals),
2
),
[ValidatorFields.STAKE_NEEDED_FOR_PROMOTION]:
individualStakeNeededForPromotion || null,
[ValidatorFields.STAKE_NEEDED_FOR_PROMOTION_DESCRIPTION]:
individualStakeNeededForPromotionDescription || t('n/a'),
[ValidatorFields.STAKE_SHARE]: stakedTotalPercentage(stakeScore),
[ValidatorFields.STAKED_BY_DELEGATES]: formatNumber(
toBigNum(stakedByDelegates, decimals),
2
),
[ValidatorFields.STAKED_BY_OPERATOR]: formatNumber(
toBigNum(stakedByOperator, decimals),
2
),
[ValidatorFields.PERFORMANCE_SCORE]:
getFormattedPerformanceScore(performanceScore).toString(),
[ValidatorFields.PERFORMANCE_PENALTY]:
getPerformancePenalty(performanceScore),
[ValidatorFields.OVERSTAKED_AMOUNT]: overstakedAmount.toString(),
[ValidatorFields.OVERSTAKING_PENALTY]: getOverstakingPenalty(
overstakedAmount,
totalStake
),
[ValidatorFields.TOTAL_PENALTIES]: getTotalPenalties(
rawValidatorScore,
performanceScore,
stakedTotal,
totalStake
),
[ValidatorFields.PENDING_STAKE]: formatNumber(
toBigNum(pendingStake, decimals),
2
),
};
}
);
),
[ValidatorFields.PENDING_STAKE]: formatNumber(
toBigNum(pendingStake, decimals),
2
),
};
}
);
}, [
data,
decimals,
@@ -174,12 +158,6 @@ export const StandbyPendingValidatorsTable = ({
const StandbyPendingTable = forwardRef<AgGridReact>((_, gridRef) => {
const colDefs = useMemo<ColDef[]>(
() => [
{
field: ValidatorFields.RANKING_INDEX,
headerName: '#',
width: 60,
pinned: 'left',
},
{
field: ValidatorFields.VALIDATOR,
headerName: t(ValidatorFields.VALIDATOR).toString(),
+2 -8
View File
@@ -59,17 +59,11 @@ export const getOverstakedAmount = (
export const getOverstakingPenalty = (
overstakedAmount: BigNumber,
stakedOnNode: string
) => {
// avoid division by zero
if (new BigNumber(stakedOnNode).isZero() || overstakedAmount.isZero()) {
return '0';
}
return formatNumberPercentage(
) =>
formatNumberPercentage(
overstakedAmount.dividedBy(new BigNumber(stakedOnNode)).times(100),
2
);
};
export const getTotalPenalties = (
rawValidatorScore: string | null | undefined,
@@ -25,38 +25,12 @@ const generateProposal = (code: string): ProposalListFieldsFragment => ({
totalWeight: '',
},
},
requiredMajority: '',
party: {
__typename: 'Party',
id: '',
},
rationale: {
__typename: 'ProposalRationale',
description: '',
title: '',
},
requiredParticipation: '',
errorDetails: '',
rejectionReason: null,
requiredLpMajority: '',
requiredLpParticipation: '',
terms: {
__typename: 'ProposalTerms',
closingDatetime: '',
enactmentDatetime: undefined,
change: {
__typename: 'NewMarket',
decimalPlaces: 1,
lpPriceRange: '',
riskParameters: {
__typename: 'SimpleRiskModel',
params: {
__typename: 'SimpleRiskModelParams',
factorLong: 0,
factorShort: 1,
},
},
metadata: [],
instrument: {
__typename: 'InstrumentConfiguration',
code: code,
@@ -68,34 +42,6 @@ const generateProposal = (code: string): ProposalListFieldsFragment => ({
id: 'A',
name: 'A',
symbol: 'A',
decimals: 1,
quantum: '',
},
quoteName: '',
dataSourceSpecBinding: {
__typename: 'DataSourceSpecToFutureBinding',
settlementDataProperty: '',
tradingTerminationProperty: '',
},
dataSourceSpecForSettlementData: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionInternal',
sourceType: {
__typename: 'DataSourceSpecConfigurationTime',
conditions: [],
},
},
},
dataSourceSpecForTradingTermination: {
__typename: 'DataSourceDefinition',
sourceType: {
__typename: 'DataSourceDefinitionInternal',
sourceType: {
__typename: 'DataSourceSpecConfigurationTime',
conditions: [],
},
},
},
},
},
@@ -3,8 +3,6 @@ import { aliasGQLQuery } from '@vegaprotocol/cypress';
import { marketsQuery } from '@vegaprotocol/mock';
import { getDateTimeFormat } from '@vegaprotocol/react-helpers';
const dialogCloseBtn = 'dialog-close';
describe('markets table', { tags: '@smoke' }, () => {
beforeEach(() => {
cy.clearLocalStorage().then(() => {
@@ -149,7 +147,7 @@ describe('markets table', { tags: '@smoke' }, () => {
});
cy.visit('#/markets/market-0');
cy.url().should('contain', 'market-0');
cy.getByTestId(dialogCloseBtn).click();
cy.getByTestId('dialog-close').click();
cy.getByTestId('item-value').contains('Opening auction').realHover();
cy.getByTestId('opening-auction-sub-status').should(
'contain.text',
@@ -173,8 +171,7 @@ describe('markets table', { tags: '@smoke' }, () => {
});
function openMarketDropDown() {
cy.getByTestId(dialogCloseBtn).should('be.visible');
cy.getByTestId(dialogCloseBtn).click();
cy.getByTestId('dialog-close').click();
cy.getByTestId('popover-trigger').click();
cy.contains('Loading market data...').should('not.exist');
}
+1 -9
View File
@@ -96,15 +96,7 @@ export const MarketPage = () => {
const tradeView = useMemo(() => {
if (w > 960) {
return (
<TradeGrid
market={data}
onSelect={onSelect}
pinnedAsset={
data?.tradableInstrument.instrument.product.settlementAsset
}
/>
);
return <TradeGrid market={data} onSelect={onSelect} />;
}
return (
<TradePanels
@@ -28,7 +28,6 @@ import { NO_MARKET } from './constants';
import { LiquidityContainer } from '../liquidity/liquidity';
import { useNavigate } from 'react-router-dom';
import { Links, Routes } from '../../pages/client-router';
import type { PinnedAsset } from '@vegaprotocol/accounts';
type MarketDependantView =
| typeof CandlesChartContainer
@@ -66,17 +65,14 @@ type TradingView = keyof typeof TradingViews;
interface TradeGridProps {
market: Market | null;
onSelect: (marketId: string) => void;
pinnedAsset?: PinnedAsset;
}
const MainGrid = ({
marketId,
onSelect,
pinnedAsset,
}: {
marketId: string;
onSelect?: (marketId: string) => void;
pinnedAsset?: PinnedAsset;
}) => {
const navigate = useNavigate();
const onMarketClick = (marketId: string) => {
@@ -179,7 +175,7 @@ const MainGrid = ({
</Tab>
<Tab id="accounts" name={t('Collateral')}>
<VegaWalletContainer>
<TradingViews.Collateral pinnedAsset={pinnedAsset} />
<TradingViews.Collateral />
</VegaWalletContainer>
</Tab>
</Tabs>
@@ -190,19 +186,11 @@ const MainGrid = ({
};
const MainGridWrapped = memo(MainGrid);
export const TradeGrid = ({
market,
onSelect,
pinnedAsset,
}: TradeGridProps) => {
export const TradeGrid = ({ market, onSelect }: TradeGridProps) => {
return (
<div className="h-full grid grid-rows-[min-content_1fr]">
<TradeMarketHeader market={market} onSelect={onSelect} />
<MainGridWrapped
marketId={market?.id || ''}
onSelect={onSelect}
pinnedAsset={pinnedAsset}
/>
<MainGridWrapped marketId={market?.id || ''} onSelect={onSelect} />
</div>
);
};
@@ -226,14 +214,12 @@ interface TradePanelsProps {
onSelect: (marketId: string) => void;
onMarketClick?: (marketId: string) => void;
onClickCollateral: () => void;
pinnedAsset?: PinnedAsset;
}
export const TradePanels = ({
market,
onSelect,
onClickCollateral,
pinnedAsset,
}: TradePanelsProps) => {
const [view, setView] = useState<TradingView>('Candles');
const renderView = () => {
@@ -242,7 +228,6 @@ export const TradePanels = ({
onSelect: (marketId: string) => void;
onMarketClick?: (marketId: string) => void;
onClickCollateral: () => void;
pinnedAsset?: PinnedAsset;
}>(TradingViews[view]);
if (!Component) {
@@ -256,7 +241,6 @@ export const TradePanels = ({
marketId={market?.id}
onSelect={onSelect}
onClickCollateral={onClickCollateral}
pinnedAsset={pinnedAsset}
/>
);
};
@@ -6,7 +6,7 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
export const DepositsContainer = () => {
const { pubKey, isReadOnly } = useVegaWallet();
const { data, loading, error, reload } = useDataProvider({
const { data, loading, error } = useDataProvider({
dataProvider: depositsProvider,
variables: { partyId: pubKey || '' },
skip: !pubKey,
@@ -27,7 +27,6 @@ export const DepositsContainer = () => {
error={error}
noDataCondition={(data) => !(data && data.length)}
noDataMessage={t('No deposits')}
reload={reload}
/>
</div>
</div>
@@ -10,7 +10,7 @@ import { VegaWalletContainer } from '../../components/vega-wallet-container';
export const WithdrawalsContainer = () => {
const { pubKey, isReadOnly } = useVegaWallet();
const { data, loading, error, reload } = useDataProvider({
const { data, loading, error } = useDataProvider({
dataProvider: withdrawalProvider,
variables: { partyId: pubKey || '' },
skip: !pubKey,
@@ -33,7 +33,6 @@ export const WithdrawalsContainer = () => {
error={error}
noDataCondition={(data) => !(data && data.length)}
noDataMessage={t('No withdrawals')}
reload={reload}
/>
</div>
</div>
@@ -5,15 +5,10 @@ import { useWithdrawalDialog } from '@vegaprotocol/withdraws';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import type { PinnedAsset } from '@vegaprotocol/accounts';
import { AccountManager, useTransferDialog } from '@vegaprotocol/accounts';
import { useDepositDialog } from '@vegaprotocol/deposits';
export const AccountsContainer = ({
pinnedAsset,
}: {
pinnedAsset?: PinnedAsset;
}) => {
export const AccountsContainer = () => {
const { pubKey, isReadOnly } = useVegaWallet();
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
const openWithdrawalDialog = useWithdrawalDialog((store) => store.open);
@@ -44,7 +39,6 @@ export const AccountsContainer = ({
onClickWithdraw={openWithdrawalDialog}
onClickDeposit={openDepositDialog}
isReadOnly={isReadOnly}
pinnedAsset={pinnedAsset}
/>
</div>
{!isReadOnly && (
@@ -1,6 +1,6 @@
import { useMemo } from 'react';
import { t, useDataProvider } from '@vegaprotocol/react-helpers';
import { proposalsDataProvider } from '@vegaprotocol/governance';
import { proposalsListDataProvider } from '@vegaprotocol/governance';
import take from 'lodash/take';
import * as Types from '@vegaprotocol/types';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
@@ -19,7 +19,7 @@ export const ProposedMarkets = () => {
};
}, []);
const { data } = useDataProvider({
dataProvider: proposalsDataProvider,
dataProvider: proposalsListDataProvider,
variables,
skipUpdates: true,
});
+3 -11
View File
@@ -4,7 +4,6 @@ import type { AgGridReact } from 'ag-grid-react';
import { useRef, useMemo, memo } from 'react';
import type { AccountFields } from './accounts-data-provider';
import { aggregatedAccountsDataProvider } from './accounts-data-provider';
import type { PinnedAsset } from './accounts-table';
import { AccountTable } from './accounts-table';
interface AccountManagerProps {
@@ -13,7 +12,6 @@ interface AccountManagerProps {
onClickWithdraw?: (assetId?: string) => void;
onClickDeposit?: (assetId?: string) => void;
isReadOnly: boolean;
pinnedAsset?: PinnedAsset;
}
export const AccountManager = ({
@@ -22,15 +20,11 @@ export const AccountManager = ({
onClickDeposit,
partyId,
isReadOnly,
pinnedAsset,
}: AccountManagerProps) => {
const gridRef = useRef<AgGridReact | null>(null);
const variables = useMemo(() => ({ partyId }), [partyId]);
const { data, loading, error, reload } = useDataProvider<
AccountFields[],
never
>({
const { data, loading, error } = useDataProvider<AccountFields[], never>({
dataProvider: aggregatedAccountsDataProvider,
variables,
});
@@ -38,13 +32,12 @@ export const AccountManager = ({
<div className="relative h-full">
<AccountTable
ref={gridRef}
rowData={error ? [] : data}
rowData={data}
onClickAsset={onClickAsset}
onClickDeposit={onClickDeposit}
onClickWithdraw={onClickWithdraw}
isReadOnly={isReadOnly}
noRowsOverlayComponent={() => null}
pinnedAsset={pinnedAsset}
/>
<div className="pointer-events-none absolute inset-0">
<AsyncRenderer
@@ -52,8 +45,7 @@ export const AccountManager = ({
noDataCondition={(data) => !(data && data.length)}
error={error}
loading={loading}
noDataMessage={pinnedAsset ? ' ' : t('No accounts')}
reload={reload}
noDataMessage={t('No accounts')}
/>
</div>
</div>
+40 -62
View File
@@ -72,74 +72,52 @@ describe('AccountsTable', () => {
cells.forEach((cell, i) => {
expect(cell).toHaveTextContent(expectedValues[i]);
});
const rows = await screen.findAllByRole('row');
expect(rows.length).toBe(6);
});
});
it('should not add first asset as pinned', async () => {
await act(async () => {
render(
<AccountTable
rowData={singleRowData}
onClickAsset={() => null}
isReadOnly={false}
pinnedAsset={{
it('should get correct account data', () => {
const result = getAccountData([singleRow]);
const expected = [
{
asset: {
__typename: 'Asset',
decimals: 5,
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
symbol: 'tBTC',
},
available: '0',
balance: '0',
breakdown: [
{
__typename: 'AccountBalance',
asset: {
__typename: 'Asset',
decimals: 5,
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
symbol: 'tBTC',
name: 'tBTC',
}}
/>
);
});
const rows = await screen.findAllByRole('row');
expect(rows.length).toBe(6);
});
it('should get correct account data', () => {
const result = getAccountData([singleRow]);
const expected = [
{
asset: {
__typename: 'Asset',
decimals: 5,
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
symbol: 'tBTC',
},
available: '0',
balance: '0',
breakdown: [
{
__typename: 'AccountBalance',
asset: {
__typename: 'Asset',
decimals: 5,
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
symbol: 'tBTC',
},
available: '0',
balance: '125600000',
deposited: '125600000',
market: {
__typename: 'Market',
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
name: 'BTCUSD Monthly (30 Jun 2022)',
},
},
available: '0',
balance: '125600000',
deposited: '125600000',
market: {
__typename: 'Market',
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
name: 'BTCUSD Monthly (30 Jun 2022)',
},
},
type: 'ACCOUNT_TYPE_MARGIN',
used: '125600000',
},
],
deposited: '125600000',
type: 'ACCOUNT_TYPE_GENERAL',
used: '125600000',
},
];
expect(result).toEqual(expected);
});
type: 'ACCOUNT_TYPE_MARGIN',
used: '125600000',
},
],
deposited: '125600000',
type: 'ACCOUNT_TYPE_GENERAL',
used: '125600000',
},
];
expect(result).toEqual(expected);
});
+32 -76
View File
@@ -1,4 +1,4 @@
import { forwardRef, useMemo, useState } from 'react';
import { forwardRef, useState } from 'react';
import {
addDecimalsFormatNumber,
isNumeric,
@@ -14,8 +14,6 @@ import type { AgGridReact, AgGridReactProps } from 'ag-grid-react';
import type { VegaValueFormatterParams } from '@vegaprotocol/ui-toolkit';
import BreakdownTable from './breakdown-table';
import type { AccountFields } from './accounts-data-provider';
import type { Asset } from '@vegaprotocol/types';
import BigNumber from 'bignumber.js';
export interface GetRowsParams extends Omit<IGetRowsParams, 'successCallback'> {
successCallback(rowsThisBlock: AccountFields[], lastRow?: number): void;
@@ -25,8 +23,6 @@ export interface Datasource extends IDatasource {
getRows(params: GetRowsParams): void;
}
export type PinnedAsset = Pick<Asset, 'symbol' | 'name' | 'id' | 'decimals'>;
export interface AccountTableProps extends AgGridReactProps {
rowData?: AccountFields[] | null;
datasource?: Datasource;
@@ -34,33 +30,12 @@ export interface AccountTableProps extends AgGridReactProps {
onClickWithdraw?: (assetId: string) => void;
onClickDeposit?: (assetId: string) => void;
isReadOnly: boolean;
pinnedAsset?: PinnedAsset;
}
export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
({ onClickAsset, onClickWithdraw, onClickDeposit, ...props }, ref) => {
const [openBreakdown, setOpenBreakdown] = useState(false);
const [breakdown, setBreakdown] = useState<AccountFields[] | null>(null);
const pinnedAssetId = props.pinnedAsset?.id;
const pinnedAssetRow = useMemo(() => {
const currentPinnedAssetRow = props.rowData?.find(
(row) => row.asset.id === pinnedAssetId
);
if (!currentPinnedAssetRow) {
if (props.pinnedAsset) {
return {
asset: props.pinnedAsset,
available: '0',
used: '0',
deposited: '0',
balance: '0',
};
}
}
return undefined;
}, [pinnedAssetId, props.pinnedAsset, props.rowData]);
return (
<>
<AgGrid
@@ -76,7 +51,6 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
sortable: true,
}}
{...props}
pinnedTopRowData={pinnedAssetRow ? [pinnedAssetRow] : undefined}
>
<AgGridColumn
headerName={t('Asset')}
@@ -164,55 +138,37 @@ export const AccountTable = forwardRef<AgGridReact, AccountTableProps>(
cellRenderer={({
data,
}: VegaICellRendererParams<AccountFields>) => {
if (!data) return null;
else {
if (
data.asset.id === pinnedAssetId &&
new BigNumber(data.deposited).isLessThanOrEqualTo(0)
) {
return (
<ButtonLink
data-testid="deposit"
onClick={() => {
onClickDeposit && onClickDeposit(data.asset.id);
}}
>
{t('Deposit to trade')}
</ButtonLink>
);
}
return (
<>
<ButtonLink
data-testid="breakdown"
onClick={() => {
setOpenBreakdown(!openBreakdown);
setBreakdown(data.breakdown || null);
}}
>
{t('Breakdown')}
</ButtonLink>
<span className="mx-1" />
<ButtonLink
data-testid="deposit"
onClick={() => {
onClickDeposit && onClickDeposit(data.asset.id);
}}
>
{t('Deposit')}
</ButtonLink>
<span className="mx-1" />
<ButtonLink
data-testid="withdraw"
onClick={() =>
onClickWithdraw && onClickWithdraw(data.asset.id)
}
>
{t('Withdraw')}
</ButtonLink>
</>
);
}
return data ? (
<>
<ButtonLink
data-testid="breakdown"
onClick={() => {
setOpenBreakdown(!openBreakdown);
setBreakdown(data.breakdown || null);
}}
>
{t('Breakdown')}
</ButtonLink>
<span className="mx-1" />
<ButtonLink
data-testid="deposit"
onClick={() => {
onClickDeposit && onClickDeposit(data.asset.id);
}}
>
{t('Deposit')}
</ButtonLink>
<span className="mx-1" />
<ButtonLink
data-testid="withdraw"
onClick={() =>
onClickWithdraw && onClickWithdraw(data.asset.id)
}
>
{t('Withdraw')}
</ButtonLink>
</>
) : null;
}}
/>
)}
@@ -25,7 +25,6 @@ export const DealTicketContainer = ({
data: marketData,
error: marketDataError,
loading: marketDataLoading,
reload,
} = useThrottledDataProvider(
{
dataProvider: marketDataProvider,
@@ -40,7 +39,6 @@ export const DealTicketContainer = ({
data={market && marketData}
loading={marketLoading || marketDataLoading}
error={marketError || marketDataError}
reload={reload}
>
{market && marketData ? (
<DealTicket
@@ -1,4 +1,4 @@
import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit';
import { FormGroup, Input, NotificationError } from '@vegaprotocol/ui-toolkit';
import { t, toDecimal, validateAmount } from '@vegaprotocol/react-helpers';
import type { DealTicketAmountProps } from './deal-ticket-amount';
@@ -20,17 +20,17 @@ export const DealTicketLimitAmount = ({
const renderError = () => {
if (sizeError) {
return (
<InputError testId="dealticket-error-message-size-limit">
<NotificationError testId="dealticket-error-message-size-limit">
{sizeError}
</InputError>
</NotificationError>
);
}
if (priceError) {
return (
<InputError testId="dealticket-error-message-price-limit">
<NotificationError testId="dealticket-error-message-price-limit">
{priceError}
</InputError>
</NotificationError>
);
}
@@ -4,7 +4,7 @@ import {
toDecimal,
validateAmount,
} from '@vegaprotocol/react-helpers';
import { Input, InputError, Tooltip } from '@vegaprotocol/ui-toolkit';
import { Input, NotificationError, Tooltip } from '@vegaprotocol/ui-toolkit';
import { isMarketInAuction } from '../../utils';
import type { DealTicketAmountProps } from './deal-ticket-amount';
import { getMarketPrice } from '../../utils/get-price';
@@ -77,12 +77,12 @@ export const DealTicketMarketAmount = ({
</div>
</div>
{sizeError && (
<InputError
<NotificationError
intent="danger"
testId="dealticket-error-message-size-market"
>
{sizeError}
</InputError>
</NotificationError>
)}
</div>
);

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