Compare commits
57
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6ed5016d1 | ||
|
|
d4e151d4be | ||
|
|
efee4f9e1a | ||
|
|
a3257b33b4 | ||
|
|
96007bd230 | ||
|
|
554d9ecb06 | ||
|
|
a12a5b3a15 | ||
|
|
a79e2f85bc | ||
|
|
a2190fdde0 | ||
|
|
849415a78e | ||
|
|
d33f5428b6 | ||
|
|
c73b01d549 | ||
|
|
4286bc37b3 | ||
|
|
d4ecb88fb4 | ||
|
|
57a8955795 | ||
|
|
ac53b1f97a | ||
|
|
4b83a10475 | ||
|
|
7e957a2841 | ||
|
|
38c0e70bbc | ||
|
|
94c40cce7d | ||
|
|
0059050440 | ||
|
|
f7be890eb9 | ||
|
|
59ae971a07 | ||
|
|
cfcd5efa2c | ||
|
|
fb1e85893c | ||
|
|
084972577e | ||
|
|
94df5ca048 | ||
|
|
6142a7afbb | ||
|
|
d2e0c57676 | ||
|
|
e48d8d8715 | ||
|
|
7de333e1ea | ||
|
|
2123afc4b0 | ||
|
|
ea91fa1723 | ||
|
|
4dcdec7a35 | ||
|
|
196f1914a3 | ||
|
|
ea4c7a4a37 | ||
|
|
c22fec97b4 | ||
|
|
989a0456c0 | ||
|
|
8c79a94a8c | ||
|
|
a7a6a820c0 | ||
|
|
1fa09a690e | ||
|
|
b0a84970f2 | ||
|
|
b2a115f935 | ||
|
|
9dfed0a211 | ||
|
|
5283676be7 | ||
|
|
a82509f0e0 | ||
|
|
64c92ce91d | ||
|
|
b7426e73d8 | ||
|
|
969bfd6945 | ||
|
|
200487a6fc | ||
|
|
3f330e78c5 | ||
|
|
05726c6a5a | ||
|
|
fcace22393 | ||
|
|
b95462ea38 | ||
|
|
42cd32f376 | ||
|
|
55d6dd4dce | ||
|
|
7017e24adf |
@@ -18,6 +18,10 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
|
||||||
- name: Run Cypress tests
|
- name: Run Cypress tests
|
||||||
uses: cypress-io/github-action@v4
|
uses: cypress-io/github-action@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -50,20 +50,6 @@ jobs:
|
|||||||
projects=[${projects// /,}]
|
projects=[${projects// /,}]
|
||||||
echo PROJECTS=$projects >> $GITHUB_ENV
|
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:
|
outputs:
|
||||||
projects: ${{ env.PROJECTS }}
|
projects: ${{ env.PROJECTS }}
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,10 @@ jobs:
|
|||||||
mv "${file}" "$(echo ${file} | sed 's|:|-|g')"
|
mv "${file}" "$(echo ${file} | sed 's|:|-|g')"
|
||||||
done< <(find /home/runner/.vegacapsule/testnet/logs -type f)
|
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
|
- uses: actions/upload-artifact@v3
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -4,15 +4,6 @@ context('Asset page', { tags: '@regression' }, () => {
|
|||||||
describe('Verify elements on page', () => {
|
describe('Verify elements on page', () => {
|
||||||
before('Navigate to assets page', () => {
|
before('Navigate to assets page', () => {
|
||||||
cy.visit('/assets');
|
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', () => {
|
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) => {
|
cy.getAssets().then((assets) => {
|
||||||
Object.values(assets).forEach((asset) => {
|
Object.values(assets).forEach((asset) => {
|
||||||
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`)
|
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`)
|
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`)
|
||||||
.eq(0)
|
.eq(0)
|
||||||
.click();
|
.click();
|
||||||
cy.getByTestId('dialog-content').should('be.visible');
|
cy.getByTestId('asset-header').should('have.text', asset.name);
|
||||||
cy.getByTestId('dialog-close').click();
|
cy.go('back');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ context('Network parameters page', { tags: '@smoke' }, function () {
|
|||||||
.and('include', darkThemeSideMenuBackgroundColor);
|
.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.common_switch_to_mobile_and_click_toggle();
|
||||||
cy.get(networkParametersNavigation).click();
|
cy.get(networkParametersNavigation).click();
|
||||||
cy.get_network_parameters().then((network_parameters) => {
|
cy.get_network_parameters().then((network_parameters) => {
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { useState, useEffect } from 'react';
|
import { NetworkLoader, useInitializeEnv } from '@vegaprotocol/environment';
|
||||||
import { useLocation } from 'react-router-dom';
|
|
||||||
import { EnvironmentProvider, NetworkLoader } from '@vegaprotocol/environment';
|
|
||||||
import { Nav } from './components/nav';
|
import { Nav } from './components/nav';
|
||||||
import { Header } from './components/header';
|
import { Header } from './components/header';
|
||||||
import { Main } from './components/main';
|
import { Main } from './components/main';
|
||||||
import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-websocket-provider';
|
import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-websocket-provider';
|
||||||
import type { InMemoryCacheConfig } from '@apollo/client';
|
|
||||||
import { Footer } from './components/footer/footer';
|
import { Footer } from './components/footer/footer';
|
||||||
import { AnnouncementBanner, ExternalLink } from '@vegaprotocol/ui-toolkit';
|
import { AnnouncementBanner, ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||||
import {
|
import {
|
||||||
AssetDetailsDialog,
|
AssetDetailsDialog,
|
||||||
useAssetDetailsDialogStore,
|
useAssetDetailsDialogStore,
|
||||||
} from '@vegaprotocol/assets';
|
} from '@vegaprotocol/assets';
|
||||||
|
import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client';
|
||||||
|
|
||||||
const DialogsContainer = () => {
|
const DialogsContainer = () => {
|
||||||
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
|
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
|
||||||
@@ -28,36 +26,18 @@ const DialogsContainer = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
|
||||||
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setMenuOpen(false);
|
|
||||||
}, [location]);
|
|
||||||
|
|
||||||
const cacheConfig: InMemoryCacheConfig = {
|
|
||||||
typePolicies: {
|
|
||||||
statistics: {
|
|
||||||
keyFields: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const layoutClasses = classnames(
|
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]',
|
'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',
|
'min-h-[100vh] mx-auto my-0',
|
||||||
'border-neutral-700 dark:border-neutral-300 lg:border-l lg:border-r',
|
'border-neutral-700 dark:border-neutral-300 lg:border-l lg:border-r',
|
||||||
'bg-white dark:bg-black',
|
'bg-white dark:bg-black',
|
||||||
'antialiased text-black dark:text-white',
|
'antialiased text-black dark:text-white',
|
||||||
{
|
'overflow-hidden relative'
|
||||||
'h-[100vh] min-h-auto overflow-hidden': menuOpen,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TendermintWebsocketProvider>
|
<TendermintWebsocketProvider>
|
||||||
<NetworkLoader cache={cacheConfig}>
|
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
|
||||||
<AnnouncementBanner>
|
<AnnouncementBanner>
|
||||||
<div className="font-alpha calt uppercase text-center text-lg text-white">
|
<div className="font-alpha calt uppercase text-center text-lg text-white">
|
||||||
<span className="pr-4">Mainnet sim 2 coming in March!</span>
|
<span className="pr-4">Mainnet sim 2 coming in March!</span>
|
||||||
@@ -68,8 +48,8 @@ function App() {
|
|||||||
</AnnouncementBanner>
|
</AnnouncementBanner>
|
||||||
|
|
||||||
<div className={layoutClasses}>
|
<div className={layoutClasses}>
|
||||||
<Header menuOpen={menuOpen} setMenuOpen={setMenuOpen} />
|
<Header />
|
||||||
<Nav menuOpen={menuOpen} />
|
<Nav />
|
||||||
<Main />
|
<Main />
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
@@ -81,11 +61,8 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Wrapper = () => {
|
const Wrapper = () => {
|
||||||
return (
|
useInitializeEnv();
|
||||||
<EnvironmentProvider>
|
return <App />;
|
||||||
<App />
|
|
||||||
</EnvironmentProvider>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Wrapper;
|
export default Wrapper;
|
||||||
|
|||||||
@@ -1,22 +1,35 @@
|
|||||||
import { render, waitFor } from '@testing-library/react';
|
import { render, waitFor } from '@testing-library/react';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { assetsList } from '../../mocks/assets';
|
import { assetsList } from '../../mocks/assets';
|
||||||
import { AssetsTable } from './assets-table';
|
import { AssetsTable } from './assets-table';
|
||||||
|
|
||||||
describe('AssetsTable', () => {
|
describe('AssetsTable', () => {
|
||||||
it('shows loading message on first render', async () => {
|
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();
|
expect(await res.findByText('Loading...')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows no data message if no assets found', async () => {
|
it('shows no data message if no assets found', async () => {
|
||||||
const res = render(<AssetsTable data={[]} />);
|
const res = render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<AssetsTable data={[]} />
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
expect(
|
expect(
|
||||||
await res.findByText('This chain has no assets')
|
await res.findByText('This chain has no assets')
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows a table/list with all the assets', async () => {
|
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(() => {
|
await waitFor(() => {
|
||||||
const rowA1 = res.container.querySelector('[row-id="123"]');
|
const rowA1 = res.container.querySelector('[row-id="123"]');
|
||||||
expect(rowA1).toBeInTheDocument();
|
expect(rowA1).toBeInTheDocument();
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
|
||||||
import { AssetTypeMapping, AssetStatusMapping } from '@vegaprotocol/assets';
|
import { AssetTypeMapping, AssetStatusMapping } from '@vegaprotocol/assets';
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import type { VegaICellRendererParams } from '@vegaprotocol/ui-toolkit';
|
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 { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||||
import { useRef, useLayoutEffect } from 'react';
|
import { useRef, useLayoutEffect } from 'react';
|
||||||
import { BREAKPOINT_MD } from '../../config/breakpoints';
|
import { BREAKPOINT_MD } from '../../config/breakpoints';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import type { RowClickedEvent } from 'ag-grid-community';
|
||||||
|
|
||||||
type AssetsTableProps = {
|
type AssetsTableProps = {
|
||||||
data: AssetFieldsFragment[] | null;
|
data: AssetFieldsFragment[] | null;
|
||||||
};
|
};
|
||||||
export const AssetsTable = ({ data }: AssetsTableProps) => {
|
export const AssetsTable = ({ data }: AssetsTableProps) => {
|
||||||
const openAssetDetailsDialog = useAssetDetailsDialogStore(
|
const navigate = useNavigate();
|
||||||
(state) => state.open
|
|
||||||
);
|
|
||||||
|
|
||||||
const ref = useRef<AgGridReact>(null);
|
const ref = useRef<AgGridReact>(null);
|
||||||
const showColumnsOnDesktop = () => {
|
const showColumnsOnDesktop = () => {
|
||||||
ref.current?.columnApi.setColumnsVisible(
|
ref.current?.columnApi.setColumnsVisible(
|
||||||
@@ -49,17 +47,23 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
|
|||||||
autoHeight: true,
|
autoHeight: true,
|
||||||
}}
|
}}
|
||||||
suppressCellFocus={true}
|
suppressCellFocus={true}
|
||||||
onGridReady={() => {
|
onRowClicked={({ data }: RowClickedEvent) => {
|
||||||
showColumnsOnDesktop();
|
navigate(data.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AgGridColumn headerName={t('Symbol')} field="symbol" />
|
<AgGridColumn headerName={t('Symbol')} field="symbol" />
|
||||||
<AgGridColumn headerName={t('Name')} field="name" />
|
<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
|
<AgGridColumn
|
||||||
colId="type"
|
colId="type"
|
||||||
headerName={t('Type')}
|
headerName={t('Type')}
|
||||||
field="source.__typename"
|
field="source.__typename"
|
||||||
|
hide={window.innerWidth < BREAKPOINT_MD}
|
||||||
valueFormatter={({ value }: { value?: string }) =>
|
valueFormatter={({ value }: { value?: string }) =>
|
||||||
value && AssetTypeMapping[value].value
|
value && AssetTypeMapping[value].value
|
||||||
}
|
}
|
||||||
@@ -67,6 +71,7 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
|
|||||||
<AgGridColumn
|
<AgGridColumn
|
||||||
headerName={t('Status')}
|
headerName={t('Status')}
|
||||||
field="status"
|
field="status"
|
||||||
|
hide={window.innerWidth < BREAKPOINT_MD}
|
||||||
valueFormatter={({ value }: { value?: string }) =>
|
valueFormatter={({ value }: { value?: string }) =>
|
||||||
value && AssetStatusMapping[value].value
|
value && AssetStatusMapping[value].value
|
||||||
}
|
}
|
||||||
@@ -83,28 +88,13 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
|
|||||||
value,
|
value,
|
||||||
}: VegaICellRendererParams<AssetFieldsFragment, 'id'>) =>
|
}: VegaICellRendererParams<AssetFieldsFragment, 'id'>) =>
|
||||||
value ? (
|
value ? (
|
||||||
<div className="pb-1">
|
<ButtonLink
|
||||||
<ButtonLink
|
onClick={(e) => {
|
||||||
onClick={(e) => {
|
navigate(value);
|
||||||
openAssetDetailsDialog(value, e.target as HTMLElement);
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{t('View details')}
|
||||||
{t('View details')}
|
</ButtonLink>
|
||||||
</ButtonLink>{' '}
|
|
||||||
<span className="max-md:hidden">
|
|
||||||
<ButtonLink
|
|
||||||
onClick={(e) => {
|
|
||||||
openAssetDetailsDialog(
|
|
||||||
value,
|
|
||||||
e.target as HTMLElement,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('View JSON')}
|
|
||||||
</ButtonLink>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
) : (
|
||||||
''
|
''
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 { NodeSwitcherDialog, useEnvironment } from '@vegaprotocol/environment';
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t, useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
import { ExternalLink, Link } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { ENV } from '../../config/env';
|
||||||
|
|
||||||
export const Footer = () => {
|
export const Footer = () => {
|
||||||
const { VEGA_URL, GIT_COMMIT_HASH, GIT_ORIGIN_URL, setNodeSwitcherOpen } =
|
const { VEGA_URL, GIT_COMMIT_HASH, GIT_ORIGIN_URL } = useEnvironment();
|
||||||
useEnvironment();
|
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useState(false);
|
||||||
return (
|
const { screenSize } = useScreenDimensions();
|
||||||
<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">
|
const showFullFeedbackLabel = useMemo(
|
||||||
<div className="flex justify-between gap-2 align-middle">
|
() => ['lg', 'xl'].includes(screenSize),
|
||||||
<div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4">
|
[screenSize]
|
||||||
{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>
|
|
||||||
|
|
||||||
<div className="flex pl-2 content-center">
|
return (
|
||||||
{VEGA_URL && <NodeUrl url={VEGA_URL} />}
|
<>
|
||||||
<Link className="ml-2" onClick={setNodeSwitcherOpen}>
|
<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">
|
||||||
{t('Change')}
|
<div className="flex justify-between gap-2 align-middle">
|
||||||
</Link>
|
{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>
|
||||||
</div>
|
</footer>
|
||||||
</footer>
|
<NodeSwitcherDialog
|
||||||
|
open={nodeSwitcherOpen}
|
||||||
|
setOpen={setNodeSwitcherOpen}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jest.mock('../search', () => ({
|
|||||||
|
|
||||||
const renderComponent = () => (
|
const renderComponent = () => (
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<Header menuOpen={false} setMenuOpen={jest.fn()} />
|
<Header />
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,11 @@ import { ThemeSwitcher, Icon } from '@vegaprotocol/ui-toolkit';
|
|||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import { Search } from '../search';
|
import { Search } from '../search';
|
||||||
import { Routes } from '../../routes/route-names';
|
import { Routes } from '../../routes/route-names';
|
||||||
import type { Dispatch, SetStateAction } from 'react';
|
|
||||||
import { NetworkSwitcher } from '@vegaprotocol/environment';
|
import { NetworkSwitcher } from '@vegaprotocol/environment';
|
||||||
|
import { useNavStore } from '../nav';
|
||||||
|
|
||||||
interface ThemeToggleProps {
|
export const Header = () => {
|
||||||
menuOpen: boolean;
|
const [open, toggle] = useNavStore((state) => [state.open, state.toggle]);
|
||||||
setMenuOpen: Dispatch<SetStateAction<boolean>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Header = ({ menuOpen, setMenuOpen }: ThemeToggleProps) => {
|
|
||||||
const headerClasses = classnames(
|
const headerClasses = classnames(
|
||||||
'md:col-span-2',
|
'md:col-span-2',
|
||||||
'grid grid-rows-2 md:grid-rows-1 grid-cols-[1fr_auto] md:grid-cols-[auto_1fr_auto] items-center',
|
'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
|
<button
|
||||||
data-testid="open-menu"
|
data-testid="open-menu"
|
||||||
className="md:hidden text-white"
|
className="md:hidden text-white"
|
||||||
onClick={() => setMenuOpen(!menuOpen)}
|
onClick={() => toggle()}
|
||||||
>
|
>
|
||||||
<Icon name={menuOpen ? 'cross' : 'menu'} />
|
<Icon name={open ? 'cross' : 'menu'} />
|
||||||
</button>
|
</button>
|
||||||
<Search />
|
<Search />
|
||||||
<ThemeSwitcher className="-my-4" />
|
<ThemeSwitcher className="-my-4" />
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ import {
|
|||||||
useAssetDataProvider,
|
useAssetDataProvider,
|
||||||
useAssetDetailsDialogStore,
|
useAssetDetailsDialogStore,
|
||||||
} from '@vegaprotocol/assets';
|
} from '@vegaprotocol/assets';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Routes } from '../../../routes/route-names';
|
||||||
|
|
||||||
export type AssetLinkProps = Partial<ComponentProps<typeof ButtonLink>> & {
|
export type AssetLinkProps = Partial<ComponentProps<typeof ButtonLink>> & {
|
||||||
assetId: string;
|
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
|
* with a link to the assets modal. If the name does not come back
|
||||||
* it will use the ID instead.
|
* 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 { data: asset } = useAssetDataProvider(assetId);
|
||||||
|
|
||||||
const open = useAssetDetailsDialogStore((state) => state.open);
|
const open = useAssetDetailsDialogStore((state) => state.open);
|
||||||
|
const navigate = useNavigate();
|
||||||
const label = asset?.name ? asset.name : assetId;
|
const label = asset?.name ? asset.name : assetId;
|
||||||
return (
|
return (
|
||||||
<ButtonLink
|
<ButtonLink
|
||||||
data-testid="asset-link"
|
data-testid="asset-link"
|
||||||
disabled={!asset}
|
disabled={!asset}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
open(assetId, e.target as HTMLElement);
|
if (asDialog) {
|
||||||
|
open(assetId, e.target as HTMLElement);
|
||||||
|
} else {
|
||||||
|
navigate(`${Routes.ASSETS}/${asset?.id}`);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { AppRouter } from '../../routes';
|
|||||||
|
|
||||||
export const Main = () => {
|
export const Main = () => {
|
||||||
return (
|
return (
|
||||||
<main className="p-4 overflow-scroll">
|
<main className="p-4">
|
||||||
<AppRouter />
|
<AppRouter />
|
||||||
</main>
|
</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';
|
export * from './nav';
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,181 @@
|
|||||||
|
import { NavLink, useLocation } from 'react-router-dom';
|
||||||
|
import type { Navigable } from '../../routes/router-config';
|
||||||
|
import routerConfig from '../../routes/router-config';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
import { Icon } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import first from 'lodash/first';
|
||||||
|
import last from 'lodash/last';
|
||||||
|
import { BREAKPOINT_MD } from '../../config/breakpoints';
|
||||||
|
|
||||||
|
type NavStore = {
|
||||||
|
open: boolean;
|
||||||
|
toggle: () => void;
|
||||||
|
hide: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useNavStore = create<NavStore>((set, get) => ({
|
||||||
|
open: false,
|
||||||
|
toggle: () => set({ open: !get().open }),
|
||||||
|
hide: () => set({ open: false }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const NavLinks = ({ links }: { links: Navigable[] }) => {
|
||||||
|
const navLinks = links.map((r) => (
|
||||||
|
<li key={r.name}>
|
||||||
|
<NavLink
|
||||||
|
to={r.path}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
classnames(
|
||||||
|
'block mb-2 px-2',
|
||||||
|
'text-lg hover:bg-vega-pink dark:hover:bg-vega-yellow hover:text-white dark:hover:text-black',
|
||||||
|
{
|
||||||
|
'bg-vega-pink text-white dark:bg-vega-yellow dark:text-black':
|
||||||
|
isActive,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{r.text}
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
));
|
||||||
|
|
||||||
|
return <ul className="pr-8 md:pr-0">{navLinks}</ul>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Nav = () => {
|
||||||
|
const [open, hide] = useNavStore((state) => [state.open, state.hide]);
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const navRef = useRef<HTMLElement>(null);
|
||||||
|
const btnRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
const focusable = useMemo(
|
||||||
|
() =>
|
||||||
|
navRef.current
|
||||||
|
? [
|
||||||
|
...(navRef.current.querySelectorAll(
|
||||||
|
'a, button'
|
||||||
|
) as NodeListOf<HTMLElement>),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[navRef.current] // do not remove `navRef.current` from deps
|
||||||
|
);
|
||||||
|
|
||||||
|
const closeNav = useCallback(() => {
|
||||||
|
hide();
|
||||||
|
console.log(focusable);
|
||||||
|
focusable.forEach((fe) =>
|
||||||
|
fe.setAttribute(
|
||||||
|
'tabindex',
|
||||||
|
window.innerWidth > BREAKPOINT_MD ? '0' : '-1'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, [focusable, hide]);
|
||||||
|
|
||||||
|
// close navigation when location changes
|
||||||
|
useEffect(() => {
|
||||||
|
closeNav();
|
||||||
|
}, [closeNav, location]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
focusable.forEach((fe) => fe.setAttribute('tabindex', '0'));
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.style.overflow = open ? 'hidden' : '';
|
||||||
|
const offset =
|
||||||
|
document.querySelector('header')?.getBoundingClientRect().top || 0;
|
||||||
|
if (navRef.current) {
|
||||||
|
navRef.current.style.height = `calc(100vh - ${offset}px)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// focus current by default
|
||||||
|
if (navRef.current && open) {
|
||||||
|
(navRef.current.querySelector('a[aria-current]') as HTMLElement)?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeOnEsc = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closeNav();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// tabbing loop
|
||||||
|
const focusLast = (e: FocusEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const isNavElement =
|
||||||
|
e.relatedTarget && navRef.current?.contains(e.relatedTarget as Node);
|
||||||
|
if (!isNavElement && open) {
|
||||||
|
last(focusable)?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const focusFirst = (e: FocusEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const isNavElement =
|
||||||
|
e.relatedTarget && navRef.current?.contains(e.relatedTarget as Node);
|
||||||
|
if (!isNavElement && open) {
|
||||||
|
first(focusable)?.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetOnDesktop = () => {
|
||||||
|
focusable.forEach((fe) =>
|
||||||
|
fe.setAttribute(
|
||||||
|
'tabindex',
|
||||||
|
window.innerWidth > BREAKPOINT_MD ? '0' : '-1'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', resetOnDesktop);
|
||||||
|
|
||||||
|
first(focusable)?.addEventListener('focusout', focusLast);
|
||||||
|
last(focusable)?.addEventListener('focusout', focusFirst);
|
||||||
|
|
||||||
|
document.addEventListener('keydown', closeOnEsc);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', resetOnDesktop);
|
||||||
|
document.removeEventListener('keydown', closeOnEsc);
|
||||||
|
first(focusable)?.removeEventListener('focusout', focusLast);
|
||||||
|
last(focusable)?.removeEventListener('focusout', focusFirst);
|
||||||
|
};
|
||||||
|
}, [closeNav, focusable, open]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
ref={navRef}
|
||||||
|
className={classnames(
|
||||||
|
'absolute top-0 z-20 overflow-y-auto',
|
||||||
|
'transition-[right]',
|
||||||
|
{
|
||||||
|
'right-[-200vw] h-full': !open,
|
||||||
|
'right-0 h-[100vh]': open,
|
||||||
|
},
|
||||||
|
'w-full p-4 border-neutral-700 dark:border-neutral-300',
|
||||||
|
'bg-white dark:bg-black',
|
||||||
|
'md:static md:border-r'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<NavLinks links={routerConfig} />
|
||||||
|
<button
|
||||||
|
ref={btnRef}
|
||||||
|
className="absolute top-0 right-0 p-4 md:hidden"
|
||||||
|
onClick={() => {
|
||||||
|
closeNav();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon name="cross" />
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
import type { ProposalListFieldsFragment } from '@vegaprotocol/governance';
|
||||||
|
import { VoteProgress } from '@vegaprotocol/governance';
|
||||||
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
|
import { AgGridColumn } from 'ag-grid-react';
|
||||||
|
import type {
|
||||||
|
VegaICellRendererParams,
|
||||||
|
VegaValueFormatterParams,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import type { RowClickedEvent } from 'ag-grid-community';
|
||||||
|
import {
|
||||||
|
getDateTimeFormat,
|
||||||
|
NetworkParams,
|
||||||
|
t,
|
||||||
|
useNetworkParams,
|
||||||
|
} from '@vegaprotocol/react-helpers';
|
||||||
|
import { ProposalStateMapping } from '@vegaprotocol/types';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
|
||||||
|
import { BREAKPOINT_MD } from '../../config/breakpoints';
|
||||||
|
import { JsonViewerDialog } from '../dialogs/json-viewer-dialog';
|
||||||
|
|
||||||
|
type ProposalTermsDialog = {
|
||||||
|
open: boolean;
|
||||||
|
title: string;
|
||||||
|
content: unknown;
|
||||||
|
};
|
||||||
|
type ProposalsTableProps = {
|
||||||
|
data: ProposalListFieldsFragment[] | null;
|
||||||
|
};
|
||||||
|
export const ProposalsTable = ({ data }: ProposalsTableProps) => {
|
||||||
|
const { params } = useNetworkParams([
|
||||||
|
NetworkParams.governance_proposal_market_requiredMajority,
|
||||||
|
]);
|
||||||
|
const tokenLink = useLinks(DApp.Token);
|
||||||
|
const requiredMajorityPercentage = useMemo(() => {
|
||||||
|
const requiredMajority =
|
||||||
|
params?.governance_proposal_market_requiredMajority ?? 1;
|
||||||
|
return new BigNumber(requiredMajority).times(100);
|
||||||
|
}, [params?.governance_proposal_market_requiredMajority]);
|
||||||
|
|
||||||
|
const gridRef = useRef<AgGridReact>(null);
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const showColumnsOnDesktop = () => {
|
||||||
|
gridRef.current?.columnApi.setColumnsVisible(
|
||||||
|
['voting', 'cDate', 'eDate', 'type'],
|
||||||
|
window.innerWidth > BREAKPOINT_MD
|
||||||
|
);
|
||||||
|
gridRef.current?.columnApi.setColumnWidth(
|
||||||
|
'actions',
|
||||||
|
window.innerWidth > BREAKPOINT_MD ? 221 : 80
|
||||||
|
);
|
||||||
|
};
|
||||||
|
window.addEventListener('resize', showColumnsOnDesktop);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', showColumnsOnDesktop);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [dialog, setDialog] = useState<ProposalTermsDialog>({
|
||||||
|
open: false,
|
||||||
|
title: '',
|
||||||
|
content: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AgGrid
|
||||||
|
ref={gridRef}
|
||||||
|
rowData={data}
|
||||||
|
getRowId={({ data }: { data: ProposalListFieldsFragment }) =>
|
||||||
|
data.id || data.rationale.title
|
||||||
|
}
|
||||||
|
overlayNoRowsTemplate={t('This chain has no markets')}
|
||||||
|
domLayout="autoHeight"
|
||||||
|
defaultColDef={{
|
||||||
|
flex: 1,
|
||||||
|
resizable: true,
|
||||||
|
sortable: true,
|
||||||
|
filter: true,
|
||||||
|
filterParams: { buttons: ['reset'] },
|
||||||
|
autoHeight: true,
|
||||||
|
}}
|
||||||
|
suppressCellFocus={true}
|
||||||
|
onRowClicked={({ data, event }: RowClickedEvent) => {
|
||||||
|
if (
|
||||||
|
(event?.target as HTMLElement).tagName.toUpperCase() !== 'BUTTON'
|
||||||
|
) {
|
||||||
|
const proposalPage = tokenLink(
|
||||||
|
TOKEN_PROPOSAL.replace(':id', data.id)
|
||||||
|
);
|
||||||
|
window.open(proposalPage, '_blank');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AgGridColumn
|
||||||
|
colId="title"
|
||||||
|
headerName={t('Title')}
|
||||||
|
field="rationale.title"
|
||||||
|
flex={2}
|
||||||
|
wrapText={true}
|
||||||
|
/>
|
||||||
|
<AgGridColumn
|
||||||
|
colId="type"
|
||||||
|
maxWidth={180}
|
||||||
|
hide={window.innerWidth <= BREAKPOINT_MD}
|
||||||
|
headerName={t('Type')}
|
||||||
|
field="terms.change.__typename"
|
||||||
|
/>
|
||||||
|
<AgGridColumn
|
||||||
|
maxWidth={100}
|
||||||
|
headerName={t('State')}
|
||||||
|
field="state"
|
||||||
|
valueFormatter={({
|
||||||
|
value,
|
||||||
|
}: VegaValueFormatterParams<ProposalListFieldsFragment, 'state'>) => {
|
||||||
|
return value ? ProposalStateMapping[value] : '-';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AgGridColumn
|
||||||
|
colId="voting"
|
||||||
|
maxWidth={100}
|
||||||
|
hide={window.innerWidth <= BREAKPOINT_MD}
|
||||||
|
headerName={t('Voting')}
|
||||||
|
cellRenderer={({
|
||||||
|
data,
|
||||||
|
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
|
||||||
|
if (data) {
|
||||||
|
const yesTokens = new BigNumber(data.votes.yes.totalTokens);
|
||||||
|
const noTokens = new BigNumber(data.votes.no.totalTokens);
|
||||||
|
const totalTokensVoted = yesTokens.plus(noTokens);
|
||||||
|
const yesPercentage = totalTokensVoted.isZero()
|
||||||
|
? new BigNumber(0)
|
||||||
|
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
|
||||||
|
return (
|
||||||
|
<div className="uppercase flex h-full items-center justify-center pt-2">
|
||||||
|
<VoteProgress
|
||||||
|
threshold={requiredMajorityPercentage}
|
||||||
|
progress={yesPercentage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return '-';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AgGridColumn
|
||||||
|
colId="cDate"
|
||||||
|
maxWidth={150}
|
||||||
|
hide={window.innerWidth <= BREAKPOINT_MD}
|
||||||
|
headerName={t('Closing date')}
|
||||||
|
field="terms.closingDatetime"
|
||||||
|
valueFormatter={({
|
||||||
|
value,
|
||||||
|
}: VegaValueFormatterParams<
|
||||||
|
ProposalListFieldsFragment,
|
||||||
|
'terms.closingDatetime'
|
||||||
|
>) => {
|
||||||
|
return value ? getDateTimeFormat().format(new Date(value)) : '-';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AgGridColumn
|
||||||
|
colId="eDate"
|
||||||
|
maxWidth={150}
|
||||||
|
hide={window.innerWidth <= BREAKPOINT_MD}
|
||||||
|
headerName={t('Enactment date')}
|
||||||
|
field="terms.enactmentDatetime"
|
||||||
|
valueFormatter={({
|
||||||
|
value,
|
||||||
|
}: VegaValueFormatterParams<
|
||||||
|
ProposalListFieldsFragment,
|
||||||
|
'terms.enactmentDatetime'
|
||||||
|
>) => {
|
||||||
|
return value ? getDateTimeFormat().format(new Date(value)) : '-';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<AgGridColumn
|
||||||
|
colId="actions"
|
||||||
|
minWidth={window.innerWidth > BREAKPOINT_MD ? 221 : 80}
|
||||||
|
maxWidth={221}
|
||||||
|
sortable={false}
|
||||||
|
filter={false}
|
||||||
|
resizable={false}
|
||||||
|
cellRenderer={({
|
||||||
|
data,
|
||||||
|
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
|
||||||
|
const proposalPage = tokenLink(
|
||||||
|
TOKEN_PROPOSAL.replace(':id', data?.id || '')
|
||||||
|
);
|
||||||
|
const openDialog = () => {
|
||||||
|
if (!data) return;
|
||||||
|
setDialog({
|
||||||
|
open: true,
|
||||||
|
title: data.rationale.title,
|
||||||
|
content: data.terms,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="pb-1">
|
||||||
|
<button
|
||||||
|
className="underline max-md:hidden"
|
||||||
|
onClick={openDialog}
|
||||||
|
>
|
||||||
|
{t('View terms')}
|
||||||
|
</button>{' '}
|
||||||
|
<ExternalLink className="max-md:hidden" href={proposalPage}>
|
||||||
|
{t('Open in Governance')}
|
||||||
|
</ExternalLink>
|
||||||
|
<ExternalLink className="md:hidden" href={proposalPage}>
|
||||||
|
{t('Open')}
|
||||||
|
</ExternalLink>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</AgGrid>
|
||||||
|
<JsonViewerDialog
|
||||||
|
open={dialog.open}
|
||||||
|
onChange={(isOpen) => setDialog({ ...dialog, open: isOpen })}
|
||||||
|
title={dialog.title}
|
||||||
|
content={dialog.content}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -33,4 +33,7 @@ export const ENV = {
|
|||||||
parties: truthy.includes(windowOrDefault('NX_EXPLORER_PARTIES')),
|
parties: truthy.includes(windowOrDefault('NX_EXPLORER_PARTIES')),
|
||||||
validators: truthy.includes(windowOrDefault('NX_EXPLORER_VALIDATORS')),
|
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 { useAssetsDataProvider } from '@vegaprotocol/assets';
|
||||||
import { AssetsTable } from '../../components/assets/assets-table';
|
import { AssetsTable } from '../../components/assets/assets-table';
|
||||||
|
|
||||||
export const Assets = () => {
|
export const AssetsPage = () => {
|
||||||
useDocumentTitle(['Assets']);
|
useDocumentTitle(['Assets']);
|
||||||
useScrollToLocation();
|
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';
|
export * from './proposals-page';
|
||||||
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;
|
|
||||||
|
|||||||
@@ -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';
|
export * from './markets-page';
|
||||||
import { Loader, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
|
export * from './market-page';
|
||||||
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;
|
|
||||||
|
|||||||
@@ -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 BlockPage from './blocks';
|
||||||
import Governance from './governance';
|
import { Proposals } from './governance';
|
||||||
import Home from './home';
|
import Home from './home';
|
||||||
import Markets from './markets';
|
|
||||||
import OraclePage from './oracles';
|
import OraclePage from './oracles';
|
||||||
import Oracles from './oracles/home';
|
import Oracles from './oracles/home';
|
||||||
import { Oracle } from './oracles/id';
|
import { Oracle } from './oracles/id';
|
||||||
@@ -21,8 +20,13 @@ import flags from '../config/flags';
|
|||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import { Routes } from './route-names';
|
import { Routes } from './route-names';
|
||||||
import { NetworkParameters } from './network-parameters';
|
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,
|
path: Routes.PARTIES,
|
||||||
@@ -43,18 +47,27 @@ const partiesRoutes = flags.parties
|
|||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const assetsRoutes = flags.assets
|
const assetsRoutes: Route[] = flags.assets
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
path: Routes.ASSETS,
|
path: Routes.ASSETS,
|
||||||
text: t('Assets'),
|
text: t('Assets'),
|
||||||
name: '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,
|
path: Routes.GENESIS,
|
||||||
@@ -65,29 +78,38 @@ const genesisRoutes = flags.genesis
|
|||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const governanceRoutes = flags.governance
|
const governanceRoutes: Route[] = flags.governance
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
path: Routes.GOVERNANCE,
|
path: Routes.GOVERNANCE,
|
||||||
name: 'Governance proposals',
|
name: 'Governance proposals',
|
||||||
text: t('Governance Proposals'),
|
text: t('Governance Proposals'),
|
||||||
element: <Governance />,
|
element: <Proposals />,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const marketsRoutes = flags.markets
|
const marketsRoutes: Route[] = flags.markets
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
path: Routes.MARKETS,
|
path: Routes.MARKETS,
|
||||||
name: 'Markets',
|
name: 'Markets',
|
||||||
text: t('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,
|
path: Routes.NETWORK_PARAMETERS,
|
||||||
@@ -97,7 +119,7 @@ const networkParametersRoutes = flags.networkParameters
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
const validators = flags.validators
|
const validators: Route[] = flags.validators
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
path: Routes.VALIDATORS,
|
path: Routes.VALIDATORS,
|
||||||
@@ -108,7 +130,7 @@ const validators = flags.validators
|
|||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const routerConfig = [
|
const routerConfig: Route[] = [
|
||||||
{
|
{
|
||||||
path: Routes.HOME,
|
path: Routes.HOME,
|
||||||
name: '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 { useRoutes } from 'react-router-dom';
|
||||||
|
|
||||||
import '../styles.scss';
|
import '../styles.scss';
|
||||||
@@ -5,14 +7,40 @@ import { Navbar } from './components/navbar';
|
|||||||
|
|
||||||
import { routerConfig } from './routes/router-config';
|
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);
|
const AppRouter = () => useRoutes(routerConfig);
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
|
useInitializeEnv();
|
||||||
return (
|
return (
|
||||||
<div className="max-h-full min-h-full bg-white">
|
<NetworkLoader cache={cache}>
|
||||||
<Navbar />
|
<div className="max-h-full min-h-full bg-white">
|
||||||
<AppRouter />
|
<Navbar />
|
||||||
</div>
|
<AppRouter />
|
||||||
|
</div>
|
||||||
|
</NetworkLoader>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +1,15 @@
|
|||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import { EnvironmentProvider, NetworkLoader } from '@vegaprotocol/environment';
|
|
||||||
|
|
||||||
import App from './app/app';
|
import App from './app/app';
|
||||||
import type { InMemoryCacheConfig } from '@apollo/client';
|
|
||||||
|
|
||||||
const rootElement = document.getElementById('root');
|
const rootElement = document.getElementById('root');
|
||||||
const root = rootElement && createRoot(rootElement);
|
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(
|
root?.render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<EnvironmentProvider>
|
<App />
|
||||||
<NetworkLoader cache={cache}>
|
|
||||||
<App />
|
|
||||||
</NetworkLoader>
|
|
||||||
</EnvironmentProvider>
|
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</StrictMode>
|
</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_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_NETWORKS='{"TESTNET":"https://multisig-signer.fairground.wtf","MAINNET":"https://multisig-signer.vega.xyz"}'
|
||||||
NX_VEGA_ENV=STAGNET3
|
NX_VEGA_ENV=STAGNET3
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import classnames from 'classnames';
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { BrowserTracing } from '@sentry/tracing';
|
import { BrowserTracing } from '@sentry/tracing';
|
||||||
import {
|
import {
|
||||||
EnvironmentProvider,
|
|
||||||
NetworkLoader,
|
NetworkLoader,
|
||||||
useEnvironment,
|
useEnvironment,
|
||||||
|
useInitializeEnv,
|
||||||
} from '@vegaprotocol/environment';
|
} from '@vegaprotocol/environment';
|
||||||
import { AsyncRenderer, Button, Lozenge } from '@vegaprotocol/ui-toolkit';
|
import { AsyncRenderer, Button, Lozenge } from '@vegaprotocol/ui-toolkit';
|
||||||
import type { EthereumConfig } from '@vegaprotocol/web3';
|
import type { EthereumConfig } from '@vegaprotocol/web3';
|
||||||
@@ -64,6 +64,7 @@ function App() {
|
|||||||
environment: VEGA_ENV,
|
environment: VEGA_ENV,
|
||||||
});
|
});
|
||||||
}, [VEGA_ENV]);
|
}, [VEGA_ENV]);
|
||||||
|
|
||||||
const Connectors = useMemo(() => {
|
const Connectors = useMemo(() => {
|
||||||
if (config?.chain_id) {
|
if (config?.chain_id) {
|
||||||
return createConnectors(ETHEREUM_PROVIDER_URL, Number(config.chain_id));
|
return createConnectors(ETHEREUM_PROVIDER_URL, Number(config.chain_id));
|
||||||
@@ -107,12 +108,11 @@ const Wrapper = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
useInitializeEnv();
|
||||||
return (
|
return (
|
||||||
<EnvironmentProvider>
|
<NetworkLoader cache={cache}>
|
||||||
<NetworkLoader cache={cache}>
|
<App />
|
||||||
<App />
|
</NetworkLoader>
|
||||||
</NetworkLoader>
|
|
||||||
</EnvironmentProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
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.wait_for_spinner();
|
||||||
cy.connectVegaWallet();
|
cy.connectVegaWallet();
|
||||||
cy.ethereum_wallet_connect();
|
cy.ethereum_wallet_connect();
|
||||||
|
cy.ensure_specified_unstaked_tokens_are_associated('1');
|
||||||
cy.navigate_to('proposals');
|
cy.navigate_to('proposals');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Able to submit valid update network parameter proposal', function () {
|
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);
|
cy.go_to_make_new_proposal(governanceProposalType.NETWORK_PARAMETER);
|
||||||
// 3002-PROP-006
|
// 3002-PROP-006
|
||||||
cy.get(newProposalTitle).type('Test update network parameter proposal');
|
cy.get(newProposalTitle).type('Test update network parameter proposal');
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import {
|
||||||
|
createFreeformProposal,
|
||||||
|
createRawProposal,
|
||||||
|
generateFreeFormProposalTitle,
|
||||||
|
governanceProposalType,
|
||||||
|
} from '../../support/governance.functions';
|
||||||
|
|
||||||
|
const proposalDetailsTitle = '[data-testid="proposal-title"]';
|
||||||
|
const openProposals = '[data-testid="open-proposals"]';
|
||||||
|
const voteStatus = '[data-testid="vote-status"]';
|
||||||
|
const viewProposalButton = '[data-testid="view-proposal-btn"]';
|
||||||
|
|
||||||
|
describe('Governance flow for proposal list', { tags: '@slow' }, function () {
|
||||||
|
before('connect wallets and set approval limit', function () {
|
||||||
|
cy.vega_wallet_set_specified_approval_amount('1000');
|
||||||
|
cy.visit('/');
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach('visit proposals tab', function () {
|
||||||
|
cy.reload();
|
||||||
|
cy.wait_for_spinner();
|
||||||
|
cy.connectVegaWallet();
|
||||||
|
cy.ethereum_wallet_connect();
|
||||||
|
cy.ensure_specified_unstaked_tokens_are_associated(1);
|
||||||
|
cy.navigate_to_page_if_not_already_loaded('proposals');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Newly created proposals list - proposals closest to closing date appear higher in list', function () {
|
||||||
|
const minCloseDays = 2;
|
||||||
|
const maxCloseDays = 3;
|
||||||
|
|
||||||
|
// 3001-VOTE-005
|
||||||
|
let proposalDays = [
|
||||||
|
minCloseDays + 1,
|
||||||
|
maxCloseDays,
|
||||||
|
minCloseDays + 3,
|
||||||
|
minCloseDays + 2,
|
||||||
|
];
|
||||||
|
for (var index = 0; index < proposalDays.length; index++) {
|
||||||
|
cy.go_to_make_new_proposal(governanceProposalType.RAW);
|
||||||
|
cy.create_ten_digit_unix_timestamp_for_specified_days(
|
||||||
|
proposalDays[index]
|
||||||
|
).then((closingDateTimestamp) => {
|
||||||
|
cy.enter_raw_proposal_body(closingDateTimestamp);
|
||||||
|
});
|
||||||
|
cy.wait_for_proposal_submitted();
|
||||||
|
cy.wait_for_proposal_sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
let arrayOfProposals = [];
|
||||||
|
|
||||||
|
cy.navigate_to('proposals');
|
||||||
|
cy.get(proposalDetailsTitle)
|
||||||
|
.each((proposalTitleElement) => {
|
||||||
|
arrayOfProposals.push(proposalTitleElement.text());
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
cy.get_sort_order_of_supplied_array(arrayOfProposals).should(
|
||||||
|
'equal',
|
||||||
|
'descending'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Newly created proposals list - able to filter by proposerID to show it in list', function () {
|
||||||
|
const proposerId = Cypress.env('vegaWalletPublicKey');
|
||||||
|
const proposalTitle = generateFreeFormProposalTitle();
|
||||||
|
|
||||||
|
createFreeformProposal(proposalTitle);
|
||||||
|
cy.get_proposal_id_from_list(proposalTitle);
|
||||||
|
cy.get('@proposalIdText').then((proposalId) => {
|
||||||
|
cy.get('[data-testid="set-proposals-filter-visible"]').click();
|
||||||
|
cy.get('[data-testid="filter-input"]').type(proposerId);
|
||||||
|
cy.get(`#${proposalId}`).should('contain', proposalId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Newly created proposals list - shows title and portion of summary', function () {
|
||||||
|
createRawProposal(this.minProposerBalance); // 3001-VOTE-052
|
||||||
|
cy.get('@rawProposal').then((rawProposal) => {
|
||||||
|
cy.get_proposal_id_from_list(rawProposal.rationale.title);
|
||||||
|
cy.get('@proposalIdText').then((proposalId) => {
|
||||||
|
cy.get(openProposals).within(() => {
|
||||||
|
// 3001-VOTE-008
|
||||||
|
// 3001-VOTE-034
|
||||||
|
cy.get(`#${proposalId}`)
|
||||||
|
// 3001-VOTE-097
|
||||||
|
.should('contain', rawProposal.rationale.title)
|
||||||
|
.and('be.visible');
|
||||||
|
cy.get(`#${proposalId}`)
|
||||||
|
.should(
|
||||||
|
'contain',
|
||||||
|
rawProposal.rationale.description.substring(0, 59)
|
||||||
|
)
|
||||||
|
.and('be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Newly created proposals list - shows open proposals in an open state', function () {
|
||||||
|
// 3001-VOTE-004
|
||||||
|
// 3001-VOTE-035
|
||||||
|
createRawProposal(this.minProposerBalance);
|
||||||
|
cy.get('@rawProposal').then((rawProposal) => {
|
||||||
|
cy.get_submitted_proposal_from_proposal_list(
|
||||||
|
rawProposal.rationale.title
|
||||||
|
).within(() => {
|
||||||
|
cy.get(viewProposalButton).should('be.visible').click();
|
||||||
|
});
|
||||||
|
cy.get('@proposalIdText').then((proposalId) => {
|
||||||
|
cy.get_proposal_information_from_table('ID')
|
||||||
|
.contains(proposalId)
|
||||||
|
.and('be.visible');
|
||||||
|
});
|
||||||
|
cy.get_proposal_information_from_table('State')
|
||||||
|
.contains('Open')
|
||||||
|
.and('be.visible');
|
||||||
|
cy.get_proposal_information_from_table('Type')
|
||||||
|
.contains('Freeform')
|
||||||
|
.and('be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3001-VOTE-071
|
||||||
|
it('Newly created freeform proposals list - shows proposal participation - both met and not', function () {
|
||||||
|
const proposalTitle = generateFreeFormProposalTitle();
|
||||||
|
const requiredParticipation = 0.001;
|
||||||
|
|
||||||
|
createFreeformProposal(proposalTitle);
|
||||||
|
|
||||||
|
cy.get_submitted_proposal_from_proposal_list(proposalTitle)
|
||||||
|
.as('submittedProposal')
|
||||||
|
.within(() => {
|
||||||
|
// 3001-VOTE-039
|
||||||
|
cy.get(voteStatus).should('have.text', 'Participation not reached');
|
||||||
|
cy.get(viewProposalButton).click();
|
||||||
|
});
|
||||||
|
cy.vote_for_proposal('for');
|
||||||
|
cy.get_proposal_information_from_table('Total Supply')
|
||||||
|
.invoke('text')
|
||||||
|
.then((totalSupply) => {
|
||||||
|
let tokensRequiredToAchieveResult = parseFloat(
|
||||||
|
(totalSupply.replace(/,/g, '') * requiredParticipation) / 100
|
||||||
|
).toFixed(2);
|
||||||
|
cy.ensure_specified_unstaked_tokens_are_associated(
|
||||||
|
tokensRequiredToAchieveResult
|
||||||
|
);
|
||||||
|
cy.navigate_to_page_if_not_already_loaded('proposals');
|
||||||
|
cy.get('@submittedProposal').within(() =>
|
||||||
|
cy.get(viewProposalButton).click()
|
||||||
|
);
|
||||||
|
cy.get_proposal_information_from_table('Token participation met')
|
||||||
|
.contains('👍')
|
||||||
|
.should('be.visible');
|
||||||
|
cy.navigate_to('proposals');
|
||||||
|
cy.get('@submittedProposal').within(() =>
|
||||||
|
cy.get(voteStatus).should('have.text', 'Set to pass')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -63,6 +63,39 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
|||||||
.and('have.text', 'Browse, vote, and propose');
|
.and('have.text', 'Browse, vote, and propose');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it('should show open or enacted proposals with proposal summary', function () {
|
||||||
|
cy.get('body').then(($body) => {
|
||||||
|
if (!$body.find('[data-testid="proposals-list-item"]').length) {
|
||||||
|
cy.createMarket();
|
||||||
|
cy.reload();
|
||||||
|
cy.wait_for_spinner();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cy.getByTestId('proposals-list-item')
|
||||||
|
.should('have.length.at.least', 1)
|
||||||
|
.first()
|
||||||
|
.within(() => {
|
||||||
|
cy.getByTestId('proposal-title')
|
||||||
|
.invoke('text')
|
||||||
|
.should('not.be.empty');
|
||||||
|
cy.getByTestId('proposal-type')
|
||||||
|
.invoke('text')
|
||||||
|
.should('not.be.empty');
|
||||||
|
cy.getByTestId('proposal-description')
|
||||||
|
.invoke('text')
|
||||||
|
.should('not.be.empty');
|
||||||
|
cy.getByTestId('proposal-details')
|
||||||
|
.invoke('text')
|
||||||
|
.should('not.be.empty');
|
||||||
|
cy.getByTestId('proposal-status')
|
||||||
|
.invoke('text')
|
||||||
|
.should('not.be.empty');
|
||||||
|
cy.getByTestId('vote-details')
|
||||||
|
.invoke('text')
|
||||||
|
.should('not.be.empty');
|
||||||
|
cy.getByTestId('view-proposal-btn').should('be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
it('should have external link for governance', function () {
|
it('should have external link for governance', function () {
|
||||||
cy.getByTestId('home-proposals').within(() => {
|
cy.getByTestId('home-proposals').within(() => {
|
||||||
cy.getByTestId('external-link')
|
cy.getByTestId('external-link')
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ context('View functionality with public key', { tags: '@smoke' }, function () {
|
|||||||
cy.navigate_to('proposals');
|
cy.navigate_to('proposals');
|
||||||
cy.go_to_make_new_proposal('Freeform');
|
cy.go_to_make_new_proposal('Freeform');
|
||||||
cy.enter_unique_freeform_proposal_body('50', 'pub key proposal test');
|
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.getByTestId('dialog-content').within(() => {
|
||||||
cy.get('h1').should('have.text', 'Transaction failed');
|
cy.get('h1').should('have.text', 'Transaction failed');
|
||||||
cy.getByTestId('Error').should('have.text', expectedErrorTxt);
|
cy.getByTestId('Error').should('have.text', expectedErrorTxt);
|
||||||
|
|||||||
+2
-2
@@ -30,11 +30,11 @@ context('Staking Page - verify elements on page', function () {
|
|||||||
|
|
||||||
describe('with wallets disconnected', { tags: '@smoke' }, function () {
|
describe('with wallets disconnected', { tags: '@smoke' }, function () {
|
||||||
describe('description section', function () {
|
describe('description section', function () {
|
||||||
it('Should have staking tab highlighted', function () {
|
it('Should have validators tab highlighted', function () {
|
||||||
cy.verify_tab_highlighted('validators');
|
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');
|
cy.verify_page_header('Validators');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -338,8 +338,8 @@ context(
|
|||||||
.contains(currency.id)
|
.contains(currency.id)
|
||||||
.parent()
|
.parent()
|
||||||
.siblings()
|
.siblings()
|
||||||
.within(() => cy.contains_exactly('10.00').should('be.visible'));
|
.invoke('text')
|
||||||
|
.should('not.be.empty');
|
||||||
cy.get(vegaWalletCurrencyTitle)
|
cy.get(vegaWalletCurrencyTitle)
|
||||||
.contains(currency.id)
|
.contains(currency.id)
|
||||||
.parent()
|
.parent()
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
const epochTimeout = Cypress.env('epochTimeout');
|
||||||
|
const txTimeout = Cypress.env('txTimeout');
|
||||||
|
|
||||||
Cypress.Commands.add(
|
Cypress.Commands.add(
|
||||||
'convert_token_value_to_number',
|
'convert_token_value_to_number',
|
||||||
{ prevSubject: true },
|
{ 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, Cypress.env('epochTimeout')).should('exist');
|
||||||
cy.get(navigation.pageSpinner, { timeout: 20000 }).should('not.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 voteButtons = '[data-testid="vote-buttons"]';
|
||||||
const dialogTitle = '[data-testid="dialog-title"]';
|
const dialogTitle = '[data-testid="dialog-title"]';
|
||||||
const proposalVoteDeadline = '[data-testid="proposal-vote-deadline"]';
|
const proposalVoteDeadline = '[data-testid="proposal-vote-deadline"]';
|
||||||
|
const newProposalSubmitButton = '[data-testid="proposal-submit"]';
|
||||||
const dialogCloseButton = '[data-testid="dialog-close"]';
|
const dialogCloseButton = '[data-testid="dialog-close"]';
|
||||||
const epochTimeout = Cypress.env('epochTimeout');
|
const epochTimeout = Cypress.env('epochTimeout');
|
||||||
const proposalTimeout = { timeout: 14000 };
|
const proposalTimeout = { timeout: 14000 };
|
||||||
@@ -61,6 +62,7 @@ Cypress.Commands.add('enter_raw_proposal_body', (timestamp) => {
|
|||||||
parseSpecialCharSequences: false,
|
parseSpecialCharSequences: false,
|
||||||
delay: 2,
|
delay: 2,
|
||||||
});
|
});
|
||||||
|
cy.get(newProposalSubmitButton).should('be.visible').click();
|
||||||
cy.wrap(rawProposal);
|
cy.wrap(rawProposal);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -73,6 +75,7 @@ Cypress.Commands.add(
|
|||||||
'this is a e2e freeform proposal description'
|
'this is a e2e freeform proposal description'
|
||||||
);
|
);
|
||||||
cy.get(proposalVoteDeadline).clear().click().type(timestamp);
|
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.contains('Proposal submitted', proposalTimeout).should('be.visible');
|
||||||
cy.get(dialogCloseButton).click();
|
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-eth.functions.js';
|
||||||
import './wallet-teardown.functions.js';
|
import './wallet-teardown.functions.js';
|
||||||
import './wallet-vega.functions.js';
|
import './wallet-vega.functions.js';
|
||||||
import './proposal.functions.js';
|
import './proposal.functions.ts';
|
||||||
import registerCypressGrep from '@cypress/grep';
|
import registerCypressGrep from '@cypress/grep';
|
||||||
import { aliasGQLQuery } from '@vegaprotocol/cypress';
|
import { aliasGQLQuery } from '@vegaprotocol/cypress';
|
||||||
import { chainIdQuery, statisticsQuery } from '@vegaprotocol/mock';
|
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 { useEthereumConfig } from '@vegaprotocol/web3';
|
||||||
import {
|
import {
|
||||||
useEnvironment,
|
useEnvironment,
|
||||||
EnvironmentProvider,
|
|
||||||
NetworkLoader,
|
NetworkLoader,
|
||||||
|
useInitializeEnv,
|
||||||
} from '@vegaprotocol/environment';
|
} from '@vegaprotocol/environment';
|
||||||
import { createConnectors } from './lib/web3-connectors';
|
import { createConnectors } from './lib/web3-connectors';
|
||||||
import { ENV } from './config/env';
|
import { ENV } from './config/env';
|
||||||
@@ -166,12 +166,12 @@ const AppContainer = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
useInitializeEnv();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnvironmentProvider>
|
<NetworkLoader cache={cache}>
|
||||||
<NetworkLoader cache={cache}>
|
<AppContainer />
|
||||||
<AppContainer />
|
</NetworkLoader>
|
||||||
</NetworkLoader>
|
|
||||||
</EnvironmentProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const NavDropDown = ({ navbarTheme }: { navbarTheme: NavbarTheme }) => {
|
|||||||
<AppNavLink
|
<AppNavLink
|
||||||
name={
|
name={
|
||||||
<NavDropdownMenuTrigger
|
<NavDropdownMenuTrigger
|
||||||
className="w-auto flex items-center"
|
className="w-auto flex items-center -m-3 p-3 cursor-pointer"
|
||||||
data-testid="state-trigger"
|
data-testid="state-trigger"
|
||||||
onClick={() => setOpen(!isOpen)}
|
onClick={() => setOpen(!isOpen)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,23 +1,18 @@
|
|||||||
import { render, screen, within } from '@testing-library/react';
|
import { render, screen, within } from '@testing-library/react';
|
||||||
import { EnvironmentProvider, Networks } from '@vegaprotocol/environment';
|
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { Nav } from './nav';
|
import { Nav } from './nav';
|
||||||
|
|
||||||
jest.mock('@vegaprotocol/environment', () => ({
|
jest.mock('@vegaprotocol/environment', () => ({
|
||||||
...jest.requireActual('@vegaprotocol/environment'),
|
...jest.requireActual('@vegaprotocol/environment'),
|
||||||
NetworkSwitcher: () => <div data-testid="network-switcher" />,
|
NetworkSwitcher: () => <div data-testid="network-switcher" />,
|
||||||
|
useEnvironment: () => ({ VEGA_ENV: 'MAINNET' }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const renderComponent = (initialEntries?: string[]) => {
|
const renderComponent = (initialEntries?: string[]) => {
|
||||||
return render(
|
return render(
|
||||||
<EnvironmentProvider
|
<MemoryRouter initialEntries={initialEntries}>
|
||||||
definitions={{ VEGA_ENV: Networks.MAINNET }}
|
<Nav />
|
||||||
config={{ hosts: [] }}
|
</MemoryRouter>
|
||||||
>
|
|
||||||
<MemoryRouter initialEntries={initialEntries}>
|
|
||||||
<Nav />
|
|
||||||
</MemoryRouter>
|
|
||||||
</EnvironmentProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 { usePendingBalancesStore } from './use-pending-balances-manager';
|
||||||
import type { Event } from 'ethers';
|
import type { Event } from 'ethers';
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { Event } from 'ethers';
|
|
||||||
import uniqBy from 'lodash/uniqBy';
|
import uniqBy from 'lodash/uniqBy';
|
||||||
|
import { create } from 'zustand';
|
||||||
import create from 'zustand';
|
import type { Event } from 'ethers';
|
||||||
|
|
||||||
export type PendingTxsStore = {
|
export type PendingTxsStore = {
|
||||||
pendingBalances: Event[];
|
pendingBalances: Event[];
|
||||||
|
|||||||
@@ -432,6 +432,7 @@
|
|||||||
"associatedWithVegaKeys": "Associated with Vega keys",
|
"associatedWithVegaKeys": "Associated with Vega keys",
|
||||||
"thisEpoch": "This Epoch",
|
"thisEpoch": "This Epoch",
|
||||||
"nextEpoch": "Next epoch",
|
"nextEpoch": "Next epoch",
|
||||||
|
"toSeeYourRewardsConnectYourWallet": "TO SEE YOUR REWARDS, CONNECT YOUR WALLET",
|
||||||
"rewardsIntro": "Earn rewards and infrastructure fees for trading and maintaining the network.",
|
"rewardsIntro": "Earn rewards and infrastructure fees for trading and maintaining the network.",
|
||||||
"rewardsCallout": "Rewards are credited {{duration}} after the epoch ends.",
|
"rewardsCallout": "Rewards are credited {{duration}} after the epoch ends.",
|
||||||
"rewardsCalloutDetail": "This delay is set by a network parameter",
|
"rewardsCalloutDetail": "This delay is set by a network parameter",
|
||||||
@@ -457,6 +458,7 @@
|
|||||||
"rewardsColMarketCreationHeader": "MARKET CREATION",
|
"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]",
|
"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",
|
"rewardsColTotalHeader": "TOTAL",
|
||||||
|
"ofTotalDistributed": "of total distributed",
|
||||||
"checkBackSoon": "Check back soon",
|
"checkBackSoon": "Check back soon",
|
||||||
"yourStake": "Your stake",
|
"yourStake": "Your stake",
|
||||||
"reward": "Reward",
|
"reward": "Reward",
|
||||||
|
|||||||
@@ -137,6 +137,24 @@ query Proposal($proposalId: ID!) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
priceMonitoringParameters {
|
||||||
|
triggers {
|
||||||
|
horizonSecs
|
||||||
|
probability
|
||||||
|
auctionExtensionSecs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
liquidityMonitoringParameters {
|
||||||
|
triggeringRatio
|
||||||
|
targetStakeParameters {
|
||||||
|
timeWindow
|
||||||
|
scalingFactor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
positionDecimalPlaces
|
||||||
|
lpPriceRange
|
||||||
|
linearSlippageFactor
|
||||||
|
quadraticSlippageFactor
|
||||||
}
|
}
|
||||||
... on UpdateMarket {
|
... on UpdateMarket {
|
||||||
marketId
|
marketId
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,25 @@
|
|||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { AppStateProvider } from '../../contexts/app-state/app-state-provider';
|
||||||
|
import { ConnectToSeeRewards } from './connect-to-see-rewards';
|
||||||
|
|
||||||
|
describe('ConnectToSeeRewards', () => {
|
||||||
|
it('should render button correctly', () => {
|
||||||
|
const { getByTestId } = render(
|
||||||
|
<AppStateProvider>
|
||||||
|
<ConnectToSeeRewards />
|
||||||
|
</AppStateProvider>
|
||||||
|
);
|
||||||
|
expect(getByTestId('connect-to-vega-wallet-btn')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render the correct text', () => {
|
||||||
|
const { getByText } = render(
|
||||||
|
<AppStateProvider>
|
||||||
|
<ConnectToSeeRewards />
|
||||||
|
</AppStateProvider>
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
getByText('TO SEE YOUR REWARDS, CONNECT YOUR WALLET')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||||
|
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import {
|
||||||
|
AppStateActionType,
|
||||||
|
useAppState,
|
||||||
|
} from '../../contexts/app-state/app-state-context';
|
||||||
|
import { SubHeading } from '../../components/heading';
|
||||||
|
|
||||||
|
export const ConnectToSeeRewards = () => {
|
||||||
|
const { appDispatch } = useAppState();
|
||||||
|
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
|
||||||
|
openVegaWalletDialog: store.openVegaWalletDialog,
|
||||||
|
}));
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const classes = classNames(
|
||||||
|
'flex flex-col items-center justify-center h-[300px] w-full',
|
||||||
|
'border border-vega-dark-200'
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes}>
|
||||||
|
<SubHeading title={t('toSeeYourRewardsConnectYourWallet')} />
|
||||||
|
<Button
|
||||||
|
data-testid="connect-to-vega-wallet-btn"
|
||||||
|
onClick={() => {
|
||||||
|
appDispatch({
|
||||||
|
type: AppStateActionType.SET_VEGA_WALLET_OVERLAY,
|
||||||
|
isOpen: true,
|
||||||
|
});
|
||||||
|
openVegaWalletDialog();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('connectVegaWallet')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useAppState } from '../../../contexts/app-state/app-state-context';
|
|
||||||
import { formatNumber, toBigNum } from '@vegaprotocol/react-helpers';
|
|
||||||
import { BigNumber } from '../../../lib/bignumber';
|
|
||||||
import { KeyValueTable, KeyValueTableRow } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { format } from 'date-fns';
|
|
||||||
import { DATE_FORMAT_DETAILED } from '../../../lib/date-formats';
|
|
||||||
import type {
|
|
||||||
DelegationFieldsFragment,
|
|
||||||
RewardFieldsFragment,
|
|
||||||
} from '../home/__generated__/Rewards';
|
|
||||||
|
|
||||||
interface RewardTableProps {
|
|
||||||
reward: RewardFieldsFragment;
|
|
||||||
delegations: DelegationFieldsFragment[] | [];
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RewardTable = ({ reward, delegations }: RewardTableProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const {
|
|
||||||
appState: { decimals },
|
|
||||||
} = useAppState();
|
|
||||||
|
|
||||||
// Get your stake for epoch in which you have rewards
|
|
||||||
const stakeForEpoch = useMemo(() => {
|
|
||||||
if (!delegations.length) return '0';
|
|
||||||
|
|
||||||
const delegationsForEpoch = delegations
|
|
||||||
.filter((d) => d.epoch.toString() === reward.epoch.id)
|
|
||||||
.map((d) => toBigNum(d.amount, decimals));
|
|
||||||
|
|
||||||
if (delegationsForEpoch.length) {
|
|
||||||
return BigNumber.sum.apply(null, [
|
|
||||||
new BigNumber(0),
|
|
||||||
...delegationsForEpoch,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new BigNumber(0);
|
|
||||||
}, [decimals, delegations, reward.epoch.id]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-24">
|
|
||||||
<h3 className="text-lg text-white mb-4">
|
|
||||||
{t('Epoch')} {reward.epoch.id}
|
|
||||||
</h3>
|
|
||||||
<KeyValueTable>
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('rewardType')}
|
|
||||||
<span>{reward.rewardType}</span>
|
|
||||||
</KeyValueTableRow>
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('yourStake')}
|
|
||||||
<span>{stakeForEpoch.toString()}</span>
|
|
||||||
</KeyValueTableRow>
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('reward')}
|
|
||||||
<span>
|
|
||||||
{formatNumber(toBigNum(reward.amount, decimals))}{' '}
|
|
||||||
{reward.asset.symbol}
|
|
||||||
</span>
|
|
||||||
</KeyValueTableRow>
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('shareOfReward')}
|
|
||||||
<span>
|
|
||||||
{new BigNumber(reward.percentageOfTotal).dp(2).toString()}%
|
|
||||||
</span>
|
|
||||||
</KeyValueTableRow>
|
|
||||||
<KeyValueTableRow>
|
|
||||||
{t('received')}
|
|
||||||
<span>
|
|
||||||
{format(new Date(reward.receivedAt), DATE_FORMAT_DETAILED)}
|
|
||||||
</span>
|
|
||||||
</KeyValueTableRow>
|
|
||||||
</KeyValueTable>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
import { render } from '@testing-library/react';
|
||||||
|
import { AppStateProvider } from '../../../contexts/app-state/app-state-provider';
|
||||||
|
import { EpochIndividualRewardsTable } from './epoch-individual-rewards-table';
|
||||||
|
|
||||||
|
const mockData = {
|
||||||
|
epoch: '4441',
|
||||||
|
rewards: [
|
||||||
|
{
|
||||||
|
asset: 'tDAI',
|
||||||
|
totalAmount: '5',
|
||||||
|
rewardTypes: {
|
||||||
|
ACCOUNT_TYPE_GLOBAL_REWARD: {
|
||||||
|
amount: '0',
|
||||||
|
percentageOfTotal: '0',
|
||||||
|
},
|
||||||
|
ACCOUNT_TYPE_FEES_INFRASTRUCTURE: {
|
||||||
|
amount: '5',
|
||||||
|
percentageOfTotal: '0.00305237260923',
|
||||||
|
},
|
||||||
|
ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES: {
|
||||||
|
amount: '0',
|
||||||
|
percentageOfTotal: '0',
|
||||||
|
},
|
||||||
|
ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES: {
|
||||||
|
amount: '0',
|
||||||
|
percentageOfTotal: '0',
|
||||||
|
},
|
||||||
|
ACCOUNT_TYPE_FEES_LIQUIDITY: {
|
||||||
|
amount: '0',
|
||||||
|
percentageOfTotal: '0',
|
||||||
|
},
|
||||||
|
ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS: {
|
||||||
|
amount: '0',
|
||||||
|
percentageOfTotal: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('EpochIndividualRewardsTable', () => {
|
||||||
|
it('should render correctly', () => {
|
||||||
|
const { getByTestId } = render(
|
||||||
|
<AppStateProvider>
|
||||||
|
<EpochIndividualRewardsTable data={mockData} />
|
||||||
|
</AppStateProvider>
|
||||||
|
);
|
||||||
|
expect(getByTestId('epoch-individual-rewards-table')).toBeInTheDocument();
|
||||||
|
expect(getByTestId('individual-rewards-asset')).toBeInTheDocument();
|
||||||
|
expect(getByTestId('ACCOUNT_TYPE_GLOBAL_REWARD')).toBeInTheDocument();
|
||||||
|
expect(getByTestId('ACCOUNT_TYPE_FEES_INFRASTRUCTURE')).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
getByTestId('ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
getByTestId('ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(getByTestId('ACCOUNT_TYPE_FEES_LIQUIDITY')).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
getByTestId('ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
import { formatNumber, toBigNum } from '@vegaprotocol/react-helpers';
|
||||||
|
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useAppState } from '../../../contexts/app-state/app-state-context';
|
||||||
|
import {
|
||||||
|
rowGridItemStyles,
|
||||||
|
RewardsTable,
|
||||||
|
} from '../shared-rewards-table-assets/shared-rewards-table-assets';
|
||||||
|
import type { EpochIndividualReward } from './generate-epoch-individual-rewards-list';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
interface EpochIndividualRewardsGridProps {
|
||||||
|
data: EpochIndividualReward;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RewardItemProps {
|
||||||
|
value: string;
|
||||||
|
percentageOfTotal?: string;
|
||||||
|
dataTestId: string;
|
||||||
|
last?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DisplayReward = ({
|
||||||
|
reward,
|
||||||
|
percentageOfTotal,
|
||||||
|
}: {
|
||||||
|
reward: string;
|
||||||
|
percentageOfTotal?: string;
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const {
|
||||||
|
appState: { decimals },
|
||||||
|
} = useAppState();
|
||||||
|
|
||||||
|
if (Number(reward) === 0) {
|
||||||
|
return <span className="text-vega-dark-300">-</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
description={
|
||||||
|
<div className="flex flex-col items-start">
|
||||||
|
<span>{formatNumber(toBigNum(reward, decimals), decimals)}</span>
|
||||||
|
{percentageOfTotal && (
|
||||||
|
<span className="text-vega-dark-300">
|
||||||
|
({percentageOfTotal}% {t('ofTotalDistributed')})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<button>
|
||||||
|
<div className="flex flex-col items-start">
|
||||||
|
<span>{formatNumber(toBigNum(reward, decimals))}</span>
|
||||||
|
{percentageOfTotal && (
|
||||||
|
<span className="text-vega-dark-300">
|
||||||
|
({formatNumber(toBigNum(percentageOfTotal, 4)).toString()}%)
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RewardItem = ({
|
||||||
|
value,
|
||||||
|
percentageOfTotal,
|
||||||
|
dataTestId,
|
||||||
|
last,
|
||||||
|
}: RewardItemProps) => (
|
||||||
|
<div data-testid={dataTestId} className={rowGridItemStyles(last)}>
|
||||||
|
<div className="h-full w-5 absolute right-0 top-0 bg-gradient-to-r from-transparent to-black pointer-events-none" />
|
||||||
|
<div className="overflow-auto p-5">
|
||||||
|
<DisplayReward reward={value} percentageOfTotal={percentageOfTotal} />
|
||||||
|
</div>
|
||||||
|
<div className="h-full w-5 absolute left-0 top-0 bg-gradient-to-l from-transparent to-black pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const EpochIndividualRewardsTable = ({
|
||||||
|
data,
|
||||||
|
}: EpochIndividualRewardsGridProps) => {
|
||||||
|
return (
|
||||||
|
<RewardsTable
|
||||||
|
dataTestId="epoch-individual-rewards-table"
|
||||||
|
epoch={Number(data.epoch)}
|
||||||
|
>
|
||||||
|
{data.rewards.map(({ asset, rewardTypes, totalAmount }, i) => (
|
||||||
|
<div className="contents" key={i}>
|
||||||
|
<div
|
||||||
|
data-testid="individual-rewards-asset"
|
||||||
|
className={`${rowGridItemStyles()} p-5`}
|
||||||
|
>
|
||||||
|
{asset}
|
||||||
|
</div>
|
||||||
|
{Object.entries(rewardTypes).map(
|
||||||
|
([key, { amount, percentageOfTotal }]) => (
|
||||||
|
<RewardItem
|
||||||
|
key={key}
|
||||||
|
value={amount}
|
||||||
|
percentageOfTotal={percentageOfTotal}
|
||||||
|
dataTestId={key}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<RewardItem dataTestId="total" value={totalAmount} last={true} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</RewardsTable>
|
||||||
|
);
|
||||||
|
};
|
||||||
+17
-21
@@ -5,9 +5,10 @@ import { removePaginationWrapper } from '@vegaprotocol/react-helpers';
|
|||||||
import { useRewardsQuery } from '../home/__generated__/Rewards';
|
import { useRewardsQuery } from '../home/__generated__/Rewards';
|
||||||
import { ENV } from '../../../config';
|
import { ENV } from '../../../config';
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
import { RewardTable } from './reward-table';
|
import { EpochIndividualRewardsTable } from './epoch-individual-rewards-table';
|
||||||
|
import { generateEpochIndividualRewardsList } from './generate-epoch-individual-rewards-list';
|
||||||
|
|
||||||
export const RewardInfo = () => {
|
export const EpochIndividualRewards = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { pubKey } = useVegaWallet();
|
const { pubKey } = useVegaWallet();
|
||||||
const { delegationsPagination } = ENV;
|
const { delegationsPagination } = ENV;
|
||||||
@@ -30,13 +31,10 @@ export const RewardInfo = () => {
|
|||||||
return removePaginationWrapper(data.party.rewardsConnection.edges);
|
return removePaginationWrapper(data.party.rewardsConnection.edges);
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const delegations = useMemo(() => {
|
const epochIndividualRewardSummaries = useMemo(() => {
|
||||||
if (!data?.party || !data.party.delegationsConnection?.edges?.length) {
|
if (!data?.party) return [];
|
||||||
return [];
|
return generateEpochIndividualRewardsList(rewards);
|
||||||
}
|
}, [data?.party, rewards]);
|
||||||
|
|
||||||
return removePaginationWrapper(data.party.delegationsConnection.edges);
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer
|
<AsyncRenderer
|
||||||
@@ -45,20 +43,18 @@ export const RewardInfo = () => {
|
|||||||
data={data}
|
data={data}
|
||||||
render={() => (
|
render={() => (
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p className="mb-10">
|
||||||
{t('Connected Vega key')}: {pubKey}
|
{t('Connected Vega key')}:{' '}
|
||||||
|
<span className="text-white">{pubKey}</span>
|
||||||
</p>
|
</p>
|
||||||
{rewards.length ? (
|
{epochIndividualRewardSummaries.length ? (
|
||||||
rewards.map((reward, i) => {
|
epochIndividualRewardSummaries.map(
|
||||||
if (!reward) return null;
|
(epochIndividualRewardSummary) => (
|
||||||
return (
|
<EpochIndividualRewardsTable
|
||||||
<RewardTable
|
data={epochIndividualRewardSummary}
|
||||||
key={i}
|
|
||||||
reward={reward}
|
|
||||||
delegations={delegations || []}
|
|
||||||
/>
|
/>
|
||||||
);
|
)
|
||||||
})
|
)
|
||||||
) : (
|
) : (
|
||||||
<p>{t('noRewards')}</p>
|
<p>{t('noRewards')}</p>
|
||||||
)}
|
)}
|
||||||
+235
@@ -0,0 +1,235 @@
|
|||||||
|
import { generateEpochIndividualRewardsList } from './generate-epoch-individual-rewards-list';
|
||||||
|
import { AccountType } from '@vegaprotocol/types';
|
||||||
|
import type { RewardFieldsFragment } from '../home/__generated__/Rewards';
|
||||||
|
|
||||||
|
describe('generateEpochIndividualRewardsList', () => {
|
||||||
|
const reward1: RewardFieldsFragment = {
|
||||||
|
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
|
||||||
|
amount: '100',
|
||||||
|
percentageOfTotal: '0.1',
|
||||||
|
receivedAt: new Date(),
|
||||||
|
asset: { id: 'usd', symbol: 'USD' },
|
||||||
|
party: { id: 'blah' },
|
||||||
|
epoch: { id: '1' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const reward2: RewardFieldsFragment = {
|
||||||
|
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
|
||||||
|
amount: '50',
|
||||||
|
percentageOfTotal: '0.05',
|
||||||
|
receivedAt: new Date(),
|
||||||
|
asset: { id: 'eur', symbol: 'EUR' },
|
||||||
|
party: { id: 'blah' },
|
||||||
|
epoch: { id: '2' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const reward3: RewardFieldsFragment = {
|
||||||
|
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||||
|
amount: '200',
|
||||||
|
percentageOfTotal: '0.2',
|
||||||
|
receivedAt: new Date(),
|
||||||
|
asset: { id: 'gbp', symbol: 'GBP' },
|
||||||
|
party: { id: 'blah' },
|
||||||
|
epoch: { id: '2' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const reward4: RewardFieldsFragment = {
|
||||||
|
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||||
|
amount: '100',
|
||||||
|
percentageOfTotal: '0.1',
|
||||||
|
receivedAt: new Date(),
|
||||||
|
asset: { id: 'usd', symbol: 'USD' },
|
||||||
|
party: { id: 'blah' },
|
||||||
|
epoch: { id: '1' },
|
||||||
|
};
|
||||||
|
|
||||||
|
const rewardWrongType: RewardFieldsFragment = {
|
||||||
|
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE,
|
||||||
|
amount: '50',
|
||||||
|
percentageOfTotal: '0.05',
|
||||||
|
receivedAt: new Date(),
|
||||||
|
asset: { id: 'eur', symbol: 'EUR' },
|
||||||
|
party: { id: 'blah' },
|
||||||
|
epoch: { id: '2' },
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should return an empty array if no rewards are provided', () => {
|
||||||
|
expect(generateEpochIndividualRewardsList([])).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter out any rewards of the wrong type', () => {
|
||||||
|
const result = generateEpochIndividualRewardsList([rewardWrongType]);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return reward in the correct format', () => {
|
||||||
|
const result = generateEpochIndividualRewardsList([reward1]);
|
||||||
|
|
||||||
|
expect(result[0]).toEqual({
|
||||||
|
epoch: '1',
|
||||||
|
rewards: [
|
||||||
|
{
|
||||||
|
asset: 'USD',
|
||||||
|
totalAmount: '100',
|
||||||
|
rewardTypes: {
|
||||||
|
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
|
||||||
|
amount: '0',
|
||||||
|
percentageOfTotal: '0',
|
||||||
|
},
|
||||||
|
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
|
||||||
|
amount: '0',
|
||||||
|
percentageOfTotal: '0',
|
||||||
|
},
|
||||||
|
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
|
||||||
|
amount: '100',
|
||||||
|
percentageOfTotal: '0.1',
|
||||||
|
},
|
||||||
|
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
|
||||||
|
amount: '0',
|
||||||
|
percentageOfTotal: '0',
|
||||||
|
},
|
||||||
|
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
|
||||||
|
amount: '0',
|
||||||
|
percentageOfTotal: '0',
|
||||||
|
},
|
||||||
|
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
|
||||||
|
amount: '0',
|
||||||
|
percentageOfTotal: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an array sorted by epoch descending', () => {
|
||||||
|
const rewards = [reward1, reward2, reward3, reward4];
|
||||||
|
const result1 = generateEpochIndividualRewardsList(rewards);
|
||||||
|
|
||||||
|
expect(result1[0].epoch).toEqual('2');
|
||||||
|
expect(result1[1].epoch).toEqual('1');
|
||||||
|
|
||||||
|
const reorderedRewards = [reward4, reward3, reward2, reward1];
|
||||||
|
const result2 = generateEpochIndividualRewardsList(reorderedRewards);
|
||||||
|
|
||||||
|
expect(result2[0].epoch).toEqual('2');
|
||||||
|
expect(result2[1].epoch).toEqual('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly calculates the total value of rewards for an asset', () => {
|
||||||
|
const rewards = [reward1, reward4];
|
||||||
|
const result = generateEpochIndividualRewardsList(rewards);
|
||||||
|
|
||||||
|
expect(result[0].rewards[0].totalAmount).toEqual('200');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns data in the expected shape', () => {
|
||||||
|
// Just sanity checking the whole structure here
|
||||||
|
const rewards = [reward1, reward2, reward3, reward4];
|
||||||
|
const result = generateEpochIndividualRewardsList(rewards);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
epoch: '2',
|
||||||
|
rewards: [
|
||||||
|
{
|
||||||
|
asset: 'EUR',
|
||||||
|
totalAmount: '50',
|
||||||
|
rewardTypes: {
|
||||||
|
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
|
||||||
|
amount: '0',
|
||||||
|
percentageOfTotal: '0',
|
||||||
|
},
|
||||||
|
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
|
||||||
|
amount: '0',
|
||||||
|
percentageOfTotal: '0',
|
||||||
|
},
|
||||||
|
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
|
||||||
|
amount: '50',
|
||||||
|
percentageOfTotal: '0.05',
|
||||||
|
},
|
||||||
|
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
|
||||||
|
amount: '0',
|
||||||
|
percentageOfTotal: '0',
|
||||||
|
},
|
||||||
|
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
|
||||||
|
amount: '0',
|
||||||
|
percentageOfTotal: '0',
|
||||||
|
},
|
||||||
|
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
|
||||||
|
amount: '0',
|
||||||
|
percentageOfTotal: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
asset: 'GBP',
|
||||||
|
totalAmount: '200',
|
||||||
|
rewardTypes: {
|
||||||
|
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
|
||||||
|
amount: '0',
|
||||||
|
percentageOfTotal: '0',
|
||||||
|
},
|
||||||
|
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
|
||||||
|
amount: '200',
|
||||||
|
percentageOfTotal: '0.2',
|
||||||
|
},
|
||||||
|
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
|
||||||
|
amount: '0',
|
||||||
|
percentageOfTotal: '0',
|
||||||
|
},
|
||||||
|
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
|
||||||
|
amount: '0',
|
||||||
|
percentageOfTotal: '0',
|
||||||
|
},
|
||||||
|
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
|
||||||
|
amount: '0',
|
||||||
|
percentageOfTotal: '0',
|
||||||
|
},
|
||||||
|
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
|
||||||
|
amount: '0',
|
||||||
|
percentageOfTotal: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
epoch: '1',
|
||||||
|
rewards: [
|
||||||
|
{
|
||||||
|
asset: 'USD',
|
||||||
|
totalAmount: '200',
|
||||||
|
rewardTypes: {
|
||||||
|
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
|
||||||
|
amount: '0',
|
||||||
|
percentageOfTotal: '0',
|
||||||
|
},
|
||||||
|
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
|
||||||
|
amount: '100',
|
||||||
|
percentageOfTotal: '0.1',
|
||||||
|
},
|
||||||
|
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
|
||||||
|
amount: '100',
|
||||||
|
percentageOfTotal: '0.1',
|
||||||
|
},
|
||||||
|
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
|
||||||
|
amount: '0',
|
||||||
|
percentageOfTotal: '0',
|
||||||
|
},
|
||||||
|
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
|
||||||
|
amount: '0',
|
||||||
|
percentageOfTotal: '0',
|
||||||
|
},
|
||||||
|
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
|
||||||
|
amount: '0',
|
||||||
|
percentageOfTotal: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
import { BigNumber } from '../../../lib/bignumber';
|
||||||
|
import { RowAccountTypes } from '../shared-rewards-table-assets/shared-rewards-table-assets';
|
||||||
|
import type { RewardFieldsFragment } from '../home/__generated__/Rewards';
|
||||||
|
import type { AccountType } from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
export interface EpochIndividualReward {
|
||||||
|
epoch: string;
|
||||||
|
rewards: {
|
||||||
|
asset: string;
|
||||||
|
totalAmount: string;
|
||||||
|
rewardTypes: {
|
||||||
|
[key in AccountType]?: {
|
||||||
|
amount: string;
|
||||||
|
percentageOfTotal: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountTypes = Object.keys(RowAccountTypes);
|
||||||
|
|
||||||
|
const emptyRowAccountTypes = accountTypes.map((type) => [
|
||||||
|
type,
|
||||||
|
{
|
||||||
|
amount: '0',
|
||||||
|
percentageOfTotal: '0',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const generateEpochIndividualRewardsList = (
|
||||||
|
rewards: RewardFieldsFragment[]
|
||||||
|
) => {
|
||||||
|
// We take the rewards and aggregate them by epoch and asset.
|
||||||
|
const epochIndividualRewards = rewards.reduce((map, reward) => {
|
||||||
|
const epochId = reward.epoch.id;
|
||||||
|
const assetName = reward.asset.symbol;
|
||||||
|
const rewardType = reward.rewardType;
|
||||||
|
const amount = reward.amount;
|
||||||
|
const percentageOfTotal = reward.percentageOfTotal;
|
||||||
|
|
||||||
|
// if the rewardType is not of a type we display in the table, we skip it.
|
||||||
|
if (!accountTypes.includes(rewardType)) {
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!map.has(epochId)) {
|
||||||
|
map.set(epochId, { epoch: epochId, rewards: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
const epoch = map.get(epochId);
|
||||||
|
|
||||||
|
let asset = epoch?.rewards.find((r) => r.asset === assetName);
|
||||||
|
|
||||||
|
if (!asset) {
|
||||||
|
asset = {
|
||||||
|
asset: assetName,
|
||||||
|
totalAmount: '0',
|
||||||
|
rewardTypes: Object.fromEntries(emptyRowAccountTypes),
|
||||||
|
};
|
||||||
|
epoch?.rewards.push(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
asset.rewardTypes[rewardType] = { amount, percentageOfTotal };
|
||||||
|
|
||||||
|
// totalAmount is the sum of all rewardTypes amounts
|
||||||
|
asset.totalAmount = Object.values(asset.rewardTypes).reduce(
|
||||||
|
(sum, rewardType) => {
|
||||||
|
return new BigNumber(sum).plus(rewardType.amount).toString();
|
||||||
|
},
|
||||||
|
'0'
|
||||||
|
);
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}, new Map<string, EpochIndividualReward>());
|
||||||
|
|
||||||
|
return Array.from(epochIndividualRewards.values()).sort(
|
||||||
|
(a, b) => Number(b.epoch) - Number(a.epoch)
|
||||||
|
);
|
||||||
|
};
|
||||||
+38
-7
@@ -1,4 +1,5 @@
|
|||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
|
import { AppStateProvider } from '../../../contexts/app-state/app-state-provider';
|
||||||
import { EpochTotalRewardsTable } from './epoch-total-rewards-table';
|
import { EpochTotalRewardsTable } from './epoch-total-rewards-table';
|
||||||
import { AccountType } from '@vegaprotocol/types';
|
import { AccountType } from '@vegaprotocol/types';
|
||||||
|
|
||||||
@@ -10,10 +11,30 @@ const mockData = {
|
|||||||
'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
|
'b340c130096819428a62e5df407fd6abe66e444b89ad64f670beb98621c9c663',
|
||||||
name: 'tDAI TEST',
|
name: 'tDAI TEST',
|
||||||
rewards: [
|
rewards: [
|
||||||
|
{
|
||||||
|
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
|
||||||
|
amount: '0',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||||
amount: '295',
|
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',
|
totalAmount: '295',
|
||||||
},
|
},
|
||||||
@@ -22,15 +43,25 @@ const mockData = {
|
|||||||
|
|
||||||
describe('EpochTotalRewardsTable', () => {
|
describe('EpochTotalRewardsTable', () => {
|
||||||
it('should render correctly', () => {
|
it('should render correctly', () => {
|
||||||
const { getByTestId } = render(<EpochTotalRewardsTable data={mockData} />);
|
const { getByTestId } = render(
|
||||||
|
<AppStateProvider>
|
||||||
|
<EpochTotalRewardsTable data={mockData} />
|
||||||
|
</AppStateProvider>
|
||||||
|
);
|
||||||
expect(getByTestId('epoch-total-rewards-table')).toBeInTheDocument();
|
expect(getByTestId('epoch-total-rewards-table')).toBeInTheDocument();
|
||||||
expect(getByTestId('asset')).toBeInTheDocument();
|
expect(getByTestId('asset')).toBeInTheDocument();
|
||||||
expect(getByTestId('global')).toBeInTheDocument();
|
expect(getByTestId('ACCOUNT_TYPE_GLOBAL_REWARD')).toBeInTheDocument();
|
||||||
expect(getByTestId('infra')).toBeInTheDocument();
|
expect(getByTestId('ACCOUNT_TYPE_FEES_INFRASTRUCTURE')).toBeInTheDocument();
|
||||||
expect(getByTestId('taker')).toBeInTheDocument();
|
expect(
|
||||||
expect(getByTestId('maker')).toBeInTheDocument();
|
getByTestId('ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES')
|
||||||
expect(getByTestId('liquidity')).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(getByTestId('market-maker')).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();
|
expect(getByTestId('total')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
import classNames from 'classnames';
|
import { formatNumber, toBigNum } from '@vegaprotocol/react-helpers';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||||
import { formatNumber } from '@vegaprotocol/react-helpers';
|
import { useAppState } from '../../../contexts/app-state/app-state-context';
|
||||||
import { Tooltip, Icon } from '@vegaprotocol/ui-toolkit';
|
import {
|
||||||
import { AccountType } from '@vegaprotocol/types';
|
rowGridItemStyles,
|
||||||
import { SubHeading } from '../../../components/heading';
|
RewardsTable,
|
||||||
import type { AggregatedEpochSummary } from './generate-epoch-total-rewards-list';
|
} from '../shared-rewards-table-assets/shared-rewards-table-assets';
|
||||||
|
import type { EpochTotalSummary } from './generate-epoch-total-rewards-list';
|
||||||
|
|
||||||
interface EpochTotalRewardsGridProps {
|
interface EpochTotalRewardsGridProps {
|
||||||
data: AggregatedEpochSummary;
|
data: EpochTotalSummary;
|
||||||
}
|
|
||||||
|
|
||||||
interface ColumnHeaderProps {
|
|
||||||
title: string;
|
|
||||||
tooltipContent?: string;
|
|
||||||
className?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RewardItemProps {
|
interface RewardItemProps {
|
||||||
@@ -22,61 +17,28 @@ interface RewardItemProps {
|
|||||||
last?: boolean;
|
last?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayReward = (reward: string) => {
|
const DisplayReward = ({ reward }: { reward: string }) => {
|
||||||
|
const {
|
||||||
|
appState: { decimals },
|
||||||
|
} = useAppState();
|
||||||
|
|
||||||
if (Number(reward) === 0) {
|
if (Number(reward) === 0) {
|
||||||
return <span className="text-vega-dark-300">0</span>;
|
return <span className="text-vega-dark-300">-</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reward.split('.')[1] && reward.split('.')[1].length > 4) {
|
return (
|
||||||
return (
|
<Tooltip description={formatNumber(toBigNum(reward, decimals), decimals)}>
|
||||||
<Tooltip description={formatNumber(reward)}>
|
<button>{formatNumber(toBigNum(reward, decimals))}</button>
|
||||||
<button>{formatNumber(Number(reward).toFixed(4))}</button>
|
</Tooltip>
|
||||||
</Tooltip>
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <span>{formatNumber(reward)}</span>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const gridStyles = classNames(
|
|
||||||
'grid grid-cols-[repeat(8,minmax(100px,auto))] max-w-full overflow-auto',
|
|
||||||
`border-t border-vega-dark-200`,
|
|
||||||
'text-sm'
|
|
||||||
);
|
|
||||||
|
|
||||||
const headerGridItemStyles = (last = false) =>
|
|
||||||
classNames('border-r border-b border-b-vega-dark-200', 'py-3 px-5', {
|
|
||||||
'border-r-vega-dark-150': !last,
|
|
||||||
'border-r-0': last,
|
|
||||||
});
|
|
||||||
|
|
||||||
const rowGridItemStyles = (last = false) =>
|
|
||||||
classNames('relative', 'border-r border-b border-b-vega-dark-150', {
|
|
||||||
'border-r-vega-dark-150': !last,
|
|
||||||
'border-r-0': last,
|
|
||||||
});
|
|
||||||
|
|
||||||
const ColumnHeader = ({
|
|
||||||
title,
|
|
||||||
tooltipContent,
|
|
||||||
className,
|
|
||||||
}: ColumnHeaderProps) => (
|
|
||||||
<div className={className}>
|
|
||||||
<h2 className="mb-1 text-sm text-vega-dark-300">{title}</h2>
|
|
||||||
{tooltipContent && (
|
|
||||||
<Tooltip description={tooltipContent}>
|
|
||||||
<button>
|
|
||||||
<Icon name={'info-sign'} className="text-vega-dark-200" />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const RewardItem = ({ value, dataTestId, last }: RewardItemProps) => (
|
const RewardItem = ({ value, dataTestId, last }: RewardItemProps) => (
|
||||||
<div data-testid={dataTestId} className={rowGridItemStyles(last)}>
|
<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="h-full w-5 absolute right-0 top-0 bg-gradient-to-r from-transparent to-black pointer-events-none" />
|
||||||
<div className="overflow-auto p-5">{displayReward(value)}</div>
|
<div className="overflow-auto p-5">
|
||||||
|
<DisplayReward reward={value} />
|
||||||
|
</div>
|
||||||
<div className="h-full w-5 absolute left-0 top-0 bg-gradient-to-l from-transparent to-black pointer-events-none" />
|
<div className="h-full w-5 absolute left-0 top-0 bg-gradient-to-l from-transparent to-black pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -84,129 +46,19 @@ const RewardItem = ({ value, dataTestId, last }: RewardItemProps) => (
|
|||||||
export const EpochTotalRewardsTable = ({
|
export const EpochTotalRewardsTable = ({
|
||||||
data,
|
data,
|
||||||
}: EpochTotalRewardsGridProps) => {
|
}: EpochTotalRewardsGridProps) => {
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const rowData = data.assetRewards.map(({ name, rewards, totalAmount }) => ({
|
|
||||||
name,
|
|
||||||
ACCOUNT_TYPE_GLOBAL_REWARD:
|
|
||||||
rewards
|
|
||||||
.filter((r) => r.rewardType === AccountType.ACCOUNT_TYPE_GLOBAL_REWARD)
|
|
||||||
.map((r) => r.amount)[0] || '0',
|
|
||||||
ACCOUNT_TYPE_FEES_INFRASTRUCTURE:
|
|
||||||
rewards
|
|
||||||
.filter(
|
|
||||||
(r) => r.rewardType === AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE
|
|
||||||
)
|
|
||||||
.map((r) => r.amount)[0] || '0',
|
|
||||||
ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES:
|
|
||||||
rewards
|
|
||||||
.filter(
|
|
||||||
(r) =>
|
|
||||||
r.rewardType === AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES
|
|
||||||
)
|
|
||||||
.map((r) => r.amount)[0] || '0',
|
|
||||||
ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES:
|
|
||||||
rewards
|
|
||||||
.filter(
|
|
||||||
(r) =>
|
|
||||||
r.rewardType === AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES
|
|
||||||
)
|
|
||||||
.map((r) => r.amount)[0] || '0',
|
|
||||||
ACCOUNT_TYPE_FEES_LIQUIDITY:
|
|
||||||
rewards
|
|
||||||
.filter((r) => r.rewardType === AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY)
|
|
||||||
.map((r) => r.amount)[0] || '0',
|
|
||||||
ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS:
|
|
||||||
rewards
|
|
||||||
.filter(
|
|
||||||
(r) =>
|
|
||||||
r.rewardType === AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS
|
|
||||||
)
|
|
||||||
.map((r) => r.amount)[0] || '0',
|
|
||||||
totalAmount: totalAmount,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-testid="epoch-total-rewards-table" className="mb-12">
|
<RewardsTable dataTestId="epoch-total-rewards-table" epoch={data.epoch}>
|
||||||
<SubHeading title={`EPOCH ${data.epoch}`} />
|
{data.assetRewards.map(({ name, rewards, totalAmount }, i) => (
|
||||||
|
<div className="contents" key={i}>
|
||||||
<div className={gridStyles}>
|
<div data-testid="asset" className={`${rowGridItemStyles()} p-5`}>
|
||||||
<ColumnHeader
|
{name}
|
||||||
title={t('rewardsColAssetHeader')}
|
|
||||||
className={headerGridItemStyles()}
|
|
||||||
/>
|
|
||||||
<ColumnHeader
|
|
||||||
title={t('rewardsColStakingHeader')}
|
|
||||||
tooltipContent={t('rewardsColStakingTooltip')}
|
|
||||||
className={headerGridItemStyles()}
|
|
||||||
/>
|
|
||||||
<ColumnHeader
|
|
||||||
title={t('rewardsColInfraHeader')}
|
|
||||||
tooltipContent={t('rewardsColInfraTooltip')}
|
|
||||||
className={headerGridItemStyles()}
|
|
||||||
/>
|
|
||||||
<ColumnHeader
|
|
||||||
title={t('rewardsColPriceTakingHeader')}
|
|
||||||
tooltipContent={t('rewardsColPriceTakingTooltip')}
|
|
||||||
className={headerGridItemStyles()}
|
|
||||||
/>
|
|
||||||
<ColumnHeader
|
|
||||||
title={t('rewardsColPriceMakingHeader')}
|
|
||||||
tooltipContent={t('rewardsColPriceMakingTooltip')}
|
|
||||||
className={headerGridItemStyles()}
|
|
||||||
/>
|
|
||||||
<ColumnHeader
|
|
||||||
title={t('rewardsColLiquidityProvisionHeader')}
|
|
||||||
tooltipContent={t('rewardsColLiquidityProvisionTooltip')}
|
|
||||||
className={headerGridItemStyles()}
|
|
||||||
/>
|
|
||||||
<ColumnHeader
|
|
||||||
title={t('rewardsColMarketCreationHeader')}
|
|
||||||
tooltipContent={t('rewardsColMarketCreationTooltip')}
|
|
||||||
className={headerGridItemStyles()}
|
|
||||||
/>
|
|
||||||
<ColumnHeader
|
|
||||||
title={t('rewardsColTotalHeader')}
|
|
||||||
className={headerGridItemStyles(true)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{rowData.map((row, i) => (
|
|
||||||
<div className="contents" key={i}>
|
|
||||||
<div data-testid="asset" className={`${rowGridItemStyles()} p-5`}>
|
|
||||||
{row.name}
|
|
||||||
</div>
|
|
||||||
<RewardItem
|
|
||||||
dataTestId="global"
|
|
||||||
value={row.ACCOUNT_TYPE_GLOBAL_REWARD}
|
|
||||||
/>
|
|
||||||
<RewardItem
|
|
||||||
dataTestId="infra"
|
|
||||||
value={row.ACCOUNT_TYPE_FEES_INFRASTRUCTURE}
|
|
||||||
/>
|
|
||||||
<RewardItem
|
|
||||||
dataTestId="taker"
|
|
||||||
value={row.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES}
|
|
||||||
/>
|
|
||||||
<RewardItem
|
|
||||||
dataTestId="maker"
|
|
||||||
value={row.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES}
|
|
||||||
/>
|
|
||||||
<RewardItem
|
|
||||||
dataTestId="liquidity"
|
|
||||||
value={row.ACCOUNT_TYPE_FEES_LIQUIDITY}
|
|
||||||
/>
|
|
||||||
<RewardItem
|
|
||||||
dataTestId="market-maker"
|
|
||||||
value={row.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS}
|
|
||||||
/>
|
|
||||||
<RewardItem
|
|
||||||
dataTestId="total"
|
|
||||||
value={row.totalAmount}
|
|
||||||
last={true}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
{rewards.map(({ rewardType, amount }, i) => (
|
||||||
</div>
|
<RewardItem key={i} dataTestId={rewardType} value={amount} />
|
||||||
</div>
|
))}
|
||||||
|
<RewardItem dataTestId="total" value={totalAmount} last={true} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</RewardsTable>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+5
-5
@@ -5,7 +5,7 @@ import { generateEpochTotalRewardsList } from './generate-epoch-total-rewards-li
|
|||||||
import { NoRewards } from '../no-rewards';
|
import { NoRewards } from '../no-rewards';
|
||||||
import { EpochTotalRewardsTable } from './epoch-total-rewards-table';
|
import { EpochTotalRewardsTable } from './epoch-total-rewards-table';
|
||||||
|
|
||||||
export const EpochRewards = () => {
|
export const EpochTotalRewards = () => {
|
||||||
const { data, loading, error, refetch } = useEpochAssetsRewardsQuery({
|
const { data, loading, error, refetch } = useEpochAssetsRewardsQuery({
|
||||||
variables: {
|
variables: {
|
||||||
epochRewardSummariesPagination: {
|
epochRewardSummariesPagination: {
|
||||||
@@ -15,7 +15,7 @@ export const EpochRewards = () => {
|
|||||||
});
|
});
|
||||||
useRefreshAfterEpoch(data?.epoch.timestamps.expiry, refetch);
|
useRefreshAfterEpoch(data?.epoch.timestamps.expiry, refetch);
|
||||||
|
|
||||||
const epochRewardSummaries = generateEpochTotalRewardsList(data) || [];
|
const epochTotalRewardSummaries = generateEpochTotalRewardsList(data) || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer
|
<AsyncRenderer
|
||||||
@@ -27,12 +27,12 @@ export const EpochRewards = () => {
|
|||||||
className="max-w-full overflow-auto"
|
className="max-w-full overflow-auto"
|
||||||
data-testid="epoch-rewards-total"
|
data-testid="epoch-rewards-total"
|
||||||
>
|
>
|
||||||
{epochRewardSummaries.length === 0 ? (
|
{epochTotalRewardSummaries.length === 0 ? (
|
||||||
<NoRewards />
|
<NoRewards />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{epochRewardSummaries.map((aggregatedEpochSummary) => (
|
{epochTotalRewardSummaries.map((epochTotalSummary, index) => (
|
||||||
<EpochTotalRewardsTable data={aggregatedEpochSummary} />
|
<EpochTotalRewardsTable data={epochTotalSummary} key={index} />
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
+58
-57
@@ -61,7 +61,7 @@ describe('generateEpochAssetRewardsList', () => {
|
|||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return an array of unnamed assets if no assets are provided (should not happen)', () => {
|
it('should return an array of unnamed assets if no asset names are provided (should not happen)', () => {
|
||||||
const epochData = {
|
const epochData = {
|
||||||
assetsConnection: {
|
assetsConnection: {
|
||||||
edges: [],
|
edges: [],
|
||||||
@@ -72,18 +72,10 @@ describe('generateEpochAssetRewardsList', () => {
|
|||||||
node: {
|
node: {
|
||||||
epoch: 1,
|
epoch: 1,
|
||||||
assetId: '1',
|
assetId: '1',
|
||||||
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE,
|
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
|
||||||
amount: '123',
|
amount: '123',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
node: {
|
|
||||||
epoch: 2,
|
|
||||||
assetId: '1',
|
|
||||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
|
||||||
amount: '5',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
epoch: {
|
epoch: {
|
||||||
@@ -104,30 +96,34 @@ describe('generateEpochAssetRewardsList', () => {
|
|||||||
name: '',
|
name: '',
|
||||||
rewards: [
|
rewards: [
|
||||||
{
|
{
|
||||||
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE,
|
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
|
||||||
amount: '123',
|
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',
|
totalAmount: '123',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
epoch: 2,
|
|
||||||
assetRewards: [
|
|
||||||
{
|
|
||||||
assetId: '1',
|
|
||||||
name: '',
|
|
||||||
rewards: [
|
|
||||||
{
|
|
||||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
|
||||||
amount: '5',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
totalAmount: '5',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -155,7 +151,7 @@ describe('generateEpochAssetRewardsList', () => {
|
|||||||
node: {
|
node: {
|
||||||
epoch: 1,
|
epoch: 1,
|
||||||
assetId: '1',
|
assetId: '1',
|
||||||
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE,
|
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
|
||||||
amount: '123',
|
amount: '123',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -167,22 +163,6 @@ describe('generateEpochAssetRewardsList', () => {
|
|||||||
amount: '100',
|
amount: '100',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
node: {
|
|
||||||
epoch: 1,
|
|
||||||
assetId: '2',
|
|
||||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
|
||||||
amount: '17.9873',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
node: {
|
|
||||||
epoch: 1,
|
|
||||||
assetId: '2',
|
|
||||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
|
||||||
amount: '1',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
node: {
|
node: {
|
||||||
epoch: 2,
|
epoch: 2,
|
||||||
@@ -211,30 +191,31 @@ describe('generateEpochAssetRewardsList', () => {
|
|||||||
name: 'Asset 1',
|
name: 'Asset 1',
|
||||||
rewards: [
|
rewards: [
|
||||||
{
|
{
|
||||||
rewardType: AccountType.ACCOUNT_TYPE_INSURANCE,
|
rewardType: AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
|
||||||
amount: '123',
|
amount: '0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
||||||
amount: '100',
|
amount: '100',
|
||||||
},
|
},
|
||||||
],
|
|
||||||
totalAmount: '223',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
assetId: '2',
|
|
||||||
name: 'Asset 2',
|
|
||||||
rewards: [
|
|
||||||
{
|
{
|
||||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE,
|
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
|
||||||
amount: '17.9873',
|
amount: '123',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
|
||||||
|
amount: '0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||||
amount: '1',
|
amount: '0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
|
||||||
|
amount: '0',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
totalAmount: '18.9873',
|
totalAmount: '223',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -245,10 +226,30 @@ describe('generateEpochAssetRewardsList', () => {
|
|||||||
assetId: '1',
|
assetId: '1',
|
||||||
name: 'Asset 1',
|
name: 'Asset 1',
|
||||||
rewards: [
|
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,
|
rewardType: AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY,
|
||||||
amount: '5',
|
amount: '5',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
|
||||||
|
amount: '0',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
totalAmount: '5',
|
totalAmount: '5',
|
||||||
},
|
},
|
||||||
|
|||||||
+36
-11
@@ -3,6 +3,8 @@ import type {
|
|||||||
EpochRewardSummaryFieldsFragment,
|
EpochRewardSummaryFieldsFragment,
|
||||||
} from '../home/__generated__/Rewards';
|
} from '../home/__generated__/Rewards';
|
||||||
import { removePaginationWrapper } from '@vegaprotocol/react-helpers';
|
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 {
|
interface EpochSummaryWithNamedReward extends EpochRewardSummaryFieldsFragment {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -18,11 +20,16 @@ export interface AggregatedEpochRewardSummary {
|
|||||||
totalAmount: string;
|
totalAmount: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AggregatedEpochSummary {
|
export interface EpochTotalSummary {
|
||||||
epoch: EpochRewardSummaryFieldsFragment['epoch'];
|
epoch: EpochRewardSummaryFieldsFragment['epoch'];
|
||||||
assetRewards: AggregatedEpochRewardSummary[];
|
assetRewards: AggregatedEpochRewardSummary[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emptyRowAccountTypes = Object.keys(RowAccountTypes).map((type) => ({
|
||||||
|
rewardType: type as AccountType,
|
||||||
|
amount: '0',
|
||||||
|
}));
|
||||||
|
|
||||||
export const generateEpochTotalRewardsList = (
|
export const generateEpochTotalRewardsList = (
|
||||||
epochData: EpochAssetsRewardsQuery | undefined
|
epochData: EpochAssetsRewardsQuery | undefined
|
||||||
) => {
|
) => {
|
||||||
@@ -58,7 +65,7 @@ export const generateEpochTotalRewardsList = (
|
|||||||
}, [] as EpochSummaryWithNamedReward[][]);
|
}, [] as EpochSummaryWithNamedReward[][]);
|
||||||
|
|
||||||
// Now aggregate the array of arrays of epoch summaries by asset rewards.
|
// Now aggregate the array of arrays of epoch summaries by asset rewards.
|
||||||
const aggregatedEpochSummaries: AggregatedEpochSummary[] =
|
const epochTotalRewards: EpochTotalSummary[] =
|
||||||
aggregatedEpochSummariesByEpochNumber.map((epochSummaries) => {
|
aggregatedEpochSummariesByEpochNumber.map((epochSummaries) => {
|
||||||
const assetRewards = epochSummaries.reduce((acc, epochSummary) => {
|
const assetRewards = epochSummaries.reduce((acc, epochSummary) => {
|
||||||
const assetRewardIndex = acc.findIndex(
|
const assetRewardIndex = acc.findIndex(
|
||||||
@@ -72,18 +79,36 @@ export const generateEpochTotalRewardsList = (
|
|||||||
assetId: epochSummary.assetId,
|
assetId: epochSummary.assetId,
|
||||||
name: epochSummary.name,
|
name: epochSummary.name,
|
||||||
rewards: [
|
rewards: [
|
||||||
{
|
...emptyRowAccountTypes.map((emptyRowAccountType) => {
|
||||||
rewardType: epochSummary.rewardType,
|
if (
|
||||||
amount: epochSummary.amount,
|
emptyRowAccountType.rewardType === epochSummary.rewardType
|
||||||
},
|
) {
|
||||||
|
return {
|
||||||
|
rewardType: epochSummary.rewardType,
|
||||||
|
amount: epochSummary.amount,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return emptyRowAccountType;
|
||||||
|
}
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
totalAmount: epochSummary.amount,
|
totalAmount: epochSummary.amount,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
acc[assetRewardIndex].rewards.push({
|
acc[assetRewardIndex].rewards = acc[assetRewardIndex].rewards.map(
|
||||||
rewardType: epochSummary.rewardType,
|
(reward) => {
|
||||||
amount: epochSummary.amount,
|
if (reward.rewardType === epochSummary.rewardType) {
|
||||||
});
|
return {
|
||||||
|
rewardType: epochSummary.rewardType,
|
||||||
|
amount: (
|
||||||
|
Number(reward.amount) + Number(epochSummary.amount)
|
||||||
|
).toString(),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return reward;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
acc[assetRewardIndex].totalAmount = (
|
acc[assetRewardIndex].totalAmount = (
|
||||||
Number(acc[assetRewardIndex].totalAmount) +
|
Number(acc[assetRewardIndex].totalAmount) +
|
||||||
Number(epochSummary.amount)
|
Number(epochSummary.amount)
|
||||||
@@ -99,5 +124,5 @@ export const generateEpochTotalRewardsList = (
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return aggregatedEpochSummaries;
|
return epochTotalRewards;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,34 +1,30 @@
|
|||||||
// @ts-ignore No types available for duration-js
|
// @ts-ignore No types available for duration-js
|
||||||
import Duration from 'duration-js';
|
import Duration from 'duration-js';
|
||||||
import React, { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { formatDistance } from 'date-fns';
|
import { formatDistance } from 'date-fns';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
Callout,
|
Callout,
|
||||||
Intent,
|
Intent,
|
||||||
AsyncRenderer,
|
AsyncRenderer,
|
||||||
Toggle,
|
Toggle,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
import {
|
import {
|
||||||
useNetworkParams,
|
useNetworkParams,
|
||||||
NetworkParams,
|
NetworkParams,
|
||||||
createDocsLinks,
|
createDocsLinks,
|
||||||
} from '@vegaprotocol/react-helpers';
|
} from '@vegaprotocol/react-helpers';
|
||||||
import {
|
|
||||||
AppStateActionType,
|
|
||||||
useAppState,
|
|
||||||
} from '../../../contexts/app-state/app-state-context';
|
|
||||||
import { useEpochQuery } from './__generated__/Rewards';
|
import { useEpochQuery } from './__generated__/Rewards';
|
||||||
|
|
||||||
import { EpochCountdown } from '../../../components/epoch-countdown';
|
import { EpochCountdown } from '../../../components/epoch-countdown';
|
||||||
import { Heading, SubHeading } from '../../../components/heading';
|
import { Heading, SubHeading } from '../../../components/heading';
|
||||||
import { RewardInfo } from '../epoch-individual-awards/reward-info';
|
import { EpochIndividualRewards } from '../epoch-individual-rewards/epoch-individual-rewards';
|
||||||
import { EpochRewards } from '../epoch-total-rewards/epoch-rewards';
|
|
||||||
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
|
import { useRefreshAfterEpoch } from '../../../hooks/use-refresh-after-epoch';
|
||||||
import { useEnvironment } from '@vegaprotocol/environment';
|
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';
|
type RewardsView = 'total' | 'individual';
|
||||||
|
|
||||||
@@ -39,25 +35,21 @@ export const RewardsPage = () => {
|
|||||||
const [toggleRewardsView, setToggleRewardsView] =
|
const [toggleRewardsView, setToggleRewardsView] =
|
||||||
useState<RewardsView>('total');
|
useState<RewardsView>('total');
|
||||||
|
|
||||||
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
|
|
||||||
openVegaWalletDialog: store.openVegaWalletDialog,
|
|
||||||
}));
|
|
||||||
const { appDispatch } = useAppState();
|
|
||||||
|
|
||||||
const {
|
|
||||||
params,
|
|
||||||
loading: paramsLoading,
|
|
||||||
error: paramsError,
|
|
||||||
} = useNetworkParams([NetworkParams.reward_staking_delegation_payoutDelay]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: epochData,
|
data: epochData,
|
||||||
loading: epochLoading,
|
loading: epochLoading,
|
||||||
error: epochError,
|
error: epochError,
|
||||||
refetch,
|
refetch,
|
||||||
} = useEpochQuery();
|
} = useEpochQuery();
|
||||||
|
|
||||||
useRefreshAfterEpoch(epochData?.epoch.timestamps.expiry, refetch);
|
useRefreshAfterEpoch(epochData?.epoch.timestamps.expiry, refetch);
|
||||||
|
|
||||||
|
const {
|
||||||
|
params,
|
||||||
|
loading: paramsLoading,
|
||||||
|
error: paramsError,
|
||||||
|
} = useNetworkParams([NetworkParams.reward_staking_delegation_payoutDelay]);
|
||||||
|
|
||||||
const payoutDuration = useMemo(() => {
|
const payoutDuration = useMemo(() => {
|
||||||
if (!params) {
|
if (!params) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -103,8 +95,8 @@ export const RewardsPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{epochData &&
|
{epochData?.epoch &&
|
||||||
epochData.epoch.id &&
|
epochData.epoch?.id &&
|
||||||
epochData.epoch.timestamps.start &&
|
epochData.epoch.timestamps.start &&
|
||||||
epochData.epoch.timestamps.expiry && (
|
epochData.epoch.timestamps.expiry && (
|
||||||
<section className="mb-16">
|
<section className="mb-16">
|
||||||
@@ -148,26 +140,13 @@ export const RewardsPage = () => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{toggleRewardsView === 'total' ? (
|
{toggleRewardsView === 'total' ? (
|
||||||
<EpochRewards />
|
<EpochTotalRewards />
|
||||||
) : (
|
) : (
|
||||||
<section>
|
<section>
|
||||||
{pubKey && pubKeys?.length ? (
|
{pubKey && pubKeys?.length ? (
|
||||||
<RewardInfo />
|
<EpochIndividualRewards />
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<ConnectToSeeRewards />
|
||||||
<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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+125
@@ -0,0 +1,125 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { SubHeading } from '../../../components/heading';
|
||||||
|
import { AccountType } from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
// This is the data structure that matters for defining which Account types
|
||||||
|
// are displayed in the rewards tables. It sets column titles and tooltips,
|
||||||
|
// and is used to filter the data that is passed to functions to generate
|
||||||
|
// the table rows. It's important to preserve the order.
|
||||||
|
export const RowAccountTypes = {
|
||||||
|
[AccountType.ACCOUNT_TYPE_GLOBAL_REWARD]: {
|
||||||
|
columnTitle: 'rewardsColStakingHeader',
|
||||||
|
description: 'rewardsColStakingTooltip',
|
||||||
|
},
|
||||||
|
[AccountType.ACCOUNT_TYPE_FEES_INFRASTRUCTURE]: {
|
||||||
|
columnTitle: 'rewardsColInfraHeader',
|
||||||
|
description: 'rewardsColInfraTooltip',
|
||||||
|
},
|
||||||
|
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES]: {
|
||||||
|
columnTitle: 'rewardsColPriceTakingHeader',
|
||||||
|
description: 'rewardsColPriceTakingTooltip',
|
||||||
|
},
|
||||||
|
[AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES]: {
|
||||||
|
columnTitle: 'rewardsColPriceMakingHeader',
|
||||||
|
description: 'rewardsColPriceMakingTooltip',
|
||||||
|
},
|
||||||
|
[AccountType.ACCOUNT_TYPE_FEES_LIQUIDITY]: {
|
||||||
|
columnTitle: 'rewardsColLiquidityProvisionHeader',
|
||||||
|
description: 'rewardsColLiquidityProvisionTooltip',
|
||||||
|
},
|
||||||
|
[AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS]: {
|
||||||
|
columnTitle: 'rewardsColMarketCreationHeader',
|
||||||
|
description: 'rewardsColMarketCreationTooltip',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ColumnHeaderProps {
|
||||||
|
title: string;
|
||||||
|
tooltipContent?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerGridItemStyles = (last = false) =>
|
||||||
|
classNames('border-r border-b border-b-vega-dark-200', 'py-3 px-5', {
|
||||||
|
'border-r-vega-dark-150': !last,
|
||||||
|
'border-r-0': last,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const rowGridItemStyles = (last = false) =>
|
||||||
|
classNames('relative', 'border-r border-b border-b-vega-dark-150', {
|
||||||
|
'border-r-vega-dark-150': !last,
|
||||||
|
'border-r-0': last,
|
||||||
|
});
|
||||||
|
|
||||||
|
const gridStyles = classNames(
|
||||||
|
'grid grid-cols-[repeat(8,minmax(100px,auto))] max-w-full overflow-auto',
|
||||||
|
`border-t border-vega-dark-200`,
|
||||||
|
'text-sm'
|
||||||
|
);
|
||||||
|
|
||||||
|
const ColumnHeader = ({
|
||||||
|
title,
|
||||||
|
tooltipContent,
|
||||||
|
className,
|
||||||
|
}: ColumnHeaderProps) => (
|
||||||
|
<div className={className}>
|
||||||
|
<h2 className="mb-1 text-sm text-vega-dark-300">{title}</h2>
|
||||||
|
{tooltipContent && (
|
||||||
|
<Tooltip description={tooltipContent}>
|
||||||
|
<button>
|
||||||
|
<Icon name={'info-sign'} className="text-vega-dark-200" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ColumnHeaders = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div className="contents">
|
||||||
|
<ColumnHeader
|
||||||
|
title={t('rewardsColAssetHeader')}
|
||||||
|
className={headerGridItemStyles()}
|
||||||
|
/>
|
||||||
|
{Object.values(RowAccountTypes).map(({ columnTitle, description }) => (
|
||||||
|
<ColumnHeader
|
||||||
|
key={columnTitle}
|
||||||
|
title={t(columnTitle)}
|
||||||
|
tooltipContent={t(description)}
|
||||||
|
className={headerGridItemStyles()}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<ColumnHeader
|
||||||
|
title={t('rewardsColTotalHeader')}
|
||||||
|
className={headerGridItemStyles(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface RewardTableProps {
|
||||||
|
dataTestId: string;
|
||||||
|
epoch: number;
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewards table children will be the row items. Make sure they contain
|
||||||
|
// the same number of columns and map to the data of the ColumnHeaders component.
|
||||||
|
export const RewardsTable = ({
|
||||||
|
dataTestId,
|
||||||
|
epoch,
|
||||||
|
children,
|
||||||
|
}: RewardTableProps) => (
|
||||||
|
<div data-testid={dataTestId} className="mb-12">
|
||||||
|
<SubHeading title={`EPOCH ${epoch}`} />
|
||||||
|
|
||||||
|
<div className={gridStyles}>
|
||||||
|
<ColumnHeaders />
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
+111
-89
@@ -50,101 +50,117 @@ export const StandbyPendingValidatorsTable = ({
|
|||||||
const nodes = useMemo(() => {
|
const nodes = useMemo(() => {
|
||||||
if (!data) return [];
|
if (!data) return [];
|
||||||
|
|
||||||
return data.map(
|
return data
|
||||||
({
|
.sort((a, b) => {
|
||||||
id,
|
const aVotingPower = new BigNumber(a.rankingScore.votingPower);
|
||||||
name,
|
const bVotingPower = new BigNumber(b.rankingScore.votingPower);
|
||||||
avatarUrl,
|
return bVotingPower.minus(aVotingPower).toNumber();
|
||||||
stakedByDelegates,
|
})
|
||||||
stakedByOperator,
|
.map((node, index) => {
|
||||||
stakedTotal,
|
const votingPowerRanking = index + 1;
|
||||||
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 {
|
return {
|
||||||
|
...node,
|
||||||
|
votingPowerRanking,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.map(
|
||||||
|
({
|
||||||
id,
|
id,
|
||||||
[ValidatorFields.VALIDATOR]: {
|
name,
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
name,
|
stakedByDelegates,
|
||||||
},
|
stakedByOperator,
|
||||||
[ValidatorFields.STAKE]: formatNumber(
|
stakedTotal,
|
||||||
toBigNum(stakedTotal, decimals),
|
rankingScore: { stakeScore },
|
||||||
2
|
pendingStake,
|
||||||
),
|
votingPowerRanking,
|
||||||
[ValidatorFields.STAKE_NEEDED_FOR_PROMOTION]:
|
}) => {
|
||||||
individualStakeNeededForPromotion || null,
|
const { rawValidatorScore, performanceScore } =
|
||||||
[ValidatorFields.STAKE_NEEDED_FOR_PROMOTION_DESCRIPTION]:
|
getLastEpochScoreAndPerformance(previousEpochData, id);
|
||||||
individualStakeNeededForPromotionDescription || t('n/a'),
|
|
||||||
[ValidatorFields.STAKE_SHARE]: stakedTotalPercentage(stakeScore),
|
const overstakedAmount = getOverstakedAmount(
|
||||||
[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,
|
rawValidatorScore,
|
||||||
performanceScore,
|
|
||||||
stakedTotal,
|
stakedTotal,
|
||||||
totalStake
|
totalStake
|
||||||
),
|
);
|
||||||
[ValidatorFields.PENDING_STAKE]: formatNumber(
|
let individualStakeNeededForPromotion,
|
||||||
toBigNum(pendingStake, decimals),
|
individualStakeNeededForPromotionDescription;
|
||||||
2
|
|
||||||
),
|
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,
|
data,
|
||||||
decimals,
|
decimals,
|
||||||
@@ -158,6 +174,12 @@ export const StandbyPendingValidatorsTable = ({
|
|||||||
const StandbyPendingTable = forwardRef<AgGridReact>((_, gridRef) => {
|
const StandbyPendingTable = forwardRef<AgGridReact>((_, gridRef) => {
|
||||||
const colDefs = useMemo<ColDef[]>(
|
const colDefs = useMemo<ColDef[]>(
|
||||||
() => [
|
() => [
|
||||||
|
{
|
||||||
|
field: ValidatorFields.RANKING_INDEX,
|
||||||
|
headerName: '#',
|
||||||
|
width: 60,
|
||||||
|
pinned: 'left',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: ValidatorFields.VALIDATOR,
|
field: ValidatorFields.VALIDATOR,
|
||||||
headerName: t(ValidatorFields.VALIDATOR).toString(),
|
headerName: t(ValidatorFields.VALIDATOR).toString(),
|
||||||
|
|||||||
@@ -59,11 +59,17 @@ export const getOverstakedAmount = (
|
|||||||
export const getOverstakingPenalty = (
|
export const getOverstakingPenalty = (
|
||||||
overstakedAmount: BigNumber,
|
overstakedAmount: BigNumber,
|
||||||
stakedOnNode: string
|
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),
|
overstakedAmount.dividedBy(new BigNumber(stakedOnNode)).times(100),
|
||||||
2
|
2
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const getTotalPenalties = (
|
export const getTotalPenalties = (
|
||||||
rawValidatorScore: string | null | undefined,
|
rawValidatorScore: string | null | undefined,
|
||||||
|
|||||||
@@ -25,12 +25,38 @@ const generateProposal = (code: string): ProposalListFieldsFragment => ({
|
|||||||
totalWeight: '',
|
totalWeight: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
requiredMajority: '',
|
||||||
|
party: {
|
||||||
|
__typename: 'Party',
|
||||||
|
id: '',
|
||||||
|
},
|
||||||
|
rationale: {
|
||||||
|
__typename: 'ProposalRationale',
|
||||||
|
description: '',
|
||||||
|
title: '',
|
||||||
|
},
|
||||||
|
requiredParticipation: '',
|
||||||
|
errorDetails: '',
|
||||||
|
rejectionReason: null,
|
||||||
|
requiredLpMajority: '',
|
||||||
|
requiredLpParticipation: '',
|
||||||
terms: {
|
terms: {
|
||||||
__typename: 'ProposalTerms',
|
__typename: 'ProposalTerms',
|
||||||
closingDatetime: '',
|
closingDatetime: '',
|
||||||
enactmentDatetime: undefined,
|
enactmentDatetime: undefined,
|
||||||
change: {
|
change: {
|
||||||
__typename: 'NewMarket',
|
__typename: 'NewMarket',
|
||||||
|
decimalPlaces: 1,
|
||||||
|
lpPriceRange: '',
|
||||||
|
riskParameters: {
|
||||||
|
__typename: 'SimpleRiskModel',
|
||||||
|
params: {
|
||||||
|
__typename: 'SimpleRiskModelParams',
|
||||||
|
factorLong: 0,
|
||||||
|
factorShort: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
metadata: [],
|
||||||
instrument: {
|
instrument: {
|
||||||
__typename: 'InstrumentConfiguration',
|
__typename: 'InstrumentConfiguration',
|
||||||
code: code,
|
code: code,
|
||||||
@@ -42,6 +68,34 @@ const generateProposal = (code: string): ProposalListFieldsFragment => ({
|
|||||||
id: 'A',
|
id: 'A',
|
||||||
name: 'A',
|
name: 'A',
|
||||||
symbol: 'A',
|
symbol: 'A',
|
||||||
|
decimals: 1,
|
||||||
|
quantum: '',
|
||||||
|
},
|
||||||
|
quoteName: '',
|
||||||
|
dataSourceSpecBinding: {
|
||||||
|
__typename: 'DataSourceSpecToFutureBinding',
|
||||||
|
settlementDataProperty: '',
|
||||||
|
tradingTerminationProperty: '',
|
||||||
|
},
|
||||||
|
dataSourceSpecForSettlementData: {
|
||||||
|
__typename: 'DataSourceDefinition',
|
||||||
|
sourceType: {
|
||||||
|
__typename: 'DataSourceDefinitionInternal',
|
||||||
|
sourceType: {
|
||||||
|
__typename: 'DataSourceSpecConfigurationTime',
|
||||||
|
conditions: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dataSourceSpecForTradingTermination: {
|
||||||
|
__typename: 'DataSourceDefinition',
|
||||||
|
sourceType: {
|
||||||
|
__typename: 'DataSourceDefinitionInternal',
|
||||||
|
sourceType: {
|
||||||
|
__typename: 'DataSourceSpecConfigurationTime',
|
||||||
|
conditions: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const itemValue = 'item-value';
|
|||||||
|
|
||||||
describe('Market proposal notification', { tags: '@smoke' }, () => {
|
describe('Market proposal notification', { tags: '@smoke' }, () => {
|
||||||
before(() => {
|
before(() => {
|
||||||
|
cy.setVegaWallet();
|
||||||
cy.mockTradingPage(
|
cy.mockTradingPage(
|
||||||
Schema.MarketState.STATE_ACTIVE,
|
Schema.MarketState.STATE_ACTIVE,
|
||||||
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
|
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
|
||||||
@@ -239,7 +240,6 @@ describe('market states not accepting orders', { tags: '@smoke' }, function () {
|
|||||||
cy.visit('/#/markets/market-0');
|
cy.visit('/#/markets/market-0');
|
||||||
});
|
});
|
||||||
it('must display that market is not accepting orders', function () {
|
it('must display that market is not accepting orders', function () {
|
||||||
cy.getByTestId('place-order').click();
|
|
||||||
cy.getByTestId('dealticket-error-message-summary').should(
|
cy.getByTestId('dealticket-error-message-summary').should(
|
||||||
'have.text',
|
'have.text',
|
||||||
`This market is ${marketState
|
`This market is ${marketState
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { aliasGQLQuery } from '@vegaprotocol/cypress';
|
|||||||
import { marketsQuery } from '@vegaprotocol/mock';
|
import { marketsQuery } from '@vegaprotocol/mock';
|
||||||
import { getDateTimeFormat } from '@vegaprotocol/react-helpers';
|
import { getDateTimeFormat } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
|
const dialogCloseBtn = 'dialog-close';
|
||||||
|
|
||||||
describe('markets table', { tags: '@smoke' }, () => {
|
describe('markets table', { tags: '@smoke' }, () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.clearLocalStorage().then(() => {
|
cy.clearLocalStorage().then(() => {
|
||||||
@@ -145,8 +147,9 @@ describe('markets table', { tags: '@smoke' }, () => {
|
|||||||
proposal: { terms: { enactmentDatetime: '2023-01-31 12:00:01' } },
|
proposal: { terms: { enactmentDatetime: '2023-01-31 12:00:01' } },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
cy.visit('/');
|
|
||||||
cy.visit('#/markets/market-0');
|
cy.visit('#/markets/market-0');
|
||||||
|
cy.url().should('contain', 'market-0');
|
||||||
|
cy.getByTestId(dialogCloseBtn).click();
|
||||||
cy.getByTestId('item-value').contains('Opening auction').realHover();
|
cy.getByTestId('item-value').contains('Opening auction').realHover();
|
||||||
cy.getByTestId('opening-auction-sub-status').should(
|
cy.getByTestId('opening-auction-sub-status').should(
|
||||||
'contain.text',
|
'contain.text',
|
||||||
@@ -155,8 +158,7 @@ describe('markets table', { tags: '@smoke' }, () => {
|
|||||||
|
|
||||||
const now = new Date(Date.parse('2023-01-30 12:00:01')).getTime();
|
const now = new Date(Date.parse('2023-01-30 12:00:01')).getTime();
|
||||||
cy.clock(now, ['Date']); // Set "now" to BEFORE reservation
|
cy.clock(now, ['Date']); // Set "now" to BEFORE reservation
|
||||||
cy.visit('/');
|
cy.reload();
|
||||||
cy.visit('#/markets/market-0');
|
|
||||||
cy.getByTestId('item-value').contains('Opening auction').realHover();
|
cy.getByTestId('item-value').contains('Opening auction').realHover();
|
||||||
cy.getByTestId('opening-auction-sub-status').should(
|
cy.getByTestId('opening-auction-sub-status').should(
|
||||||
'contain.text',
|
'contain.text',
|
||||||
@@ -171,7 +173,8 @@ describe('markets table', { tags: '@smoke' }, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function openMarketDropDown() {
|
function openMarketDropDown() {
|
||||||
cy.getByTestId('dialog-close').click();
|
cy.getByTestId(dialogCloseBtn).should('be.visible');
|
||||||
|
cy.getByTestId(dialogCloseBtn).click();
|
||||||
cy.getByTestId('popover-trigger').click();
|
cy.getByTestId('popover-trigger').click();
|
||||||
cy.contains('Loading market data...').should('not.exist');
|
cy.contains('Loading market data...').should('not.exist');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { mockConnectWallet } from '@vegaprotocol/cypress';
|
import { mockConnectWallet } from '@vegaprotocol/cypress';
|
||||||
|
|
||||||
before(() => {
|
beforeEach(() => {
|
||||||
cy.mockTradingPage();
|
cy.mockTradingPage();
|
||||||
cy.mockSubscription();
|
cy.mockSubscription();
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
@@ -19,6 +19,7 @@ describe('Desktop view', { tags: '@smoke' }, () => {
|
|||||||
cy.getByTestId('navbar')
|
cy.getByTestId('navbar')
|
||||||
.find(`[data-testid="navbar-links"] a[data-testid=${link}]`)
|
.find(`[data-testid="navbar-links"] a[data-testid=${link}]`)
|
||||||
.then((element) => {
|
.then((element) => {
|
||||||
|
cy.contains('Loading...').should('not.exist');
|
||||||
cy.wrap(element).click();
|
cy.wrap(element).click();
|
||||||
cy.wrap(element)
|
cy.wrap(element)
|
||||||
.get('span.absolute.md\\:h-1.w-full')
|
.get('span.absolute.md\\:h-1.w-full')
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ const toggleShort = 'order-side-SIDE_SELL';
|
|||||||
const toggleLong = 'order-side-SIDE_BUY';
|
const toggleLong = 'order-side-SIDE_BUY';
|
||||||
const toggleLimit = 'order-type-TYPE_LIMIT';
|
const toggleLimit = 'order-type-TYPE_LIMIT';
|
||||||
const toggleMarket = 'order-type-TYPE_MARKET';
|
const toggleMarket = 'order-type-TYPE_MARKET';
|
||||||
const errorMessage = 'dealticket-error-message';
|
|
||||||
|
|
||||||
const TIFlist = Object.values(Schema.OrderTimeInForce).map((value) => {
|
const TIFlist = Object.values(Schema.OrderTimeInForce).map((value) => {
|
||||||
return {
|
return {
|
||||||
@@ -363,11 +362,11 @@ describe('deal ticket validation', { tags: '@smoke' }, () => {
|
|||||||
cy.wait('@Markets');
|
cy.wait('@Markets');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('must not place an order if wallet is not connected', () => {
|
it('must show place order button and connect wallet if wallet is not connected', () => {
|
||||||
cy.getByTestId('connect-vega-wallet'); // Not connected
|
cy.getByTestId('connect-vega-wallet'); // Not connected
|
||||||
cy.getByTestId('order-connect-wallet').should('exist');
|
cy.getByTestId('order-connect-wallet').should('exist');
|
||||||
cy.getByTestId(placeOrderBtn).should('not.exist');
|
cy.getByTestId(placeOrderBtn).should('exist');
|
||||||
cy.getByTestId(errorMessage).should('not.exist');
|
cy.getByTestId('deal-ticket-connect-wallet').should('exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('must be able to select order direction - long/short', function () {
|
it('must be able to select order direction - long/short', function () {
|
||||||
@@ -664,13 +663,13 @@ describe('account validation', { tags: '@regression' }, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should show an error if your balance is zero', () => {
|
it('should show an error if your balance is zero', () => {
|
||||||
cy.getByTestId('place-order').should('not.be.disabled');
|
|
||||||
cy.getByTestId('place-order').click();
|
|
||||||
cy.getByTestId('place-order').should('be.disabled');
|
cy.getByTestId('place-order').should('be.disabled');
|
||||||
//7002-SORD-003
|
//7002-SORD-003
|
||||||
cy.getByTestId('dealticket-error-message-zero-balance').should(
|
cy.getByTestId('dealticket-error-message-zero-balance').should(
|
||||||
'have.text',
|
'have.text',
|
||||||
'Insufficient balance. Deposit ' + 'tDAI'
|
'You need ' +
|
||||||
|
'tDAI' +
|
||||||
|
' in your wallet to trade in this market. See all your collateral.Make a deposit'
|
||||||
);
|
);
|
||||||
cy.getByTestId('deal-ticket-deposit-dialog-button').should('exist');
|
cy.getByTestId('deal-ticket-deposit-dialog-button').should('exist');
|
||||||
});
|
});
|
||||||
@@ -708,7 +707,7 @@ describe('account validation', { tags: '@regression' }, () => {
|
|||||||
);
|
);
|
||||||
cy.getByTestId('dealticket-warning-margin').should(
|
cy.getByTestId('dealticket-warning-margin').should(
|
||||||
'contain.text',
|
'contain.text',
|
||||||
'9,999.99 tDAI currently required, 1,000.00 tDAI available'
|
'9,999.99 tDAI is currently required. You have only 1,000.00 tDAI available.Deposit tDAI'
|
||||||
);
|
);
|
||||||
cy.getByTestId('deal-ticket-deposit-dialog-button').click();
|
cy.getByTestId('deal-ticket-deposit-dialog-button').click();
|
||||||
cy.getByTestId('dialog-content')
|
cy.getByTestId('dialog-content')
|
||||||
|
|||||||
@@ -96,10 +96,24 @@ export const MarketPage = () => {
|
|||||||
|
|
||||||
const tradeView = useMemo(() => {
|
const tradeView = useMemo(() => {
|
||||||
if (w > 960) {
|
if (w > 960) {
|
||||||
return <TradeGrid market={data} onSelect={onSelect} />;
|
return (
|
||||||
|
<TradeGrid
|
||||||
|
market={data}
|
||||||
|
onSelect={onSelect}
|
||||||
|
pinnedAsset={
|
||||||
|
data?.tradableInstrument.instrument.product.settlementAsset
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return <TradePanels market={data} onSelect={onSelect} />;
|
return (
|
||||||
}, [w, data, onSelect]);
|
<TradePanels
|
||||||
|
market={data}
|
||||||
|
onSelect={onSelect}
|
||||||
|
onClickCollateral={() => navigate('/portfolio')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [w, data, onSelect, navigate]);
|
||||||
if (!data && marketId) {
|
if (!data && marketId) {
|
||||||
return (
|
return (
|
||||||
<Splash>
|
<Splash>
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { NO_MARKET } from './constants';
|
|||||||
import { LiquidityContainer } from '../liquidity/liquidity';
|
import { LiquidityContainer } from '../liquidity/liquidity';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Links, Routes } from '../../pages/client-router';
|
import { Links, Routes } from '../../pages/client-router';
|
||||||
|
import type { PinnedAsset } from '@vegaprotocol/accounts';
|
||||||
|
|
||||||
type MarketDependantView =
|
type MarketDependantView =
|
||||||
| typeof CandlesChartContainer
|
| typeof CandlesChartContainer
|
||||||
@@ -65,14 +66,17 @@ type TradingView = keyof typeof TradingViews;
|
|||||||
interface TradeGridProps {
|
interface TradeGridProps {
|
||||||
market: Market | null;
|
market: Market | null;
|
||||||
onSelect: (marketId: string) => void;
|
onSelect: (marketId: string) => void;
|
||||||
|
pinnedAsset?: PinnedAsset;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MainGrid = ({
|
const MainGrid = ({
|
||||||
marketId,
|
marketId,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
pinnedAsset,
|
||||||
}: {
|
}: {
|
||||||
marketId: string;
|
marketId: string;
|
||||||
onSelect?: (marketId: string) => void;
|
onSelect?: (marketId: string) => void;
|
||||||
|
pinnedAsset?: PinnedAsset;
|
||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const onMarketClick = (marketId: string) => {
|
const onMarketClick = (marketId: string) => {
|
||||||
@@ -111,7 +115,10 @@ const MainGrid = ({
|
|||||||
<TradeGridChild>
|
<TradeGridChild>
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<Tab id="ticket" name={t('Ticket')}>
|
<Tab id="ticket" name={t('Ticket')}>
|
||||||
<TradingViews.Ticket marketId={marketId} />
|
<TradingViews.Ticket
|
||||||
|
marketId={marketId}
|
||||||
|
onClickCollateral={() => navigate('/portfolio')}
|
||||||
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab id="info" name={t('Info')}>
|
<Tab id="info" name={t('Info')}>
|
||||||
<TradingViews.Info
|
<TradingViews.Info
|
||||||
@@ -172,7 +179,7 @@ const MainGrid = ({
|
|||||||
</Tab>
|
</Tab>
|
||||||
<Tab id="accounts" name={t('Collateral')}>
|
<Tab id="accounts" name={t('Collateral')}>
|
||||||
<VegaWalletContainer>
|
<VegaWalletContainer>
|
||||||
<TradingViews.Collateral />
|
<TradingViews.Collateral pinnedAsset={pinnedAsset} />
|
||||||
</VegaWalletContainer>
|
</VegaWalletContainer>
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
@@ -183,11 +190,19 @@ const MainGrid = ({
|
|||||||
};
|
};
|
||||||
const MainGridWrapped = memo(MainGrid);
|
const MainGridWrapped = memo(MainGrid);
|
||||||
|
|
||||||
export const TradeGrid = ({ market, onSelect }: TradeGridProps) => {
|
export const TradeGrid = ({
|
||||||
|
market,
|
||||||
|
onSelect,
|
||||||
|
pinnedAsset,
|
||||||
|
}: TradeGridProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="h-full grid grid-rows-[min-content_1fr]">
|
<div className="h-full grid grid-rows-[min-content_1fr]">
|
||||||
<TradeMarketHeader market={market} onSelect={onSelect} />
|
<TradeMarketHeader market={market} onSelect={onSelect} />
|
||||||
<MainGridWrapped marketId={market?.id || ''} onSelect={onSelect} />
|
<MainGridWrapped
|
||||||
|
marketId={market?.id || ''}
|
||||||
|
onSelect={onSelect}
|
||||||
|
pinnedAsset={pinnedAsset}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -210,15 +225,24 @@ interface TradePanelsProps {
|
|||||||
market: Market | null;
|
market: Market | null;
|
||||||
onSelect: (marketId: string) => void;
|
onSelect: (marketId: string) => void;
|
||||||
onMarketClick?: (marketId: string) => void;
|
onMarketClick?: (marketId: string) => void;
|
||||||
|
onClickCollateral: () => void;
|
||||||
|
pinnedAsset?: PinnedAsset;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TradePanels = ({ market, onSelect }: TradePanelsProps) => {
|
export const TradePanels = ({
|
||||||
|
market,
|
||||||
|
onSelect,
|
||||||
|
onClickCollateral,
|
||||||
|
pinnedAsset,
|
||||||
|
}: TradePanelsProps) => {
|
||||||
const [view, setView] = useState<TradingView>('Candles');
|
const [view, setView] = useState<TradingView>('Candles');
|
||||||
const renderView = () => {
|
const renderView = () => {
|
||||||
const Component = memo<{
|
const Component = memo<{
|
||||||
marketId: string;
|
marketId: string;
|
||||||
onSelect: (marketId: string) => void;
|
onSelect: (marketId: string) => void;
|
||||||
onMarketClick?: (marketId: string) => void;
|
onMarketClick?: (marketId: string) => void;
|
||||||
|
onClickCollateral: () => void;
|
||||||
|
pinnedAsset?: PinnedAsset;
|
||||||
}>(TradingViews[view]);
|
}>(TradingViews[view]);
|
||||||
|
|
||||||
if (!Component) {
|
if (!Component) {
|
||||||
@@ -227,7 +251,14 @@ export const TradePanels = ({ market, onSelect }: TradePanelsProps) => {
|
|||||||
|
|
||||||
if (!market) return <Splash>{NO_MARKET}</Splash>;
|
if (!market) return <Splash>{NO_MARKET}</Splash>;
|
||||||
|
|
||||||
return <Component marketId={market?.id} onSelect={onSelect} />;
|
return (
|
||||||
|
<Component
|
||||||
|
marketId={market?.id}
|
||||||
|
onSelect={onSelect}
|
||||||
|
onClickCollateral={onClickCollateral}
|
||||||
|
pinnedAsset={pinnedAsset}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
|
|||||||
|
|
||||||
export const DepositsContainer = () => {
|
export const DepositsContainer = () => {
|
||||||
const { pubKey, isReadOnly } = useVegaWallet();
|
const { pubKey, isReadOnly } = useVegaWallet();
|
||||||
const { data, loading, error } = useDataProvider({
|
const { data, loading, error, reload } = useDataProvider({
|
||||||
dataProvider: depositsProvider,
|
dataProvider: depositsProvider,
|
||||||
variables: { partyId: pubKey || '' },
|
variables: { partyId: pubKey || '' },
|
||||||
skip: !pubKey,
|
skip: !pubKey,
|
||||||
@@ -27,6 +27,7 @@ export const DepositsContainer = () => {
|
|||||||
error={error}
|
error={error}
|
||||||
noDataCondition={(data) => !(data && data.length)}
|
noDataCondition={(data) => !(data && data.length)}
|
||||||
noDataMessage={t('No deposits')}
|
noDataMessage={t('No deposits')}
|
||||||
|
reload={reload}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { VegaWalletContainer } from '../../components/vega-wallet-container';
|
|||||||
|
|
||||||
export const WithdrawalsContainer = () => {
|
export const WithdrawalsContainer = () => {
|
||||||
const { pubKey, isReadOnly } = useVegaWallet();
|
const { pubKey, isReadOnly } = useVegaWallet();
|
||||||
const { data, loading, error } = useDataProvider({
|
const { data, loading, error, reload } = useDataProvider({
|
||||||
dataProvider: withdrawalProvider,
|
dataProvider: withdrawalProvider,
|
||||||
variables: { partyId: pubKey || '' },
|
variables: { partyId: pubKey || '' },
|
||||||
skip: !pubKey,
|
skip: !pubKey,
|
||||||
@@ -33,6 +33,7 @@ export const WithdrawalsContainer = () => {
|
|||||||
error={error}
|
error={error}
|
||||||
noDataCondition={(data) => !(data && data.length)}
|
noDataCondition={(data) => !(data && data.length)}
|
||||||
noDataMessage={t('No withdrawals')}
|
noDataMessage={t('No withdrawals')}
|
||||||
|
reload={reload}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,10 +5,15 @@ import { useWithdrawalDialog } from '@vegaprotocol/withdraws';
|
|||||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
|
import type { PinnedAsset } from '@vegaprotocol/accounts';
|
||||||
import { AccountManager, useTransferDialog } from '@vegaprotocol/accounts';
|
import { AccountManager, useTransferDialog } from '@vegaprotocol/accounts';
|
||||||
import { useDepositDialog } from '@vegaprotocol/deposits';
|
import { useDepositDialog } from '@vegaprotocol/deposits';
|
||||||
|
|
||||||
export const AccountsContainer = () => {
|
export const AccountsContainer = ({
|
||||||
|
pinnedAsset,
|
||||||
|
}: {
|
||||||
|
pinnedAsset?: PinnedAsset;
|
||||||
|
}) => {
|
||||||
const { pubKey, isReadOnly } = useVegaWallet();
|
const { pubKey, isReadOnly } = useVegaWallet();
|
||||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||||
const openWithdrawalDialog = useWithdrawalDialog((store) => store.open);
|
const openWithdrawalDialog = useWithdrawalDialog((store) => store.open);
|
||||||
@@ -39,6 +44,7 @@ export const AccountsContainer = () => {
|
|||||||
onClickWithdraw={openWithdrawalDialog}
|
onClickWithdraw={openWithdrawalDialog}
|
||||||
onClickDeposit={openDepositDialog}
|
onClickDeposit={openDepositDialog}
|
||||||
isReadOnly={isReadOnly}
|
isReadOnly={isReadOnly}
|
||||||
|
pinnedAsset={pinnedAsset}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{!isReadOnly && (
|
{!isReadOnly && (
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useGlobalStore } from '../../stores';
|
||||||
|
|
||||||
|
export const AppFailure = ({
|
||||||
|
title,
|
||||||
|
error,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
error?: string | null;
|
||||||
|
}) => {
|
||||||
|
const { setNodeSwitcher } = useGlobalStore((store) => ({
|
||||||
|
nodeSwitcherOpen: store.nodeSwitcherDialog,
|
||||||
|
setNodeSwitcher: (open: boolean) =>
|
||||||
|
store.update({ nodeSwitcherDialog: open }),
|
||||||
|
}));
|
||||||
|
const nonIdealWrapperClasses =
|
||||||
|
'h-full min-h-screen flex items-center justify-center';
|
||||||
|
return (
|
||||||
|
<div className={nonIdealWrapperClasses}>
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-xl mb-4">{title}</h1>
|
||||||
|
{error && <p className="text-sm mb-8">{error}</p>}
|
||||||
|
<Button onClick={() => setNodeSwitcher(true)}>
|
||||||
|
{t('Change node')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import type { InMemoryCacheConfig } from '@apollo/client';
|
||||||
|
import {
|
||||||
|
NetworkLoader,
|
||||||
|
NodeGuard,
|
||||||
|
useEnvironment,
|
||||||
|
} from '@vegaprotocol/environment';
|
||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import { MaintenancePage } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { VegaWalletProvider } from '@vegaprotocol/wallet';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { AppFailure } from './app-failure';
|
||||||
|
import { Web3Provider } from './web3-provider';
|
||||||
|
|
||||||
|
const DynamicLoader = dynamic(() => import('../preloader/preloader'), {
|
||||||
|
loading: () => <>Loading...</>,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AppLoader = ({ children }: { children: ReactNode }) => {
|
||||||
|
const { error, VEGA_URL, MAINTENANCE_PAGE } = useEnvironment((store) => ({
|
||||||
|
error: store.error,
|
||||||
|
VEGA_URL: store.VEGA_URL,
|
||||||
|
MAINTENANCE_PAGE: store.MAINTENANCE_PAGE,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (MAINTENANCE_PAGE) {
|
||||||
|
return <MaintenancePage />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NetworkLoader
|
||||||
|
cache={cacheConfig}
|
||||||
|
skeleton={<DynamicLoader />}
|
||||||
|
failure={
|
||||||
|
<AppFailure title={t('Could not initialize app')} error={error} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<NodeGuard
|
||||||
|
skeleton={<DynamicLoader />}
|
||||||
|
failure={<AppFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
|
||||||
|
>
|
||||||
|
<Web3Provider>
|
||||||
|
<VegaWalletProvider>{children}</VegaWalletProvider>
|
||||||
|
</Web3Provider>
|
||||||
|
</NodeGuard>
|
||||||
|
</NetworkLoader>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cacheConfig: InMemoryCacheConfig = {
|
||||||
|
typePolicies: {
|
||||||
|
Account: {
|
||||||
|
keyFields: false,
|
||||||
|
fields: {
|
||||||
|
balanceFormatted: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Instrument: {
|
||||||
|
keyFields: false,
|
||||||
|
},
|
||||||
|
TradableInstrument: {
|
||||||
|
keyFields: ['instrument'],
|
||||||
|
},
|
||||||
|
Product: {
|
||||||
|
keyFields: ['settlementAsset', ['id']],
|
||||||
|
},
|
||||||
|
MarketData: {
|
||||||
|
keyFields: ['market', ['id']],
|
||||||
|
},
|
||||||
|
Node: {
|
||||||
|
keyFields: false,
|
||||||
|
},
|
||||||
|
Withdrawal: {
|
||||||
|
fields: {
|
||||||
|
pendingOnForeignChain: {
|
||||||
|
read: (isPending = false) => isPending,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ERC20: {
|
||||||
|
keyFields: ['contractAddress'],
|
||||||
|
},
|
||||||
|
PositionUpdate: {
|
||||||
|
keyFields: false,
|
||||||
|
},
|
||||||
|
AccountUpdate: {
|
||||||
|
keyFields: false,
|
||||||
|
},
|
||||||
|
Party: {
|
||||||
|
keyFields: false,
|
||||||
|
},
|
||||||
|
Fees: {
|
||||||
|
keyFields: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,121 +1,3 @@
|
|||||||
import type { ReactNode } from 'react';
|
export * from './app-failure';
|
||||||
import { useEffect } from 'react';
|
export * from './app-loader';
|
||||||
import { NetworkLoader, useEnvironment } from '@vegaprotocol/environment';
|
export * from './web3-provider';
|
||||||
import type { InMemoryCacheConfig } from '@apollo/client';
|
|
||||||
import {
|
|
||||||
useEthereumConfig,
|
|
||||||
createConnectors,
|
|
||||||
Web3Provider as Web3ProviderInternal,
|
|
||||||
useWeb3ConnectStore,
|
|
||||||
} from '@vegaprotocol/web3';
|
|
||||||
import { AsyncRenderer, Loader } from '@vegaprotocol/ui-toolkit';
|
|
||||||
|
|
||||||
interface AppLoaderProps {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Component to handle any app initialization, startup queries and other things
|
|
||||||
* that must happen for it can be used
|
|
||||||
*/
|
|
||||||
export function AppLoader({ children }: AppLoaderProps) {
|
|
||||||
return (
|
|
||||||
<NetworkLoader skeleton={<Loader />} cache={cacheConfig}>
|
|
||||||
{children}
|
|
||||||
</NetworkLoader>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Web3Provider = ({ children }: { children: ReactNode }) => {
|
|
||||||
const { config, loading, error } = useEthereumConfig();
|
|
||||||
const { ETHEREUM_PROVIDER_URL, ETH_LOCAL_PROVIDER_URL, ETH_WALLET_MNEMONIC } =
|
|
||||||
useEnvironment();
|
|
||||||
const [connectors, initializeConnectors] = useWeb3ConnectStore((store) => [
|
|
||||||
store.connectors,
|
|
||||||
store.initialize,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (config?.chain_id) {
|
|
||||||
return initializeConnectors(
|
|
||||||
createConnectors(
|
|
||||||
ETHEREUM_PROVIDER_URL,
|
|
||||||
Number(config?.chain_id),
|
|
||||||
ETH_LOCAL_PROVIDER_URL,
|
|
||||||
ETH_WALLET_MNEMONIC
|
|
||||||
),
|
|
||||||
Number(config.chain_id)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
config?.chain_id,
|
|
||||||
ETHEREUM_PROVIDER_URL,
|
|
||||||
initializeConnectors,
|
|
||||||
ETH_LOCAL_PROVIDER_URL,
|
|
||||||
ETH_WALLET_MNEMONIC,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AsyncRenderer
|
|
||||||
loading={loading}
|
|
||||||
error={error}
|
|
||||||
data={connectors}
|
|
||||||
noDataCondition={(d) => {
|
|
||||||
if (!d) return true;
|
|
||||||
return d.length < 1;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Web3ProviderInternal connectors={connectors}>
|
|
||||||
<>{children}</>
|
|
||||||
</Web3ProviderInternal>
|
|
||||||
</AsyncRenderer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const cacheConfig: InMemoryCacheConfig = {
|
|
||||||
typePolicies: {
|
|
||||||
Account: {
|
|
||||||
keyFields: false,
|
|
||||||
fields: {
|
|
||||||
balanceFormatted: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Instrument: {
|
|
||||||
keyFields: false,
|
|
||||||
},
|
|
||||||
TradableInstrument: {
|
|
||||||
keyFields: ['instrument'],
|
|
||||||
},
|
|
||||||
Product: {
|
|
||||||
keyFields: ['settlementAsset', ['id']],
|
|
||||||
},
|
|
||||||
MarketData: {
|
|
||||||
keyFields: ['market', ['id']],
|
|
||||||
},
|
|
||||||
Node: {
|
|
||||||
keyFields: false,
|
|
||||||
},
|
|
||||||
Withdrawal: {
|
|
||||||
fields: {
|
|
||||||
pendingOnForeignChain: {
|
|
||||||
read: (isPending = false) => isPending,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ERC20: {
|
|
||||||
keyFields: ['contractAddress'],
|
|
||||||
},
|
|
||||||
PositionUpdate: {
|
|
||||||
keyFields: false,
|
|
||||||
},
|
|
||||||
AccountUpdate: {
|
|
||||||
keyFields: false,
|
|
||||||
},
|
|
||||||
Party: {
|
|
||||||
keyFields: false,
|
|
||||||
},
|
|
||||||
Fees: {
|
|
||||||
keyFields: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import {
|
||||||
|
useEthereumConfig,
|
||||||
|
createConnectors,
|
||||||
|
Web3Provider as Web3ProviderInternal,
|
||||||
|
useWeb3ConnectStore,
|
||||||
|
} from '@vegaprotocol/web3';
|
||||||
|
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import { useEnvironment } from '@vegaprotocol/environment';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export const Web3Provider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const { config, loading, error } = useEthereumConfig();
|
||||||
|
const { ETHEREUM_PROVIDER_URL, ETH_LOCAL_PROVIDER_URL, ETH_WALLET_MNEMONIC } =
|
||||||
|
useEnvironment();
|
||||||
|
const [connectors, initializeConnectors] = useWeb3ConnectStore((store) => [
|
||||||
|
store.connectors,
|
||||||
|
store.initialize,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (config?.chain_id) {
|
||||||
|
return initializeConnectors(
|
||||||
|
createConnectors(
|
||||||
|
ETHEREUM_PROVIDER_URL,
|
||||||
|
Number(config?.chain_id),
|
||||||
|
ETH_LOCAL_PROVIDER_URL,
|
||||||
|
ETH_WALLET_MNEMONIC
|
||||||
|
),
|
||||||
|
Number(config.chain_id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
config?.chain_id,
|
||||||
|
ETHEREUM_PROVIDER_URL,
|
||||||
|
initializeConnectors,
|
||||||
|
ETH_LOCAL_PROVIDER_URL,
|
||||||
|
ETH_WALLET_MNEMONIC,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncRenderer
|
||||||
|
loading={loading}
|
||||||
|
error={error}
|
||||||
|
data={connectors}
|
||||||
|
noDataCondition={(d) => {
|
||||||
|
if (!d) return true;
|
||||||
|
return d.length < 1;
|
||||||
|
}}
|
||||||
|
noDataMessage={t('Could not fetch Ethereum configuration')}
|
||||||
|
>
|
||||||
|
<Web3ProviderInternal connectors={connectors}>
|
||||||
|
<>{children}</>
|
||||||
|
</Web3ProviderInternal>
|
||||||
|
</AsyncRenderer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,57 +1,38 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
import { Footer, NodeHealth } from './footer';
|
import { NodeUrl, NodeHealth } from './footer';
|
||||||
import { useEnvironment } from '@vegaprotocol/environment';
|
|
||||||
|
|
||||||
jest.mock('@vegaprotocol/environment');
|
describe('NodeUrl', () => {
|
||||||
|
|
||||||
describe('Footer', () => {
|
|
||||||
it('can open node switcher by clicking the node url', () => {
|
it('can open node switcher by clicking the node url', () => {
|
||||||
const mockOpenNodeSwitcher = jest.fn();
|
const mockOpenNodeSwitcher = jest.fn();
|
||||||
const node = 'n99.somenetwork.vega.xyz';
|
const node = 'https://api.n99.somenetwork.vega.xyz';
|
||||||
|
|
||||||
// @ts-ignore mock env hook
|
render(<NodeUrl url={node} openNodeSwitcher={mockOpenNodeSwitcher} />);
|
||||||
useEnvironment.mockImplementation(() => ({
|
|
||||||
VEGA_URL: `https://api.${node}/graphql`,
|
|
||||||
blockDifference: 0,
|
|
||||||
setNodeSwitcherOpen: mockOpenNodeSwitcher,
|
|
||||||
}));
|
|
||||||
|
|
||||||
render(<Footer />);
|
fireEvent.click(screen.getByText(/n99/));
|
||||||
|
|
||||||
fireEvent.click(screen.getByText(node));
|
|
||||||
expect(mockOpenNodeSwitcher).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('can open node switcher by clicking health', () => {
|
|
||||||
const mockOpenNodeSwitcher = jest.fn();
|
|
||||||
const node = 'n99.somenetwork.vega.xyz';
|
|
||||||
|
|
||||||
// @ts-ignore mock env hook
|
|
||||||
useEnvironment.mockImplementation(() => ({
|
|
||||||
VEGA_URL: `https://api.${node}/graphql`,
|
|
||||||
blockDifference: 0,
|
|
||||||
setNodeSwitcherOpen: mockOpenNodeSwitcher,
|
|
||||||
}));
|
|
||||||
|
|
||||||
render(<Footer />);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByText('Operational'));
|
|
||||||
expect(mockOpenNodeSwitcher).toHaveBeenCalled();
|
expect(mockOpenNodeSwitcher).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('NodeHealth', () => {
|
describe('NodeHealth', () => {
|
||||||
|
const mockOpenNodeSwitcher = jest.fn();
|
||||||
const cases = [
|
const cases = [
|
||||||
{ diff: 0, classname: 'bg-success', text: 'Operational' },
|
{ diff: 0, classname: 'bg-vega-green-550', text: 'Operational' },
|
||||||
{ diff: 5, classname: 'bg-warning', text: '5 Blocks behind' },
|
{ diff: 5, classname: 'bg-warning', text: '5 Blocks behind' },
|
||||||
{ diff: -1, classname: 'bg-danger', text: 'Non operational' },
|
{ diff: null, classname: 'bg-danger', text: 'Non operational' },
|
||||||
];
|
];
|
||||||
it.each(cases)(
|
it.each(cases)(
|
||||||
'renders correct text and indicator color for $diff block difference',
|
'renders correct text and indicator color for $diff block difference',
|
||||||
(elem) => {
|
(elem) => {
|
||||||
render(<NodeHealth blockDiff={elem.diff} openNodeSwitcher={jest.fn()} />);
|
render(
|
||||||
|
<NodeHealth
|
||||||
|
blockDiff={elem.diff}
|
||||||
|
openNodeSwitcher={mockOpenNodeSwitcher}
|
||||||
|
/>
|
||||||
|
);
|
||||||
expect(screen.getByTestId('indicator')).toHaveClass(elem.classname);
|
expect(screen.getByTestId('indicator')).toHaveClass(elem.classname);
|
||||||
expect(screen.getByText(elem.text)).toBeInTheDocument();
|
expect(screen.getByText(elem.text)).toBeInTheDocument();
|
||||||
|
fireEvent.click(screen.getByText(elem.text));
|
||||||
|
expect(mockOpenNodeSwitcher).toHaveBeenCalled();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { useEnvironment } from '@vegaprotocol/environment';
|
import { useEnvironment, useNodeHealth } from '@vegaprotocol/environment';
|
||||||
import { t, useNavigatorOnline } from '@vegaprotocol/react-helpers';
|
import { t, useNavigatorOnline } from '@vegaprotocol/react-helpers';
|
||||||
import { ButtonLink, Indicator, Intent } from '@vegaprotocol/ui-toolkit';
|
import { ButtonLink, Indicator, Intent } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useGlobalStore } from '../../stores';
|
||||||
|
|
||||||
export const Footer = () => {
|
export const Footer = () => {
|
||||||
const { VEGA_URL, blockDifference, setNodeSwitcherOpen } = useEnvironment();
|
const { VEGA_URL } = useEnvironment();
|
||||||
|
const setNodeSwitcher = useGlobalStore(
|
||||||
|
(store) => (open: boolean) => store.update({ nodeSwitcherDialog: open })
|
||||||
|
);
|
||||||
|
const { blockDiff } = useNodeHealth();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="px-4 py-1 text-xs border-t border-default">
|
<footer className="px-4 py-1 text-xs border-t border-default">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
@@ -11,11 +17,14 @@ export const Footer = () => {
|
|||||||
{VEGA_URL && (
|
{VEGA_URL && (
|
||||||
<>
|
<>
|
||||||
<NodeHealth
|
<NodeHealth
|
||||||
blockDiff={blockDifference}
|
blockDiff={blockDiff}
|
||||||
openNodeSwitcher={setNodeSwitcherOpen}
|
openNodeSwitcher={() => setNodeSwitcher(true)}
|
||||||
/>
|
/>
|
||||||
{' | '}
|
{' | '}
|
||||||
<NodeUrl url={VEGA_URL} openNodeSwitcher={setNodeSwitcherOpen} />
|
<NodeUrl
|
||||||
|
url={VEGA_URL}
|
||||||
|
openNodeSwitcher={() => setNodeSwitcher(true)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -29,7 +38,7 @@ interface NodeUrlProps {
|
|||||||
openNodeSwitcher: () => void;
|
openNodeSwitcher: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NodeUrl = ({ url, openNodeSwitcher }: NodeUrlProps) => {
|
export const NodeUrl = ({ url, openNodeSwitcher }: NodeUrlProps) => {
|
||||||
// get base url from api url, api sub domain
|
// get base url from api url, api sub domain
|
||||||
const urlObj = new URL(url);
|
const urlObj = new URL(url);
|
||||||
const nodeUrl = urlObj.origin.replace(/^[^.]+\./g, '');
|
const nodeUrl = urlObj.origin.replace(/^[^.]+\./g, '');
|
||||||
@@ -38,7 +47,7 @@ const NodeUrl = ({ url, openNodeSwitcher }: NodeUrlProps) => {
|
|||||||
|
|
||||||
interface NodeHealthProps {
|
interface NodeHealthProps {
|
||||||
openNodeSwitcher: () => void;
|
openNodeSwitcher: () => void;
|
||||||
blockDiff: number;
|
blockDiff: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// How many blocks behind the most advanced block that is
|
// How many blocks behind the most advanced block that is
|
||||||
@@ -57,7 +66,7 @@ export const NodeHealth = ({
|
|||||||
if (!online) {
|
if (!online) {
|
||||||
text = t('Offline');
|
text = t('Offline');
|
||||||
intent = Intent.Danger;
|
intent = Intent.Danger;
|
||||||
} else if (blockDiff < 0) {
|
} else if (blockDiff === null) {
|
||||||
// Block height query failed and null was returned
|
// Block height query failed and null was returned
|
||||||
text = t('Non operational');
|
text = t('Non operational');
|
||||||
intent = Intent.Danger;
|
intent = Intent.Danger;
|
||||||
@@ -67,9 +76,9 @@ export const NodeHealth = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span>
|
<>
|
||||||
<Indicator variant={intent} />
|
<Indicator variant={intent} />
|
||||||
<ButtonLink onClick={openNodeSwitcher}>{text}</ButtonLink>
|
<ButtonLink onClick={openNodeSwitcher}>{text}</ButtonLink>
|
||||||
</span>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { t, useDataProvider } from '@vegaprotocol/react-helpers';
|
import { t, useDataProvider } from '@vegaprotocol/react-helpers';
|
||||||
import { proposalsListDataProvider } from '@vegaprotocol/governance';
|
import { proposalsDataProvider } from '@vegaprotocol/governance';
|
||||||
import take from 'lodash/take';
|
import take from 'lodash/take';
|
||||||
import * as Types from '@vegaprotocol/types';
|
import * as Types from '@vegaprotocol/types';
|
||||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||||
@@ -19,7 +19,7 @@ export const ProposedMarkets = () => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
const { data } = useDataProvider({
|
const { data } = useDataProvider({
|
||||||
dataProvider: proposalsListDataProvider,
|
dataProvider: proposalsDataProvider,
|
||||||
variables,
|
variables,
|
||||||
skipUpdates: true,
|
skipUpdates: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import { render, screen, fireEvent } from '@testing-library/react';
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
import { Networks, EnvironmentProvider } from '@vegaprotocol/environment';
|
import { Networks, useEnvironment } from '@vegaprotocol/environment';
|
||||||
import { RiskNoticeDialog } from './risk-notice-dialog';
|
import { RiskNoticeDialog } from './risk-notice-dialog';
|
||||||
import { WelcomeDialog } from './welcome-dialog';
|
import { WelcomeDialog } from './welcome-dialog';
|
||||||
|
|
||||||
|
jest.mock('@vegaprotocol/environment');
|
||||||
|
|
||||||
const mockEnvDefinitions = {
|
const mockEnvDefinitions = {
|
||||||
VEGA_CONFIG_URL: 'https://config.url',
|
VEGA_CONFIG_URL: 'https://config.url',
|
||||||
VEGA_URL: 'https://test.url',
|
VEGA_URL: 'https://test.url',
|
||||||
@@ -28,15 +30,16 @@ describe('Risk notice dialog', () => {
|
|||||||
`(
|
`(
|
||||||
'$assertion the risk notice on $network',
|
'$assertion the risk notice on $network',
|
||||||
async ({ assertion, network }) => {
|
async ({ assertion, network }) => {
|
||||||
|
// @ts-ignore ignore mock implementation
|
||||||
|
useEnvironment.mockImplementation(() => ({
|
||||||
|
...mockEnvDefinitions,
|
||||||
|
VEGA_ENV: network,
|
||||||
|
}));
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<EnvironmentProvider
|
<MockedProvider>
|
||||||
definitions={{ ...mockEnvDefinitions, VEGA_ENV: network }}
|
<WelcomeDialog />
|
||||||
config={{ hosts: [] }}
|
</MockedProvider>,
|
||||||
>
|
|
||||||
<MockedProvider>
|
|
||||||
<WelcomeDialog />
|
|
||||||
</MockedProvider>
|
|
||||||
</EnvironmentProvider>,
|
|
||||||
{ wrapper: BrowserRouter }
|
{ wrapper: BrowserRouter }
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -51,14 +54,13 @@ describe('Risk notice dialog', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
it("doesn't display the risk notice when previously acknowledged", () => {
|
it("doesn't display the risk notice when previously acknowledged", () => {
|
||||||
render(
|
// @ts-ignore ignore mock implementation
|
||||||
<EnvironmentProvider
|
useEnvironment.mockImplementation(() => ({
|
||||||
definitions={{ ...mockEnvDefinitions, VEGA_ENV: Networks.MAINNET }}
|
...mockEnvDefinitions,
|
||||||
config={{ hosts: [] }}
|
VEGA_ENV: Networks.MAINNET,
|
||||||
>
|
}));
|
||||||
<RiskNoticeDialog onClose={mockOnClose} />
|
|
||||||
</EnvironmentProvider>
|
render(<RiskNoticeDialog onClose={mockOnClose} />);
|
||||||
);
|
|
||||||
|
|
||||||
expect(screen.queryByText(introText)).toBeInTheDocument();
|
expect(screen.queryByText(introText)).toBeInTheDocument();
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
import type { AppProps } from 'next/app';
|
import type { AppProps } from 'next/app';
|
||||||
import { Navbar } from '../components/navbar';
|
import { Navbar } from '../components/navbar';
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
import {
|
import {
|
||||||
useEagerConnect as useVegaEagerConnect,
|
useEagerConnect as useVegaEagerConnect,
|
||||||
VegaWalletProvider,
|
|
||||||
useVegaTransactionManager,
|
useVegaTransactionManager,
|
||||||
useVegaTransactionUpdater,
|
useVegaTransactionUpdater,
|
||||||
useVegaWallet,
|
useVegaWallet,
|
||||||
@@ -17,17 +15,17 @@ import {
|
|||||||
useEthWithdrawApprovalsManager,
|
useEthWithdrawApprovalsManager,
|
||||||
} from '@vegaprotocol/web3';
|
} from '@vegaprotocol/web3';
|
||||||
import {
|
import {
|
||||||
EnvironmentProvider,
|
|
||||||
envTriggerMapping,
|
envTriggerMapping,
|
||||||
Networks,
|
Networks,
|
||||||
|
NodeSwitcherDialog,
|
||||||
useEnvironment,
|
useEnvironment,
|
||||||
|
useInitializeEnv,
|
||||||
} from '@vegaprotocol/environment';
|
} from '@vegaprotocol/environment';
|
||||||
import { AppLoader, Web3Provider } from '../components/app-loader';
|
|
||||||
import './styles.css';
|
import './styles.css';
|
||||||
import './gen-styles.scss';
|
import './gen-styles.scss';
|
||||||
import { usePageTitleStore } from '../stores';
|
import { useGlobalStore, usePageTitleStore } from '../stores';
|
||||||
import { Footer } from '../components/footer';
|
import { Footer } from '../components/footer';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import DialogsContainer from './dialogs-container';
|
import DialogsContainer from './dialogs-container';
|
||||||
import ToastsManager from './toasts-manager';
|
import ToastsManager from './toasts-manager';
|
||||||
import { HashRouter, useLocation, useSearchParams } from 'react-router-dom';
|
import { HashRouter, useLocation, useSearchParams } from 'react-router-dom';
|
||||||
@@ -35,6 +33,7 @@ import { Connectors } from '../lib/vega-connectors';
|
|||||||
import { ViewingBanner } from '../components/viewing-banner';
|
import { ViewingBanner } from '../components/viewing-banner';
|
||||||
import { Banner } from '../components/banner';
|
import { Banner } from '../components/banner';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { AppLoader } from '../components/app-loader';
|
||||||
|
|
||||||
const DEFAULT_TITLE = t('Welcome to Vega trading!');
|
const DEFAULT_TITLE = t('Welcome to Vega trading!');
|
||||||
|
|
||||||
@@ -84,57 +83,47 @@ function AppBody({ Component }: AppProps) {
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
</Head>
|
</Head>
|
||||||
<Title />
|
<Title />
|
||||||
<VegaWalletProvider>
|
<div className={gridClasses}>
|
||||||
<AppLoader>
|
<Navbar
|
||||||
<Web3Provider>
|
navbarTheme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'}
|
||||||
<div className={gridClasses}>
|
/>
|
||||||
<Navbar
|
<Banner />
|
||||||
navbarTheme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'dark'}
|
<ViewingBanner />
|
||||||
/>
|
<main data-testid={location.pathname}>
|
||||||
<Banner />
|
<Component />
|
||||||
<ViewingBanner />
|
</main>
|
||||||
<main data-testid={location.pathname}>
|
<Footer />
|
||||||
<Component />
|
</div>
|
||||||
</main>
|
<DialogsContainer />
|
||||||
<Footer />
|
<ToastsManager />
|
||||||
</div>
|
<TransactionsHandler />
|
||||||
<DialogsContainer />
|
<MaybeConnectEagerly />
|
||||||
<ToastsManager />
|
|
||||||
<TransactionsHandler />
|
|
||||||
<MaybeConnectEagerly />
|
|
||||||
</Web3Provider>
|
|
||||||
</AppLoader>
|
|
||||||
</VegaWalletProvider>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DynamicLoader = dynamic(
|
|
||||||
() => import('../components/preloader/preloader'),
|
|
||||||
{
|
|
||||||
loading: () => <>Loading...</>,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
function VegaTradingApp(props: AppProps) {
|
function VegaTradingApp(props: AppProps) {
|
||||||
const [mounted, setMounted] = useState(false);
|
const status = useEnvironment((store) => store.status);
|
||||||
|
const { nodeSwitcherOpen, setNodeSwitcher } = useGlobalStore((store) => ({
|
||||||
|
nodeSwitcherOpen: store.nodeSwitcherDialog,
|
||||||
|
setNodeSwitcher: (open: boolean) =>
|
||||||
|
store.update({ nodeSwitcherDialog: open }),
|
||||||
|
}));
|
||||||
|
|
||||||
// Hash router requires access to the document object. At compile time that doesn't exist
|
useInitializeEnv();
|
||||||
// so we need to ensure client side rendering only from this point onwards in
|
|
||||||
// the component tree
|
|
||||||
useEffect(() => {
|
|
||||||
setMounted(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!mounted) {
|
// Prevent HashRouter from being server side rendered as it
|
||||||
return <DynamicLoader />;
|
// relies on presence of document object
|
||||||
|
if (status === 'default') {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<EnvironmentProvider>
|
<AppLoader>
|
||||||
<AppBody {...props} />
|
<AppBody {...props} />
|
||||||
</EnvironmentProvider>
|
</AppLoader>
|
||||||
|
<NodeSwitcherDialog open={nodeSwitcherOpen} setOpen={setNodeSwitcher} />
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { create } from 'zustand';
|
|||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
|
|
||||||
interface GlobalStore {
|
interface GlobalStore {
|
||||||
networkSwitcherDialog: boolean;
|
nodeSwitcherDialog: boolean;
|
||||||
marketId: string | null;
|
marketId: string | null;
|
||||||
update: (store: Partial<Omit<GlobalStore, 'update'>>) => void;
|
update: (store: Partial<Omit<GlobalStore, 'update'>>) => void;
|
||||||
shouldDisplayWelcomeDialog: boolean;
|
shouldDisplayWelcomeDialog: boolean;
|
||||||
@@ -16,7 +16,7 @@ interface PageTitleStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useGlobalStore = create<GlobalStore>((set) => ({
|
export const useGlobalStore = create<GlobalStore>((set) => ({
|
||||||
networkSwitcherDialog: false,
|
nodeSwitcherDialog: false,
|
||||||
marketId: LocalStorage.getItem('marketId') || null,
|
marketId: LocalStorage.getItem('marketId') || null,
|
||||||
shouldDisplayWelcomeDialog: false,
|
shouldDisplayWelcomeDialog: false,
|
||||||
shouldDisplayAnnouncementBanner: true,
|
shouldDisplayAnnouncementBanner: true,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user