Compare commits
79
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 | ||
|
|
a4d597d426 | ||
|
|
b51376ba0d | ||
|
|
c72b5d7912 | ||
|
|
462a5bc84b | ||
|
|
3edbc71787 | ||
|
|
ee67736ac5 | ||
|
|
8bb6b7a942 | ||
|
|
c1c0d960eb | ||
|
|
a25e2ad101 | ||
|
|
7ee8bd47c8 | ||
|
|
b72ed66bb3 | ||
|
|
589bd8d29f | ||
|
|
7f7c425e34 | ||
|
|
062ea5e968 | ||
|
|
87e187c225 | ||
|
|
c0f15d8212 | ||
|
|
499de6e836 | ||
|
|
e72d117a62 | ||
|
|
abede79a4c | ||
|
|
fa415318ff | ||
|
|
718959b920 | ||
|
|
6d0ebc3a1b |
@@ -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,10 +1,12 @@
|
||||
# App configuration variables
|
||||
NX_TENDERMINT_URL=https://n04.d.vega.xyz/tm
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://n04.d.vega.xyz/tm/websocket
|
||||
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/tm
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.be.devnet1.vega.xyz/websocket
|
||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet-network.json
|
||||
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||
NX_VEGA_ENV=DEVNET
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_BLOCK_EXPLORER=https://be.devnet1.vega.xyz/rest
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_VEGA_GOVERNANCE_URL=https://dev.token.vega.xyz
|
||||
NX_VEGA_GOVERNANCE_URL=https://dev.token.vega.xyz
|
||||
NX_VEGA_URL=https://api.devnet1.vega.xyz/graphql
|
||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet1-network.json
|
||||
|
||||
@@ -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,48 +26,30 @@ 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">The Mainnet sims are live!</span>
|
||||
<span className="pr-4">Mainnet sim 2 coming in March!</span>
|
||||
<ExternalLink href="https://fairground.wtf/">
|
||||
Come help stress test the network
|
||||
Learn more
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useExplorerDeterministicOrderQuery } from '../order-details/__generated__/Order';
|
||||
import PriceInMarket from '../price-in-market/price-in-market';
|
||||
import { sideText } from '../order-details/lib/order-labels';
|
||||
import SizeInMarket from '../size-in-market/size-in-market';
|
||||
|
||||
// Note: Edited has no style currently
|
||||
export type OrderSummaryModifier = 'cancelled' | 'edited';
|
||||
@@ -41,7 +42,8 @@ const OrderSummary = ({ id, modifier }: OrderSummaryProps) => {
|
||||
return (
|
||||
<div data-testid="order-summary" className={getClassName(modifier)}>
|
||||
<span>{sideText[order.side]}</span>
|
||||
<span>{order.size}</span> <i>@</i>
|
||||
<SizeInMarket marketId={order.market.id} size={order.size} />
|
||||
<i>@</i>
|
||||
<PriceInMarket marketId={order.market.id} price={order.price} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -112,13 +112,14 @@ describe('Order TX Summary component', () => {
|
||||
const res = renderComponent(o, [mock]);
|
||||
expect(res.queryByTestId('order-summary')).toBeInTheDocument();
|
||||
expect(res.getByText('Buy')).toBeInTheDocument();
|
||||
expect(res.getByText('10')).toBeInTheDocument();
|
||||
|
||||
// Initially renders price alone
|
||||
// Initially renders price and size unformatted
|
||||
expect(res.getByText('333')).toBeInTheDocument();
|
||||
expect(res.getByText('10')).toBeInTheDocument();
|
||||
|
||||
// After fetch renders formatted price and asset quotename
|
||||
expect(await res.findByText('3.33')).toBeInTheDocument();
|
||||
expect(await res.findByText('TEST')).toBeInTheDocument();
|
||||
expect(await res.getByText('0.10')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { components } from '../../../types/explorer';
|
||||
|
||||
import PriceInMarket from '../price-in-market/price-in-market';
|
||||
import { sideText } from '../order-details/lib/order-labels';
|
||||
import SizeInMarket from '../size-in-market/size-in-market';
|
||||
|
||||
export type OrderSummaryProps = {
|
||||
order: components['schemas']['v1OrderSubmission'];
|
||||
@@ -29,7 +30,12 @@ const OrderTxSummary = ({ order }: OrderSummaryProps) => {
|
||||
return (
|
||||
<div data-testid="order-summary">
|
||||
<span>{sideText[order.side]}</span>
|
||||
<span>{order.size}</span> <i className="text-xs">@</i>
|
||||
{order.size ? (
|
||||
<SizeInMarket size={order.size} marketId={order.marketId} />
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
<i className="text-xs">@</i>
|
||||
<PriceInMarket
|
||||
marketId={order.marketId}
|
||||
price={order.price}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -7,7 +7,7 @@ import type { DecimalSource } from './size-in-market';
|
||||
import { ExplorerMarketDocument } from '../links/market-link/__generated__/Market';
|
||||
|
||||
function renderComponent(
|
||||
price: string,
|
||||
size: string | undefined,
|
||||
marketId: string,
|
||||
mocks: MockedResponse[],
|
||||
decimalSource: DecimalSource = 'MARKET'
|
||||
@@ -17,7 +17,7 @@ function renderComponent(
|
||||
<MemoryRouter>
|
||||
<SizeInMarket
|
||||
marketId={marketId}
|
||||
size={price}
|
||||
size={size}
|
||||
decimalSource={decimalSource}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
@@ -57,6 +57,11 @@ const fullMock = {
|
||||
};
|
||||
|
||||
describe('Size in Market component', () => {
|
||||
it('Renders a dash size when there is no size', () => {
|
||||
const res = render(renderComponent(undefined, '123', []));
|
||||
expect(res.getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders the raw size when there is no market data', () => {
|
||||
const res = render(renderComponent('100', '123', []));
|
||||
expect(res.getByText('100')).toBeInTheDocument();
|
||||
|
||||
@@ -5,7 +5,7 @@ export type DecimalSource = 'MARKET';
|
||||
|
||||
export type PriceInMarketProps = {
|
||||
marketId: string;
|
||||
size: string | number;
|
||||
size?: string | number;
|
||||
decimalSource?: DecimalSource;
|
||||
};
|
||||
|
||||
@@ -22,6 +22,9 @@ const SizeInMarket = ({
|
||||
variables: { id: marketId },
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
if (!size) {
|
||||
return <span>-</span>;
|
||||
}
|
||||
|
||||
let label = size;
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import { TxDetailsDataSubmission } from './tx-data-submission';
|
||||
import { TxProposalVote } from './tx-proposal-vote';
|
||||
import { TxDetailsProtocolUpgrade } from './tx-details-protocol-upgrade';
|
||||
import { TxDetailsIssueSignatures } from './tx-issue-signatures';
|
||||
import { TxDetailsNodeAnnounce } from './tx-node-announce';
|
||||
import { TxDetailsStateVariable } from './tx-state-variable-proposal';
|
||||
|
||||
interface TxDetailsWrapperProps {
|
||||
@@ -69,6 +70,8 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
|
||||
|
||||
// These come from https://github.com/vegaprotocol/vega/blob/develop/core/txn/command.go#L72-L98
|
||||
switch (txData.type) {
|
||||
case 'Register new Node':
|
||||
return TxDetailsNodeAnnounce;
|
||||
case 'Issue Signatures':
|
||||
return TxDetailsIssueSignatures;
|
||||
case 'Submit Order':
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
||||
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||
import { TxDetailsShared } from './shared/tx-details-shared';
|
||||
import { TableRow, TableCell, TableWithTbody } from '../../table';
|
||||
|
||||
import type { components } from '../../../../types/explorer';
|
||||
import {
|
||||
EthExplorerLink,
|
||||
EthExplorerLinkTypes,
|
||||
} from '../../links/eth-explorer-link/eth-explorer-link';
|
||||
import { PartyLink } from '../../links';
|
||||
import Hash from '../../links/hash';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
type Command = components['schemas']['v1AnnounceNode'];
|
||||
|
||||
interface TxDetailsNodeAnnounceProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
pubKey: string | undefined;
|
||||
blockData: TendermintBlocksResponse | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* When a new potential validator node comes online, it announces
|
||||
* itself with this transaction.
|
||||
*
|
||||
* Design decisions:
|
||||
* - Signatures are not rendered. You can still access them via the
|
||||
* TX details. This is consistent with explorers for other chains
|
||||
* - The avatar icon is rendered as a link rather than embedding
|
||||
*/
|
||||
export const TxDetailsNodeAnnounce = ({
|
||||
txData,
|
||||
pubKey,
|
||||
blockData,
|
||||
}: TxDetailsNodeAnnounceProps) => {
|
||||
if (!txData) {
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
|
||||
const cmd: Command = txData.command.announceNode;
|
||||
|
||||
return (
|
||||
<TableWithTbody className="mb-8">
|
||||
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
|
||||
{cmd.name ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Name')}</TableCell>
|
||||
<TableCell>
|
||||
<span>{cmd.name}</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{cmd.id ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('ID')}</TableCell>
|
||||
<TableCell>
|
||||
<Hash text={cmd.id} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{cmd.chainPubKey ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Chain public key')}</TableCell>
|
||||
<TableCell>
|
||||
<Hash text={cmd.chainPubKey} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{cmd.ethereumAddress ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Ethereum Address')}</TableCell>
|
||||
<TableCell>
|
||||
<EthExplorerLink
|
||||
type={EthExplorerLinkTypes.address}
|
||||
id={cmd.ethereumAddress}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{cmd.vegaPubKey ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Vega public key')}</TableCell>
|
||||
<TableCell>
|
||||
<PartyLink id={cmd.vegaPubKey} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{cmd.avatarUrl ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Avatar URL')}</TableCell>
|
||||
<TableCell>
|
||||
<ExternalLink href={cmd.avatarUrl} rel="noreferrer noopener">
|
||||
{cmd.avatarUrl}
|
||||
</ExternalLink>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{cmd.infoUrl ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Info link')}</TableCell>
|
||||
<TableCell>
|
||||
<ExternalLink href={cmd.infoUrl} rel="noreferrer noopener">
|
||||
{cmd.infoUrl}
|
||||
</ExternalLink>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
</TableWithTbody>
|
||||
);
|
||||
};
|
||||
@@ -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')
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -7,26 +7,9 @@ const navWithdraw = '[href="/token/withdraw"]';
|
||||
const navGovernance = '[href="/proposals"]';
|
||||
const navRedeem = '[href="/token/redeem"]';
|
||||
|
||||
const tokenDetailsTable = '.token-details';
|
||||
const address = '[data-testid="token-address"]';
|
||||
const contract = '[data-testid="token-contract"]';
|
||||
const totalSupply = '[data-testid="total-supply"]';
|
||||
const circulatingSupply = '[data-testid="circulating-supply"]';
|
||||
const staked = '[data-testid="staked"]';
|
||||
const tranchesLink = '[data-testid="tranches-link"]';
|
||||
const redeemBtn = '[data-testid="check-vesting-page-btn"]';
|
||||
const getVegaWalletLink = '[data-testid="get-vega-wallet-link"]';
|
||||
const associateVegaLink =
|
||||
'[data-testid="associate-vega-tokens-link-on-homepage"]';
|
||||
const stakingBtn = '[data-testid="staking-button-on-homepage"]';
|
||||
const governanceBtn = '[data-testid="governance-button-on-homepage"]';
|
||||
|
||||
const vegaTokenAddress = Cypress.env('vegaTokenAddress');
|
||||
const vegaTokenContractAddress = Cypress.env('vegaTokenContractAddress');
|
||||
|
||||
context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
||||
before('visit token home page', function () {
|
||||
cy.visit('/token');
|
||||
cy.visit('/');
|
||||
});
|
||||
|
||||
describe('with wallets disconnected', function () {
|
||||
@@ -54,7 +37,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
||||
describe('Token dropdown', function () {
|
||||
before('click on token dropdown', function () {
|
||||
cy.get(navSection).within(() => {
|
||||
cy.getByTestId('state-trigger').click();
|
||||
cy.getByTestId('state-trigger').realClick();
|
||||
});
|
||||
});
|
||||
it('should have token dropdown', function () {
|
||||
@@ -72,80 +55,106 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('THE $VEGA TOKEN table', function () {
|
||||
it('should have TOKEN ADDRESS', function () {
|
||||
cy.get(tokenDetailsTable).within(() => {
|
||||
cy.get(address)
|
||||
.should('be.visible')
|
||||
.invoke('text')
|
||||
.should('be.equal', vegaTokenAddress);
|
||||
describe('Links and buttons', function () {
|
||||
it('should have link for proposal page', function () {
|
||||
cy.getByTestId('home-proposals').within(() => {
|
||||
cy.get('[href="/proposals"]')
|
||||
.should('exist')
|
||||
.and('have.text', 'Browse, vote, and propose');
|
||||
});
|
||||
});
|
||||
it('should have VESTING CONTRACT', function () {
|
||||
// 1004-ASSO-001
|
||||
cy.get(tokenDetailsTable).within(() => {
|
||||
cy.get(contract)
|
||||
.should('be.visible')
|
||||
.invoke('text')
|
||||
.should('be.equal', vegaTokenContractAddress);
|
||||
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')
|
||||
.should('have.attr', 'href')
|
||||
.and('contain', 'https://vega.xyz/governance');
|
||||
});
|
||||
});
|
||||
it('should have TOTAL SUPPLY', function () {
|
||||
cy.get(tokenDetailsTable).within(() => {
|
||||
cy.get(totalSupply).should('be.visible');
|
||||
it('should have link for validator page', function () {
|
||||
cy.getByTestId('home-validators').within(() => {
|
||||
cy.get('[href="/validators"]')
|
||||
.first()
|
||||
.should('exist')
|
||||
.and('have.text', 'Browse, and stake');
|
||||
});
|
||||
});
|
||||
it('should have CIRCULATING SUPPLY', function () {
|
||||
cy.get(tokenDetailsTable).within(() => {
|
||||
cy.get(circulatingSupply).should('be.visible');
|
||||
it('should have external link for validators', function () {
|
||||
cy.getByTestId('home-validators').within(() => {
|
||||
cy.getByTestId('external-link')
|
||||
.should('have.attr', 'href')
|
||||
.and(
|
||||
'contain',
|
||||
'https://community.vega.xyz/c/mainnet-validator-candidates'
|
||||
);
|
||||
});
|
||||
});
|
||||
it('should have STAKED $VEGA', function () {
|
||||
cy.get(tokenDetailsTable).within(() => {
|
||||
cy.get(staked).should('be.visible');
|
||||
it('should have information on active nodes', function () {
|
||||
cy.getByTestId('node-information')
|
||||
.first()
|
||||
.should('contain.text', '2')
|
||||
.and('contain.text', 'active nodes');
|
||||
});
|
||||
it('should have information on consensus nodes', function () {
|
||||
cy.getByTestId('node-information')
|
||||
.last()
|
||||
.should('contain.text', '2')
|
||||
.and('contain.text', 'consensus nodes');
|
||||
});
|
||||
it('should contain link to specific validators', function () {
|
||||
cy.getByTestId('validators')
|
||||
.should('have.length', '2')
|
||||
.each(($validator) => {
|
||||
cy.wrap($validator).find('a').should('have.attr', 'href');
|
||||
});
|
||||
});
|
||||
it('should have link for rewards page', function () {
|
||||
cy.getByTestId('home-rewards').within(() => {
|
||||
cy.get('[href="/rewards"]')
|
||||
.first()
|
||||
.should('exist')
|
||||
.and('have.text', 'See rewards');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('links and buttons', function () {
|
||||
it('should have TRANCHES link', function () {
|
||||
cy.get(tranchesLink)
|
||||
.should('be.visible')
|
||||
.and('have.attr', 'href')
|
||||
.and('equal', '/token/tranches');
|
||||
});
|
||||
it('should have REDEEM button', function () {
|
||||
cy.get(redeemBtn)
|
||||
.should('be.visible')
|
||||
.parent()
|
||||
.should('have.attr', 'href')
|
||||
.and('equal', '/token/redeem');
|
||||
});
|
||||
it('should have GET VEGA WALLET link', function () {
|
||||
cy.get(getVegaWalletLink)
|
||||
.should('be.visible')
|
||||
.and('have.attr', 'href')
|
||||
.and('equal', 'https://vega.xyz/wallet');
|
||||
});
|
||||
it('should have ASSOCIATE VEGA TOKENS link', function () {
|
||||
cy.get(associateVegaLink)
|
||||
.should('be.visible')
|
||||
.and('have.attr', 'href')
|
||||
.and('equal', '/token/associate');
|
||||
});
|
||||
it('should have STAKING button', function () {
|
||||
cy.get(stakingBtn)
|
||||
.should('be.visible')
|
||||
.parent()
|
||||
.should('have.attr', 'href')
|
||||
.and('equal', '/validators');
|
||||
});
|
||||
it('should have GOVERNANCE button', function () {
|
||||
cy.get(governanceBtn)
|
||||
.should('be.visible')
|
||||
.parent()
|
||||
.should('have.attr', 'href')
|
||||
.and('equal', '/proposals');
|
||||
it('should have link for withdrawal page', function () {
|
||||
cy.getByTestId('home-vega-token').within(() => {
|
||||
cy.get('[href="/token/withdraw"]')
|
||||
.first()
|
||||
.should('exist')
|
||||
.and('have.text', 'Manage tokens');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const connectToVegaBtn = '[data-testid="connect-to-vega-wallet-btn"]';
|
||||
const viewToggle = '[data-testid="epoch-reward-view-toggle-total"]';
|
||||
const warning = '[data-testid="callout"]';
|
||||
|
||||
context(
|
||||
@@ -15,7 +15,7 @@ context(
|
||||
});
|
||||
|
||||
it('should have rewards header visible', function () {
|
||||
cy.verify_page_header('Rewards');
|
||||
cy.verify_page_header('Rewards and fees');
|
||||
});
|
||||
|
||||
it('should have epoch warning', function () {
|
||||
@@ -27,10 +27,8 @@ context(
|
||||
);
|
||||
});
|
||||
|
||||
it('should have connect Vega wallet button', function () {
|
||||
cy.get(connectToVegaBtn)
|
||||
.should('be.visible')
|
||||
.and('have.text', 'Connect Vega wallet');
|
||||
it('should have toggle for seeing total vs individual rewards', function () {
|
||||
cy.get(viewToggle).should('be.visible');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
const tokenDetailsTable = '.token-details';
|
||||
const address = '[data-testid="token-address"]';
|
||||
const contract = '[data-testid="token-contract"]';
|
||||
const totalSupply = '[data-testid="total-supply"]';
|
||||
const circulatingSupply = '[data-testid="circulating-supply"]';
|
||||
const staked = '[data-testid="staked"]';
|
||||
const tranchesLink = '[data-testid="tranches-link"]';
|
||||
const redeemBtn = '[data-testid="check-vesting-page-btn"]';
|
||||
const getVegaWalletLink = '[data-testid="get-vega-wallet-link"]';
|
||||
const associateVegaLink =
|
||||
'[data-testid="associate-vega-tokens-link-on-homepage"]';
|
||||
const stakingBtn = '[data-testid="staking-button-on-homepage"]';
|
||||
const governanceBtn = '[data-testid="governance-button-on-homepage"]';
|
||||
|
||||
const vegaTokenAddress = Cypress.env('vegaTokenAddress');
|
||||
const vegaTokenContractAddress = Cypress.env('vegaTokenContractAddress');
|
||||
|
||||
context('Verify elements on Token page', { tags: '@smoke' }, function () {
|
||||
before('Visit token page', function () {
|
||||
cy.visit('/');
|
||||
cy.navigate_to('token');
|
||||
});
|
||||
describe('THE $VEGA TOKEN table', function () {
|
||||
it('should have TOKEN ADDRESS', function () {
|
||||
cy.get(tokenDetailsTable).within(() => {
|
||||
cy.get(address)
|
||||
.should('be.visible')
|
||||
.invoke('text')
|
||||
.should('be.equal', vegaTokenAddress);
|
||||
});
|
||||
});
|
||||
it('should have VESTING CONTRACT', function () {
|
||||
// 1004-ASSO-001
|
||||
cy.get(tokenDetailsTable).within(() => {
|
||||
cy.get(contract)
|
||||
.should('be.visible')
|
||||
.invoke('text')
|
||||
.should('be.equal', vegaTokenContractAddress);
|
||||
});
|
||||
});
|
||||
it('should have TOTAL SUPPLY', function () {
|
||||
cy.get(tokenDetailsTable).within(() => {
|
||||
cy.get(totalSupply).should('be.visible');
|
||||
});
|
||||
});
|
||||
it('should have CIRCULATING SUPPLY', function () {
|
||||
cy.get(tokenDetailsTable).within(() => {
|
||||
cy.get(circulatingSupply).should('be.visible');
|
||||
});
|
||||
});
|
||||
it('should have STAKED $VEGA', function () {
|
||||
cy.get(tokenDetailsTable).within(() => {
|
||||
cy.get(staked).should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('links and buttons', function () {
|
||||
it('should have TRANCHES link', function () {
|
||||
cy.get(tranchesLink)
|
||||
.should('be.visible')
|
||||
.and('have.attr', 'href')
|
||||
.and('equal', '/token/tranches');
|
||||
});
|
||||
it('should have REDEEM button', function () {
|
||||
cy.get(redeemBtn)
|
||||
.should('be.visible')
|
||||
.parent()
|
||||
.should('have.attr', 'href')
|
||||
.and('equal', '/token/redeem');
|
||||
});
|
||||
it('should have GET VEGA WALLET link', function () {
|
||||
cy.get(getVegaWalletLink)
|
||||
.should('be.visible')
|
||||
.and('have.attr', 'href')
|
||||
.and('equal', 'https://vega.xyz/wallet');
|
||||
});
|
||||
it('should have ASSOCIATE VEGA TOKENS link', function () {
|
||||
cy.get(associateVegaLink)
|
||||
.should('be.visible')
|
||||
.and('have.attr', 'href')
|
||||
.and('equal', '/token/associate');
|
||||
});
|
||||
it('should have STAKING button', function () {
|
||||
cy.get(stakingBtn)
|
||||
.should('be.visible')
|
||||
.parent()
|
||||
.should('have.attr', 'href')
|
||||
.and('equal', '/validators');
|
||||
});
|
||||
it('should have GOVERNANCE button', function () {
|
||||
cy.get(governanceBtn)
|
||||
.should('be.visible')
|
||||
.parent()
|
||||
.should('have.attr', 'href')
|
||||
.and('equal', '/proposals');
|
||||
});
|
||||
});
|
||||
});
|
||||
+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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -21,10 +21,8 @@ export function TemplateSidebar({ children, sidebar }: TemplateSidebarProps) {
|
||||
<>
|
||||
<AnnouncementBanner>
|
||||
<div className="font-alpha calt uppercase text-center text-lg text-white">
|
||||
<span className="pr-4">The Mainnet sims are live!</span>
|
||||
<ExternalLink href="https://fairground.wtf/">
|
||||
Come help stress test the network
|
||||
</ExternalLink>
|
||||
<span className="pr-4">Mainnet sim 2 coming in March!</span>
|
||||
<ExternalLink href="https://fairground.wtf/">Learn more</ExternalLink>
|
||||
</div>
|
||||
</AnnouncementBanner>
|
||||
<Nav navbarTheme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'} />
|
||||
@@ -32,7 +30,9 @@ export function TemplateSidebar({ children, sidebar }: TemplateSidebarProps) {
|
||||
<ViewingAsBanner pubKey={pubKey} disconnect={disconnect} />
|
||||
) : null}
|
||||
<div className="w-full border-b border-neutral-700 lg:grid lg:grid-rows-[1fr] lg:grid-cols-[1fr_450px]">
|
||||
<main className="col-start-1 p-4">{children}</main>
|
||||
<main className="max-w-[100vw] col-start-1 p-4 overflow-auto">
|
||||
{children}
|
||||
</main>
|
||||
<aside className="col-start-2 row-start-1 row-span-2 hidden lg:block p-4 bg-banner bg-contain border-l border-neutral-700">
|
||||
{sidebar.map((Component, i) => (
|
||||
<section className="mb-4 last:mb-0" key={i}>
|
||||
|
||||
@@ -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[];
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import type { ObservableQuery } from '@apollo/client';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const useRefreshValidators = (
|
||||
export const useRefreshAfterEpoch = (
|
||||
epochExpiry: string | undefined,
|
||||
refetch: ObservableQuery['refetch']
|
||||
) => {
|
||||
@@ -14,7 +14,7 @@
|
||||
"pageTitleProposals": "Proposals",
|
||||
"pageTitleDepositLp": "Deposit liquidity token for $VEGA rewards",
|
||||
"pageTitleWithdrawLp": "Withdraw SLP and Rewards",
|
||||
"pageTitleRewards": "Rewards",
|
||||
"pageTitleRewards": "Rewards and fees",
|
||||
"pageTitleRejectedProposals": "Rejected proposals",
|
||||
"pageTitleValidators": "Validators",
|
||||
"Vesting": "Vesting",
|
||||
@@ -197,10 +197,6 @@
|
||||
"STATE_PASSED": "Passed",
|
||||
"STATE_OPEN": "Open",
|
||||
"STATE_WAITING_FOR_NODE_VOTE": "Waiting for node vote",
|
||||
"NewMarket": "New market",
|
||||
"UpdateMarket": "Update market",
|
||||
"NewAsset": "New asset",
|
||||
"UpdateAsset": "Update asset",
|
||||
"UpdateNetworkParameter": "Network parameter",
|
||||
"NewFreeform": "Freeform",
|
||||
"tokenVotes": "Token votes",
|
||||
@@ -436,12 +432,34 @@
|
||||
"associatedWithVegaKeys": "Associated with Vega keys",
|
||||
"thisEpoch": "This Epoch",
|
||||
"nextEpoch": "Next epoch",
|
||||
"rewardsPara1": "Rewards are paid out from the treasury at the end of an epoch.",
|
||||
"rewardsPara2": "This page lists all the rewards that your Vega key has received.",
|
||||
"rewardsPara3": "This delay is set by a network parameter",
|
||||
"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",
|
||||
"noRewards": "The Vega key has not been credited any rewards since the previous network checkpoint.",
|
||||
"seeHowRewardsAreCalculated": "See how rewards are calculated",
|
||||
"rewardType": "Reward type",
|
||||
"rewardsAndFeesReceived": "Rewards and fees received",
|
||||
"ThisDoesNotIncludeFeesReceivedForMakersOrLiquidityProviders": "This does not include fees received for makers or liquidity providers",
|
||||
"totalDistributed": "TOTAL DISTRIBUTED",
|
||||
"earnedByMe": "EARNED BY ME",
|
||||
"noRewardsHaveBeenDistributedYet": "NO REWARDS HAVE BEEN DISTRIBUTED YET",
|
||||
"rewardsColAssetHeader": "ASSET",
|
||||
"rewardsColStakingHeader": "STAKING",
|
||||
"rewardsColStakingTooltip": "Staking rewards supplement infrastructure fees in the early stages of the network, rewarding validators and those who stake them for maintaining the network",
|
||||
"rewardsColInfraHeader": "INFRA FEES",
|
||||
"rewardsColInfraTooltip": "Infrastructure fees are incurred across the network during trading. They are distributed to validators according to their share of total stake on the network, and passed onto those who stake them, proportionate to their own stake after validator commission is taken",
|
||||
"rewardsColPriceTakingHeader": "PRICE TAKING",
|
||||
"rewardsColPriceTakingTooltip": "Price taking rewards are based on the proportion of the total maker fees you paid while trading, on markets where there is a funded reward",
|
||||
"rewardsColPriceMakingHeader": "PRICE MAKING",
|
||||
"rewardsColPriceMakingTooltip": "Price making rewards are based on the proportion of the total maker fees you received while trading, on markets where there is a funded reward",
|
||||
"rewardsColLiquidityProvisionHeader": "LIQUIDITY PROVISION",
|
||||
"rewardsColLiquidityProvisionTooltip": "Liquidity provision rewards are distributed based on how much you have earned in liquidity fees, funded by a liquidity reward pool for that market",
|
||||
"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",
|
||||
"shareOfReward": "Share of reward",
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
RoundedWrapper,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
import { useRefreshValidators } from '../../hooks/use-refresh-validators';
|
||||
import { useRefreshAfterEpoch } from '../../hooks/use-refresh-after-epoch';
|
||||
import { ProposalsListItem } from '../proposals/components/proposals-list-item';
|
||||
import Routes from '../routes';
|
||||
import {
|
||||
@@ -94,7 +94,10 @@ const HomeNodes = ({
|
||||
<div key={index} className="col-span-3">
|
||||
<Link to={Routes.VALIDATORS}>
|
||||
<RoundedWrapper paddingBottom={true}>
|
||||
<div className="flex flex-col items-center m-[-1rem] px-4 py-6 hover:bg-neutral-800">
|
||||
<div
|
||||
data-testid="node-information"
|
||||
className="flex flex-col items-center m-[-1rem] px-4 py-6 hover:bg-neutral-800"
|
||||
>
|
||||
<span className="text-5xl">{length}</span>
|
||||
<span className="text-sm uppercase text-neutral-400">
|
||||
{title}
|
||||
@@ -106,7 +109,7 @@ const HomeNodes = ({
|
||||
))}
|
||||
|
||||
{trimmedActiveNodes.map(({ id, avatarUrl, name }) => (
|
||||
<div key={id} className="col-span-2">
|
||||
<div key={id} data-testid="validators" className="col-span-2">
|
||||
<Link to={`${Routes.VALIDATORS}/${id}`}>
|
||||
<RoundedWrapper paddingBottom={true} border={false}>
|
||||
<div className="flex items-center justify-center m-[-1rem] p-4 bg-neutral-900 hover:bg-neutral-800">
|
||||
@@ -156,7 +159,7 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
|
||||
refetch,
|
||||
} = useNodesQuery();
|
||||
|
||||
useRefreshValidators(validatorsData?.epoch.timestamps.expiry, refetch);
|
||||
useRefreshAfterEpoch(validatorsData?.epoch.timestamps.expiry, refetch);
|
||||
|
||||
const proposals = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
+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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { removePaginationWrapper } from '@vegaprotocol/react-helpers';
|
||||
import { useRewardsQuery } from '../home/__generated__/Rewards';
|
||||
import { ENV } from '../../../config';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { EpochIndividualRewardsTable } from './epoch-individual-rewards-table';
|
||||
import { generateEpochIndividualRewardsList } from './generate-epoch-individual-rewards-list';
|
||||
|
||||
export const EpochIndividualRewards = () => {
|
||||
const { t } = useTranslation();
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { delegationsPagination } = ENV;
|
||||
|
||||
const { data, loading, error } = useRewardsQuery({
|
||||
variables: {
|
||||
partyId: pubKey || '',
|
||||
delegationsPagination: delegationsPagination
|
||||
? {
|
||||
first: Number(delegationsPagination),
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
skip: !pubKey,
|
||||
});
|
||||
|
||||
const rewards = useMemo(() => {
|
||||
if (!data?.party || !data.party.rewardsConnection?.edges?.length) return [];
|
||||
|
||||
return removePaginationWrapper(data.party.rewardsConnection.edges);
|
||||
}, [data]);
|
||||
|
||||
const epochIndividualRewardSummaries = useMemo(() => {
|
||||
if (!data?.party) return [];
|
||||
return generateEpochIndividualRewardsList(rewards);
|
||||
}, [data?.party, rewards]);
|
||||
|
||||
return (
|
||||
<AsyncRenderer
|
||||
loading={loading}
|
||||
error={error}
|
||||
data={data}
|
||||
render={() => (
|
||||
<div>
|
||||
<p className="mb-10">
|
||||
{t('Connected Vega key')}:{' '}
|
||||
<span className="text-white">{pubKey}</span>
|
||||
</p>
|
||||
{epochIndividualRewardSummaries.length ? (
|
||||
epochIndividualRewardSummaries.map(
|
||||
(epochIndividualRewardSummary) => (
|
||||
<EpochIndividualRewardsTable
|
||||
data={epochIndividualRewardSummary}
|
||||
/>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<p>{t('noRewards')}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+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)
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
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';
|
||||
|
||||
const mockData = {
|
||||
epoch: 4431,
|
||||
assetRewards: [
|
||||
{
|
||||
assetId:
|
||||
'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',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('EpochTotalRewardsTable', () => {
|
||||
it('should render correctly', () => {
|
||||
const { getByTestId } = render(
|
||||
<AppStateProvider>
|
||||
<EpochTotalRewardsTable data={mockData} />
|
||||
</AppStateProvider>
|
||||
);
|
||||
expect(getByTestId('epoch-total-rewards-table')).toBeInTheDocument();
|
||||
expect(getByTestId('asset')).toBeInTheDocument();
|
||||
expect(getByTestId('ACCOUNT_TYPE_GLOBAL_REWARD')).toBeInTheDocument();
|
||||
expect(getByTestId('ACCOUNT_TYPE_FEES_INFRASTRUCTURE')).toBeInTheDocument();
|
||||
expect(
|
||||
getByTestId('ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
getByTestId('ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES')
|
||||
).toBeInTheDocument();
|
||||
expect(getByTestId('ACCOUNT_TYPE_FEES_LIQUIDITY')).toBeInTheDocument();
|
||||
expect(
|
||||
getByTestId('ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS')
|
||||
).toBeInTheDocument();
|
||||
expect(getByTestId('total')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
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: EpochTotalSummary;
|
||||
}
|
||||
|
||||
interface RewardItemProps {
|
||||
value: string;
|
||||
dataTestId: string;
|
||||
last?: boolean;
|
||||
}
|
||||
|
||||
const DisplayReward = ({ reward }: { reward: string }) => {
|
||||
const {
|
||||
appState: { decimals },
|
||||
} = useAppState();
|
||||
|
||||
if (Number(reward) === 0) {
|
||||
return <span className="text-vega-dark-300">-</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip description={formatNumber(toBigNum(reward, decimals), decimals)}>
|
||||
<button>{formatNumber(toBigNum(reward, decimals))}</button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const RewardItem = ({ value, dataTestId, last }: RewardItemProps) => (
|
||||
<div data-testid={dataTestId} className={rowGridItemStyles(last)}>
|
||||
<div className="h-full w-5 absolute right-0 top-0 bg-gradient-to-r from-transparent to-black pointer-events-none" />
|
||||
<div className="overflow-auto p-5">
|
||||
<DisplayReward reward={value} />
|
||||
</div>
|
||||
<div className="h-full w-5 absolute left-0 top-0 bg-gradient-to-l from-transparent to-black pointer-events-none" />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const EpochTotalRewardsTable = ({
|
||||
data,
|
||||
}: EpochTotalRewardsGridProps) => {
|
||||
return (
|
||||
<RewardsTable dataTestId="epoch-total-rewards-table" epoch={data.epoch}>
|
||||
{data.assetRewards.map(({ name, rewards, totalAmount }, i) => (
|
||||
<div className="contents" key={i}>
|
||||
<div data-testid="asset" className={`${rowGridItemStyles()} p-5`}>
|
||||
{name}
|
||||
</div>
|
||||
{rewards.map(({ rewardType, amount }, i) => (
|
||||
<RewardItem key={i} dataTestId={rewardType} value={amount} />
|
||||
))}
|
||||
<RewardItem dataTestId="total" value={totalAmount} last={true} />
|
||||
</div>
|
||||
))}
|
||||
</RewardsTable>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEpochAssetsRewardsQuery } from '../home/__generated__/Rewards';
|
||||
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
|
||||
import { generateEpochTotalRewardsList } from './generate-epoch-total-rewards-list';
|
||||
import { NoRewards } from '../no-rewards';
|
||||
import { EpochTotalRewardsTable } from './epoch-total-rewards-table';
|
||||
|
||||
export const EpochTotalRewards = () => {
|
||||
const { data, loading, error, refetch } = useEpochAssetsRewardsQuery({
|
||||
variables: {
|
||||
epochRewardSummariesPagination: {
|
||||
first: 10,
|
||||
},
|
||||
},
|
||||
});
|
||||
useRefreshAfterEpoch(data?.epoch.timestamps.expiry, refetch);
|
||||
|
||||
const epochTotalRewardSummaries = generateEpochTotalRewardsList(data) || [];
|
||||
|
||||
return (
|
||||
<AsyncRenderer
|
||||
loading={loading}
|
||||
error={error}
|
||||
data={data}
|
||||
render={() => (
|
||||
<div
|
||||
className="max-w-full overflow-auto"
|
||||
data-testid="epoch-rewards-total"
|
||||
>
|
||||
{epochTotalRewardSummaries.length === 0 ? (
|
||||
<NoRewards />
|
||||
) : (
|
||||
<>
|
||||
{epochTotalRewardSummaries.map((epochTotalSummary, index) => (
|
||||
<EpochTotalRewardsTable data={epochTotalSummary} key={index} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
import { generateEpochTotalRewardsList } from './generate-epoch-total-rewards-list';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
|
||||
describe('generateEpochAssetRewardsList', () => {
|
||||
it('should return an empty array if data is undefined', () => {
|
||||
const result = generateEpochTotalRewardsList(undefined);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an empty array if empty data is provided', () => {
|
||||
const epochData = {
|
||||
assetsConnection: {
|
||||
edges: [],
|
||||
},
|
||||
epochRewardSummaries: {
|
||||
edges: [],
|
||||
},
|
||||
epoch: {
|
||||
timestamps: {
|
||||
expiry: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = generateEpochTotalRewardsList(epochData);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an empty array if no epochRewardSummaries are provided', () => {
|
||||
const epochData = {
|
||||
assetsConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: '1',
|
||||
name: 'Asset 1',
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: '2',
|
||||
name: 'Asset 2',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
epochRewardSummaries: {
|
||||
edges: [],
|
||||
},
|
||||
epoch: {
|
||||
timestamps: {
|
||||
expiry: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = generateEpochTotalRewardsList(epochData);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an array of unnamed assets if no asset names are provided (should not happen)', () => {
|
||||
const epochData = {
|
||||
assetsConnection: {
|
||||
edges: [],
|
||||
},
|
||||
epochRewardSummaries: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
epoch: 1,
|
||||
assetId: '1',
|
||||
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
|
||||
amount: '123',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
epoch: {
|
||||
timestamps: {
|
||||
expiry: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = generateEpochTotalRewardsList(epochData);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
epoch: 1,
|
||||
assetRewards: [
|
||||
{
|
||||
assetId: '1',
|
||||
name: '',
|
||||
rewards: [
|
||||
{
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return an array of aggregated epoch summaries', () => {
|
||||
const epochData = {
|
||||
assetsConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
id: '1',
|
||||
name: 'Asset 1',
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
id: '2',
|
||||
name: 'Asset 2',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
epochRewardSummaries: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
epoch: 1,
|
||||
assetId: '1',
|
||||
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
|
||||
amount: '123',
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
epoch: 1,
|
||||
assetId: '1',
|
||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||
amount: '100',
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
epoch: 2,
|
||||
assetId: '1',
|
||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||
amount: '5',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
epoch: {
|
||||
timestamps: {
|
||||
expiry: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = generateEpochTotalRewardsList(epochData);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
epoch: 1,
|
||||
assetRewards: [
|
||||
{
|
||||
assetId: '1',
|
||||
name: 'Asset 1',
|
||||
rewards: [
|
||||
{
|
||||
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
|
||||
amount: '0',
|
||||
},
|
||||
{
|
||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||
amount: '100',
|
||||
},
|
||||
{
|
||||
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: '0',
|
||||
},
|
||||
{
|
||||
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
|
||||
amount: '0',
|
||||
},
|
||||
],
|
||||
totalAmount: '223',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
epoch: 2,
|
||||
assetRewards: [
|
||||
{
|
||||
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',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
import type {
|
||||
EpochAssetsRewardsQuery,
|
||||
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;
|
||||
}
|
||||
|
||||
export interface AggregatedEpochRewardSummary {
|
||||
assetId: EpochRewardSummaryFieldsFragment['assetId'];
|
||||
name: EpochSummaryWithNamedReward['name'];
|
||||
rewards: {
|
||||
rewardType: EpochRewardSummaryFieldsFragment['rewardType'];
|
||||
amount: EpochRewardSummaryFieldsFragment['amount'];
|
||||
}[];
|
||||
totalAmount: string;
|
||||
}
|
||||
|
||||
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
|
||||
) => {
|
||||
const epochRewardSummaries = removePaginationWrapper(
|
||||
epochData?.epochRewardSummaries?.edges
|
||||
);
|
||||
|
||||
const assets = removePaginationWrapper(epochData?.assetsConnection?.edges);
|
||||
|
||||
// Because the epochRewardSummaries don't have the asset name, we need to find it in the assets list
|
||||
const epochSummariesWithNamedReward: EpochSummaryWithNamedReward[] =
|
||||
epochRewardSummaries.map((epochReward) => ({
|
||||
...epochReward,
|
||||
name:
|
||||
assets.find((asset) => asset.id === epochReward.assetId)?.name || '',
|
||||
}));
|
||||
|
||||
// Aggregating the epoch summaries by epoch number
|
||||
const aggregatedEpochSummariesByEpochNumber =
|
||||
epochSummariesWithNamedReward.reduce((acc, epochReward) => {
|
||||
const epoch = epochReward.epoch;
|
||||
const epochSummaryIndex = acc.findIndex(
|
||||
(epochSummary) => epochSummary[0].epoch === epoch
|
||||
);
|
||||
|
||||
if (epochSummaryIndex === -1) {
|
||||
acc.push([epochReward]);
|
||||
} else {
|
||||
acc[epochSummaryIndex].push(epochReward);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as EpochSummaryWithNamedReward[][]);
|
||||
|
||||
// Now aggregate the array of arrays of epoch summaries by asset rewards.
|
||||
const epochTotalRewards: EpochTotalSummary[] =
|
||||
aggregatedEpochSummariesByEpochNumber.map((epochSummaries) => {
|
||||
const assetRewards = epochSummaries.reduce((acc, epochSummary) => {
|
||||
const assetRewardIndex = acc.findIndex(
|
||||
(assetReward) =>
|
||||
assetReward.assetId === epochSummary.assetId &&
|
||||
assetReward.name === epochSummary.name
|
||||
);
|
||||
|
||||
if (assetRewardIndex === -1) {
|
||||
acc.push({
|
||||
assetId: epochSummary.assetId,
|
||||
name: epochSummary.name,
|
||||
rewards: [
|
||||
...emptyRowAccountTypes.map((emptyRowAccountType) => {
|
||||
if (
|
||||
emptyRowAccountType.rewardType === epochSummary.rewardType
|
||||
) {
|
||||
return {
|
||||
rewardType: epochSummary.rewardType,
|
||||
amount: epochSummary.amount,
|
||||
};
|
||||
} else {
|
||||
return emptyRowAccountType;
|
||||
}
|
||||
}),
|
||||
],
|
||||
totalAmount: epochSummary.amount,
|
||||
});
|
||||
} else {
|
||||
acc[assetRewardIndex].rewards = acc[assetRewardIndex].rewards.map(
|
||||
(reward) => {
|
||||
if (reward.rewardType === epochSummary.rewardType) {
|
||||
return {
|
||||
rewardType: epochSummary.rewardType,
|
||||
amount: (
|
||||
Number(reward.amount) + Number(epochSummary.amount)
|
||||
).toString(),
|
||||
};
|
||||
} else {
|
||||
return reward;
|
||||
}
|
||||
}
|
||||
);
|
||||
acc[assetRewardIndex].totalAmount = (
|
||||
Number(acc[assetRewardIndex].totalAmount) +
|
||||
Number(epochSummary.amount)
|
||||
).toString();
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as AggregatedEpochRewardSummary[]);
|
||||
|
||||
return {
|
||||
epoch: epochSummaries[0].epoch,
|
||||
assetRewards,
|
||||
};
|
||||
});
|
||||
|
||||
return epochTotalRewards;
|
||||
};
|
||||
@@ -47,3 +47,48 @@ query Rewards($partyId: ID!, $delegationsPagination: Pagination) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment EpochRewardSummaryFields on EpochRewardSummary {
|
||||
epoch
|
||||
assetId
|
||||
amount
|
||||
rewardType
|
||||
}
|
||||
|
||||
query EpochAssetsRewards($epochRewardSummariesPagination: Pagination) {
|
||||
assetsConnection {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
epochRewardSummaries(pagination: $epochRewardSummariesPagination) {
|
||||
edges {
|
||||
node {
|
||||
...EpochRewardSummaryFields
|
||||
}
|
||||
}
|
||||
}
|
||||
epoch {
|
||||
timestamps {
|
||||
expiry
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment EpochFields on Epoch {
|
||||
id
|
||||
timestamps {
|
||||
start
|
||||
end
|
||||
expiry
|
||||
}
|
||||
}
|
||||
|
||||
query Epoch {
|
||||
epoch {
|
||||
...EpochFields
|
||||
}
|
||||
}
|
||||
|
||||
+121
-1
@@ -15,6 +15,22 @@ export type RewardsQueryVariables = Types.Exact<{
|
||||
|
||||
export type RewardsQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, rewardsConnection?: { __typename?: 'RewardsConnection', edges?: Array<{ __typename?: 'RewardEdge', node: { __typename?: 'Reward', rewardType: Types.AccountType, amount: string, percentageOfTotal: string, receivedAt: any, asset: { __typename?: 'Asset', id: string, symbol: string }, party: { __typename?: 'Party', id: string }, epoch: { __typename?: 'Epoch', id: string } } } | null> | null } | null, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number } } | null> | null } | null } | null, epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null, expiry?: any | null } } };
|
||||
|
||||
export type EpochRewardSummaryFieldsFragment = { __typename?: 'EpochRewardSummary', epoch: number, assetId: string, amount: string, rewardType: Types.AccountType };
|
||||
|
||||
export type EpochAssetsRewardsQueryVariables = Types.Exact<{
|
||||
epochRewardSummariesPagination?: Types.InputMaybe<Types.Pagination>;
|
||||
}>;
|
||||
|
||||
|
||||
export type EpochAssetsRewardsQuery = { __typename?: 'Query', assetsConnection?: { __typename?: 'AssetsConnection', edges?: Array<{ __typename?: 'AssetEdge', node: { __typename?: 'Asset', id: string, name: string } } | null> | null } | null, epochRewardSummaries?: { __typename?: 'EpochRewardSummaryConnection', edges?: Array<{ __typename?: 'EpochRewardSummaryEdge', node: { __typename?: 'EpochRewardSummary', epoch: number, assetId: string, amount: string, rewardType: Types.AccountType } } | null> | null } | null, epoch: { __typename?: 'Epoch', timestamps: { __typename?: 'EpochTimestamps', expiry?: any | null } } };
|
||||
|
||||
export type EpochFieldsFragment = { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null, expiry?: any | null } };
|
||||
|
||||
export type EpochQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type EpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null, expiry?: any | null } } };
|
||||
|
||||
export const RewardFieldsFragmentDoc = gql`
|
||||
fragment RewardFields on Reward {
|
||||
rewardType
|
||||
@@ -39,6 +55,24 @@ export const DelegationFieldsFragmentDoc = gql`
|
||||
epoch
|
||||
}
|
||||
`;
|
||||
export const EpochRewardSummaryFieldsFragmentDoc = gql`
|
||||
fragment EpochRewardSummaryFields on EpochRewardSummary {
|
||||
epoch
|
||||
assetId
|
||||
amount
|
||||
rewardType
|
||||
}
|
||||
`;
|
||||
export const EpochFieldsFragmentDoc = gql`
|
||||
fragment EpochFields on Epoch {
|
||||
id
|
||||
timestamps {
|
||||
start
|
||||
end
|
||||
expiry
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const RewardsDocument = gql`
|
||||
query Rewards($partyId: ID!, $delegationsPagination: Pagination) {
|
||||
party(id: $partyId) {
|
||||
@@ -97,4 +131,90 @@ export function useRewardsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<Re
|
||||
}
|
||||
export type RewardsQueryHookResult = ReturnType<typeof useRewardsQuery>;
|
||||
export type RewardsLazyQueryHookResult = ReturnType<typeof useRewardsLazyQuery>;
|
||||
export type RewardsQueryResult = Apollo.QueryResult<RewardsQuery, RewardsQueryVariables>;
|
||||
export type RewardsQueryResult = Apollo.QueryResult<RewardsQuery, RewardsQueryVariables>;
|
||||
export const EpochAssetsRewardsDocument = gql`
|
||||
query EpochAssetsRewards($epochRewardSummariesPagination: Pagination) {
|
||||
assetsConnection {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
epochRewardSummaries(pagination: $epochRewardSummariesPagination) {
|
||||
edges {
|
||||
node {
|
||||
...EpochRewardSummaryFields
|
||||
}
|
||||
}
|
||||
}
|
||||
epoch {
|
||||
timestamps {
|
||||
expiry
|
||||
}
|
||||
}
|
||||
}
|
||||
${EpochRewardSummaryFieldsFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useEpochAssetsRewardsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useEpochAssetsRewardsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useEpochAssetsRewardsQuery` 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 } = useEpochAssetsRewardsQuery({
|
||||
* variables: {
|
||||
* epochRewardSummariesPagination: // value for 'epochRewardSummariesPagination'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useEpochAssetsRewardsQuery(baseOptions?: Apollo.QueryHookOptions<EpochAssetsRewardsQuery, EpochAssetsRewardsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<EpochAssetsRewardsQuery, EpochAssetsRewardsQueryVariables>(EpochAssetsRewardsDocument, options);
|
||||
}
|
||||
export function useEpochAssetsRewardsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<EpochAssetsRewardsQuery, EpochAssetsRewardsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<EpochAssetsRewardsQuery, EpochAssetsRewardsQueryVariables>(EpochAssetsRewardsDocument, options);
|
||||
}
|
||||
export type EpochAssetsRewardsQueryHookResult = ReturnType<typeof useEpochAssetsRewardsQuery>;
|
||||
export type EpochAssetsRewardsLazyQueryHookResult = ReturnType<typeof useEpochAssetsRewardsLazyQuery>;
|
||||
export type EpochAssetsRewardsQueryResult = Apollo.QueryResult<EpochAssetsRewardsQuery, EpochAssetsRewardsQueryVariables>;
|
||||
export const EpochDocument = gql`
|
||||
query Epoch {
|
||||
epoch {
|
||||
...EpochFields
|
||||
}
|
||||
}
|
||||
${EpochFieldsFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useEpochQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useEpochQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useEpochQuery` 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 } = useEpochQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useEpochQuery(baseOptions?: Apollo.QueryHookOptions<EpochQuery, EpochQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<EpochQuery, EpochQueryVariables>(EpochDocument, options);
|
||||
}
|
||||
export function useEpochLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<EpochQuery, EpochQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<EpochQuery, EpochQueryVariables>(EpochDocument, options);
|
||||
}
|
||||
export type EpochQueryHookResult = ReturnType<typeof useEpochQuery>;
|
||||
export type EpochLazyQueryHookResult = ReturnType<typeof useEpochLazyQuery>;
|
||||
export type EpochQueryResult = Apollo.QueryResult<EpochQuery, EpochQueryVariables>;
|
||||
@@ -1,130 +0,0 @@
|
||||
import { format } from 'date-fns';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
|
||||
import { BigNumber } from '../../../lib/bignumber';
|
||||
import { DATE_FORMAT_DETAILED } from '../../../lib/date-formats';
|
||||
import type {
|
||||
RewardsQuery,
|
||||
RewardFieldsFragment,
|
||||
DelegationFieldsFragment,
|
||||
} from './__generated__/Rewards';
|
||||
import {
|
||||
formatNumber,
|
||||
removePaginationWrapper,
|
||||
toBigNum,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { useAppState } from '../../../contexts/app-state/app-state-context';
|
||||
|
||||
interface RewardInfoProps {
|
||||
data: RewardsQuery | undefined;
|
||||
currVegaKey: string;
|
||||
}
|
||||
|
||||
export const RewardInfo = ({ data, currVegaKey }: RewardInfoProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const rewards = React.useMemo(() => {
|
||||
if (!data?.party || !data.party.rewardsConnection?.edges?.length) return [];
|
||||
|
||||
return removePaginationWrapper(data.party.rewardsConnection.edges);
|
||||
}, [data]);
|
||||
|
||||
const delegations = React.useMemo(() => {
|
||||
if (!data?.party || !data.party.delegationsConnection?.edges?.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return removePaginationWrapper(data.party.delegationsConnection.edges);
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
{t('Connected Vega key')}: {currVegaKey}
|
||||
</p>
|
||||
{rewards.length ? (
|
||||
rewards.map((reward, i) => {
|
||||
if (!reward) return null;
|
||||
return (
|
||||
<RewardTable
|
||||
key={i}
|
||||
reward={reward}
|
||||
delegations={delegations || []}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p>{t('noRewards')}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 = React.useMemo(() => {
|
||||
if (!delegations.length) return '0';
|
||||
|
||||
const delegationsForEpoch = delegations
|
||||
.filter((d) => d.epoch.toString() === reward.epoch.id)
|
||||
.map((d) => toBigNum(d.amount, decimals));
|
||||
|
||||
if (delegationsForEpoch.length) {
|
||||
return BigNumber.sum.apply(null, [
|
||||
new BigNumber(0),
|
||||
...delegationsForEpoch,
|
||||
]);
|
||||
}
|
||||
|
||||
return new BigNumber(0);
|
||||
}, [decimals, delegations, reward.epoch.id]);
|
||||
|
||||
return (
|
||||
<div className="mb-24">
|
||||
<h3 className="text-lg text-white mb-4">
|
||||
{t('Epoch')} {reward.epoch.id}
|
||||
</h3>
|
||||
<KeyValueTable>
|
||||
<KeyValueTableRow>
|
||||
{t('rewardType')}
|
||||
<span>{reward.rewardType}</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('yourStake')}
|
||||
<span>{stakeForEpoch.toString()}</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('reward')}
|
||||
<span>
|
||||
{formatNumber(toBigNum(reward.amount, decimals))}{' '}
|
||||
{reward.asset.symbol}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('shareOfReward')}
|
||||
<span>
|
||||
{new BigNumber(reward.percentageOfTotal).dp(2).toString()}%
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
<KeyValueTableRow>
|
||||
{t('received')}
|
||||
<span>
|
||||
{format(new Date(reward.receivedAt), DATE_FORMAT_DETAILED)}
|
||||
</span>
|
||||
</KeyValueTableRow>
|
||||
</KeyValueTable>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,47 +1,56 @@
|
||||
import { Button, Callout, Intent, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { formatDistance } from 'date-fns';
|
||||
// @ts-ignore No types available for duration-js
|
||||
import Duration from 'duration-js';
|
||||
import React from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { formatDistance } from 'date-fns';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ENV } from '../../../config';
|
||||
import {
|
||||
Callout,
|
||||
Intent,
|
||||
AsyncRenderer,
|
||||
Toggle,
|
||||
ExternalLink,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
useNetworkParams,
|
||||
NetworkParams,
|
||||
createDocsLinks,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { useEpochQuery } from './__generated__/Rewards';
|
||||
|
||||
import { EpochCountdown } from '../../../components/epoch-countdown';
|
||||
import { Heading } from '../../../components/heading';
|
||||
import { SplashLoader } from '../../../components/splash-loader';
|
||||
import {
|
||||
AppStateActionType,
|
||||
useAppState,
|
||||
} from '../../../contexts/app-state/app-state-context';
|
||||
import { RewardInfo } from './reward-info';
|
||||
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||
import { useNetworkParams, NetworkParams } from '@vegaprotocol/react-helpers';
|
||||
import { useRewardsQuery } from './__generated__/Rewards';
|
||||
import { Heading, SubHeading } from '../../../components/heading';
|
||||
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';
|
||||
|
||||
export const RewardsPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { VEGA_DOCS_URL } = useEnvironment();
|
||||
const { pubKey, pubKeys } = useVegaWallet();
|
||||
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
|
||||
openVegaWalletDialog: store.openVegaWalletDialog,
|
||||
}));
|
||||
const { appDispatch } = useAppState();
|
||||
const { delegationsPagination } = ENV;
|
||||
const { data, loading, error } = useRewardsQuery({
|
||||
variables: {
|
||||
partyId: pubKey || '',
|
||||
delegationsPagination: delegationsPagination
|
||||
? {
|
||||
first: Number(delegationsPagination),
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
skip: !pubKey,
|
||||
});
|
||||
const { params } = useNetworkParams([
|
||||
NetworkParams.reward_staking_delegation_payoutDelay,
|
||||
]);
|
||||
const [toggleRewardsView, setToggleRewardsView] =
|
||||
useState<RewardsView>('total');
|
||||
|
||||
const payoutDuration = React.useMemo(() => {
|
||||
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;
|
||||
}
|
||||
@@ -50,76 +59,99 @@ export const RewardsPage = () => {
|
||||
).milliseconds();
|
||||
}, [params]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<section>
|
||||
<p>{t('Something went wrong')}</p>
|
||||
{error && <pre>{error.message}</pre>}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading || !params) {
|
||||
return (
|
||||
<Splash>
|
||||
<SplashLoader />
|
||||
</Splash>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="rewards">
|
||||
<Heading title={t('pageTitleRewards')} />
|
||||
<p>{t('rewardsPara1')}</p>
|
||||
<p>{t('rewardsPara2')}</p>
|
||||
{payoutDuration ? (
|
||||
<div className="my-8">
|
||||
<Callout
|
||||
title={t('rewardsCallout', {
|
||||
duration: formatDistance(new Date(0), payoutDuration),
|
||||
})}
|
||||
headingLevel={3}
|
||||
intent={Intent.Warning}
|
||||
>
|
||||
<p className="mb-0">{t('rewardsPara3')}</p>
|
||||
</Callout>
|
||||
</div>
|
||||
) : null}
|
||||
{!loading &&
|
||||
data &&
|
||||
!error &&
|
||||
data.epoch.timestamps.start &&
|
||||
data.epoch.timestamps.expiry && (
|
||||
<section className="mb-8">
|
||||
<EpochCountdown
|
||||
// eslint-disable-next-line
|
||||
id={data!.epoch.id}
|
||||
startDate={new Date(data.epoch.timestamps.start)}
|
||||
// eslint-disable-next-line
|
||||
endDate={new Date(data.epoch.timestamps.expiry!)}
|
||||
/>
|
||||
<AsyncRenderer
|
||||
loading={paramsLoading || epochLoading}
|
||||
error={paramsError || epochError}
|
||||
data={epochData}
|
||||
render={() => (
|
||||
<section className="rewards">
|
||||
<Heading title={t('pageTitleRewards')} />
|
||||
<p className="mb-12">
|
||||
{t('rewardsIntro')}{' '}
|
||||
{VEGA_DOCS_URL && (
|
||||
<ExternalLink
|
||||
href={createDocsLinks(VEGA_DOCS_URL).REWARDS_GUIDE}
|
||||
target="_blank"
|
||||
data-testid="rewards-guide-link"
|
||||
className="text-white"
|
||||
>
|
||||
{t('seeHowRewardsAreCalculated')}
|
||||
</ExternalLink>
|
||||
)}
|
||||
</p>
|
||||
|
||||
{payoutDuration ? (
|
||||
<div className="my-8">
|
||||
<Callout
|
||||
title={t('rewardsCallout', {
|
||||
duration: formatDistance(new Date(0), payoutDuration),
|
||||
})}
|
||||
headingLevel={3}
|
||||
intent={Intent.Warning}
|
||||
>
|
||||
<p className="mb-0">{t('rewardsCalloutDetail')}</p>
|
||||
</Callout>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{epochData?.epoch &&
|
||||
epochData.epoch?.id &&
|
||||
epochData.epoch.timestamps.start &&
|
||||
epochData.epoch.timestamps.expiry && (
|
||||
<section className="mb-16">
|
||||
<EpochCountdown
|
||||
id={epochData.epoch.id}
|
||||
startDate={new Date(epochData.epoch.timestamps.start)}
|
||||
endDate={new Date(epochData.epoch.timestamps.expiry)}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="grid xl:grid-cols-2 gap-12 items-center mb-8">
|
||||
<div>
|
||||
<SubHeading title={t('rewardsAndFeesReceived')} />
|
||||
<p>
|
||||
{t(
|
||||
'ThisDoesNotIncludeFeesReceivedForMakersOrLiquidityProviders'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="max-w-[600px]">
|
||||
<Toggle
|
||||
name="epoch-reward-view-toggle"
|
||||
toggles={[
|
||||
{
|
||||
label: t('totalDistributed'),
|
||||
value: 'total',
|
||||
},
|
||||
{
|
||||
label: t('earnedByMe'),
|
||||
value: 'individual',
|
||||
},
|
||||
]}
|
||||
checkedValue={toggleRewardsView}
|
||||
onChange={(e) =>
|
||||
setToggleRewardsView(e.target.value as RewardsView)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
<section>
|
||||
{pubKey && pubKeys?.length ? (
|
||||
<RewardInfo currVegaKey={pubKey} data={data} />
|
||||
) : (
|
||||
<div>
|
||||
<Button
|
||||
data-testid="connect-to-vega-wallet-btn"
|
||||
onClick={() => {
|
||||
appDispatch({
|
||||
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY,
|
||||
isOpen: true,
|
||||
});
|
||||
openVegaWalletDialog();
|
||||
}}
|
||||
>
|
||||
{t('connectVegaWallet')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
|
||||
{toggleRewardsView === 'total' ? (
|
||||
<EpochTotalRewards />
|
||||
) : (
|
||||
<section>
|
||||
{pubKey && pubKeys?.length ? (
|
||||
<EpochIndividualRewards />
|
||||
) : (
|
||||
<ConnectToSeeRewards />
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import classNames from 'classnames';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SubHeading } from '../../components/heading';
|
||||
|
||||
export const NoRewards = () => {
|
||||
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('noRewardsHaveBeenDistributedYet')} />
|
||||
<p className="font-alpha text-xl">{t('checkBackSoon')}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+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>
|
||||
);
|
||||
@@ -3,7 +3,7 @@ import { EpochCountdown } from '../../../components/epoch-countdown';
|
||||
import { useNodesQuery } from './__generated___/Nodes';
|
||||
import { usePreviousEpochQuery } from '../__generated___/PreviousEpoch';
|
||||
import { ValidatorTables } from './validator-tables';
|
||||
import { useRefreshValidators } from '../../../hooks/use-refresh-validators';
|
||||
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
|
||||
|
||||
export const EpochData = () => {
|
||||
// errorPolicy due to vegaprotocol/vega issue 5898
|
||||
@@ -15,7 +15,7 @@ export const EpochData = () => {
|
||||
skip: !data?.epoch.id,
|
||||
});
|
||||
|
||||
useRefreshValidators(data?.epoch.timestamps.expiry, refetch);
|
||||
useRefreshAfterEpoch(data?.epoch.timestamps.expiry, refetch);
|
||||
|
||||
return (
|
||||
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||
|
||||
+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(),
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ENV } from '../../../config';
|
||||
import { Callout, Intent, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useRefreshValidators } from '../../../hooks/use-refresh-validators';
|
||||
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
|
||||
import { SplashLoader } from '../../../components/splash-loader';
|
||||
import { useStakingQuery } from './__generated__/Staking';
|
||||
import { usePreviousEpochQuery } from '../__generated___/PreviousEpoch';
|
||||
@@ -45,7 +45,7 @@ export const NodeContainer = ({
|
||||
skip: !data?.epoch.id,
|
||||
});
|
||||
|
||||
useRefreshValidators(data?.epoch.timestamps.expiry, refetch);
|
||||
useRefreshAfterEpoch(data?.epoch.timestamps.expiry, refetch);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
|
||||
@@ -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: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -72,6 +72,9 @@ describe('market info is displayed', { tags: '@smoke' }, () => {
|
||||
|
||||
it('settlement asset displayed', () => {
|
||||
cy.getByTestId(marketTitle).contains('Settlement asset').click();
|
||||
cy.window().then((win) => {
|
||||
cy.stub(win, 'prompt').returns('DISABLED WINDOW PROMPT');
|
||||
});
|
||||
validateMarketDataRow(0, 'ID', 'asset-id');
|
||||
validateMarketDataRow(1, 'Type', 'ERC20');
|
||||
validateMarketDataRow(2, 'Name', 'Euro');
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user