Compare commits
57
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6ed5016d1 | ||
|
|
d4e151d4be | ||
|
|
efee4f9e1a | ||
|
|
a3257b33b4 | ||
|
|
96007bd230 | ||
|
|
554d9ecb06 | ||
|
|
a12a5b3a15 | ||
|
|
a79e2f85bc | ||
|
|
a2190fdde0 | ||
|
|
849415a78e | ||
|
|
d33f5428b6 | ||
|
|
c73b01d549 | ||
|
|
4286bc37b3 | ||
|
|
d4ecb88fb4 | ||
|
|
57a8955795 | ||
|
|
ac53b1f97a | ||
|
|
4b83a10475 | ||
|
|
7e957a2841 | ||
|
|
38c0e70bbc | ||
|
|
94c40cce7d | ||
|
|
0059050440 | ||
|
|
f7be890eb9 | ||
|
|
59ae971a07 | ||
|
|
cfcd5efa2c | ||
|
|
fb1e85893c | ||
|
|
084972577e | ||
|
|
94df5ca048 | ||
|
|
6142a7afbb | ||
|
|
d2e0c57676 | ||
|
|
e48d8d8715 | ||
|
|
7de333e1ea | ||
|
|
2123afc4b0 | ||
|
|
ea91fa1723 | ||
|
|
4dcdec7a35 | ||
|
|
196f1914a3 | ||
|
|
ea4c7a4a37 | ||
|
|
c22fec97b4 | ||
|
|
989a0456c0 | ||
|
|
8c79a94a8c | ||
|
|
a7a6a820c0 | ||
|
|
1fa09a690e | ||
|
|
b0a84970f2 | ||
|
|
b2a115f935 | ||
|
|
9dfed0a211 | ||
|
|
5283676be7 | ||
|
|
a82509f0e0 | ||
|
|
64c92ce91d | ||
|
|
b7426e73d8 | ||
|
|
969bfd6945 | ||
|
|
200487a6fc | ||
|
|
3f330e78c5 | ||
|
|
05726c6a5a | ||
|
|
fcace22393 | ||
|
|
b95462ea38 | ||
|
|
42cd32f376 | ||
|
|
55d6dd4dce | ||
|
|
7017e24adf |
@@ -18,6 +18,10 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Run Cypress tests
|
||||
uses: cypress-io/github-action@v4
|
||||
with:
|
||||
|
||||
@@ -50,20 +50,6 @@ jobs:
|
||||
projects=[${projects// /,}]
|
||||
echo PROJECTS=$projects >> $GITHUB_ENV
|
||||
|
||||
# Rename required because some of the files contains the colon character (in the dates)
|
||||
- name: Rename files to allow archive
|
||||
if: ${{ always() }}
|
||||
run: |
|
||||
while read -r file; do
|
||||
mv "${file}" "$(echo ${file} | sed 's|:|-|g')"
|
||||
done< <(find /home/runner/.vegacapsule/testnet/logs -type f)
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
name: logs-${{ matrix.project }}
|
||||
path: /home/runner/.vegacapsule/testnet/logs
|
||||
|
||||
outputs:
|
||||
projects: ${{ env.PROJECTS }}
|
||||
|
||||
|
||||
@@ -82,6 +82,10 @@ jobs:
|
||||
mv "${file}" "$(echo ${file} | sed 's|:|-|g')"
|
||||
done< <(find /home/runner/.vegacapsule/testnet/logs -type f)
|
||||
|
||||
- name: Print logs files
|
||||
if: ${{ always() }}
|
||||
run: ls -alsh /home/runner/.vegacapsule/testnet/logs/
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
|
||||
@@ -4,15 +4,6 @@ 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', () => {
|
||||
@@ -40,7 +31,7 @@ context('Asset page', { tags: '@regression' }, () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should open details dialog when clicked on "View details"', () => {
|
||||
it('should open details page when clicked on "View details"', () => {
|
||||
cy.getAssets().then((assets) => {
|
||||
Object.values(assets).forEach((asset) => {
|
||||
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`)
|
||||
@@ -49,8 +40,8 @@ context('Asset page', { tags: '@regression' }, () => {
|
||||
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`)
|
||||
.eq(0)
|
||||
.click();
|
||||
cy.getByTestId('dialog-content').should('be.visible');
|
||||
cy.getByTestId('dialog-close').click();
|
||||
cy.getByTestId('asset-header').should('have.text', asset.name);
|
||||
cy.go('back');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -247,7 +247,7 @@ context('Network parameters page', { tags: '@smoke' }, function () {
|
||||
.and('include', darkThemeSideMenuBackgroundColor);
|
||||
});
|
||||
|
||||
it('should be able to see network parameters - on mobile', function () {
|
||||
it.skip('should be able to see network parameters - on mobile', function () {
|
||||
cy.common_switch_to_mobile_and_click_toggle();
|
||||
cy.get(networkParametersNavigation).click();
|
||||
cy.get_network_parameters().then((network_parameters) => {
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import classnames from 'classnames';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { EnvironmentProvider, NetworkLoader } from '@vegaprotocol/environment';
|
||||
import { NetworkLoader, useInitializeEnv } from '@vegaprotocol/environment';
|
||||
import { Nav } from './components/nav';
|
||||
import { Header } from './components/header';
|
||||
import { Main } from './components/main';
|
||||
import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-websocket-provider';
|
||||
import type { InMemoryCacheConfig } from '@apollo/client';
|
||||
import { Footer } from './components/footer/footer';
|
||||
import { AnnouncementBanner, ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
AssetDetailsDialog,
|
||||
useAssetDetailsDialogStore,
|
||||
} from '@vegaprotocol/assets';
|
||||
import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client';
|
||||
|
||||
const DialogsContainer = () => {
|
||||
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
|
||||
@@ -28,36 +26,18 @@ const DialogsContainer = () => {
|
||||
};
|
||||
|
||||
function App() {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
setMenuOpen(false);
|
||||
}, [location]);
|
||||
|
||||
const cacheConfig: InMemoryCacheConfig = {
|
||||
typePolicies: {
|
||||
statistics: {
|
||||
keyFields: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const layoutClasses = classnames(
|
||||
'grid grid-rows-[auto_1fr_auto] grid-cols-[1fr] md:grid-rows-[auto_minmax(700px,_1fr)_auto] md:grid-cols-[300px_1fr]',
|
||||
'min-h-[100vh] mx-auto my-0',
|
||||
'border-neutral-700 dark:border-neutral-300 lg:border-l lg:border-r',
|
||||
'bg-white dark:bg-black',
|
||||
'antialiased text-black dark:text-white',
|
||||
{
|
||||
'h-[100vh] min-h-auto overflow-hidden': menuOpen,
|
||||
}
|
||||
'overflow-hidden relative'
|
||||
);
|
||||
|
||||
return (
|
||||
<TendermintWebsocketProvider>
|
||||
<NetworkLoader cache={cacheConfig}>
|
||||
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
|
||||
<AnnouncementBanner>
|
||||
<div className="font-alpha calt uppercase text-center text-lg text-white">
|
||||
<span className="pr-4">Mainnet sim 2 coming in March!</span>
|
||||
@@ -68,8 +48,8 @@ function App() {
|
||||
</AnnouncementBanner>
|
||||
|
||||
<div className={layoutClasses}>
|
||||
<Header menuOpen={menuOpen} setMenuOpen={setMenuOpen} />
|
||||
<Nav menuOpen={menuOpen} />
|
||||
<Header />
|
||||
<Nav />
|
||||
<Main />
|
||||
<Footer />
|
||||
</div>
|
||||
@@ -81,11 +61,8 @@ function App() {
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
return (
|
||||
<EnvironmentProvider>
|
||||
<App />
|
||||
</EnvironmentProvider>
|
||||
);
|
||||
useInitializeEnv();
|
||||
return <App />;
|
||||
};
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -1,22 +1,35 @@
|
||||
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(<AssetsTable data={null} />);
|
||||
const res = render(
|
||||
<MemoryRouter>
|
||||
<AssetsTable data={null} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(await res.findByText('Loading...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows no data message if no assets found', async () => {
|
||||
const res = render(<AssetsTable data={[]} />);
|
||||
const res = render(
|
||||
<MemoryRouter>
|
||||
<AssetsTable data={[]} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(
|
||||
await res.findByText('This chain has no assets')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows a table/list with all the assets', async () => {
|
||||
const res = render(<AssetsTable data={assetsList} />);
|
||||
const res = render(
|
||||
<MemoryRouter>
|
||||
<AssetsTable data={assetsList} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
await waitFor(() => {
|
||||
const rowA1 = res.container.querySelector('[row-id="123"]');
|
||||
expect(rowA1).toBeInTheDocument();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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';
|
||||
@@ -9,15 +8,14 @@ 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 openAssetDetailsDialog = useAssetDetailsDialogStore(
|
||||
(state) => state.open
|
||||
);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const ref = useRef<AgGridReact>(null);
|
||||
const showColumnsOnDesktop = () => {
|
||||
ref.current?.columnApi.setColumnsVisible(
|
||||
@@ -49,17 +47,23 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
|
||||
autoHeight: true,
|
||||
}}
|
||||
suppressCellFocus={true}
|
||||
onGridReady={() => {
|
||||
showColumnsOnDesktop();
|
||||
onRowClicked={({ data }: RowClickedEvent) => {
|
||||
navigate(data.id);
|
||||
}}
|
||||
>
|
||||
<AgGridColumn headerName={t('Symbol')} field="symbol" />
|
||||
<AgGridColumn headerName={t('Name')} field="name" />
|
||||
<AgGridColumn flex="2" headerName={t('ID')} field="id" />
|
||||
<AgGridColumn
|
||||
flex="2"
|
||||
headerName={t('ID')}
|
||||
field="id"
|
||||
hide={window.innerWidth < BREAKPOINT_MD}
|
||||
/>
|
||||
<AgGridColumn
|
||||
colId="type"
|
||||
headerName={t('Type')}
|
||||
field="source.__typename"
|
||||
hide={window.innerWidth < BREAKPOINT_MD}
|
||||
valueFormatter={({ value }: { value?: string }) =>
|
||||
value && AssetTypeMapping[value].value
|
||||
}
|
||||
@@ -67,6 +71,7 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
|
||||
<AgGridColumn
|
||||
headerName={t('Status')}
|
||||
field="status"
|
||||
hide={window.innerWidth < BREAKPOINT_MD}
|
||||
valueFormatter={({ value }: { value?: string }) =>
|
||||
value && AssetStatusMapping[value].value
|
||||
}
|
||||
@@ -83,28 +88,13 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
|
||||
value,
|
||||
}: VegaICellRendererParams<AssetFieldsFragment, 'id'>) =>
|
||||
value ? (
|
||||
<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>
|
||||
<ButtonLink
|
||||
onClick={(e) => {
|
||||
navigate(value);
|
||||
}}
|
||||
>
|
||||
{t('View details')}
|
||||
</ButtonLink>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
Icon,
|
||||
SyntaxHighlighter,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
type JsonViewerDialogProps = {
|
||||
title: string;
|
||||
content: unknown;
|
||||
open: boolean;
|
||||
onChange: (isOpen: boolean) => void;
|
||||
trigger?: HTMLElement;
|
||||
};
|
||||
export const JsonViewerDialog = ({
|
||||
title,
|
||||
content,
|
||||
open,
|
||||
onChange,
|
||||
trigger,
|
||||
}: JsonViewerDialogProps) => {
|
||||
return (
|
||||
<Dialog
|
||||
size="medium"
|
||||
title={title}
|
||||
icon={<Icon name="info-sign"></Icon>}
|
||||
open={open}
|
||||
onChange={(isOpen) => onChange(isOpen)}
|
||||
onCloseAutoFocus={(e) => {
|
||||
/**
|
||||
* This mimics radix's default behaviour that focuses the dialog's
|
||||
* trigger after closing itself
|
||||
*/
|
||||
if (trigger) {
|
||||
e.preventDefault();
|
||||
trigger.focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="pr-8 mb-8 max-h-[70vh] overflow-y-scroll">
|
||||
<SyntaxHighlighter size="smaller" data={content} />
|
||||
</div>
|
||||
<div className="w-1/4">
|
||||
<Button
|
||||
data-testid="close-asset-details-dialog"
|
||||
fill={true}
|
||||
size="sm"
|
||||
onClick={() => onChange(false)}
|
||||
>
|
||||
{t('Close')}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,39 +1,59 @@
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
||||
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';
|
||||
|
||||
export const Footer = () => {
|
||||
const { VEGA_URL, GIT_COMMIT_HASH, GIT_ORIGIN_URL, setNodeSwitcherOpen } =
|
||||
useEnvironment();
|
||||
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">
|
||||
<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
|
||||
href={
|
||||
GIT_ORIGIN_URL
|
||||
? `${GIT_ORIGIN_URL}/commit/${GIT_COMMIT_HASH}`
|
||||
: undefined
|
||||
}
|
||||
target={GIT_ORIGIN_URL ? '_blank' : undefined}
|
||||
>
|
||||
{GIT_COMMIT_HASH}
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
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]
|
||||
);
|
||||
|
||||
<div className="flex pl-2 content-center">
|
||||
{VEGA_URL && <NodeUrl url={VEGA_URL} />}
|
||||
<Link className="ml-2" onClick={setNodeSwitcherOpen}>
|
||||
{t('Change')}
|
||||
</Link>
|
||||
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">
|
||||
<p data-testid="git-commit-hash">
|
||||
{t('Version')}:{' '}
|
||||
<Link
|
||||
href={
|
||||
GIT_ORIGIN_URL
|
||||
? `${GIT_ORIGIN_URL}/commit/${GIT_COMMIT_HASH}`
|
||||
: undefined
|
||||
}
|
||||
target={GIT_ORIGIN_URL ? '_blank' : undefined}
|
||||
>
|
||||
{GIT_COMMIT_HASH}
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="content-center flex pl-2 md:border-r border-neutral-700 dark:border-neutral-300 pr-4">
|
||||
{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>
|
||||
</div>
|
||||
</footer>
|
||||
</footer>
|
||||
<NodeSwitcherDialog
|
||||
open={nodeSwitcherOpen}
|
||||
setOpen={setNodeSwitcherOpen}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ jest.mock('../search', () => ({
|
||||
|
||||
const renderComponent = () => (
|
||||
<MemoryRouter>
|
||||
<Header menuOpen={false} setMenuOpen={jest.fn()} />
|
||||
<Header />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
|
||||
@@ -4,15 +4,11 @@ import { ThemeSwitcher, Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { Search } from '../search';
|
||||
import { Routes } from '../../routes/route-names';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { NetworkSwitcher } from '@vegaprotocol/environment';
|
||||
import { useNavStore } from '../nav';
|
||||
|
||||
interface ThemeToggleProps {
|
||||
menuOpen: boolean;
|
||||
setMenuOpen: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export const Header = ({ menuOpen, setMenuOpen }: ThemeToggleProps) => {
|
||||
export const Header = () => {
|
||||
const [open, toggle] = useNavStore((state) => [state.open, state.toggle]);
|
||||
const headerClasses = classnames(
|
||||
'md:col-span-2',
|
||||
'grid grid-rows-2 md:grid-rows-1 grid-cols-[1fr_auto] md:grid-cols-[auto_1fr_auto] items-center',
|
||||
@@ -36,9 +32,9 @@ export const Header = ({ menuOpen, setMenuOpen }: ThemeToggleProps) => {
|
||||
<button
|
||||
data-testid="open-menu"
|
||||
className="md:hidden text-white"
|
||||
onClick={() => setMenuOpen(!menuOpen)}
|
||||
onClick={() => toggle()}
|
||||
>
|
||||
<Icon name={menuOpen ? 'cross' : 'menu'} />
|
||||
<Icon name={open ? 'cross' : 'menu'} />
|
||||
</button>
|
||||
<Search />
|
||||
<ThemeSwitcher className="-my-4" />
|
||||
|
||||
@@ -5,9 +5,12 @@ 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;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -15,17 +18,22 @@ 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, ...props }: AssetLinkProps) => {
|
||||
export const AssetLink = ({ assetId, asDialog, ...props }: AssetLinkProps) => {
|
||||
const { data: asset } = useAssetDataProvider(assetId);
|
||||
|
||||
const open = useAssetDetailsDialogStore((state) => state.open);
|
||||
const navigate = useNavigate();
|
||||
const label = asset?.name ? asset.name : assetId;
|
||||
return (
|
||||
<ButtonLink
|
||||
data-testid="asset-link"
|
||||
disabled={!asset}
|
||||
onClick={(e) => {
|
||||
open(assetId, e.target as HTMLElement);
|
||||
if (asDialog) {
|
||||
open(assetId, e.target as HTMLElement);
|
||||
} else {
|
||||
navigate(`${Routes.ASSETS}/${asset?.id}`);
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { AppRouter } from '../../routes';
|
||||
|
||||
export const Main = () => {
|
||||
return (
|
||||
<main className="p-4 overflow-scroll">
|
||||
<main className="p-4">
|
||||
<AppRouter />
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumberPercentage,
|
||||
getMarketExpiryDateFormatted,
|
||||
t,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import type { MarketInfoNoCandlesQuery } from '@vegaprotocol/market-info';
|
||||
import { MarketInfoTable } from '@vegaprotocol/market-info';
|
||||
import pick from 'lodash/pick';
|
||||
import {
|
||||
MarketStateMapping,
|
||||
MarketTradingModeMapping,
|
||||
} from '@vegaprotocol/types';
|
||||
import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { useMemo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export const MarketDetails = ({
|
||||
market,
|
||||
}: {
|
||||
market: MarketInfoNoCandlesQuery['market'];
|
||||
}) => {
|
||||
const assetSymbol =
|
||||
market?.tradableInstrument.instrument.product?.settlementAsset.symbol;
|
||||
const assetId = useMemo(
|
||||
() => market?.tradableInstrument.instrument.product?.settlementAsset.id,
|
||||
[market]
|
||||
);
|
||||
const { data: asset } = useAssetDataProvider(assetId ?? '');
|
||||
|
||||
if (!market) return null;
|
||||
|
||||
const keyDetails = {
|
||||
...pick(market, 'decimalPlaces', 'positionDecimalPlaces', 'tradingMode'),
|
||||
state: MarketStateMapping[market.state],
|
||||
};
|
||||
const assetDecimals =
|
||||
market.tradableInstrument.instrument.product.settlementAsset.decimals;
|
||||
|
||||
const panels = [
|
||||
{
|
||||
title: t('Key details'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={{
|
||||
name: market.tradableInstrument.instrument.name,
|
||||
marketID: market.id,
|
||||
tradingMode:
|
||||
keyDetails.tradingMode &&
|
||||
MarketTradingModeMapping[keyDetails.tradingMode],
|
||||
marketDecimalPlaces: market.decimalPlaces,
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
settlementAssetDecimalPlaces: assetDecimals,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Instrument'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={{
|
||||
marketName: market.tradableInstrument.instrument.name,
|
||||
code: market.tradableInstrument.instrument.code,
|
||||
productType:
|
||||
market.tradableInstrument.instrument.product.__typename,
|
||||
...market.tradableInstrument.instrument.product,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Settlement asset'),
|
||||
content: asset ? (
|
||||
<AssetDetailsTable
|
||||
asset={asset}
|
||||
inline={true}
|
||||
noBorder={false}
|
||||
dtClassName="text-black dark:text-white text-ui !px-0 !font-normal"
|
||||
ddClassName="text-black dark:text-white text-ui !px-0 !font-normal max-w-full"
|
||||
/>
|
||||
) : (
|
||||
<Splash>{t('No data')}</Splash>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Metadata'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={{
|
||||
expiryDate: getMarketExpiryDateFormatted(
|
||||
market.tradableInstrument.instrument.metadata.tags
|
||||
),
|
||||
...market.tradableInstrument.instrument.metadata.tags
|
||||
?.map((tag) => {
|
||||
const [key, value] = tag.split(':');
|
||||
return { [key]: value };
|
||||
})
|
||||
.reduce((acc, curr) => ({ ...acc, ...curr }), {}),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Risk model'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={market.tradableInstrument.riskModel}
|
||||
unformatted={true}
|
||||
omits={[]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Risk parameters'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={market.tradableInstrument.riskModel.params}
|
||||
unformatted={true}
|
||||
omits={[]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Risk factors'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={market.riskFactors}
|
||||
unformatted={true}
|
||||
omits={['market', '__typename']}
|
||||
/>
|
||||
),
|
||||
},
|
||||
...(market.priceMonitoringSettings?.parameters?.triggers || []).map(
|
||||
(trigger, i) => ({
|
||||
title: t(`Price monitoring trigger ${i + 1}`),
|
||||
content: <MarketInfoTable noBorder={false} data={trigger} />,
|
||||
})
|
||||
),
|
||||
...(market.data?.priceMonitoringBounds || []).map((trigger, i) => ({
|
||||
title: t(`Price monitoring bound ${i + 1}`),
|
||||
content: (
|
||||
<>
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={trigger}
|
||||
decimalPlaces={market.decimalPlaces}
|
||||
omits={['referencePrice', '__typename']}
|
||||
/>
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={{ referencePrice: trigger.referencePrice }}
|
||||
decimalPlaces={assetDecimals}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
})),
|
||||
{
|
||||
title: t('Liquidity monitoring parameters'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={{
|
||||
triggeringRatio:
|
||||
market.liquidityMonitoringParameters.triggeringRatio,
|
||||
...market.liquidityMonitoringParameters.targetStakeParameters,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Liquidity price range'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={{
|
||||
liquidityPriceRange: formatNumberPercentage(
|
||||
new BigNumber(market.lpPriceRange).times(100)
|
||||
),
|
||||
LPVolumeMin:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.minus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${assetSymbol}`,
|
||||
LPVolumeMax:
|
||||
market.data?.midPrice &&
|
||||
`${addDecimalsFormatNumber(
|
||||
new BigNumber(1)
|
||||
.plus(market.lpPriceRange)
|
||||
.times(market.data.midPrice)
|
||||
.toString(),
|
||||
market.decimalPlaces
|
||||
)} ${assetSymbol}`,
|
||||
}}
|
||||
></MarketInfoTable>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t('Oracle'),
|
||||
content: (
|
||||
<MarketInfoTable
|
||||
noBorder={false}
|
||||
data={
|
||||
market.tradableInstrument.instrument.product.dataSourceSpecBinding
|
||||
}
|
||||
>
|
||||
<Link
|
||||
className="text-xs hover:underline"
|
||||
to={`/oracles#${market.tradableInstrument.instrument.product.dataSourceSpecForSettlementData.id}`}
|
||||
>
|
||||
{t('View settlement data oracle specification')}
|
||||
</Link>
|
||||
<Link
|
||||
className="text-xs hover:underline"
|
||||
to={`/oracles#${market.tradableInstrument.instrument.product.dataSourceSpecForTradingTermination.id}`}
|
||||
>
|
||||
{t('View termination oracle specification')}
|
||||
</Link>
|
||||
</MarketInfoTable>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{panels.map((p) => (
|
||||
<div className="mb-3">
|
||||
<h2 className="font-alpha text-xl">{p.title}</h2>
|
||||
{p.content}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,132 @@
|
||||
import type { MarketFieldsFragment } from '@vegaprotocol/market-list';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import type {
|
||||
VegaICellRendererParams,
|
||||
VegaValueGetterParams,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
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 { MarketStateMapping } from '@vegaprotocol/types';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
import type { RowClickedEvent } from 'ag-grid-community';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
type MarketsTableProps = {
|
||||
data: MarketFieldsFragment[] | null;
|
||||
};
|
||||
export const MarketsTable = ({ data }: MarketsTableProps) => {
|
||||
const openAssetDetailsDialog = useAssetDetailsDialogStore(
|
||||
(state) => state.open
|
||||
);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const gridRef = useRef<AgGridReact>(null);
|
||||
useLayoutEffect(() => {
|
||||
const showColumnsOnDesktop = () => {
|
||||
gridRef.current?.columnApi.setColumnsVisible(
|
||||
['id', 'state', 'asset'],
|
||||
window.innerWidth > BREAKPOINT_MD
|
||||
);
|
||||
};
|
||||
window.addEventListener('resize', showColumnsOnDesktop);
|
||||
return () => {
|
||||
window.removeEventListener('resize', showColumnsOnDesktop);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AgGrid
|
||||
ref={gridRef}
|
||||
rowData={data}
|
||||
getRowId={({ data }: { data: MarketFieldsFragment }) => data.id}
|
||||
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') {
|
||||
navigate(data.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AgGridColumn
|
||||
colId="code"
|
||||
headerName={t('Code')}
|
||||
field="tradableInstrument.instrument.code"
|
||||
/>
|
||||
<AgGridColumn
|
||||
colId="name"
|
||||
headerName={t('Name')}
|
||||
field="tradableInstrument.instrument.name"
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Status')}
|
||||
field="state"
|
||||
hide={window.innerWidth <= BREAKPOINT_MD}
|
||||
valueGetter={({
|
||||
data,
|
||||
}: VegaValueGetterParams<MarketFieldsFragment, 'state'>) => {
|
||||
return data?.state ? MarketStateMapping[data?.state] : '-';
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
colId="asset"
|
||||
headerName={t('Settlement asset')}
|
||||
field="tradableInstrument.instrument.product.settlementAsset"
|
||||
hide={window.innerWidth <= BREAKPOINT_MD}
|
||||
cellRenderer={({
|
||||
value,
|
||||
}: VegaICellRendererParams<
|
||||
MarketFieldsFragment,
|
||||
'tradableInstrument.instrument.product.settlementAsset'
|
||||
>) =>
|
||||
value ? (
|
||||
<ButtonLink
|
||||
onClick={(e) => {
|
||||
openAssetDetailsDialog(value.id, e.target as HTMLElement);
|
||||
}}
|
||||
>
|
||||
{value.symbol}
|
||||
</ButtonLink>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
/>
|
||||
<AgGridColumn
|
||||
flex={2}
|
||||
headerName={t('Market ID')}
|
||||
field="id"
|
||||
hide={window.innerWidth <= BREAKPOINT_MD}
|
||||
/>
|
||||
<AgGridColumn
|
||||
colId="actions"
|
||||
headerName=""
|
||||
field="id"
|
||||
cellRenderer={({
|
||||
value,
|
||||
}: VegaICellRendererParams<MarketFieldsFragment, 'id'>) =>
|
||||
value ? (
|
||||
<Link className="underline" to={value}>
|
||||
{t('View details')}
|
||||
</Link>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
/>
|
||||
</AgGrid>
|
||||
);
|
||||
};
|
||||
@@ -1,45 +1 @@
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import routerConfig from '../../routes/router-config';
|
||||
import classnames from 'classnames';
|
||||
|
||||
interface NavProps {
|
||||
menuOpen: boolean;
|
||||
}
|
||||
|
||||
export const Nav = ({ menuOpen }: NavProps) => {
|
||||
return (
|
||||
<nav className="relative">
|
||||
<div
|
||||
className={classnames(
|
||||
'absolute top-0 z-50 md:static',
|
||||
'w-full p-4 md:border-r border-neutral-700 dark:border-neutral-300',
|
||||
'bg-white dark:bg-black',
|
||||
'transition-[right]',
|
||||
{
|
||||
'right-0 h-[100vh]': menuOpen,
|
||||
'right-[200vw] h-full': !menuOpen,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{routerConfig.map((r) => (
|
||||
<NavLink
|
||||
key={r.name}
|
||||
to={r.path}
|
||||
className={({ isActive }) =>
|
||||
classnames(
|
||||
'block mb-2 px-2',
|
||||
'text-lg hover:bg-vega-pink dark:hover:bg-vega-yellow hover:text-white dark:hover:text-black',
|
||||
{
|
||||
'bg-vega-pink text-white dark:bg-vega-yellow dark:text-black':
|
||||
isActive,
|
||||
}
|
||||
)
|
||||
}
|
||||
>
|
||||
{r.text}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
export * from './nav';
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import { NavLink, useLocation } from 'react-router-dom';
|
||||
import type { Navigable } from '../../routes/router-config';
|
||||
import routerConfig from '../../routes/router-config';
|
||||
import classnames from 'classnames';
|
||||
import { create } from 'zustand';
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import first from 'lodash/first';
|
||||
import last from 'lodash/last';
|
||||
import { BREAKPOINT_MD } from '../../config/breakpoints';
|
||||
|
||||
type NavStore = {
|
||||
open: boolean;
|
||||
toggle: () => void;
|
||||
hide: () => void;
|
||||
};
|
||||
|
||||
export const useNavStore = create<NavStore>((set, get) => ({
|
||||
open: false,
|
||||
toggle: () => set({ open: !get().open }),
|
||||
hide: () => set({ open: false }),
|
||||
}));
|
||||
|
||||
const NavLinks = ({ links }: { links: Navigable[] }) => {
|
||||
const navLinks = links.map((r) => (
|
||||
<li key={r.name}>
|
||||
<NavLink
|
||||
to={r.path}
|
||||
className={({ isActive }) =>
|
||||
classnames(
|
||||
'block mb-2 px-2',
|
||||
'text-lg hover:bg-vega-pink dark:hover:bg-vega-yellow hover:text-white dark:hover:text-black',
|
||||
{
|
||||
'bg-vega-pink text-white dark:bg-vega-yellow dark:text-black':
|
||||
isActive,
|
||||
}
|
||||
)
|
||||
}
|
||||
>
|
||||
{r.text}
|
||||
</NavLink>
|
||||
</li>
|
||||
));
|
||||
|
||||
return <ul className="pr-8 md:pr-0">{navLinks}</ul>;
|
||||
};
|
||||
|
||||
export const Nav = () => {
|
||||
const [open, hide] = useNavStore((state) => [state.open, state.hide]);
|
||||
const location = useLocation();
|
||||
|
||||
const navRef = useRef<HTMLElement>(null);
|
||||
const btnRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const focusable = useMemo(
|
||||
() =>
|
||||
navRef.current
|
||||
? [
|
||||
...(navRef.current.querySelectorAll(
|
||||
'a, button'
|
||||
) as NodeListOf<HTMLElement>),
|
||||
]
|
||||
: [],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[navRef.current] // do not remove `navRef.current` from deps
|
||||
);
|
||||
|
||||
const closeNav = useCallback(() => {
|
||||
hide();
|
||||
console.log(focusable);
|
||||
focusable.forEach((fe) =>
|
||||
fe.setAttribute(
|
||||
'tabindex',
|
||||
window.innerWidth > BREAKPOINT_MD ? '0' : '-1'
|
||||
)
|
||||
);
|
||||
}, [focusable, hide]);
|
||||
|
||||
// close navigation when location changes
|
||||
useEffect(() => {
|
||||
closeNav();
|
||||
}, [closeNav, location]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (open) {
|
||||
focusable.forEach((fe) => fe.setAttribute('tabindex', '0'));
|
||||
}
|
||||
|
||||
document.body.style.overflow = open ? 'hidden' : '';
|
||||
const offset =
|
||||
document.querySelector('header')?.getBoundingClientRect().top || 0;
|
||||
if (navRef.current) {
|
||||
navRef.current.style.height = `calc(100vh - ${offset}px)`;
|
||||
}
|
||||
|
||||
// focus current by default
|
||||
if (navRef.current && open) {
|
||||
(navRef.current.querySelector('a[aria-current]') as HTMLElement)?.focus();
|
||||
}
|
||||
|
||||
const closeOnEsc = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeNav();
|
||||
}
|
||||
};
|
||||
|
||||
// tabbing loop
|
||||
const focusLast = (e: FocusEvent) => {
|
||||
e.preventDefault();
|
||||
const isNavElement =
|
||||
e.relatedTarget && navRef.current?.contains(e.relatedTarget as Node);
|
||||
if (!isNavElement && open) {
|
||||
last(focusable)?.focus();
|
||||
}
|
||||
};
|
||||
const focusFirst = (e: FocusEvent) => {
|
||||
e.preventDefault();
|
||||
const isNavElement =
|
||||
e.relatedTarget && navRef.current?.contains(e.relatedTarget as Node);
|
||||
if (!isNavElement && open) {
|
||||
first(focusable)?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
const resetOnDesktop = () => {
|
||||
focusable.forEach((fe) =>
|
||||
fe.setAttribute(
|
||||
'tabindex',
|
||||
window.innerWidth > BREAKPOINT_MD ? '0' : '-1'
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', resetOnDesktop);
|
||||
|
||||
first(focusable)?.addEventListener('focusout', focusLast);
|
||||
last(focusable)?.addEventListener('focusout', focusFirst);
|
||||
|
||||
document.addEventListener('keydown', closeOnEsc);
|
||||
return () => {
|
||||
window.removeEventListener('resize', resetOnDesktop);
|
||||
document.removeEventListener('keydown', closeOnEsc);
|
||||
first(focusable)?.removeEventListener('focusout', focusLast);
|
||||
last(focusable)?.removeEventListener('focusout', focusFirst);
|
||||
};
|
||||
}, [closeNav, focusable, open]);
|
||||
|
||||
return (
|
||||
<nav
|
||||
ref={navRef}
|
||||
className={classnames(
|
||||
'absolute top-0 z-20 overflow-y-auto',
|
||||
'transition-[right]',
|
||||
{
|
||||
'right-[-200vw] h-full': !open,
|
||||
'right-0 h-[100vh]': open,
|
||||
},
|
||||
'w-full p-4 border-neutral-700 dark:border-neutral-300',
|
||||
'bg-white dark:bg-black',
|
||||
'md:static md:border-r'
|
||||
)}
|
||||
>
|
||||
<NavLinks links={routerConfig} />
|
||||
<button
|
||||
ref={btnRef}
|
||||
className="absolute top-0 right-0 p-4 md:hidden"
|
||||
onClick={() => {
|
||||
closeNav();
|
||||
}}
|
||||
>
|
||||
<Icon name="cross" />
|
||||
</button>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,227 @@
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -33,4 +33,7 @@ export const ENV = {
|
||||
parties: truthy.includes(windowOrDefault('NX_EXPLORER_PARTIES')),
|
||||
validators: truthy.includes(windowOrDefault('NX_EXPLORER_VALIDATORS')),
|
||||
},
|
||||
addresses: {
|
||||
feedback: windowOrDefault('NX_GITHUB_FEEDBACK_URL'),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -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 Assets = () => {
|
||||
export const AssetsPage = () => {
|
||||
useDocumentTitle(['Assets']);
|
||||
useScrollToLocation();
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './assets';
|
||||
export * from './assets-page';
|
||||
export * from './asset-page';
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,122 +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 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,63 +1 @@
|
||||
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;
|
||||
export * from './proposals-page';
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,140 +0,0 @@
|
||||
query ExplorerMarkets {
|
||||
marketsConnection {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
fees {
|
||||
factors {
|
||||
makerFee
|
||||
infrastructureFee
|
||||
liquidityFee
|
||||
}
|
||||
}
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
name
|
||||
metadata {
|
||||
tags
|
||||
}
|
||||
code
|
||||
product {
|
||||
... on Future {
|
||||
settlementAsset {
|
||||
id
|
||||
name
|
||||
decimals
|
||||
globalRewardPoolAccount {
|
||||
balance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
riskModel {
|
||||
... on LogNormalRiskModel {
|
||||
tau
|
||||
riskAversionParameter
|
||||
params {
|
||||
r
|
||||
sigma
|
||||
mu
|
||||
}
|
||||
}
|
||||
... on SimpleRiskModel {
|
||||
params {
|
||||
factorLong
|
||||
factorShort
|
||||
}
|
||||
}
|
||||
}
|
||||
marginCalculator {
|
||||
scalingFactors {
|
||||
searchLevel
|
||||
initialMargin
|
||||
collateralRelease
|
||||
}
|
||||
}
|
||||
}
|
||||
decimalPlaces
|
||||
openingAuction {
|
||||
durationSecs
|
||||
volume
|
||||
}
|
||||
priceMonitoringSettings {
|
||||
parameters {
|
||||
triggers {
|
||||
horizonSecs
|
||||
probability
|
||||
auctionExtensionSecs
|
||||
}
|
||||
}
|
||||
}
|
||||
liquidityMonitoringParameters {
|
||||
triggeringRatio
|
||||
targetStakeParameters {
|
||||
timeWindow
|
||||
scalingFactor
|
||||
}
|
||||
}
|
||||
tradingMode
|
||||
state
|
||||
proposal {
|
||||
id
|
||||
}
|
||||
state
|
||||
accountsConnection {
|
||||
edges {
|
||||
node {
|
||||
asset {
|
||||
id
|
||||
name
|
||||
}
|
||||
balance
|
||||
type
|
||||
}
|
||||
}
|
||||
}
|
||||
data {
|
||||
markPrice
|
||||
bestBidPrice
|
||||
bestBidVolume
|
||||
bestOfferPrice
|
||||
bestOfferVolume
|
||||
bestStaticBidPrice
|
||||
bestStaticBidVolume
|
||||
bestStaticOfferPrice
|
||||
bestStaticOfferVolume
|
||||
midPrice
|
||||
staticMidPrice
|
||||
timestamp
|
||||
openInterest
|
||||
auctionEnd
|
||||
auctionStart
|
||||
indicativePrice
|
||||
indicativeVolume
|
||||
trigger
|
||||
extensionTrigger
|
||||
targetStake
|
||||
suppliedStake
|
||||
priceMonitoringBounds {
|
||||
minValidPrice
|
||||
maxValidPrice
|
||||
trigger {
|
||||
auctionExtensionSecs
|
||||
probability
|
||||
}
|
||||
referencePrice
|
||||
}
|
||||
marketValueProxy
|
||||
liquidityProviderFeeShare {
|
||||
party {
|
||||
id
|
||||
}
|
||||
equityLikeShare
|
||||
averageEntryValuation
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,180 +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 ExplorerMarketsQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ExplorerMarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, decimalPlaces: number, tradingMode: Types.MarketTradingMode, state: Types.MarketState, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null }, product: { __typename?: 'Future', settlementAsset: { __typename?: 'Asset', id: string, name: string, decimals: number, globalRewardPoolAccount?: { __typename?: 'AccountBalance', balance: string } | null } } }, riskModel: { __typename?: 'LogNormalRiskModel', tau: number, riskAversionParameter: number, params: { __typename?: 'LogNormalModelParams', r: number, sigma: number, mu: number } } | { __typename?: 'SimpleRiskModel', params: { __typename?: 'SimpleRiskModelParams', factorLong: number, factorShort: number } }, marginCalculator?: { __typename?: 'MarginCalculator', scalingFactors: { __typename?: 'ScalingFactors', searchLevel: number, initialMargin: number, collateralRelease: number } } | null }, openingAuction: { __typename?: 'AuctionDuration', durationSecs: number, volume: number }, priceMonitoringSettings: { __typename?: 'PriceMonitoringSettings', parameters?: { __typename?: 'PriceMonitoringParameters', triggers?: Array<{ __typename?: 'PriceMonitoringTrigger', horizonSecs: number, probability: number, auctionExtensionSecs: number }> | null } | null }, liquidityMonitoringParameters: { __typename?: 'LiquidityMonitoringParameters', triggeringRatio: string, targetStakeParameters: { __typename?: 'TargetStakeParameters', timeWindow: number, scalingFactor: number } }, proposal?: { __typename?: 'Proposal', id?: string | null } | null, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', balance: string, type: Types.AccountType, asset: { __typename?: 'Asset', id: string, name: string } } } | null> | null } | null, data?: { __typename?: 'MarketData', markPrice: string, bestBidPrice: string, bestBidVolume: string, bestOfferPrice: string, bestOfferVolume: string, bestStaticBidPrice: string, bestStaticBidVolume: string, bestStaticOfferPrice: string, bestStaticOfferVolume: string, midPrice: string, staticMidPrice: string, timestamp: any, openInterest: string, auctionEnd?: string | null, auctionStart?: string | null, indicativePrice: string, indicativeVolume: string, trigger: Types.AuctionTrigger, extensionTrigger: Types.AuctionTrigger, targetStake?: string | null, suppliedStake?: string | null, marketValueProxy: string, priceMonitoringBounds?: Array<{ __typename?: 'PriceMonitoringBounds', minValidPrice: string, maxValidPrice: string, referencePrice: string, trigger: { __typename?: 'PriceMonitoringTrigger', auctionExtensionSecs: number, probability: number } }> | null, liquidityProviderFeeShare?: Array<{ __typename?: 'LiquidityProviderFeeShare', equityLikeShare: string, averageEntryValuation: string, party: { __typename?: 'Party', id: string } }> | null } | null } }> } | null };
|
||||
|
||||
|
||||
export const ExplorerMarketsDocument = gql`
|
||||
query ExplorerMarkets {
|
||||
marketsConnection {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
fees {
|
||||
factors {
|
||||
makerFee
|
||||
infrastructureFee
|
||||
liquidityFee
|
||||
}
|
||||
}
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
name
|
||||
metadata {
|
||||
tags
|
||||
}
|
||||
code
|
||||
product {
|
||||
... on Future {
|
||||
settlementAsset {
|
||||
id
|
||||
name
|
||||
decimals
|
||||
globalRewardPoolAccount {
|
||||
balance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
riskModel {
|
||||
... on LogNormalRiskModel {
|
||||
tau
|
||||
riskAversionParameter
|
||||
params {
|
||||
r
|
||||
sigma
|
||||
mu
|
||||
}
|
||||
}
|
||||
... on SimpleRiskModel {
|
||||
params {
|
||||
factorLong
|
||||
factorShort
|
||||
}
|
||||
}
|
||||
}
|
||||
marginCalculator {
|
||||
scalingFactors {
|
||||
searchLevel
|
||||
initialMargin
|
||||
collateralRelease
|
||||
}
|
||||
}
|
||||
}
|
||||
decimalPlaces
|
||||
openingAuction {
|
||||
durationSecs
|
||||
volume
|
||||
}
|
||||
priceMonitoringSettings {
|
||||
parameters {
|
||||
triggers {
|
||||
horizonSecs
|
||||
probability
|
||||
auctionExtensionSecs
|
||||
}
|
||||
}
|
||||
}
|
||||
liquidityMonitoringParameters {
|
||||
triggeringRatio
|
||||
targetStakeParameters {
|
||||
timeWindow
|
||||
scalingFactor
|
||||
}
|
||||
}
|
||||
tradingMode
|
||||
state
|
||||
proposal {
|
||||
id
|
||||
}
|
||||
state
|
||||
accountsConnection {
|
||||
edges {
|
||||
node {
|
||||
asset {
|
||||
id
|
||||
name
|
||||
}
|
||||
balance
|
||||
type
|
||||
}
|
||||
}
|
||||
}
|
||||
data {
|
||||
markPrice
|
||||
bestBidPrice
|
||||
bestBidVolume
|
||||
bestOfferPrice
|
||||
bestOfferVolume
|
||||
bestStaticBidPrice
|
||||
bestStaticBidVolume
|
||||
bestStaticOfferPrice
|
||||
bestStaticOfferVolume
|
||||
midPrice
|
||||
staticMidPrice
|
||||
timestamp
|
||||
openInterest
|
||||
auctionEnd
|
||||
auctionStart
|
||||
indicativePrice
|
||||
indicativeVolume
|
||||
trigger
|
||||
extensionTrigger
|
||||
targetStake
|
||||
suppliedStake
|
||||
priceMonitoringBounds {
|
||||
minValidPrice
|
||||
maxValidPrice
|
||||
trigger {
|
||||
auctionExtensionSecs
|
||||
probability
|
||||
}
|
||||
referencePrice
|
||||
}
|
||||
marketValueProxy
|
||||
liquidityProviderFeeShare {
|
||||
party {
|
||||
id
|
||||
}
|
||||
equityLikeShare
|
||||
averageEntryValuation
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useExplorerMarketsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useExplorerMarketsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useExplorerMarketsQuery` 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 } = useExplorerMarketsQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useExplorerMarketsQuery(baseOptions?: Apollo.QueryHookOptions<ExplorerMarketsQuery, ExplorerMarketsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ExplorerMarketsQuery, ExplorerMarketsQueryVariables>(ExplorerMarketsDocument, options);
|
||||
}
|
||||
export function useExplorerMarketsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerMarketsQuery, ExplorerMarketsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ExplorerMarketsQuery, ExplorerMarketsQueryVariables>(ExplorerMarketsDocument, options);
|
||||
}
|
||||
export type ExplorerMarketsQueryHookResult = ReturnType<typeof useExplorerMarketsQuery>;
|
||||
export type ExplorerMarketsLazyQueryHookResult = ReturnType<typeof useExplorerMarketsLazyQuery>;
|
||||
export type ExplorerMarketsQueryResult = Apollo.QueryResult<ExplorerMarketsQuery, ExplorerMarketsQueryVariables>;
|
||||
@@ -1,48 +0,0 @@
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { render } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import Markets from './index';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { ExplorerMarketsDocument } from './__generated__/Markets';
|
||||
|
||||
function renderComponent(mock: MockedResponse[]) {
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<MockedProvider mocks={mock}>
|
||||
<Markets />
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
describe('Markets index', () => {
|
||||
it('Renders loader when loading', async () => {
|
||||
const mock = {
|
||||
request: {
|
||||
query: ExplorerMarketsDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
marketsConnection: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
const res = render(renderComponent([mock]));
|
||||
expect(await res.findByTestId('loader')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders EmptyList when loading completes and there are no results', async () => {
|
||||
const mock = {
|
||||
request: {
|
||||
query: ExplorerMarketsDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
marketsConnection: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
const res = render(renderComponent([mock]));
|
||||
expect(await res.findByTestId('emptylist')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,44 +1,2 @@
|
||||
import React from 'react';
|
||||
import { Loader, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import { SubHeading } from '../../components/sub-heading';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { useExplorerMarketsQuery } from './__generated__/Markets';
|
||||
import { useScrollToLocation } from '../../hooks/scroll-to-location';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
import EmptyList from '../../components/empty-list/empty-list';
|
||||
|
||||
const Markets = () => {
|
||||
const { data, loading } = useExplorerMarketsQuery();
|
||||
|
||||
useScrollToLocation();
|
||||
useDocumentTitle(['Markets']);
|
||||
|
||||
const m = data?.marketsConnection?.edges;
|
||||
|
||||
return (
|
||||
<section key="markets">
|
||||
<RouteTitle data-testid="markets-heading">{t('Markets')}</RouteTitle>
|
||||
|
||||
{m ? (
|
||||
m.map((e) => (
|
||||
<React.Fragment key={e.node.id}>
|
||||
<SubHeading data-testid="markets-header" id={e.node.id}>
|
||||
{e.node.tradableInstrument.instrument.name}
|
||||
</SubHeading>
|
||||
<SyntaxHighlighter data={e.node} />
|
||||
</React.Fragment>
|
||||
))
|
||||
) : loading ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<EmptyList
|
||||
heading={t('This chain has no markets')}
|
||||
label={t('0 markets')}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Markets;
|
||||
export * from './markets-page';
|
||||
export * from './market-page';
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { t, useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { MarketDetails } from '../../components/markets/market-details';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import { useScrollToLocation } from '../../hooks/scroll-to-location';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
import compact from 'lodash/compact';
|
||||
import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog';
|
||||
import { marketInfoNoCandlesDataProvider } from '@vegaprotocol/market-info';
|
||||
|
||||
export const MarketPage = () => {
|
||||
useScrollToLocation();
|
||||
|
||||
const { marketId } = useParams<{ marketId: string }>();
|
||||
|
||||
const variables = useMemo(
|
||||
() => ({
|
||||
marketId,
|
||||
}),
|
||||
[marketId]
|
||||
);
|
||||
|
||||
const { data, loading, error } = useDataProvider({
|
||||
dataProvider: marketInfoNoCandlesDataProvider,
|
||||
skipUpdates: true,
|
||||
variables,
|
||||
});
|
||||
|
||||
useDocumentTitle(
|
||||
compact([
|
||||
'Market details',
|
||||
data?.market?.tradableInstrument.instrument.name,
|
||||
])
|
||||
);
|
||||
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="relative">
|
||||
<RouteTitle data-testid="markets-heading">
|
||||
{data?.market?.tradableInstrument.instrument.name}
|
||||
</RouteTitle>
|
||||
<AsyncRenderer
|
||||
noDataMessage={t('This chain has no markets')}
|
||||
data={data}
|
||||
loading={loading}
|
||||
error={error}
|
||||
>
|
||||
<div className="absolute top-0 right-0">
|
||||
<Button size="xs" onClick={() => setDialogOpen(true)}>
|
||||
{t('View JSON')}
|
||||
</Button>
|
||||
</div>
|
||||
<MarketDetails market={data?.market} />
|
||||
</AsyncRenderer>
|
||||
</section>
|
||||
<JsonViewerDialog
|
||||
open={dialogOpen}
|
||||
onChange={(isOpen) => setDialogOpen(isOpen)}
|
||||
title={data?.market?.tradableInstrument.instrument.name || ''}
|
||||
content={data?.market}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useScrollToLocation } from '../../hooks/scroll-to-location';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
import { marketsProvider } from '@vegaprotocol/market-list';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { t, useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { MarketsTable } from '../../components/markets/markets-table';
|
||||
|
||||
export const MarketsPage = () => {
|
||||
useDocumentTitle(['Markets']);
|
||||
useScrollToLocation();
|
||||
|
||||
const { data, loading, error } = useDataProvider({
|
||||
dataProvider: marketsProvider,
|
||||
skipUpdates: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<section>
|
||||
<RouteTitle data-testid="markets-heading">{t('Markets')}</RouteTitle>
|
||||
<AsyncRenderer
|
||||
noDataMessage={t('This chain has no markets')}
|
||||
data={data}
|
||||
loading={loading}
|
||||
error={error}
|
||||
>
|
||||
<MarketsTable data={data} />
|
||||
</AsyncRenderer>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Assets } from './assets';
|
||||
import { AssetPage, AssetsPage } from './assets';
|
||||
import BlockPage from './blocks';
|
||||
import Governance from './governance';
|
||||
import { Proposals } from './governance';
|
||||
import Home from './home';
|
||||
import Markets from './markets';
|
||||
import OraclePage from './oracles';
|
||||
import Oracles from './oracles/home';
|
||||
import { Oracle } from './oracles/id';
|
||||
@@ -21,8 +20,13 @@ import flags from '../config/flags';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { Routes } from './route-names';
|
||||
import { NetworkParameters } from './network-parameters';
|
||||
import type { RouteObject } from 'react-router-dom';
|
||||
import { MarketPage, MarketsPage } from './markets';
|
||||
|
||||
const partiesRoutes = flags.parties
|
||||
export type Navigable = { path: string; name: string; text: string };
|
||||
type Route = RouteObject & Navigable;
|
||||
|
||||
const partiesRoutes: Route[] = flags.parties
|
||||
? [
|
||||
{
|
||||
path: Routes.PARTIES,
|
||||
@@ -43,18 +47,27 @@ const partiesRoutes = flags.parties
|
||||
]
|
||||
: [];
|
||||
|
||||
const assetsRoutes = flags.assets
|
||||
const assetsRoutes: Route[] = flags.assets
|
||||
? [
|
||||
{
|
||||
path: Routes.ASSETS,
|
||||
text: t('Assets'),
|
||||
name: 'Assets',
|
||||
element: <Assets />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <AssetsPage />,
|
||||
},
|
||||
{
|
||||
path: ':assetId',
|
||||
element: <AssetPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const genesisRoutes = flags.genesis
|
||||
const genesisRoutes: Route[] = flags.genesis
|
||||
? [
|
||||
{
|
||||
path: Routes.GENESIS,
|
||||
@@ -65,29 +78,38 @@ const genesisRoutes = flags.genesis
|
||||
]
|
||||
: [];
|
||||
|
||||
const governanceRoutes = flags.governance
|
||||
const governanceRoutes: Route[] = flags.governance
|
||||
? [
|
||||
{
|
||||
path: Routes.GOVERNANCE,
|
||||
name: 'Governance proposals',
|
||||
text: t('Governance Proposals'),
|
||||
element: <Governance />,
|
||||
element: <Proposals />,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const marketsRoutes = flags.markets
|
||||
const marketsRoutes: Route[] = flags.markets
|
||||
? [
|
||||
{
|
||||
path: Routes.MARKETS,
|
||||
name: 'Markets',
|
||||
text: t('Markets'),
|
||||
element: <Markets />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <MarketsPage />,
|
||||
},
|
||||
{
|
||||
path: ':marketId',
|
||||
element: <MarketPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const networkParametersRoutes = flags.networkParameters
|
||||
const networkParametersRoutes: Route[] = flags.networkParameters
|
||||
? [
|
||||
{
|
||||
path: Routes.NETWORK_PARAMETERS,
|
||||
@@ -97,7 +119,7 @@ const networkParametersRoutes = flags.networkParameters
|
||||
},
|
||||
]
|
||||
: [];
|
||||
const validators = flags.validators
|
||||
const validators: Route[] = flags.validators
|
||||
? [
|
||||
{
|
||||
path: Routes.VALIDATORS,
|
||||
@@ -108,7 +130,7 @@ const validators = flags.validators
|
||||
]
|
||||
: [];
|
||||
|
||||
const routerConfig = [
|
||||
const routerConfig: Route[] = [
|
||||
{
|
||||
path: Routes.HOME,
|
||||
name: 'Home',
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { InMemoryCacheConfig } from '@apollo/client';
|
||||
import { NetworkLoader, useInitializeEnv } from '@vegaprotocol/environment';
|
||||
import { useRoutes } from 'react-router-dom';
|
||||
|
||||
import '../styles.scss';
|
||||
@@ -5,14 +7,40 @@ import { Navbar } from './components/navbar';
|
||||
|
||||
import { routerConfig } from './routes/router-config';
|
||||
|
||||
const cache: InMemoryCacheConfig = {
|
||||
typePolicies: {
|
||||
Market: {
|
||||
merge: true,
|
||||
},
|
||||
Party: {
|
||||
merge: true,
|
||||
},
|
||||
Query: {},
|
||||
Account: {
|
||||
keyFields: false,
|
||||
fields: {
|
||||
balanceFormatted: {},
|
||||
},
|
||||
},
|
||||
Node: {
|
||||
keyFields: false,
|
||||
},
|
||||
Instrument: {
|
||||
keyFields: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
const AppRouter = () => useRoutes(routerConfig);
|
||||
|
||||
export function App() {
|
||||
useInitializeEnv();
|
||||
return (
|
||||
<div className="max-h-full min-h-full bg-white">
|
||||
<Navbar />
|
||||
<AppRouter />
|
||||
</div>
|
||||
<NetworkLoader cache={cache}>
|
||||
<div className="max-h-full min-h-full bg-white">
|
||||
<Navbar />
|
||||
<AppRouter />
|
||||
</div>
|
||||
</NetworkLoader>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +1,15 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { EnvironmentProvider, NetworkLoader } from '@vegaprotocol/environment';
|
||||
|
||||
import App from './app/app';
|
||||
import type { InMemoryCacheConfig } from '@apollo/client';
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
const root = rootElement && createRoot(rootElement);
|
||||
const cache: InMemoryCacheConfig = {
|
||||
typePolicies: {
|
||||
Market: {
|
||||
merge: true,
|
||||
},
|
||||
Party: {
|
||||
merge: true,
|
||||
},
|
||||
Query: {},
|
||||
Account: {
|
||||
keyFields: false,
|
||||
fields: {
|
||||
balanceFormatted: {},
|
||||
},
|
||||
},
|
||||
Node: {
|
||||
keyFields: false,
|
||||
},
|
||||
Instrument: {
|
||||
keyFields: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
root?.render(
|
||||
<StrictMode>
|
||||
<BrowserRouter>
|
||||
<EnvironmentProvider>
|
||||
<NetworkLoader cache={cache}>
|
||||
<App />
|
||||
</NetworkLoader>
|
||||
</EnvironmentProvider>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</StrictMode>
|
||||
);
|
||||
|
||||
@@ -2,3 +2,4 @@ NX_VEGA_URL=https://api.stagnet3.vega.xyz/graphql
|
||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/stagnet3-network.json
|
||||
NX_VEGA_NETWORKS='{"TESTNET":"https://multisig-signer.fairground.wtf","MAINNET":"https://multisig-signer.vega.xyz"}'
|
||||
NX_VEGA_ENV=STAGNET3
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ import classnames from 'classnames';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { BrowserTracing } from '@sentry/tracing';
|
||||
import {
|
||||
EnvironmentProvider,
|
||||
NetworkLoader,
|
||||
useEnvironment,
|
||||
useInitializeEnv,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { AsyncRenderer, Button, Lozenge } from '@vegaprotocol/ui-toolkit';
|
||||
import type { EthereumConfig } from '@vegaprotocol/web3';
|
||||
@@ -64,6 +64,7 @@ function App() {
|
||||
environment: VEGA_ENV,
|
||||
});
|
||||
}, [VEGA_ENV]);
|
||||
|
||||
const Connectors = useMemo(() => {
|
||||
if (config?.chain_id) {
|
||||
return createConnectors(ETHEREUM_PROVIDER_URL, Number(config.chain_id));
|
||||
@@ -107,12 +108,11 @@ const Wrapper = () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
useInitializeEnv();
|
||||
return (
|
||||
<EnvironmentProvider>
|
||||
<NetworkLoader cache={cache}>
|
||||
<App />
|
||||
</NetworkLoader>
|
||||
</EnvironmentProvider>
|
||||
<NetworkLoader cache={cache}>
|
||||
<App />
|
||||
</NetworkLoader>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,952 +0,0 @@
|
||||
/// <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 closedProposals = '[data-testid="closed-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 proposalStatus = '[data-testid="proposal-status"]';
|
||||
const rawProposalData = '[data-testid="proposal-data"]';
|
||||
const votesTable = '[data-testid="votes-table"]';
|
||||
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');
|
||||
});
|
||||
|
||||
// 3001-VOTE-006
|
||||
it('Able to view enacted proposal', function () {
|
||||
cy.createMarket();
|
||||
cy.reload();
|
||||
cy.wait_for_spinner();
|
||||
cy.get(closedProposals).within(() => {
|
||||
cy.get(proposalDetailsTitle).should(
|
||||
'have.text',
|
||||
'Add Lorem Ipsum market'
|
||||
);
|
||||
cy.get(proposalStatus).should('have.text', 'Enacted ');
|
||||
cy.get(viewProposalButton).click();
|
||||
});
|
||||
cy.getByTestId('proposal-type').should('have.text', 'New market');
|
||||
cy.get_proposal_information_from_table('State')
|
||||
.contains('Enacted')
|
||||
.and('be.visible');
|
||||
cy.get(votesTable).within(() => {
|
||||
cy.contains('Vote passed.').should('be.visible');
|
||||
cy.contains('Voting has ended.').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
// 3001-VOTE-047
|
||||
it('Able to enact freeform proposal', function () {
|
||||
const proposalTitle = 'Add New free form proposal with short enactment';
|
||||
cy.ensure_specified_unstaked_tokens_are_associated(
|
||||
this.minProposerBalance
|
||||
);
|
||||
cy.sendWalletTxFreeFormProposal();
|
||||
cy.navigate_to('proposals');
|
||||
cy.reload();
|
||||
cy.wait_for_spinner();
|
||||
cy.contains(proposalTitle)
|
||||
.parentsUntil('[data-testid="proposals-list-item"]')
|
||||
.within(() => cy.get(viewProposalButton).click());
|
||||
cy.get_proposal_information_from_table('State')
|
||||
.contains('Open')
|
||||
.and('be.visible');
|
||||
cy.vote_for_proposal('for');
|
||||
cy.get_proposal_information_from_table('State')
|
||||
.contains('Enacted', epochTimeout)
|
||||
.and('be.visible');
|
||||
});
|
||||
|
||||
// 3001-VOTE-046 3001-VOTE-044 3001-VOTE-074 3001-VOTE-074
|
||||
it('Able to enact proposal by voting', function () {
|
||||
const proposalTitle = 'Add New proposal with short enactment';
|
||||
cy.ensure_specified_unstaked_tokens_are_associated(
|
||||
this.minProposerBalance
|
||||
);
|
||||
cy.sendWalletTxUpdateNetworkProposal();
|
||||
cy.navigate_to('proposals');
|
||||
cy.reload();
|
||||
cy.wait_for_spinner();
|
||||
cy.contains(proposalTitle)
|
||||
.parentsUntil('[data-testid="proposals-list-item"]')
|
||||
.within(() => cy.get(viewProposalButton).click());
|
||||
cy.get_proposal_information_from_table('State')
|
||||
.contains('Open')
|
||||
.and('be.visible');
|
||||
cy.vote_for_proposal('for');
|
||||
cy.get_proposal_information_from_table('State') // 3001-VOTE-047
|
||||
.contains('Passed', txTimeout)
|
||||
.and('be.visible');
|
||||
cy.get_proposal_information_from_table('State')
|
||||
.contains('Enacted', epochTimeout)
|
||||
.and('be.visible');
|
||||
cy.get(votesTable).within(() => {
|
||||
cy.contains('Vote passed.').should('be.visible');
|
||||
cy.contains('Voting has ended.').should('be.visible');
|
||||
});
|
||||
cy.get(proposalVoteProgressForPercentage)
|
||||
.contains('100.00%')
|
||||
.and('be.visible');
|
||||
});
|
||||
|
||||
// 3001-VOTE-048 3001-VOTE-049
|
||||
it('Able to fail proposal due to lack of participation', function () {
|
||||
const proposalTitle = 'Add New free form proposal with short enactment';
|
||||
cy.ensure_specified_unstaked_tokens_are_associated(
|
||||
this.minProposerBalance
|
||||
);
|
||||
cy.sendWalletTxFreeFormProposal();
|
||||
cy.navigate_to('proposals');
|
||||
cy.reload();
|
||||
cy.wait_for_spinner();
|
||||
cy.contains(proposalTitle)
|
||||
.parentsUntil('[data-testid="proposals-list-item"]')
|
||||
.within(() => cy.get(viewProposalButton).click());
|
||||
cy.get_proposal_information_from_table('State')
|
||||
.contains('Open')
|
||||
.and('be.visible');
|
||||
cy.get_proposal_information_from_table('State') // 3001-VOTE-047
|
||||
.contains('Declined', txTimeout)
|
||||
.and('be.visible');
|
||||
cy.get_proposal_information_from_table('Rejection reason')
|
||||
.contains('PROPOSAL_ERROR_PARTICIPATION_THRESHOLD_NOT_REACHED')
|
||||
.and('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();
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,273 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,145 @@
|
||||
/// <reference types="cypress" />
|
||||
import { associateTokenStartOfTests } from '../../support/governance.functions';
|
||||
|
||||
import {
|
||||
createUpdateNetworkProposalTxBody,
|
||||
createFreeFormProposalTxBody,
|
||||
} from '../../support/proposal.functions';
|
||||
|
||||
const closedProposals = '[data-testid="closed-proposals"]';
|
||||
const proposalStatus = '[data-testid="proposal-status"]';
|
||||
const viewProposalButton = '[data-testid="view-proposal-btn"]';
|
||||
const votesTable = '[data-testid="votes-table"]';
|
||||
const openProposals = '[data-testid="open-proposals"]';
|
||||
const proposalVoteProgressForPercentage =
|
||||
'[data-testid="vote-progress-indicator-percentage-for"]';
|
||||
const proposalTimeout = { timeout: 8000 };
|
||||
|
||||
context(
|
||||
'Proposal flow - with proposals enacted or failed',
|
||||
{ tags: '@slow' },
|
||||
function () {
|
||||
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);
|
||||
cy.clearLocalStorage();
|
||||
});
|
||||
|
||||
beforeEach('visit proposals', function () {
|
||||
cy.reload();
|
||||
cy.wait_for_spinner();
|
||||
cy.connectVegaWallet();
|
||||
cy.ethereum_wallet_connect();
|
||||
cy.navigate_to('proposals');
|
||||
});
|
||||
|
||||
// 3001-VOTE-006
|
||||
it('Able to view enacted proposal', function () {
|
||||
const proposalTitle = 'Add Lorem Ipsum market';
|
||||
|
||||
cy.createMarket();
|
||||
cy.reload();
|
||||
cy.wait_for_spinner();
|
||||
cy.get(closedProposals).within(() => {
|
||||
cy.contains(proposalTitle)
|
||||
.parentsUntil('[data-testid="proposals-list-item"]')
|
||||
.within(() => {
|
||||
cy.get(proposalStatus).should('have.text', 'Enacted ');
|
||||
cy.get(viewProposalButton).click();
|
||||
});
|
||||
});
|
||||
cy.getByTestId('proposal-type').should('have.text', 'New market');
|
||||
cy.get_proposal_information_from_table('State')
|
||||
.contains('Enacted')
|
||||
.and('be.visible');
|
||||
cy.get(votesTable).within(() => {
|
||||
cy.contains('Vote passed.').should('be.visible');
|
||||
cy.contains('Voting has ended.').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
// 3001-VOTE-046 3001-VOTE-044 3001-VOTE-074 3001-VOTE-074
|
||||
it('Able to enact proposal by voting', function () {
|
||||
const proposalTitle = 'Add New proposal with short enactment';
|
||||
const proposalTx = createUpdateNetworkProposalTxBody();
|
||||
|
||||
cy.VegaWalletSubmitProposal(proposalTx);
|
||||
cy.navigate_to('proposals');
|
||||
cy.reload();
|
||||
cy.wait_for_spinner();
|
||||
cy.get(openProposals).within(() => {
|
||||
cy.contains(proposalTitle)
|
||||
.parentsUntil('[data-testid="proposals-list-item"]')
|
||||
.within(() => cy.get(viewProposalButton).click());
|
||||
});
|
||||
cy.get_proposal_information_from_table('State')
|
||||
.contains('Open')
|
||||
.and('be.visible');
|
||||
cy.vote_for_proposal('for');
|
||||
cy.get_proposal_information_from_table('State') // 3001-VOTE-047
|
||||
.contains('Passed', proposalTimeout)
|
||||
.and('be.visible');
|
||||
cy.get_proposal_information_from_table('State')
|
||||
.contains('Enacted', proposalTimeout)
|
||||
.and('be.visible');
|
||||
cy.get(votesTable).within(() => {
|
||||
cy.contains('Vote passed.').should('be.visible');
|
||||
cy.contains('Voting has ended.').should('be.visible');
|
||||
});
|
||||
cy.get(proposalVoteProgressForPercentage)
|
||||
.contains('100.00%')
|
||||
.and('be.visible');
|
||||
});
|
||||
|
||||
// 3001-VOTE-047
|
||||
it('Able to enact freeform proposal', function () {
|
||||
const proposalTitle = 'Add New free form proposal with short enactment';
|
||||
const proposalTx = createFreeFormProposalTxBody();
|
||||
|
||||
cy.VegaWalletSubmitProposal(proposalTx);
|
||||
cy.navigate_to('proposals');
|
||||
cy.reload();
|
||||
cy.wait_for_spinner();
|
||||
cy.get(openProposals).within(() => {
|
||||
cy.contains(proposalTitle)
|
||||
.parentsUntil('[data-testid="proposals-list-item"]')
|
||||
.within(() => cy.get(viewProposalButton).click());
|
||||
});
|
||||
cy.get_proposal_information_from_table('State')
|
||||
.contains('Open')
|
||||
.and('be.visible');
|
||||
cy.vote_for_proposal('for');
|
||||
cy.get_proposal_information_from_table('State')
|
||||
.contains('Enacted', proposalTimeout)
|
||||
.and('be.visible');
|
||||
});
|
||||
|
||||
// 3001-VOTE-048 3001-VOTE-049
|
||||
it('Able to fail proposal due to lack of participation', function () {
|
||||
const proposalTitle = 'Add New free form proposal with short enactment';
|
||||
const proposalTx = createFreeFormProposalTxBody();
|
||||
cy.VegaWalletSubmitProposal(proposalTx);
|
||||
cy.navigate_to('proposals');
|
||||
cy.reload();
|
||||
cy.wait_for_spinner();
|
||||
cy.get(openProposals).within(() => {
|
||||
cy.contains(proposalTitle)
|
||||
.parentsUntil('[data-testid="proposals-list-item"]')
|
||||
.within(() => cy.get(viewProposalButton).click());
|
||||
});
|
||||
cy.get_proposal_information_from_table('State')
|
||||
.contains('Open')
|
||||
.and('be.visible');
|
||||
cy.get_proposal_information_from_table('State') // 3001-VOTE-047
|
||||
.contains('Declined', proposalTimeout)
|
||||
.and('be.visible');
|
||||
cy.get_proposal_information_from_table('Rejection reason')
|
||||
.contains('PROPOSAL_ERROR_PARTICIPATION_THRESHOLD_NOT_REACHED')
|
||||
.and('be.visible');
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,347 @@
|
||||
/// <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
-1
@@ -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');
|
||||
@@ -0,0 +1,162 @@
|
||||
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,6 +63,39 @@ 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,7 +38,6 @@ 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);
|
||||
|
||||
+2
-2
@@ -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 staking tab highlighted', function () {
|
||||
it('Should have validators tab highlighted', function () {
|
||||
cy.verify_tab_highlighted('validators');
|
||||
});
|
||||
|
||||
it('Should have STAKING ON VEGA header visible', function () {
|
||||
it('Should have validators ON VEGA header visible', function () {
|
||||
cy.verify_page_header('Validators');
|
||||
});
|
||||
|
||||
@@ -338,8 +338,8 @@ context(
|
||||
.contains(currency.id)
|
||||
.parent()
|
||||
.siblings()
|
||||
.within(() => cy.contains_exactly('10.00').should('be.visible'));
|
||||
|
||||
.invoke('text')
|
||||
.should('not.be.empty');
|
||||
cy.get(vegaWalletCurrencyTitle)
|
||||
.contains(currency.id)
|
||||
.parent()
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
const epochTimeout = Cypress.env('epochTimeout');
|
||||
const txTimeout = Cypress.env('txTimeout');
|
||||
|
||||
Cypress.Commands.add(
|
||||
'convert_token_value_to_number',
|
||||
{ prevSubject: true },
|
||||
@@ -49,3 +52,29 @@ 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,6 +8,7 @@ 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 };
|
||||
@@ -61,6 +62,7 @@ Cypress.Commands.add('enter_raw_proposal_body', (timestamp) => {
|
||||
parseSpecialCharSequences: false,
|
||||
delay: 2,
|
||||
});
|
||||
cy.get(newProposalSubmitButton).should('be.visible').click();
|
||||
cy.wrap(rawProposal);
|
||||
});
|
||||
});
|
||||
@@ -73,6 +75,7 @@ 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();
|
||||
}
|
||||
);
|
||||
|
||||
@@ -185,3 +188,40 @@ 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',
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import './governance.functions.js';
|
||||
import './wallet-eth.functions.js';
|
||||
import './wallet-teardown.functions.js';
|
||||
import './wallet-vega.functions.js';
|
||||
import './proposal.functions.js';
|
||||
import './proposal.functions.ts';
|
||||
import registerCypressGrep from '@cypress/grep';
|
||||
import { aliasGQLQuery } from '@vegaprotocol/cypress';
|
||||
import { chainIdQuery, statisticsQuery } from '@vegaprotocol/mock';
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import { addSeconds, millisecondsToSeconds } from 'date-fns';
|
||||
|
||||
const walletName = Cypress.env('vegaWalletName');
|
||||
const walletPubKey = Cypress.env('vegaWalletPublicKey');
|
||||
const walletLocation = Cypress.env('vegaWalletLocation');
|
||||
const walletPassphraseFile = './src/fixtures/wallet/passphrase';
|
||||
|
||||
Cypress.Commands.add('sendWalletTxUpdateNetworkProposal', () => {
|
||||
const MIN_CLOSE_SEC = 8;
|
||||
const MIN_ENACT_SEC = 5;
|
||||
|
||||
const closingDate = addSeconds(new Date(), MIN_CLOSE_SEC);
|
||||
const enactmentDate = addSeconds(closingDate, MIN_ENACT_SEC);
|
||||
const closingTimestamp = millisecondsToSeconds(closingDate.getTime());
|
||||
const enactmentTimestamp = millisecondsToSeconds(enactmentDate.getTime());
|
||||
|
||||
cy.exec(
|
||||
`vegawallet transaction send --wallet ${walletName} --pubkey ${walletPubKey} -p ${walletPassphraseFile} --network DV '{
|
||||
"proposalSubmission": {
|
||||
"rationale": {
|
||||
"title": "Add New proposal with short enactment",
|
||||
"description": "E2E enactment test"
|
||||
},
|
||||
"terms": {
|
||||
"updateNetworkParameter": {
|
||||
"changes": {
|
||||
"key": "governance.proposal.updateNetParam.minProposerBalance",
|
||||
"value": "2"
|
||||
}
|
||||
},
|
||||
"closingTimestamp": ${closingTimestamp},
|
||||
"enactmentTimestamp": ${enactmentTimestamp}
|
||||
}
|
||||
}
|
||||
}' --home ${walletLocation}`,
|
||||
{ failOnNonZeroExit: false }
|
||||
)
|
||||
.its('stderr')
|
||||
.should('contain', '');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('sendWalletTxUpdateAssetProposal', () => {
|
||||
const MIN_CLOSE_SEC = 8;
|
||||
const MIN_ENACT_SEC = 5;
|
||||
|
||||
const closingDate = addSeconds(new Date(), MIN_CLOSE_SEC);
|
||||
const enactmentDate = addSeconds(closingDate, MIN_ENACT_SEC);
|
||||
const closingTimestamp = millisecondsToSeconds(closingDate.getTime());
|
||||
const enactmentTimestamp = millisecondsToSeconds(enactmentDate.getTime());
|
||||
|
||||
cy.exec(
|
||||
`vegawallet transaction send --wallet ${walletName} --pubkey ${walletPubKey} -p ${walletPassphraseFile} --network DV '{
|
||||
"proposalSubmission": {
|
||||
"rationale": {
|
||||
"title": "Update Asset set to fail",
|
||||
"description": "E2E fail test"
|
||||
},
|
||||
"terms": {
|
||||
"updateAsset": {
|
||||
"assetId": "ebcd94151ae1f0d39a4bde3b21a9c7ae81a80ea4352fb075a92e07608d9c953d",
|
||||
"changes": {
|
||||
"quantum": "1",
|
||||
"erc20": {
|
||||
"withdrawThreshold": "10",
|
||||
"lifetimeLimit": "10"
|
||||
}
|
||||
}
|
||||
},
|
||||
"closingTimestamp": ${closingTimestamp},
|
||||
"enactmentTimestamp": ${enactmentTimestamp}
|
||||
}
|
||||
}
|
||||
}' --home ${walletLocation}`,
|
||||
{ failOnNonZeroExit: false }
|
||||
)
|
||||
.its('stderr')
|
||||
.should('contain', '');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('sendWalletTxFreeFormProposal', () => {
|
||||
const MIN_CLOSE_SEC = 5;
|
||||
|
||||
const closingDate = addSeconds(new Date(), MIN_CLOSE_SEC);
|
||||
const closingTimestamp = millisecondsToSeconds(closingDate.getTime());
|
||||
|
||||
cy.exec(
|
||||
`vegawallet transaction send --wallet ${walletName} --pubkey ${walletPubKey} -p ${walletPassphraseFile} --network DV '{
|
||||
"proposalSubmission": {
|
||||
"rationale": {
|
||||
"title": "Add New free form proposal with short enactment",
|
||||
"description": "E2E enactment test"
|
||||
},
|
||||
"terms": {
|
||||
"newFreeform": {},
|
||||
"closingTimestamp": ${closingTimestamp}
|
||||
}
|
||||
}
|
||||
}' --home ${walletLocation}`,
|
||||
{ failOnNonZeroExit: false }
|
||||
)
|
||||
.its('stderr')
|
||||
.should('contain', '');
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import { addSeconds, millisecondsToSeconds } from 'date-fns';
|
||||
import type { ProposalSubmissionBody } from '@vegaprotocol/wallet';
|
||||
|
||||
export function createUpdateNetworkProposalTxBody(): ProposalSubmissionBody {
|
||||
const MIN_CLOSE_SEC = 5;
|
||||
const MIN_ENACT_SEC = 7;
|
||||
|
||||
const closingDate = addSeconds(new Date(), MIN_CLOSE_SEC);
|
||||
const enactmentDate = addSeconds(closingDate, MIN_ENACT_SEC);
|
||||
const closingTimestamp = millisecondsToSeconds(closingDate.getTime());
|
||||
const enactmentTimestamp = millisecondsToSeconds(enactmentDate.getTime());
|
||||
return {
|
||||
proposalSubmission: {
|
||||
rationale: {
|
||||
title: 'Add New proposal with short enactment',
|
||||
description: 'E2E enactment test',
|
||||
},
|
||||
terms: {
|
||||
updateNetworkParameter: {
|
||||
changes: {
|
||||
key: 'governance.proposal.updateNetParam.minProposerBalance',
|
||||
value: '2',
|
||||
},
|
||||
},
|
||||
closingTimestamp,
|
||||
enactmentTimestamp,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createUpdateAssetProposalTxBody(): ProposalSubmissionBody {
|
||||
const MIN_CLOSE_SEC = 5;
|
||||
const MIN_ENACT_SEC = 7;
|
||||
|
||||
const closingDate = addSeconds(new Date(), MIN_CLOSE_SEC);
|
||||
const enactmentDate = addSeconds(closingDate, MIN_ENACT_SEC);
|
||||
const closingTimestamp = millisecondsToSeconds(closingDate.getTime());
|
||||
const enactmentTimestamp = millisecondsToSeconds(enactmentDate.getTime());
|
||||
return {
|
||||
proposalSubmission: {
|
||||
rationale: {
|
||||
title: 'Update Asset set to fail',
|
||||
description: 'E2E fail test',
|
||||
},
|
||||
terms: {
|
||||
updateAsset: {
|
||||
assetId:
|
||||
'ebcd94151ae1f0d39a4bde3b21a9c7ae81a80ea4352fb075a92e07608d9c953d',
|
||||
changes: {
|
||||
quantum: '1',
|
||||
erc20: {
|
||||
withdrawThreshold: '10',
|
||||
lifetimeLimit: '10',
|
||||
},
|
||||
},
|
||||
},
|
||||
closingTimestamp,
|
||||
enactmentTimestamp,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createFreeFormProposalTxBody(): ProposalSubmissionBody {
|
||||
const MIN_CLOSE_SEC = 7;
|
||||
|
||||
const closingDate = addSeconds(new Date(), MIN_CLOSE_SEC);
|
||||
const closingTimestamp = millisecondsToSeconds(closingDate.getTime());
|
||||
return {
|
||||
proposalSubmission: {
|
||||
rationale: {
|
||||
title: 'Add New free form proposal with short enactment',
|
||||
description: 'E2E enactment test',
|
||||
},
|
||||
terms: {
|
||||
newFreeform: {},
|
||||
closingTimestamp,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -24,8 +24,8 @@ import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEthereumConfig } from '@vegaprotocol/web3';
|
||||
import {
|
||||
useEnvironment,
|
||||
EnvironmentProvider,
|
||||
NetworkLoader,
|
||||
useInitializeEnv,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { createConnectors } from './lib/web3-connectors';
|
||||
import { ENV } from './config/env';
|
||||
@@ -166,12 +166,12 @@ const AppContainer = () => {
|
||||
};
|
||||
|
||||
function App() {
|
||||
useInitializeEnv();
|
||||
|
||||
return (
|
||||
<EnvironmentProvider>
|
||||
<NetworkLoader cache={cache}>
|
||||
<AppContainer />
|
||||
</NetworkLoader>
|
||||
</EnvironmentProvider>
|
||||
<NetworkLoader cache={cache}>
|
||||
<AppContainer />
|
||||
</NetworkLoader>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export const NavDropDown = ({ navbarTheme }: { navbarTheme: NavbarTheme }) => {
|
||||
<AppNavLink
|
||||
name={
|
||||
<NavDropdownMenuTrigger
|
||||
className="w-auto flex items-center"
|
||||
className="w-auto flex items-center -m-3 p-3 cursor-pointer"
|
||||
data-testid="state-trigger"
|
||||
onClick={() => setOpen(!isOpen)}
|
||||
>
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import { EnvironmentProvider, Networks } from '@vegaprotocol/environment';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Nav } from './nav';
|
||||
|
||||
jest.mock('@vegaprotocol/environment', () => ({
|
||||
...jest.requireActual('@vegaprotocol/environment'),
|
||||
NetworkSwitcher: () => <div data-testid="network-switcher" />,
|
||||
useEnvironment: () => ({ VEGA_ENV: 'MAINNET' }),
|
||||
}));
|
||||
|
||||
const renderComponent = (initialEntries?: string[]) => {
|
||||
return render(
|
||||
<EnvironmentProvider
|
||||
definitions={{ VEGA_ENV: Networks.MAINNET }}
|
||||
config={{ hosts: [] }}
|
||||
>
|
||||
<MemoryRouter initialEntries={initialEntries}>
|
||||
<Nav />
|
||||
</MemoryRouter>
|
||||
</EnvironmentProvider>
|
||||
<MemoryRouter initialEntries={initialEntries}>
|
||||
<Nav />
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { act } from '@testing-library/react-hooks';
|
||||
import { act } from '@testing-library/react';
|
||||
import { usePendingBalancesStore } from './use-pending-balances-manager';
|
||||
import type { Event } from 'ethers';
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Event } from 'ethers';
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
|
||||
import create from 'zustand';
|
||||
import { create } from 'zustand';
|
||||
import type { Event } from 'ethers';
|
||||
|
||||
export type PendingTxsStore = {
|
||||
pendingBalances: Event[];
|
||||
|
||||
@@ -432,6 +432,7 @@
|
||||
"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",
|
||||
@@ -457,6 +458,7 @@
|
||||
"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,6 +137,24 @@ 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
@@ -0,0 +1,25 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -1,79 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
+17
-21
@@ -5,9 +5,10 @@ import { removePaginationWrapper } from '@vegaprotocol/react-helpers';
|
||||
import { useRewardsQuery } from '../home/__generated__/Rewards';
|
||||
import { ENV } from '../../../config';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { RewardTable } from './reward-table';
|
||||
import { EpochIndividualRewardsTable } from './epoch-individual-rewards-table';
|
||||
import { generateEpochIndividualRewardsList } from './generate-epoch-individual-rewards-list';
|
||||
|
||||
export const RewardInfo = () => {
|
||||
export const EpochIndividualRewards = () => {
|
||||
const { t } = useTranslation();
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { delegationsPagination } = ENV;
|
||||
@@ -30,13 +31,10 @@ export const RewardInfo = () => {
|
||||
return removePaginationWrapper(data.party.rewardsConnection.edges);
|
||||
}, [data]);
|
||||
|
||||
const delegations = useMemo(() => {
|
||||
if (!data?.party || !data.party.delegationsConnection?.edges?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return removePaginationWrapper(data.party.delegationsConnection.edges);
|
||||
}, [data]);
|
||||
const epochIndividualRewardSummaries = useMemo(() => {
|
||||
if (!data?.party) return [];
|
||||
return generateEpochIndividualRewardsList(rewards);
|
||||
}, [data?.party, rewards]);
|
||||
|
||||
return (
|
||||
<AsyncRenderer
|
||||
@@ -45,20 +43,18 @@ export const RewardInfo = () => {
|
||||
data={data}
|
||||
render={() => (
|
||||
<div>
|
||||
<p>
|
||||
{t('Connected Vega key')}: {pubKey}
|
||||
<p className="mb-10">
|
||||
{t('Connected Vega key')}:{' '}
|
||||
<span className="text-white">{pubKey}</span>
|
||||
</p>
|
||||
{rewards.length ? (
|
||||
rewards.map((reward, i) => {
|
||||
if (!reward) return null;
|
||||
return (
|
||||
<RewardTable
|
||||
key={i}
|
||||
reward={reward}
|
||||
delegations={delegations || []}
|
||||
{epochIndividualRewardSummaries.length ? (
|
||||
epochIndividualRewardSummaries.map(
|
||||
(epochIndividualRewardSummary) => (
|
||||
<EpochIndividualRewardsTable
|
||||
data={epochIndividualRewardSummary}
|
||||
/>
|
||||
);
|
||||
})
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<p>{t('noRewards')}</p>
|
||||
)}
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
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',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
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)
|
||||
);
|
||||
};
|
||||
+38
-7
@@ -1,4 +1,5 @@
|
||||
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';
|
||||
|
||||
@@ -10,10 +11,30 @@ 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',
|
||||
},
|
||||
@@ -22,15 +43,25 @@ const mockData = {
|
||||
|
||||
describe('EpochTotalRewardsTable', () => {
|
||||
it('should render correctly', () => {
|
||||
const { getByTestId } = render(<EpochTotalRewardsTable data={mockData} />);
|
||||
const { getByTestId } = render(
|
||||
<AppStateProvider>
|
||||
<EpochTotalRewardsTable data={mockData} />
|
||||
</AppStateProvider>
|
||||
);
|
||||
expect(getByTestId('epoch-total-rewards-table')).toBeInTheDocument();
|
||||
expect(getByTestId('asset')).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('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('total')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
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';
|
||||
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';
|
||||
|
||||
interface EpochTotalRewardsGridProps {
|
||||
data: AggregatedEpochSummary;
|
||||
}
|
||||
|
||||
interface ColumnHeaderProps {
|
||||
title: string;
|
||||
tooltipContent?: string;
|
||||
className?: string;
|
||||
data: EpochTotalSummary;
|
||||
}
|
||||
|
||||
interface RewardItemProps {
|
||||
@@ -22,61 +17,28 @@ interface RewardItemProps {
|
||||
last?: boolean;
|
||||
}
|
||||
|
||||
const displayReward = (reward: string) => {
|
||||
const DisplayReward = ({ reward }: { reward: string }) => {
|
||||
const {
|
||||
appState: { decimals },
|
||||
} = useAppState();
|
||||
|
||||
if (Number(reward) === 0) {
|
||||
return <span className="text-vega-dark-300">0</span>;
|
||||
return <span className="text-vega-dark-300">-</span>;
|
||||
}
|
||||
|
||||
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>;
|
||||
return (
|
||||
<Tooltip description={formatNumber(toBigNum(reward, decimals), decimals)}>
|
||||
<button>{formatNumber(toBigNum(reward, decimals))}</button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
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(value)}</div>
|
||||
<div className="overflow-auto p-5">
|
||||
<DisplayReward reward={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>
|
||||
);
|
||||
@@ -84,129 +46,19 @@ 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 (
|
||||
<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}
|
||||
/>
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{rewards.map(({ rewardType, amount }, i) => (
|
||||
<RewardItem key={i} dataTestId={rewardType} value={amount} />
|
||||
))}
|
||||
<RewardItem dataTestId="total" value={totalAmount} last={true} />
|
||||
</div>
|
||||
))}
|
||||
</RewardsTable>
|
||||
);
|
||||
};
|
||||
|
||||
+5
-5
@@ -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 EpochRewards = () => {
|
||||
export const EpochTotalRewards = () => {
|
||||
const { data, loading, error, refetch } = useEpochAssetsRewardsQuery({
|
||||
variables: {
|
||||
epochRewardSummariesPagination: {
|
||||
@@ -15,7 +15,7 @@ export const EpochRewards = () => {
|
||||
});
|
||||
useRefreshAfterEpoch(data?.epoch.timestamps.expiry, refetch);
|
||||
|
||||
const epochRewardSummaries = generateEpochTotalRewardsList(data) || [];
|
||||
const epochTotalRewardSummaries = generateEpochTotalRewardsList(data) || [];
|
||||
|
||||
return (
|
||||
<AsyncRenderer
|
||||
@@ -27,12 +27,12 @@ export const EpochRewards = () => {
|
||||
className="max-w-full overflow-auto"
|
||||
data-testid="epoch-rewards-total"
|
||||
>
|
||||
{epochRewardSummaries.length === 0 ? (
|
||||
{epochTotalRewardSummaries.length === 0 ? (
|
||||
<NoRewards />
|
||||
) : (
|
||||
<>
|
||||
{epochRewardSummaries.map((aggregatedEpochSummary) => (
|
||||
<EpochTotalRewardsTable data={aggregatedEpochSummary} />
|
||||
{epochTotalRewardSummaries.map((epochTotalSummary, index) => (
|
||||
<EpochTotalRewardsTable data={epochTotalSummary} key={index} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
+58
-57
@@ -61,7 +61,7 @@ describe('generateEpochAssetRewardsList', () => {
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an array of unnamed assets if no assets are provided (should not happen)', () => {
|
||||
it('should return an array of unnamed assets if no asset names are provided (should not happen)', () => {
|
||||
const epochData = {
|
||||
assetsConnection: {
|
||||
edges: [],
|
||||
@@ -72,18 +72,10 @@ describe('generateEpochAssetRewardsList', () => {
|
||||
node: {
|
||||
epoch: 1,
|
||||
assetId: '1',
|
||||
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE,
|
||||
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
|
||||
amount: '123',
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
epoch: 2,
|
||||
assetId: '1',
|
||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||
amount: '5',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
epoch: {
|
||||
@@ -104,30 +96,34 @@ describe('generateEpochAssetRewardsList', () => {
|
||||
name: '',
|
||||
rewards: [
|
||||
{
|
||||
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE,
|
||||
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -155,7 +151,7 @@ describe('generateEpochAssetRewardsList', () => {
|
||||
node: {
|
||||
epoch: 1,
|
||||
assetId: '1',
|
||||
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE,
|
||||
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
|
||||
amount: '123',
|
||||
},
|
||||
},
|
||||
@@ -167,22 +163,6 @@ 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,
|
||||
@@ -211,30 +191,31 @@ describe('generateEpochAssetRewardsList', () => {
|
||||
name: 'Asset 1',
|
||||
rewards: [
|
||||
{
|
||||
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE,
|
||||
amount: '123',
|
||||
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
|
||||
amount: '0',
|
||||
},
|
||||
{
|
||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||
amount: '100',
|
||||
},
|
||||
],
|
||||
totalAmount: '223',
|
||||
},
|
||||
{
|
||||
assetId: '2',
|
||||
name: 'Asset 2',
|
||||
rewards: [
|
||||
{
|
||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||
amount: '17.9873',
|
||||
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_LIQUIDITY,
|
||||
amount: '1',
|
||||
amount: '0',
|
||||
},
|
||||
{
|
||||
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
|
||||
amount: '0',
|
||||
},
|
||||
],
|
||||
totalAmount: '18.9873',
|
||||
totalAmount: '223',
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -245,10 +226,30 @@ 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',
|
||||
},
|
||||
|
||||
+36
-11
@@ -3,6 +3,8 @@ 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;
|
||||
@@ -18,11 +20,16 @@ export interface AggregatedEpochRewardSummary {
|
||||
totalAmount: string;
|
||||
}
|
||||
|
||||
export interface AggregatedEpochSummary {
|
||||
export interface EpochTotalSummary {
|
||||
epoch: EpochRewardSummaryFieldsFragment['epoch'];
|
||||
assetRewards: AggregatedEpochRewardSummary[];
|
||||
}
|
||||
|
||||
const emptyRowAccountTypes = Object.keys(RowAccountTypes).map((type) => ({
|
||||
rewardType: type as AccountType,
|
||||
amount: '0',
|
||||
}));
|
||||
|
||||
export const generateEpochTotalRewardsList = (
|
||||
epochData: EpochAssetsRewardsQuery | undefined
|
||||
) => {
|
||||
@@ -58,7 +65,7 @@ export const generateEpochTotalRewardsList = (
|
||||
}, [] as EpochSummaryWithNamedReward[][]);
|
||||
|
||||
// Now aggregate the array of arrays of epoch summaries by asset rewards.
|
||||
const aggregatedEpochSummaries: AggregatedEpochSummary[] =
|
||||
const epochTotalRewards: EpochTotalSummary[] =
|
||||
aggregatedEpochSummariesByEpochNumber.map((epochSummaries) => {
|
||||
const assetRewards = epochSummaries.reduce((acc, epochSummary) => {
|
||||
const assetRewardIndex = acc.findIndex(
|
||||
@@ -72,18 +79,36 @@ export const generateEpochTotalRewardsList = (
|
||||
assetId: epochSummary.assetId,
|
||||
name: epochSummary.name,
|
||||
rewards: [
|
||||
{
|
||||
rewardType: epochSummary.rewardType,
|
||||
amount: epochSummary.amount,
|
||||
},
|
||||
...emptyRowAccountTypes.map((emptyRowAccountType) => {
|
||||
if (
|
||||
emptyRowAccountType.rewardType === epochSummary.rewardType
|
||||
) {
|
||||
return {
|
||||
rewardType: epochSummary.rewardType,
|
||||
amount: epochSummary.amount,
|
||||
};
|
||||
} else {
|
||||
return emptyRowAccountType;
|
||||
}
|
||||
}),
|
||||
],
|
||||
totalAmount: epochSummary.amount,
|
||||
});
|
||||
} else {
|
||||
acc[assetRewardIndex].rewards.push({
|
||||
rewardType: epochSummary.rewardType,
|
||||
amount: epochSummary.amount,
|
||||
});
|
||||
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].totalAmount = (
|
||||
Number(acc[assetRewardIndex].totalAmount) +
|
||||
Number(epochSummary.amount)
|
||||
@@ -99,5 +124,5 @@ export const generateEpochTotalRewardsList = (
|
||||
};
|
||||
});
|
||||
|
||||
return aggregatedEpochSummaries;
|
||||
return epochTotalRewards;
|
||||
};
|
||||
|
||||
@@ -1,34 +1,30 @@
|
||||
// @ts-ignore No types available for duration-js
|
||||
import Duration from 'duration-js';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { 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, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||
import { useVegaWallet } 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 { RewardInfo } from '../epoch-individual-awards/reward-info';
|
||||
import { EpochRewards } from '../epoch-total-rewards/epoch-rewards';
|
||||
import { EpochIndividualRewards } from '../epoch-individual-rewards/epoch-individual-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';
|
||||
|
||||
@@ -39,25 +35,21 @@ export const RewardsPage = () => {
|
||||
const [toggleRewardsView, setToggleRewardsView] =
|
||||
useState<RewardsView>('total');
|
||||
|
||||
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
|
||||
openVegaWalletDialog: store.openVegaWalletDialog,
|
||||
}));
|
||||
const { appDispatch } = useAppState();
|
||||
|
||||
const {
|
||||
params,
|
||||
loading: paramsLoading,
|
||||
error: paramsError,
|
||||
} = useNetworkParams([NetworkParams.reward_staking_delegation_payoutDelay]);
|
||||
|
||||
const {
|
||||
data: epochData,
|
||||
loading: epochLoading,
|
||||
error: epochError,
|
||||
refetch,
|
||||
} = useEpochQuery();
|
||||
|
||||
useRefreshAfterEpoch(epochData?.epoch.timestamps.expiry, refetch);
|
||||
|
||||
const {
|
||||
params,
|
||||
loading: paramsLoading,
|
||||
error: paramsError,
|
||||
} = useNetworkParams([NetworkParams.reward_staking_delegation_payoutDelay]);
|
||||
|
||||
const payoutDuration = useMemo(() => {
|
||||
if (!params) {
|
||||
return 0;
|
||||
@@ -103,8 +95,8 @@ export const RewardsPage = () => {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{epochData &&
|
||||
epochData.epoch.id &&
|
||||
{epochData?.epoch &&
|
||||
epochData.epoch?.id &&
|
||||
epochData.epoch.timestamps.start &&
|
||||
epochData.epoch.timestamps.expiry && (
|
||||
<section className="mb-16">
|
||||
@@ -148,26 +140,13 @@ export const RewardsPage = () => {
|
||||
</section>
|
||||
|
||||
{toggleRewardsView === 'total' ? (
|
||||
<EpochRewards />
|
||||
<EpochTotalRewards />
|
||||
) : (
|
||||
<section>
|
||||
{pubKey && pubKeys?.length ? (
|
||||
<RewardInfo />
|
||||
<EpochIndividualRewards />
|
||||
) : (
|
||||
<div>
|
||||
<Button
|
||||
data-testid="connect-to-vega-wallet-btn"
|
||||
onClick={() => {
|
||||
appDispatch({
|
||||
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY,
|
||||
isOpen: true,
|
||||
});
|
||||
openVegaWalletDialog();
|
||||
}}
|
||||
>
|
||||
{t('connectVegaWallet')}
|
||||
</Button>
|
||||
</div>
|
||||
<ConnectToSeeRewards />
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
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>
|
||||
);
|
||||
+111
-89
@@ -50,101 +50,117 @@ export const StandbyPendingValidatorsTable = ({
|
||||
const nodes = useMemo(() => {
|
||||
if (!data) return [];
|
||||
|
||||
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 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 {
|
||||
...node,
|
||||
votingPowerRanking,
|
||||
};
|
||||
})
|
||||
.map(
|
||||
({
|
||||
id,
|
||||
[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(
|
||||
name,
|
||||
avatarUrl,
|
||||
stakedByDelegates,
|
||||
stakedByOperator,
|
||||
stakedTotal,
|
||||
rankingScore: { stakeScore },
|
||||
pendingStake,
|
||||
votingPowerRanking,
|
||||
}) => {
|
||||
const { rawValidatorScore, performanceScore } =
|
||||
getLastEpochScoreAndPerformance(previousEpochData, id);
|
||||
|
||||
const overstakedAmount = getOverstakedAmount(
|
||||
rawValidatorScore,
|
||||
performanceScore,
|
||||
stakedTotal,
|
||||
totalStake
|
||||
),
|
||||
[ValidatorFields.PENDING_STAKE]: formatNumber(
|
||||
toBigNum(pendingStake, decimals),
|
||||
2
|
||||
),
|
||||
};
|
||||
}
|
||||
);
|
||||
);
|
||||
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
|
||||
),
|
||||
};
|
||||
}
|
||||
);
|
||||
}, [
|
||||
data,
|
||||
decimals,
|
||||
@@ -158,6 +174,12 @@ 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(),
|
||||
|
||||
@@ -59,11 +59,17 @@ export const getOverstakedAmount = (
|
||||
export const getOverstakingPenalty = (
|
||||
overstakedAmount: BigNumber,
|
||||
stakedOnNode: string
|
||||
) =>
|
||||
formatNumberPercentage(
|
||||
) => {
|
||||
// avoid division by zero
|
||||
if (new BigNumber(stakedOnNode).isZero() || overstakedAmount.isZero()) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
return formatNumberPercentage(
|
||||
overstakedAmount.dividedBy(new BigNumber(stakedOnNode)).times(100),
|
||||
2
|
||||
);
|
||||
};
|
||||
|
||||
export const getTotalPenalties = (
|
||||
rawValidatorScore: string | null | undefined,
|
||||
|
||||
@@ -25,12 +25,38 @@ 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,
|
||||
@@ -42,6 +68,34 @@ 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: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -17,6 +17,7 @@ const itemValue = 'item-value';
|
||||
|
||||
describe('Market proposal notification', { tags: '@smoke' }, () => {
|
||||
before(() => {
|
||||
cy.setVegaWallet();
|
||||
cy.mockTradingPage(
|
||||
Schema.MarketState.STATE_ACTIVE,
|
||||
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
|
||||
@@ -239,7 +240,6 @@ describe('market states not accepting orders', { tags: '@smoke' }, function () {
|
||||
cy.visit('/#/markets/market-0');
|
||||
});
|
||||
it('must display that market is not accepting orders', function () {
|
||||
cy.getByTestId('place-order').click();
|
||||
cy.getByTestId('dealticket-error-message-summary').should(
|
||||
'have.text',
|
||||
`This market is ${marketState
|
||||
|
||||
@@ -3,6 +3,8 @@ 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(() => {
|
||||
@@ -145,8 +147,9 @@ describe('markets table', { tags: '@smoke' }, () => {
|
||||
proposal: { terms: { enactmentDatetime: '2023-01-31 12:00:01' } },
|
||||
});
|
||||
});
|
||||
cy.visit('/');
|
||||
cy.visit('#/markets/market-0');
|
||||
cy.url().should('contain', 'market-0');
|
||||
cy.getByTestId(dialogCloseBtn).click();
|
||||
cy.getByTestId('item-value').contains('Opening auction').realHover();
|
||||
cy.getByTestId('opening-auction-sub-status').should(
|
||||
'contain.text',
|
||||
@@ -155,8 +158,7 @@ describe('markets table', { tags: '@smoke' }, () => {
|
||||
|
||||
const now = new Date(Date.parse('2023-01-30 12:00:01')).getTime();
|
||||
cy.clock(now, ['Date']); // Set "now" to BEFORE reservation
|
||||
cy.visit('/');
|
||||
cy.visit('#/markets/market-0');
|
||||
cy.reload();
|
||||
cy.getByTestId('item-value').contains('Opening auction').realHover();
|
||||
cy.getByTestId('opening-auction-sub-status').should(
|
||||
'contain.text',
|
||||
@@ -171,7 +173,8 @@ describe('markets table', { tags: '@smoke' }, () => {
|
||||
});
|
||||
|
||||
function openMarketDropDown() {
|
||||
cy.getByTestId('dialog-close').click();
|
||||
cy.getByTestId(dialogCloseBtn).should('be.visible');
|
||||
cy.getByTestId(dialogCloseBtn).click();
|
||||
cy.getByTestId('popover-trigger').click();
|
||||
cy.contains('Loading market data...').should('not.exist');
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mockConnectWallet } from '@vegaprotocol/cypress';
|
||||
|
||||
before(() => {
|
||||
beforeEach(() => {
|
||||
cy.mockTradingPage();
|
||||
cy.mockSubscription();
|
||||
cy.visit('/');
|
||||
@@ -19,6 +19,7 @@ describe('Desktop view', { tags: '@smoke' }, () => {
|
||||
cy.getByTestId('navbar')
|
||||
.find(`[data-testid="navbar-links"] a[data-testid=${link}]`)
|
||||
.then((element) => {
|
||||
cy.contains('Loading...').should('not.exist');
|
||||
cy.wrap(element).click();
|
||||
cy.wrap(element)
|
||||
.get('span.absolute.md\\:h-1.w-full')
|
||||
|
||||
@@ -13,7 +13,6 @@ const toggleShort = 'order-side-SIDE_SELL';
|
||||
const toggleLong = 'order-side-SIDE_BUY';
|
||||
const toggleLimit = 'order-type-TYPE_LIMIT';
|
||||
const toggleMarket = 'order-type-TYPE_MARKET';
|
||||
const errorMessage = 'dealticket-error-message';
|
||||
|
||||
const TIFlist = Object.values(Schema.OrderTimeInForce).map((value) => {
|
||||
return {
|
||||
@@ -363,11 +362,11 @@ describe('deal ticket validation', { tags: '@smoke' }, () => {
|
||||
cy.wait('@Markets');
|
||||
});
|
||||
|
||||
it('must not place an order if wallet is not connected', () => {
|
||||
it('must show place order button and connect wallet if wallet is not connected', () => {
|
||||
cy.getByTestId('connect-vega-wallet'); // Not connected
|
||||
cy.getByTestId('order-connect-wallet').should('exist');
|
||||
cy.getByTestId(placeOrderBtn).should('not.exist');
|
||||
cy.getByTestId(errorMessage).should('not.exist');
|
||||
cy.getByTestId(placeOrderBtn).should('exist');
|
||||
cy.getByTestId('deal-ticket-connect-wallet').should('exist');
|
||||
});
|
||||
|
||||
it('must be able to select order direction - long/short', function () {
|
||||
@@ -664,13 +663,13 @@ describe('account validation', { tags: '@regression' }, () => {
|
||||
});
|
||||
|
||||
it('should show an error if your balance is zero', () => {
|
||||
cy.getByTestId('place-order').should('not.be.disabled');
|
||||
cy.getByTestId('place-order').click();
|
||||
cy.getByTestId('place-order').should('be.disabled');
|
||||
//7002-SORD-003
|
||||
cy.getByTestId('dealticket-error-message-zero-balance').should(
|
||||
'have.text',
|
||||
'Insufficient balance. Deposit ' + 'tDAI'
|
||||
'You need ' +
|
||||
'tDAI' +
|
||||
' in your wallet to trade in this market. See all your collateral.Make a deposit'
|
||||
);
|
||||
cy.getByTestId('deal-ticket-deposit-dialog-button').should('exist');
|
||||
});
|
||||
@@ -708,7 +707,7 @@ describe('account validation', { tags: '@regression' }, () => {
|
||||
);
|
||||
cy.getByTestId('dealticket-warning-margin').should(
|
||||
'contain.text',
|
||||
'9,999.99 tDAI currently required, 1,000.00 tDAI available'
|
||||
'9,999.99 tDAI is currently required. You have only 1,000.00 tDAI available.Deposit tDAI'
|
||||
);
|
||||
cy.getByTestId('deal-ticket-deposit-dialog-button').click();
|
||||
cy.getByTestId('dialog-content')
|
||||
|
||||
@@ -96,10 +96,24 @@ export const MarketPage = () => {
|
||||
|
||||
const tradeView = useMemo(() => {
|
||||
if (w > 960) {
|
||||
return <TradeGrid market={data} onSelect={onSelect} />;
|
||||
return (
|
||||
<TradeGrid
|
||||
market={data}
|
||||
onSelect={onSelect}
|
||||
pinnedAsset={
|
||||
data?.tradableInstrument.instrument.product.settlementAsset
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <TradePanels market={data} onSelect={onSelect} />;
|
||||
}, [w, data, onSelect]);
|
||||
return (
|
||||
<TradePanels
|
||||
market={data}
|
||||
onSelect={onSelect}
|
||||
onClickCollateral={() => navigate('/portfolio')}
|
||||
/>
|
||||
);
|
||||
}, [w, data, onSelect, navigate]);
|
||||
if (!data && marketId) {
|
||||
return (
|
||||
<Splash>
|
||||
|
||||
@@ -28,6 +28,7 @@ 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
|
||||
@@ -65,14 +66,17 @@ 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) => {
|
||||
@@ -111,7 +115,10 @@ const MainGrid = ({
|
||||
<TradeGridChild>
|
||||
<Tabs>
|
||||
<Tab id="ticket" name={t('Ticket')}>
|
||||
<TradingViews.Ticket marketId={marketId} />
|
||||
<TradingViews.Ticket
|
||||
marketId={marketId}
|
||||
onClickCollateral={() => navigate('/portfolio')}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab id="info" name={t('Info')}>
|
||||
<TradingViews.Info
|
||||
@@ -172,7 +179,7 @@ const MainGrid = ({
|
||||
</Tab>
|
||||
<Tab id="accounts" name={t('Collateral')}>
|
||||
<VegaWalletContainer>
|
||||
<TradingViews.Collateral />
|
||||
<TradingViews.Collateral pinnedAsset={pinnedAsset} />
|
||||
</VegaWalletContainer>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
@@ -183,11 +190,19 @@ const MainGrid = ({
|
||||
};
|
||||
const MainGridWrapped = memo(MainGrid);
|
||||
|
||||
export const TradeGrid = ({ market, onSelect }: TradeGridProps) => {
|
||||
export const TradeGrid = ({
|
||||
market,
|
||||
onSelect,
|
||||
pinnedAsset,
|
||||
}: TradeGridProps) => {
|
||||
return (
|
||||
<div className="h-full grid grid-rows-[min-content_1fr]">
|
||||
<TradeMarketHeader market={market} onSelect={onSelect} />
|
||||
<MainGridWrapped marketId={market?.id || ''} onSelect={onSelect} />
|
||||
<MainGridWrapped
|
||||
marketId={market?.id || ''}
|
||||
onSelect={onSelect}
|
||||
pinnedAsset={pinnedAsset}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -210,15 +225,24 @@ interface TradePanelsProps {
|
||||
market: Market | null;
|
||||
onSelect: (marketId: string) => void;
|
||||
onMarketClick?: (marketId: string) => void;
|
||||
onClickCollateral: () => void;
|
||||
pinnedAsset?: PinnedAsset;
|
||||
}
|
||||
|
||||
export const TradePanels = ({ market, onSelect }: TradePanelsProps) => {
|
||||
export const TradePanels = ({
|
||||
market,
|
||||
onSelect,
|
||||
onClickCollateral,
|
||||
pinnedAsset,
|
||||
}: TradePanelsProps) => {
|
||||
const [view, setView] = useState<TradingView>('Candles');
|
||||
const renderView = () => {
|
||||
const Component = memo<{
|
||||
marketId: string;
|
||||
onSelect: (marketId: string) => void;
|
||||
onMarketClick?: (marketId: string) => void;
|
||||
onClickCollateral: () => void;
|
||||
pinnedAsset?: PinnedAsset;
|
||||
}>(TradingViews[view]);
|
||||
|
||||
if (!Component) {
|
||||
@@ -227,7 +251,14 @@ export const TradePanels = ({ market, onSelect }: TradePanelsProps) => {
|
||||
|
||||
if (!market) return <Splash>{NO_MARKET}</Splash>;
|
||||
|
||||
return <Component marketId={market?.id} onSelect={onSelect} />;
|
||||
return (
|
||||
<Component
|
||||
marketId={market?.id}
|
||||
onSelect={onSelect}
|
||||
onClickCollateral={onClickCollateral}
|
||||
pinnedAsset={pinnedAsset}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
|
||||
export const DepositsContainer = () => {
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
const { data, loading, error } = useDataProvider({
|
||||
const { data, loading, error, reload } = useDataProvider({
|
||||
dataProvider: depositsProvider,
|
||||
variables: { partyId: pubKey || '' },
|
||||
skip: !pubKey,
|
||||
@@ -27,6 +27,7 @@ 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 } = useDataProvider({
|
||||
const { data, loading, error, reload } = useDataProvider({
|
||||
dataProvider: withdrawalProvider,
|
||||
variables: { partyId: pubKey || '' },
|
||||
skip: !pubKey,
|
||||
@@ -33,6 +33,7 @@ export const WithdrawalsContainer = () => {
|
||||
error={error}
|
||||
noDataCondition={(data) => !(data && data.length)}
|
||||
noDataMessage={t('No withdrawals')}
|
||||
reload={reload}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,10 +5,15 @@ 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 = () => {
|
||||
export const AccountsContainer = ({
|
||||
pinnedAsset,
|
||||
}: {
|
||||
pinnedAsset?: PinnedAsset;
|
||||
}) => {
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
const openWithdrawalDialog = useWithdrawalDialog((store) => store.open);
|
||||
@@ -39,6 +44,7 @@ export const AccountsContainer = () => {
|
||||
onClickWithdraw={openWithdrawalDialog}
|
||||
onClickDeposit={openDepositDialog}
|
||||
isReadOnly={isReadOnly}
|
||||
pinnedAsset={pinnedAsset}
|
||||
/>
|
||||
</div>
|
||||
{!isReadOnly && (
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { useGlobalStore } from '../../stores';
|
||||
|
||||
export const AppFailure = ({
|
||||
title,
|
||||
error,
|
||||
}: {
|
||||
title: string;
|
||||
error?: string | null;
|
||||
}) => {
|
||||
const { setNodeSwitcher } = useGlobalStore((store) => ({
|
||||
nodeSwitcherOpen: store.nodeSwitcherDialog,
|
||||
setNodeSwitcher: (open: boolean) =>
|
||||
store.update({ nodeSwitcherDialog: open }),
|
||||
}));
|
||||
const nonIdealWrapperClasses =
|
||||
'h-full min-h-screen flex items-center justify-center';
|
||||
return (
|
||||
<div className={nonIdealWrapperClasses}>
|
||||
<div className="text-center">
|
||||
<h1 className="text-xl mb-4">{title}</h1>
|
||||
{error && <p className="text-sm mb-8">{error}</p>}
|
||||
<Button onClick={() => setNodeSwitcher(true)}>
|
||||
{t('Change node')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,96 @@
|
||||
import type { InMemoryCacheConfig } from '@apollo/client';
|
||||
import {
|
||||
NetworkLoader,
|
||||
NodeGuard,
|
||||
useEnvironment,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { MaintenancePage } from '@vegaprotocol/ui-toolkit';
|
||||
import { VegaWalletProvider } from '@vegaprotocol/wallet';
|
||||
import dynamic from 'next/dynamic';
|
||||
import type { ReactNode } from 'react';
|
||||
import { AppFailure } from './app-failure';
|
||||
import { Web3Provider } from './web3-provider';
|
||||
|
||||
const DynamicLoader = dynamic(() => import('../preloader/preloader'), {
|
||||
loading: () => <>Loading...</>,
|
||||
});
|
||||
|
||||
export const AppLoader = ({ children }: { children: ReactNode }) => {
|
||||
const { error, VEGA_URL, MAINTENANCE_PAGE } = useEnvironment((store) => ({
|
||||
error: store.error,
|
||||
VEGA_URL: store.VEGA_URL,
|
||||
MAINTENANCE_PAGE: store.MAINTENANCE_PAGE,
|
||||
}));
|
||||
|
||||
if (MAINTENANCE_PAGE) {
|
||||
return <MaintenancePage />;
|
||||
}
|
||||
|
||||
return (
|
||||
<NetworkLoader
|
||||
cache={cacheConfig}
|
||||
skeleton={<DynamicLoader />}
|
||||
failure={
|
||||
<AppFailure title={t('Could not initialize app')} error={error} />
|
||||
}
|
||||
>
|
||||
<NodeGuard
|
||||
skeleton={<DynamicLoader />}
|
||||
failure={<AppFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
|
||||
>
|
||||
<Web3Provider>
|
||||
<VegaWalletProvider>{children}</VegaWalletProvider>
|
||||
</Web3Provider>
|
||||
</NodeGuard>
|
||||
</NetworkLoader>
|
||||
);
|
||||
};
|
||||
|
||||
const cacheConfig: InMemoryCacheConfig = {
|
||||
typePolicies: {
|
||||
Account: {
|
||||
keyFields: false,
|
||||
fields: {
|
||||
balanceFormatted: {},
|
||||
},
|
||||
},
|
||||
Instrument: {
|
||||
keyFields: false,
|
||||
},
|
||||
TradableInstrument: {
|
||||
keyFields: ['instrument'],
|
||||
},
|
||||
Product: {
|
||||
keyFields: ['settlementAsset', ['id']],
|
||||
},
|
||||
MarketData: {
|
||||
keyFields: ['market', ['id']],
|
||||
},
|
||||
Node: {
|
||||
keyFields: false,
|
||||
},
|
||||
Withdrawal: {
|
||||
fields: {
|
||||
pendingOnForeignChain: {
|
||||
read: (isPending = false) => isPending,
|
||||
},
|
||||
},
|
||||
},
|
||||
ERC20: {
|
||||
keyFields: ['contractAddress'],
|
||||
},
|
||||
PositionUpdate: {
|
||||
keyFields: false,
|
||||
},
|
||||
AccountUpdate: {
|
||||
keyFields: false,
|
||||
},
|
||||
Party: {
|
||||
keyFields: false,
|
||||
},
|
||||
Fees: {
|
||||
keyFields: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,121 +1,3 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { NetworkLoader, useEnvironment } from '@vegaprotocol/environment';
|
||||
import type { InMemoryCacheConfig } from '@apollo/client';
|
||||
import {
|
||||
useEthereumConfig,
|
||||
createConnectors,
|
||||
Web3Provider as Web3ProviderInternal,
|
||||
useWeb3ConnectStore,
|
||||
} from '@vegaprotocol/web3';
|
||||
import { AsyncRenderer, Loader } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
interface AppLoaderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component to handle any app initialization, startup queries and other things
|
||||
* that must happen for it can be used
|
||||
*/
|
||||
export function AppLoader({ children }: AppLoaderProps) {
|
||||
return (
|
||||
<NetworkLoader skeleton={<Loader />} cache={cacheConfig}>
|
||||
{children}
|
||||
</NetworkLoader>
|
||||
);
|
||||
}
|
||||
|
||||
export const Web3Provider = ({ children }: { children: ReactNode }) => {
|
||||
const { config, loading, error } = useEthereumConfig();
|
||||
const { ETHEREUM_PROVIDER_URL, ETH_LOCAL_PROVIDER_URL, ETH_WALLET_MNEMONIC } =
|
||||
useEnvironment();
|
||||
const [connectors, initializeConnectors] = useWeb3ConnectStore((store) => [
|
||||
store.connectors,
|
||||
store.initialize,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (config?.chain_id) {
|
||||
return initializeConnectors(
|
||||
createConnectors(
|
||||
ETHEREUM_PROVIDER_URL,
|
||||
Number(config?.chain_id),
|
||||
ETH_LOCAL_PROVIDER_URL,
|
||||
ETH_WALLET_MNEMONIC
|
||||
),
|
||||
Number(config.chain_id)
|
||||
);
|
||||
}
|
||||
}, [
|
||||
config?.chain_id,
|
||||
ETHEREUM_PROVIDER_URL,
|
||||
initializeConnectors,
|
||||
ETH_LOCAL_PROVIDER_URL,
|
||||
ETH_WALLET_MNEMONIC,
|
||||
]);
|
||||
|
||||
return (
|
||||
<AsyncRenderer
|
||||
loading={loading}
|
||||
error={error}
|
||||
data={connectors}
|
||||
noDataCondition={(d) => {
|
||||
if (!d) return true;
|
||||
return d.length < 1;
|
||||
}}
|
||||
>
|
||||
<Web3ProviderInternal connectors={connectors}>
|
||||
<>{children}</>
|
||||
</Web3ProviderInternal>
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
||||
|
||||
const cacheConfig: InMemoryCacheConfig = {
|
||||
typePolicies: {
|
||||
Account: {
|
||||
keyFields: false,
|
||||
fields: {
|
||||
balanceFormatted: {},
|
||||
},
|
||||
},
|
||||
Instrument: {
|
||||
keyFields: false,
|
||||
},
|
||||
TradableInstrument: {
|
||||
keyFields: ['instrument'],
|
||||
},
|
||||
Product: {
|
||||
keyFields: ['settlementAsset', ['id']],
|
||||
},
|
||||
MarketData: {
|
||||
keyFields: ['market', ['id']],
|
||||
},
|
||||
Node: {
|
||||
keyFields: false,
|
||||
},
|
||||
Withdrawal: {
|
||||
fields: {
|
||||
pendingOnForeignChain: {
|
||||
read: (isPending = false) => isPending,
|
||||
},
|
||||
},
|
||||
},
|
||||
ERC20: {
|
||||
keyFields: ['contractAddress'],
|
||||
},
|
||||
PositionUpdate: {
|
||||
keyFields: false,
|
||||
},
|
||||
AccountUpdate: {
|
||||
keyFields: false,
|
||||
},
|
||||
Party: {
|
||||
keyFields: false,
|
||||
},
|
||||
Fees: {
|
||||
keyFields: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
export * from './app-failure';
|
||||
export * from './app-loader';
|
||||
export * from './web3-provider';
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
useEthereumConfig,
|
||||
createConnectors,
|
||||
Web3Provider as Web3ProviderInternal,
|
||||
useWeb3ConnectStore,
|
||||
} from '@vegaprotocol/web3';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const Web3Provider = ({ children }: { children: ReactNode }) => {
|
||||
const { config, loading, error } = useEthereumConfig();
|
||||
const { ETHEREUM_PROVIDER_URL, ETH_LOCAL_PROVIDER_URL, ETH_WALLET_MNEMONIC } =
|
||||
useEnvironment();
|
||||
const [connectors, initializeConnectors] = useWeb3ConnectStore((store) => [
|
||||
store.connectors,
|
||||
store.initialize,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (config?.chain_id) {
|
||||
return initializeConnectors(
|
||||
createConnectors(
|
||||
ETHEREUM_PROVIDER_URL,
|
||||
Number(config?.chain_id),
|
||||
ETH_LOCAL_PROVIDER_URL,
|
||||
ETH_WALLET_MNEMONIC
|
||||
),
|
||||
Number(config.chain_id)
|
||||
);
|
||||
}
|
||||
}, [
|
||||
config?.chain_id,
|
||||
ETHEREUM_PROVIDER_URL,
|
||||
initializeConnectors,
|
||||
ETH_LOCAL_PROVIDER_URL,
|
||||
ETH_WALLET_MNEMONIC,
|
||||
]);
|
||||
|
||||
return (
|
||||
<AsyncRenderer
|
||||
loading={loading}
|
||||
error={error}
|
||||
data={connectors}
|
||||
noDataCondition={(d) => {
|
||||
if (!d) return true;
|
||||
return d.length < 1;
|
||||
}}
|
||||
noDataMessage={t('Could not fetch Ethereum configuration')}
|
||||
>
|
||||
<Web3ProviderInternal connectors={connectors}>
|
||||
<>{children}</>
|
||||
</Web3ProviderInternal>
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
||||
@@ -1,57 +1,38 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { Footer, NodeHealth } from './footer';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { NodeUrl, NodeHealth } from './footer';
|
||||
|
||||
jest.mock('@vegaprotocol/environment');
|
||||
|
||||
describe('Footer', () => {
|
||||
describe('NodeUrl', () => {
|
||||
it('can open node switcher by clicking the node url', () => {
|
||||
const mockOpenNodeSwitcher = jest.fn();
|
||||
const node = 'n99.somenetwork.vega.xyz';
|
||||
const node = 'https://api.n99.somenetwork.vega.xyz';
|
||||
|
||||
// @ts-ignore mock env hook
|
||||
useEnvironment.mockImplementation(() => ({
|
||||
VEGA_URL: `https://api.${node}/graphql`,
|
||||
blockDifference: 0,
|
||||
setNodeSwitcherOpen: mockOpenNodeSwitcher,
|
||||
}));
|
||||
render(<NodeUrl url={node} openNodeSwitcher={mockOpenNodeSwitcher} />);
|
||||
|
||||
render(<Footer />);
|
||||
|
||||
fireEvent.click(screen.getByText(node));
|
||||
expect(mockOpenNodeSwitcher).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('can open node switcher by clicking health', () => {
|
||||
const mockOpenNodeSwitcher = jest.fn();
|
||||
const node = 'n99.somenetwork.vega.xyz';
|
||||
|
||||
// @ts-ignore mock env hook
|
||||
useEnvironment.mockImplementation(() => ({
|
||||
VEGA_URL: `https://api.${node}/graphql`,
|
||||
blockDifference: 0,
|
||||
setNodeSwitcherOpen: mockOpenNodeSwitcher,
|
||||
}));
|
||||
|
||||
render(<Footer />);
|
||||
|
||||
fireEvent.click(screen.getByText('Operational'));
|
||||
fireEvent.click(screen.getByText(/n99/));
|
||||
expect(mockOpenNodeSwitcher).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('NodeHealth', () => {
|
||||
const mockOpenNodeSwitcher = jest.fn();
|
||||
const cases = [
|
||||
{ diff: 0, classname: 'bg-success', text: 'Operational' },
|
||||
{ diff: 0, classname: 'bg-vega-green-550', text: 'Operational' },
|
||||
{ diff: 5, classname: 'bg-warning', text: '5 Blocks behind' },
|
||||
{ diff: -1, classname: 'bg-danger', text: 'Non operational' },
|
||||
{ diff: null, classname: 'bg-danger', text: 'Non operational' },
|
||||
];
|
||||
it.each(cases)(
|
||||
'renders correct text and indicator color for $diff block difference',
|
||||
(elem) => {
|
||||
render(<NodeHealth blockDiff={elem.diff} openNodeSwitcher={jest.fn()} />);
|
||||
render(
|
||||
<NodeHealth
|
||||
blockDiff={elem.diff}
|
||||
openNodeSwitcher={mockOpenNodeSwitcher}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('indicator')).toHaveClass(elem.classname);
|
||||
expect(screen.getByText(elem.text)).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText(elem.text));
|
||||
expect(mockOpenNodeSwitcher).toHaveBeenCalled();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { useEnvironment, useNodeHealth } from '@vegaprotocol/environment';
|
||||
import { t, useNavigatorOnline } from '@vegaprotocol/react-helpers';
|
||||
import { ButtonLink, Indicator, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { useGlobalStore } from '../../stores';
|
||||
|
||||
export const Footer = () => {
|
||||
const { VEGA_URL, blockDifference, setNodeSwitcherOpen } = useEnvironment();
|
||||
const { VEGA_URL } = useEnvironment();
|
||||
const setNodeSwitcher = useGlobalStore(
|
||||
(store) => (open: boolean) => store.update({ nodeSwitcherDialog: open })
|
||||
);
|
||||
const { blockDiff } = useNodeHealth();
|
||||
|
||||
return (
|
||||
<footer className="px-4 py-1 text-xs border-t border-default">
|
||||
<div className="flex justify-between">
|
||||
@@ -11,11 +17,14 @@ export const Footer = () => {
|
||||
{VEGA_URL && (
|
||||
<>
|
||||
<NodeHealth
|
||||
blockDiff={blockDifference}
|
||||
openNodeSwitcher={setNodeSwitcherOpen}
|
||||
blockDiff={blockDiff}
|
||||
openNodeSwitcher={() => setNodeSwitcher(true)}
|
||||
/>
|
||||
{' | '}
|
||||
<NodeUrl url={VEGA_URL} openNodeSwitcher={setNodeSwitcherOpen} />
|
||||
<NodeUrl
|
||||
url={VEGA_URL}
|
||||
openNodeSwitcher={() => setNodeSwitcher(true)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -29,7 +38,7 @@ interface NodeUrlProps {
|
||||
openNodeSwitcher: () => void;
|
||||
}
|
||||
|
||||
const NodeUrl = ({ url, openNodeSwitcher }: NodeUrlProps) => {
|
||||
export const NodeUrl = ({ url, openNodeSwitcher }: NodeUrlProps) => {
|
||||
// get base url from api url, api sub domain
|
||||
const urlObj = new URL(url);
|
||||
const nodeUrl = urlObj.origin.replace(/^[^.]+\./g, '');
|
||||
@@ -38,7 +47,7 @@ const NodeUrl = ({ url, openNodeSwitcher }: NodeUrlProps) => {
|
||||
|
||||
interface NodeHealthProps {
|
||||
openNodeSwitcher: () => void;
|
||||
blockDiff: number;
|
||||
blockDiff: number | null;
|
||||
}
|
||||
|
||||
// How many blocks behind the most advanced block that is
|
||||
@@ -57,7 +66,7 @@ export const NodeHealth = ({
|
||||
if (!online) {
|
||||
text = t('Offline');
|
||||
intent = Intent.Danger;
|
||||
} else if (blockDiff < 0) {
|
||||
} else if (blockDiff === null) {
|
||||
// Block height query failed and null was returned
|
||||
text = t('Non operational');
|
||||
intent = Intent.Danger;
|
||||
@@ -67,9 +76,9 @@ export const NodeHealth = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<>
|
||||
<Indicator variant={intent} />
|
||||
<ButtonLink onClick={openNodeSwitcher}>{text}</ButtonLink>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { t, useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { proposalsListDataProvider } from '@vegaprotocol/governance';
|
||||
import { proposalsDataProvider } 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: proposalsListDataProvider,
|
||||
dataProvider: proposalsDataProvider,
|
||||
variables,
|
||||
skipUpdates: true,
|
||||
});
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { Networks, EnvironmentProvider } from '@vegaprotocol/environment';
|
||||
import { Networks, useEnvironment } from '@vegaprotocol/environment';
|
||||
import { RiskNoticeDialog } from './risk-notice-dialog';
|
||||
import { WelcomeDialog } from './welcome-dialog';
|
||||
|
||||
jest.mock('@vegaprotocol/environment');
|
||||
|
||||
const mockEnvDefinitions = {
|
||||
VEGA_CONFIG_URL: 'https://config.url',
|
||||
VEGA_URL: 'https://test.url',
|
||||
@@ -28,15 +30,16 @@ describe('Risk notice dialog', () => {
|
||||
`(
|
||||
'$assertion the risk notice on $network',
|
||||
async ({ assertion, network }) => {
|
||||
// @ts-ignore ignore mock implementation
|
||||
useEnvironment.mockImplementation(() => ({
|
||||
...mockEnvDefinitions,
|
||||
VEGA_ENV: network,
|
||||
}));
|
||||
|
||||
render(
|
||||
<EnvironmentProvider
|
||||
definitions={{ ...mockEnvDefinitions, VEGA_ENV: network }}
|
||||
config={{ hosts: [] }}
|
||||
>
|
||||
<MockedProvider>
|
||||
<WelcomeDialog />
|
||||
</MockedProvider>
|
||||
</EnvironmentProvider>,
|
||||
<MockedProvider>
|
||||
<WelcomeDialog />
|
||||
</MockedProvider>,
|
||||
{ wrapper: BrowserRouter }
|
||||
);
|
||||
|
||||
@@ -51,14 +54,13 @@ describe('Risk notice dialog', () => {
|
||||
);
|
||||
|
||||
it("doesn't display the risk notice when previously acknowledged", () => {
|
||||
render(
|
||||
<EnvironmentProvider
|
||||
definitions={{ ...mockEnvDefinitions, VEGA_ENV: Networks.MAINNET }}
|
||||
config={{ hosts: [] }}
|
||||
>
|
||||
<RiskNoticeDialog onClose={mockOnClose} />
|
||||
</EnvironmentProvider>
|
||||
);
|
||||
// @ts-ignore ignore mock implementation
|
||||
useEnvironment.mockImplementation(() => ({
|
||||
...mockEnvDefinitions,
|
||||
VEGA_ENV: Networks.MAINNET,
|
||||
}));
|
||||
|
||||
render(<RiskNoticeDialog onClose={mockOnClose} />);
|
||||
|
||||
expect(screen.queryByText(introText)).toBeInTheDocument();
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import Head from 'next/head';
|
||||
import dynamic from 'next/dynamic';
|
||||
import type { AppProps } from 'next/app';
|
||||
import { Navbar } from '../components/navbar';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
useEagerConnect as useVegaEagerConnect,
|
||||
VegaWalletProvider,
|
||||
useVegaTransactionManager,
|
||||
useVegaTransactionUpdater,
|
||||
useVegaWallet,
|
||||
@@ -17,17 +15,17 @@ import {
|
||||
useEthWithdrawApprovalsManager,
|
||||
} from '@vegaprotocol/web3';
|
||||
import {
|
||||
EnvironmentProvider,
|
||||
envTriggerMapping,
|
||||
Networks,
|
||||
NodeSwitcherDialog,
|
||||
useEnvironment,
|
||||
useInitializeEnv,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { AppLoader, Web3Provider } from '../components/app-loader';
|
||||
import './styles.css';
|
||||
import './gen-styles.scss';
|
||||
import { usePageTitleStore } from '../stores';
|
||||
import { useGlobalStore, usePageTitleStore } from '../stores';
|
||||
import { Footer } from '../components/footer';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import DialogsContainer from './dialogs-container';
|
||||
import ToastsManager from './toasts-manager';
|
||||
import { HashRouter, useLocation, useSearchParams } from 'react-router-dom';
|
||||
@@ -35,6 +33,7 @@ import { Connectors } from '../lib/vega-connectors';
|
||||
import { ViewingBanner } from '../components/viewing-banner';
|
||||
import { Banner } from '../components/banner';
|
||||
import classNames from 'classnames';
|
||||
import { AppLoader } from '../components/app-loader';
|
||||
|
||||
const DEFAULT_TITLE = t('Welcome to Vega trading!');
|
||||
|
||||
@@ -84,57 +83,47 @@ function AppBody({ Component }: AppProps) {
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</Head>
|
||||
<Title />
|
||||
<VegaWalletProvider>
|
||||
<AppLoader>
|
||||
<Web3Provider>
|
||||
<div className={gridClasses}>
|
||||
<Navbar
|
||||
navbarTheme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'}
|
||||
/>
|
||||
<Banner />
|
||||
<ViewingBanner />
|
||||
<main data-testid={location.pathname}>
|
||||
<Component />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
<DialogsContainer />
|
||||
<ToastsManager />
|
||||
<TransactionsHandler />
|
||||
<MaybeConnectEagerly />
|
||||
</Web3Provider>
|
||||
</AppLoader>
|
||||
</VegaWalletProvider>
|
||||
<div className={gridClasses}>
|
||||
<Navbar
|
||||
navbarTheme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'}
|
||||
/>
|
||||
<Banner />
|
||||
<ViewingBanner />
|
||||
<main data-testid={location.pathname}>
|
||||
<Component />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
<DialogsContainer />
|
||||
<ToastsManager />
|
||||
<TransactionsHandler />
|
||||
<MaybeConnectEagerly />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const DynamicLoader = dynamic(
|
||||
() => import('../components/preloader/preloader'),
|
||||
{
|
||||
loading: () => <>Loading...</>,
|
||||
}
|
||||
);
|
||||
|
||||
function VegaTradingApp(props: AppProps) {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const status = useEnvironment((store) => store.status);
|
||||
const { nodeSwitcherOpen, setNodeSwitcher } = useGlobalStore((store) => ({
|
||||
nodeSwitcherOpen: store.nodeSwitcherDialog,
|
||||
setNodeSwitcher: (open: boolean) =>
|
||||
store.update({ nodeSwitcherDialog: open }),
|
||||
}));
|
||||
|
||||
// Hash router requires access to the document object. At compile time that doesn't exist
|
||||
// so we need to ensure client side rendering only from this point onwards in
|
||||
// the component tree
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
useInitializeEnv();
|
||||
|
||||
if (!mounted) {
|
||||
return <DynamicLoader />;
|
||||
// Prevent HashRouter from being server side rendered as it
|
||||
// relies on presence of document object
|
||||
if (status === 'default') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<HashRouter>
|
||||
<EnvironmentProvider>
|
||||
<AppLoader>
|
||||
<AppBody {...props} />
|
||||
</EnvironmentProvider>
|
||||
</AppLoader>
|
||||
<NodeSwitcherDialog open={nodeSwitcherOpen} setOpen={setNodeSwitcher} />
|
||||
</HashRouter>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { create } from 'zustand';
|
||||
import produce from 'immer';
|
||||
|
||||
interface GlobalStore {
|
||||
networkSwitcherDialog: boolean;
|
||||
nodeSwitcherDialog: boolean;
|
||||
marketId: string | null;
|
||||
update: (store: Partial<Omit<GlobalStore, 'update'>>) => void;
|
||||
shouldDisplayWelcomeDialog: boolean;
|
||||
@@ -16,7 +16,7 @@ interface PageTitleStore {
|
||||
}
|
||||
|
||||
export const useGlobalStore = create<GlobalStore>((set) => ({
|
||||
networkSwitcherDialog: false,
|
||||
nodeSwitcherDialog: false,
|
||||
marketId: LocalStorage.getItem('marketId') || null,
|
||||
shouldDisplayWelcomeDialog: false,
|
||||
shouldDisplayAnnouncementBanner: true,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user