Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86d234b6eb | ||
|
|
587065beb4 | ||
|
|
3a1522bbf5 | ||
|
|
fc0ae5478e | ||
|
|
cf89c4f902 | ||
|
|
5e9009eaf1 | ||
|
|
7f77fceeab |
@@ -82,10 +82,6 @@ jobs:
|
||||
mv "${file}" "$(echo ${file} | sed 's|:|-|g')"
|
||||
done< <(find /home/runner/.vegacapsule/testnet/logs -type f)
|
||||
|
||||
- name: Print logs files
|
||||
if: ${{ always() }}
|
||||
run: ls -alsh /home/runner/.vegacapsule/testnet/logs/
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ always() }}
|
||||
with:
|
||||
|
||||
@@ -1,48 +1,150 @@
|
||||
context('Asset page', { tags: '@regression' }, () => {
|
||||
const columns = ['symbol', 'name', 'id', 'type', 'status', 'actions'];
|
||||
const hiddenOnMobile = ['id', 'type', 'status'];
|
||||
describe('Verify elements on page', () => {
|
||||
before('Navigate to assets page', () => {
|
||||
cy.visit('/assets');
|
||||
context('Asset page', { tags: '@regression' }, function () {
|
||||
before('gather system asset information', function () {
|
||||
cy.get_asset_information().as('assetsInfo');
|
||||
});
|
||||
|
||||
describe('Verify elements on page', function () {
|
||||
const assetsNavigation = 'a[href="/assets"]';
|
||||
const assetHeader = '[data-testid="asset-header"]';
|
||||
const jsonSection = '.language-json';
|
||||
|
||||
before('Navigate to assets page', function () {
|
||||
cy.visit('/');
|
||||
cy.get(assetsNavigation).click();
|
||||
|
||||
// Check we have enough enough assets
|
||||
const assetNames = Object.keys(this.assetsInfo);
|
||||
assert.isAtLeast(
|
||||
assetNames.length,
|
||||
5,
|
||||
'Ensuring we have at least 5 assets to test'
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to see full assets list', () => {
|
||||
cy.getAssets().then((assets) => {
|
||||
Object.values(assets).forEach((asset) => {
|
||||
cy.get(`[row-id="${asset.id}"]`).should('be.visible');
|
||||
});
|
||||
});
|
||||
columns.forEach((col) => {
|
||||
cy.get(`[col-id="${col}"]`).should('be.visible');
|
||||
it('should be able to see assets page sections', function () {
|
||||
const assetNames = Object.keys(this.assetsInfo);
|
||||
assetNames.forEach((assetName) => {
|
||||
cy.get(assetHeader)
|
||||
.contains(assetName)
|
||||
.should('be.visible')
|
||||
.next()
|
||||
.within(() => {
|
||||
cy.get(jsonSection).should('not.be.empty');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to see assets page displayed in mobile', () => {
|
||||
cy.switchToMobile();
|
||||
it('should be able to see all asset details displayed in JSON', function () {
|
||||
const assetNames = Object.keys(this.assetsInfo);
|
||||
assetNames.forEach((assetName) => {
|
||||
cy.get(assetHeader)
|
||||
.contains(assetName)
|
||||
.next()
|
||||
.within(() => {
|
||||
cy.get(jsonSection)
|
||||
.invoke('text')
|
||||
.convert_string_json_to_js_object()
|
||||
.then((assetsListedInJson) => {
|
||||
const assetInfo = this.assetsInfo[assetName];
|
||||
|
||||
hiddenOnMobile.forEach((col) => {
|
||||
cy.get(`[col-id="${col}"]`).should('have.length', 0);
|
||||
});
|
||||
assert.equal(assetsListedInJson.name, assetInfo.node.name);
|
||||
assert.equal(assetsListedInJson.id, assetInfo.node.id);
|
||||
assert.equal(
|
||||
assetsListedInJson.decimals,
|
||||
assetInfo.node.decimals
|
||||
);
|
||||
assert.equal(assetsListedInJson.symbol, assetInfo.node.symbol);
|
||||
assert.equal(
|
||||
assetsListedInJson.source.__typename,
|
||||
assetInfo.node.source.__typename
|
||||
);
|
||||
|
||||
cy.getAssets().then((assets) => {
|
||||
Object.values(assets).forEach((asset) => {
|
||||
cy.get(`[row-id="${asset.id}"]`).should('be.visible');
|
||||
});
|
||||
if (assetInfo.node.source.__typename == 'ERC20') {
|
||||
assert.equal(
|
||||
assetsListedInJson.source.contractAddress,
|
||||
assetInfo.node.source.contractAddress
|
||||
);
|
||||
}
|
||||
|
||||
if (assetInfo.node.source.__typename == 'BuiltinAsset') {
|
||||
assert.equal(
|
||||
assetsListedInJson.source.maxFaucetAmountMint,
|
||||
assetInfo.node.source.maxFaucetAmountMint
|
||||
);
|
||||
}
|
||||
|
||||
let knownAssetTypes = ['BuiltinAsset', 'ERC20'];
|
||||
assert.include(
|
||||
knownAssetTypes,
|
||||
assetInfo.node.source.__typename,
|
||||
`Checking that current asset type of ${assetInfo.node.source.__typename} /
|
||||
is one of: ${knownAssetTypes}: /
|
||||
If fail then we need to add extra tests for un-encountered asset types`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should open details page when clicked on "View details"', () => {
|
||||
cy.getAssets().then((assets) => {
|
||||
Object.values(assets).forEach((asset) => {
|
||||
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`)
|
||||
.eq(0)
|
||||
.should('contain.text', 'View details');
|
||||
cy.get(`[row-id="${asset.id}"] [col-id="actions"] button`)
|
||||
.eq(0)
|
||||
.click();
|
||||
cy.getByTestId('asset-header').should('have.text', asset.name);
|
||||
cy.go('back');
|
||||
it('should be able to switch assets between light and dark mode', function () {
|
||||
const whiteThemeSelectedMenuOptionColor = 'rgb(255, 7, 127)';
|
||||
const whiteThemeJsonFieldBackColor = 'rgb(255, 255, 255)';
|
||||
const whiteThemeSideMenuBackgroundColor = 'rgb(255, 255, 255)';
|
||||
const darkThemeSelectedMenuOptionColor = 'rgb(215, 251, 80)';
|
||||
const darkThemeJsonFieldBackColor = 'rgb(38, 38, 38)';
|
||||
const darkThemeSideMenuBackgroundColor = 'rgb(0, 0, 0)';
|
||||
const themeSwitcher = '[data-testid="theme-switcher"]';
|
||||
const jsonFields = '.hljs';
|
||||
const sideMenuBackground = '.absolute';
|
||||
|
||||
// Engage dark mode if not allready set
|
||||
cy.get(sideMenuBackground)
|
||||
.should('have.css', 'background-color')
|
||||
.then((background_color) => {
|
||||
if (background_color.includes(whiteThemeSideMenuBackgroundColor))
|
||||
cy.get(themeSwitcher).click();
|
||||
});
|
||||
|
||||
// Engage white mode
|
||||
cy.get(themeSwitcher).click();
|
||||
|
||||
// White Mode
|
||||
cy.get(assetsNavigation)
|
||||
.should('have.css', 'background-color')
|
||||
.and('include', whiteThemeSelectedMenuOptionColor);
|
||||
cy.get(jsonFields)
|
||||
.should('have.css', 'background-color')
|
||||
.and('include', whiteThemeJsonFieldBackColor);
|
||||
cy.get(sideMenuBackground)
|
||||
.should('have.css', 'background-color')
|
||||
.and('include', whiteThemeSideMenuBackgroundColor);
|
||||
|
||||
// Dark Mode
|
||||
cy.get(themeSwitcher).click();
|
||||
cy.get(assetsNavigation)
|
||||
.should('have.css', 'background-color')
|
||||
.and('include', darkThemeSelectedMenuOptionColor);
|
||||
cy.get(jsonFields)
|
||||
.should('have.css', 'background-color')
|
||||
.and('include', darkThemeJsonFieldBackColor);
|
||||
cy.get(sideMenuBackground)
|
||||
.should('have.css', 'background-color')
|
||||
.and('include', darkThemeSideMenuBackgroundColor);
|
||||
});
|
||||
|
||||
it('should be able to see assets page displayed in mobile', function () {
|
||||
cy.common_switch_to_mobile_and_click_toggle();
|
||||
cy.get(assetsNavigation).click();
|
||||
|
||||
const assetNames = Object.keys(this.assetsInfo);
|
||||
assetNames.forEach((assetName) => {
|
||||
cy.get(assetHeader)
|
||||
.contains(assetName)
|
||||
.should('be.visible')
|
||||
.next()
|
||||
.within(() => {
|
||||
cy.get(jsonSection).should('not.be.empty');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -247,7 +247,7 @@ context('Network parameters page', { tags: '@smoke' }, function () {
|
||||
.and('include', darkThemeSideMenuBackgroundColor);
|
||||
});
|
||||
|
||||
it.skip('should be able to see network parameters - on mobile', function () {
|
||||
it('should be able to see network parameters - on mobile', function () {
|
||||
cy.common_switch_to_mobile_and_click_toggle();
|
||||
cy.get(networkParametersNavigation).click();
|
||||
cy.get_network_parameters().then((network_parameters) => {
|
||||
|
||||
@@ -123,7 +123,7 @@ context.skip('Parties page', { tags: '@regression' }, function () {
|
||||
.convert_string_json_to_js_object()
|
||||
.get_party_accounts_data_from_js_object()
|
||||
.then((accountsListedInJson) => {
|
||||
cy.getAssets().then((assetsInfo) => {
|
||||
cy.get_asset_information().then((assetsInfo) => {
|
||||
const assetInfo =
|
||||
assetsInfo[accountsListedInJson[assetInTest].asset.name];
|
||||
|
||||
@@ -205,7 +205,7 @@ context.skip('Parties page', { tags: '@regression' }, function () {
|
||||
});
|
||||
|
||||
Cypress.Commands.add('get_asset_decimals', (assetID) => {
|
||||
cy.getAssets().then((assetsInfo) => {
|
||||
cy.get_asset_information().then((assetsInfo) => {
|
||||
const assetDecimals = assetsInfo[assetData[assetID].name].decimals;
|
||||
let decimals = '';
|
||||
for (let i = 0; i < assetDecimals; i++) decimals += '0';
|
||||
|
||||
@@ -21,10 +21,6 @@ Cypress.Commands.add(
|
||||
}
|
||||
);
|
||||
|
||||
Cypress.Commands.add('switchToMobile', () => {
|
||||
cy.viewport('iphone-x');
|
||||
});
|
||||
|
||||
Cypress.Commands.add('common_switch_to_mobile_and_click_toggle', function () {
|
||||
cy.viewport('iphone-x');
|
||||
cy.visit('/');
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
# App configuration variables
|
||||
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/tm
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://tm.be.devnet1.vega.xyz/websocket
|
||||
NX_TENDERMINT_URL=https://n04.d.vega.xyz/tm
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://n04.d.vega.xyz/tm/websocket
|
||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet-network.json
|
||||
NX_VEGA_NETWORKS={\"MAINNET"\:\"https://explorer.vega.xyz"\,\"TESTNET\":\"https://explorer.fairground.wtf\"}
|
||||
NX_VEGA_ENV=DEVNET
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_BLOCK_EXPLORER=https://be.devnet1.vega.xyz/rest
|
||||
NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_VEGA_GOVERNANCE_URL=https://dev.token.vega.xyz
|
||||
NX_VEGA_URL=https://api.devnet1.vega.xyz/graphql
|
||||
NX_VEGA_CONFIG_URL=https://static.vega.xyz/assets/devnet1-network.json
|
||||
NX_VEGA_GOVERNANCE_URL=https://dev.token.vega.xyz
|
||||
@@ -1,68 +1,72 @@
|
||||
import classnames from 'classnames';
|
||||
import { NetworkLoader, useInitializeEnv } from '@vegaprotocol/environment';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { EnvironmentProvider, NetworkLoader } from '@vegaprotocol/environment';
|
||||
import { Nav } from './components/nav';
|
||||
import { Header } from './components/header';
|
||||
import { Main } from './components/main';
|
||||
import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-websocket-provider';
|
||||
import type { InMemoryCacheConfig } from '@apollo/client';
|
||||
import { Footer } from './components/footer/footer';
|
||||
import { AnnouncementBanner, ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
AssetDetailsDialog,
|
||||
useAssetDetailsDialogStore,
|
||||
} from '@vegaprotocol/assets';
|
||||
import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client';
|
||||
|
||||
const DialogsContainer = () => {
|
||||
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
|
||||
return (
|
||||
<AssetDetailsDialog
|
||||
assetId={id}
|
||||
trigger={trigger || null}
|
||||
asJson={asJson}
|
||||
open={isOpen}
|
||||
onChange={setOpen}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
function App() {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
setMenuOpen(false);
|
||||
}, [location]);
|
||||
|
||||
const cacheConfig: InMemoryCacheConfig = {
|
||||
typePolicies: {
|
||||
statistics: {
|
||||
keyFields: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const layoutClasses = classnames(
|
||||
'grid grid-rows-[auto_1fr_auto] grid-cols-[1fr] md:grid-rows-[auto_minmax(700px,_1fr)_auto] md:grid-cols-[300px_1fr]',
|
||||
'min-h-[100vh] mx-auto my-0',
|
||||
'border-neutral-700 dark:border-neutral-300 lg:border-l lg:border-r',
|
||||
'bg-white dark:bg-black',
|
||||
'antialiased text-black dark:text-white',
|
||||
'overflow-hidden relative'
|
||||
{
|
||||
'h-[100vh] min-h-auto overflow-hidden': menuOpen,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<TendermintWebsocketProvider>
|
||||
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
|
||||
<NetworkLoader cache={cacheConfig}>
|
||||
<AnnouncementBanner>
|
||||
<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">The Mainnet sims are live!</span>
|
||||
<ExternalLink href="https://fairground.wtf/">
|
||||
Learn more
|
||||
Come help stress test the network
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</AnnouncementBanner>
|
||||
|
||||
<div className={layoutClasses}>
|
||||
<Header />
|
||||
<Nav />
|
||||
<Header menuOpen={menuOpen} setMenuOpen={setMenuOpen} />
|
||||
<Nav menuOpen={menuOpen} />
|
||||
<Main />
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
<DialogsContainer />
|
||||
</NetworkLoader>
|
||||
</TendermintWebsocketProvider>
|
||||
);
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
useInitializeEnv();
|
||||
return <App />;
|
||||
return (
|
||||
<EnvironmentProvider>
|
||||
<App />
|
||||
</EnvironmentProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default Wrapper;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useAssetDataProvider } from '@vegaprotocol/assets';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/react-helpers';
|
||||
import { AssetLink } from '../links';
|
||||
import { useExplorerAssetQuery } from '../links/asset-link/__generated__/Asset';
|
||||
|
||||
export type AssetBalanceProps = {
|
||||
assetId: string;
|
||||
@@ -17,17 +17,21 @@ const AssetBalance = ({
|
||||
price,
|
||||
showAssetLink = true,
|
||||
}: AssetBalanceProps) => {
|
||||
const { data: asset } = useAssetDataProvider(assetId);
|
||||
const { data } = useExplorerAssetQuery({
|
||||
variables: { id: assetId },
|
||||
});
|
||||
|
||||
const label =
|
||||
asset && asset.decimals
|
||||
? addDecimalsFormatNumber(price, asset.decimals)
|
||||
data && data.asset?.decimals
|
||||
? addDecimalsFormatNumber(price, data.asset.decimals)
|
||||
: price;
|
||||
|
||||
return (
|
||||
<div className="inline-block">
|
||||
<span>{label}</span>{' '}
|
||||
{showAssetLink && asset?.id ? <AssetLink assetId={assetId} /> : null}
|
||||
{showAssetLink && data?.asset?.id ? (
|
||||
<AssetLink id={data.asset.id} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { assetsList } from '../../mocks/assets';
|
||||
import { AssetsTable } from './assets-table';
|
||||
|
||||
describe('AssetsTable', () => {
|
||||
it('shows loading message on first render', async () => {
|
||||
const res = render(
|
||||
<MemoryRouter>
|
||||
<AssetsTable data={null} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(await res.findByText('Loading...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows no data message if no assets found', async () => {
|
||||
const res = render(
|
||||
<MemoryRouter>
|
||||
<AssetsTable data={[]} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
expect(
|
||||
await res.findByText('This chain has no assets')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows a table/list with all the assets', async () => {
|
||||
const res = render(
|
||||
<MemoryRouter>
|
||||
<AssetsTable data={assetsList} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
await waitFor(() => {
|
||||
const rowA1 = res.container.querySelector('[row-id="123"]');
|
||||
expect(rowA1).toBeInTheDocument();
|
||||
|
||||
const rowA2 = res.container.querySelector('[row-id="456"]');
|
||||
expect(rowA2).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,105 +0,0 @@
|
||||
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
import { AssetTypeMapping, AssetStatusMapping } from '@vegaprotocol/assets';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import type { VegaICellRendererParams } 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 { useNavigate } from 'react-router-dom';
|
||||
import type { RowClickedEvent } from 'ag-grid-community';
|
||||
|
||||
type AssetsTableProps = {
|
||||
data: AssetFieldsFragment[] | null;
|
||||
};
|
||||
export const AssetsTable = ({ data }: AssetsTableProps) => {
|
||||
const navigate = useNavigate();
|
||||
const ref = useRef<AgGridReact>(null);
|
||||
const showColumnsOnDesktop = () => {
|
||||
ref.current?.columnApi.setColumnsVisible(
|
||||
['id', 'type', 'status'],
|
||||
window.innerWidth > BREAKPOINT_MD
|
||||
);
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
window.addEventListener('resize', showColumnsOnDesktop);
|
||||
return () => {
|
||||
window.removeEventListener('resize', showColumnsOnDesktop);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AgGrid
|
||||
ref={ref}
|
||||
rowData={data}
|
||||
getRowId={({ data }: { data: AssetFieldsFragment }) => data.id}
|
||||
overlayNoRowsTemplate={t('This chain has no assets')}
|
||||
domLayout="autoHeight"
|
||||
defaultColDef={{
|
||||
flex: 1,
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
filterParams: { buttons: ['reset'] },
|
||||
autoHeight: true,
|
||||
}}
|
||||
suppressCellFocus={true}
|
||||
onRowClicked={({ data }: RowClickedEvent) => {
|
||||
navigate(data.id);
|
||||
}}
|
||||
>
|
||||
<AgGridColumn headerName={t('Symbol')} field="symbol" />
|
||||
<AgGridColumn headerName={t('Name')} field="name" />
|
||||
<AgGridColumn
|
||||
flex="2"
|
||||
headerName={t('ID')}
|
||||
field="id"
|
||||
hide={window.innerWidth < BREAKPOINT_MD}
|
||||
/>
|
||||
<AgGridColumn
|
||||
colId="type"
|
||||
headerName={t('Type')}
|
||||
field="source.__typename"
|
||||
hide={window.innerWidth < BREAKPOINT_MD}
|
||||
valueFormatter={({ value }: { value?: string }) =>
|
||||
value && AssetTypeMapping[value].value
|
||||
}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Status')}
|
||||
field="status"
|
||||
hide={window.innerWidth < BREAKPOINT_MD}
|
||||
valueFormatter={({ value }: { value?: string }) =>
|
||||
value && AssetStatusMapping[value].value
|
||||
}
|
||||
/>
|
||||
<AgGridColumn
|
||||
colId="actions"
|
||||
headerName=""
|
||||
sortable={false}
|
||||
filter={false}
|
||||
resizable={false}
|
||||
wrapText={true}
|
||||
field="id"
|
||||
cellRenderer={({
|
||||
value,
|
||||
}: VegaICellRendererParams<AssetFieldsFragment, 'id'>) =>
|
||||
value ? (
|
||||
<ButtonLink
|
||||
onClick={(e) => {
|
||||
navigate(value);
|
||||
}}
|
||||
>
|
||||
{t('View details')}
|
||||
</ButtonLink>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
/>
|
||||
</AgGrid>
|
||||
);
|
||||
};
|
||||
@@ -1,56 +0,0 @@
|
||||
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,50 +0,0 @@
|
||||
query ExplorerEpoch($id: ID!) {
|
||||
epoch(id: $id) {
|
||||
id
|
||||
timestamps {
|
||||
start
|
||||
end
|
||||
firstBlock
|
||||
lastBlock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query ExplorerFutureEpoch {
|
||||
networkParameter(key: "validators.epoch.length") {
|
||||
value
|
||||
}
|
||||
|
||||
epoch {
|
||||
id
|
||||
timestamps {
|
||||
start
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# query ExplorerEpoch($id: ID!) {
|
||||
#
|
||||
##### This could be useful for calculating roughly when a future epoch will
|
||||
##### occur, but epoch not exist results in a total error
|
||||
# networkParameter(key: "validators.epoch.length") {
|
||||
# value
|
||||
# }
|
||||
#
|
||||
##### This could be useful for relating where we are in time, but as above
|
||||
##### the total failure caused by epoch(id) not existing
|
||||
##### means this is useful
|
||||
# currentEpoch: epoch {
|
||||
# id
|
||||
# }
|
||||
#
|
||||
# epoch(id: $id) {
|
||||
# id
|
||||
# timestamps {
|
||||
# start
|
||||
# end
|
||||
# firstBlock
|
||||
# lastBlock
|
||||
# }
|
||||
# }
|
||||
#}
|
||||
@@ -1,99 +0,0 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type ExplorerEpochQueryVariables = Types.Exact<{
|
||||
id: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type ExplorerEpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null, firstBlock: string, lastBlock?: string | null } } };
|
||||
|
||||
export type ExplorerFutureEpochQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ExplorerFutureEpochQuery = { __typename?: 'Query', networkParameter?: { __typename?: 'NetworkParameter', value: string } | null, epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null } } };
|
||||
|
||||
|
||||
export const ExplorerEpochDocument = gql`
|
||||
query ExplorerEpoch($id: ID!) {
|
||||
epoch(id: $id) {
|
||||
id
|
||||
timestamps {
|
||||
start
|
||||
end
|
||||
firstBlock
|
||||
lastBlock
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useExplorerEpochQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useExplorerEpochQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useExplorerEpochQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useExplorerEpochQuery({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useExplorerEpochQuery(baseOptions: Apollo.QueryHookOptions<ExplorerEpochQuery, ExplorerEpochQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ExplorerEpochQuery, ExplorerEpochQueryVariables>(ExplorerEpochDocument, options);
|
||||
}
|
||||
export function useExplorerEpochLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerEpochQuery, ExplorerEpochQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ExplorerEpochQuery, ExplorerEpochQueryVariables>(ExplorerEpochDocument, options);
|
||||
}
|
||||
export type ExplorerEpochQueryHookResult = ReturnType<typeof useExplorerEpochQuery>;
|
||||
export type ExplorerEpochLazyQueryHookResult = ReturnType<typeof useExplorerEpochLazyQuery>;
|
||||
export type ExplorerEpochQueryResult = Apollo.QueryResult<ExplorerEpochQuery, ExplorerEpochQueryVariables>;
|
||||
export const ExplorerFutureEpochDocument = gql`
|
||||
query ExplorerFutureEpoch {
|
||||
networkParameter(key: "validators.epoch.length") {
|
||||
value
|
||||
}
|
||||
epoch {
|
||||
id
|
||||
timestamps {
|
||||
start
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useExplorerFutureEpochQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useExplorerFutureEpochQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useExplorerFutureEpochQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useExplorerFutureEpochQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useExplorerFutureEpochQuery(baseOptions?: Apollo.QueryHookOptions<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>(ExplorerFutureEpochDocument, options);
|
||||
}
|
||||
export function useExplorerFutureEpochLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>(ExplorerFutureEpochDocument, options);
|
||||
}
|
||||
export type ExplorerFutureEpochQueryHookResult = ReturnType<typeof useExplorerFutureEpochQuery>;
|
||||
export type ExplorerFutureEpochLazyQueryHookResult = ReturnType<typeof useExplorerFutureEpochLazyQuery>;
|
||||
export type ExplorerFutureEpochQueryResult = Apollo.QueryResult<ExplorerFutureEpochQuery, ExplorerFutureEpochQueryVariables>;
|
||||
@@ -1,56 +0,0 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { IconForEpoch } from './epoch';
|
||||
|
||||
const THE_PAST = 'Monday, 17 February 2022 11:44:09';
|
||||
const THE_FUTURE = 'Monday, 17 February 3023 11:44:09';
|
||||
|
||||
describe('IconForEpoch', () => {
|
||||
it('Handles malformed dates', () => {
|
||||
const start = 'This is n0t a d4te';
|
||||
const end = '📅';
|
||||
const screen = render(<IconForEpoch start={start} end={end} />);
|
||||
|
||||
expect(screen.getByRole('img')).toHaveAttribute(
|
||||
'aria-label',
|
||||
'calendar icon'
|
||||
);
|
||||
});
|
||||
|
||||
it('defaults to a calendar icon', () => {
|
||||
const start = null as unknown as string;
|
||||
const end = null as unknown as string;
|
||||
const screen = render(<IconForEpoch start={start} end={end} />);
|
||||
|
||||
expect(screen.getByRole('img')).toHaveAttribute(
|
||||
'aria-label',
|
||||
'calendar icon'
|
||||
);
|
||||
});
|
||||
|
||||
it('if start and end are both in the future, stick with calendar', () => {
|
||||
const screen = render(<IconForEpoch start={THE_FUTURE} end={THE_FUTURE} />);
|
||||
|
||||
expect(screen.getByRole('img')).toHaveAttribute(
|
||||
'aria-label',
|
||||
'calendar icon'
|
||||
);
|
||||
});
|
||||
|
||||
it('if start is in the past and end is in the future, this is currently active', () => {
|
||||
const screen = render(<IconForEpoch start={THE_PAST} end={THE_FUTURE} />);
|
||||
|
||||
expect(screen.getByRole('img')).toHaveAttribute(
|
||||
'aria-label',
|
||||
'circle icon'
|
||||
);
|
||||
});
|
||||
|
||||
it('if start and end are in the paste, this is done', () => {
|
||||
const screen = render(<IconForEpoch start={THE_PAST} end={THE_PAST} />);
|
||||
|
||||
expect(screen.getByRole('img')).toHaveAttribute(
|
||||
'aria-label',
|
||||
'tick-circle icon'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,125 +0,0 @@
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { render } from '@testing-library/react';
|
||||
import EpochMissingOverview, { calculateEpochData } from './epoch-missing';
|
||||
import { getSecondsFromInterval } from '@vegaprotocol/react-helpers';
|
||||
const START_DATE_PAST = 'Monday, 17 February 2022 11:44:09';
|
||||
|
||||
describe('getSecondsFromInterval', () => {
|
||||
it('returns 0 for bad data', () => {
|
||||
expect(getSecondsFromInterval(null as unknown as string)).toEqual(0);
|
||||
expect(getSecondsFromInterval('')).toEqual(0);
|
||||
expect(getSecondsFromInterval('🧙')).toEqual(0);
|
||||
expect(getSecondsFromInterval(2 as unknown as string)).toEqual(0);
|
||||
});
|
||||
|
||||
it('parses out months from a capital M', () => {
|
||||
expect(getSecondsFromInterval('2M')).toEqual(5184000);
|
||||
});
|
||||
|
||||
it('parses out days from a capital D', () => {
|
||||
expect(getSecondsFromInterval('1D')).toEqual(86400);
|
||||
});
|
||||
|
||||
it('parses out hours from a lower case h', () => {
|
||||
expect(getSecondsFromInterval('11h')).toEqual(39600);
|
||||
});
|
||||
|
||||
it('parses out minutes from a lower case m', () => {
|
||||
expect(getSecondsFromInterval('10m')).toEqual(600);
|
||||
});
|
||||
|
||||
it('parses out seconds from a lower case s', () => {
|
||||
expect(getSecondsFromInterval('99s')).toEqual(99);
|
||||
});
|
||||
|
||||
it('parses complex examples', () => {
|
||||
expect(getSecondsFromInterval('24h')).toEqual(86400);
|
||||
expect(getSecondsFromInterval('1h30m')).toEqual(5400);
|
||||
expect(getSecondsFromInterval('1D1h30m1s')).toEqual(91801);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calculateEpochData', () => {
|
||||
it('Handles bad data', () => {
|
||||
const currentEpochId = null as unknown as string;
|
||||
const missingEpochId = null as unknown as string;
|
||||
const epochStart = null as unknown as string;
|
||||
const epochLength = null as unknown as string;
|
||||
const res = calculateEpochData(
|
||||
currentEpochId,
|
||||
missingEpochId,
|
||||
epochStart,
|
||||
epochLength
|
||||
);
|
||||
|
||||
expect(res).toHaveProperty('label', 'Missing data');
|
||||
expect(res).toHaveProperty('isInFuture', false);
|
||||
});
|
||||
|
||||
it('Calculates that a bigger epoch number is in the future from basic data', () => {
|
||||
const currentEpochId = '10';
|
||||
const missingEpochId = '20';
|
||||
const epochStart = '';
|
||||
const epochLength = '';
|
||||
const res = calculateEpochData(
|
||||
currentEpochId,
|
||||
missingEpochId,
|
||||
epochStart,
|
||||
epochLength
|
||||
);
|
||||
|
||||
expect(res).toHaveProperty('isInFuture', true);
|
||||
});
|
||||
|
||||
it('If it has an epoch length and a start time, it provides an estimate', () => {
|
||||
const currentEpochId = '10';
|
||||
const missingEpochId = '20';
|
||||
const epochStart = START_DATE_PAST;
|
||||
const epochLength = '1s';
|
||||
const res = calculateEpochData(
|
||||
currentEpochId,
|
||||
missingEpochId,
|
||||
epochStart,
|
||||
epochLength
|
||||
);
|
||||
|
||||
// 'Estimate: 17/02/2022, 11:44:19 - in less than a minute')
|
||||
expect(res).toHaveProperty('label');
|
||||
expect(res.label).toMatch(/^Estimate/);
|
||||
expect(res.label).toMatch(/in less than a minute$/);
|
||||
});
|
||||
|
||||
it('Provide decent string for past', () => {
|
||||
const currentEpochId = '20';
|
||||
const missingEpochId = '10';
|
||||
const epochStart = START_DATE_PAST;
|
||||
const epochLength = '1s';
|
||||
const res = calculateEpochData(
|
||||
currentEpochId,
|
||||
missingEpochId,
|
||||
epochStart,
|
||||
epochLength
|
||||
);
|
||||
|
||||
// 'Estimate: 17/02/2022, 11:44:19 - in less than a minute')
|
||||
expect(res).toHaveProperty('label');
|
||||
expect(res.label).toMatch(/^Estimate/);
|
||||
expect(res.label).toMatch(/less than a minute ago$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('EpochMissingOverview', () => {
|
||||
function renderComponent(missingEpochId: string) {
|
||||
return render(
|
||||
<MockedProvider>
|
||||
<EpochMissingOverview missingEpochId={missingEpochId} />
|
||||
</MockedProvider>
|
||||
);
|
||||
}
|
||||
|
||||
it('renders a - if no id is provided', () => {
|
||||
const n = null as unknown as string;
|
||||
const screen = renderComponent(n);
|
||||
expect(screen.getByTestId('empty')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,111 +0,0 @@
|
||||
import { useExplorerFutureEpochQuery } from './__generated__/Epoch';
|
||||
|
||||
import addSeconds from 'date-fns/addSeconds';
|
||||
import formatDistance from 'date-fns/formatDistance';
|
||||
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import isFuture from 'date-fns/isFuture';
|
||||
import { isValidDate } from '@vegaprotocol/react-helpers';
|
||||
import { getSecondsFromInterval } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export type EpochMissingOverviewProps = {
|
||||
missingEpochId?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders a set of details for an epoch that has no representation in the
|
||||
* data node. This is primarily for one of two reasons:
|
||||
*
|
||||
* 1. The epoch hasn't happened yet
|
||||
* 2. The epoch happened before a snapshot, and thus the details don't exist
|
||||
*
|
||||
* This component is used when the API has responded with no data for an epoch
|
||||
* by ID, so we already know that we can't display start time/block etc.
|
||||
*
|
||||
* We can detect 1 if the epoch is a higher number than the current epoch
|
||||
* We can detect 2 if the epoch is in the past, but we still get no response.
|
||||
*/
|
||||
const EpochMissingOverview = ({
|
||||
missingEpochId,
|
||||
}: EpochMissingOverviewProps) => {
|
||||
const { data, error, loading } = useExplorerFutureEpochQuery();
|
||||
|
||||
// This should not happen, but it's easily handled
|
||||
if (!missingEpochId) {
|
||||
return <span data-testid="empty">-</span>;
|
||||
}
|
||||
|
||||
// No data should also not happen - we've requested the current epoch. This
|
||||
// could happen at chain restart, but shouldn't. If it does, fallback.
|
||||
if (!data || loading || error) {
|
||||
return <span data-testid="empty">{missingEpochId}</span>;
|
||||
}
|
||||
|
||||
// If we have enough information to predict a future or past block time, let's do it
|
||||
if (
|
||||
!missingEpochId ||
|
||||
!data.epoch.id ||
|
||||
!data.epoch.timestamps.start ||
|
||||
!data?.networkParameter?.value
|
||||
) {
|
||||
return <span data-testid="empty">{missingEpochId}</span>;
|
||||
}
|
||||
|
||||
const { label, isInFuture } = calculateEpochData(
|
||||
data.epoch.id,
|
||||
missingEpochId,
|
||||
data.epoch.timestamps.start,
|
||||
data.networkParameter.value
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip description={<p className="text-xs m-2">{label}</p>}>
|
||||
<p>
|
||||
{isInFuture ? (
|
||||
<Icon name="calendar" className="mr-1" />
|
||||
) : (
|
||||
<Icon name="outdated" className="mr-1" />
|
||||
)}
|
||||
{missingEpochId}
|
||||
</p>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export function calculateEpochData(
|
||||
currentEpochId: string,
|
||||
missingEpochId: string,
|
||||
epochStart: string,
|
||||
epochLength: string
|
||||
) {
|
||||
// Blank string will be return 0 seconds from getSecondsFromInterval
|
||||
const epochLengthInSeconds = getSecondsFromInterval(epochLength);
|
||||
|
||||
if (!epochStart || !epochLength) {
|
||||
// Let's just take a guess
|
||||
return {
|
||||
label: 'Missing data',
|
||||
isInFuture: parseInt(missingEpochId) > parseInt(currentEpochId),
|
||||
};
|
||||
}
|
||||
|
||||
const startFrom = new Date(epochStart);
|
||||
|
||||
const diff = parseInt(missingEpochId) - parseInt(currentEpochId);
|
||||
const futureDate = addSeconds(startFrom, diff * epochLengthInSeconds);
|
||||
|
||||
const label =
|
||||
isValidDate(futureDate) && isValidDate(startFrom)
|
||||
? `Estimate: ${futureDate.toLocaleString()} - ${formatDistance(
|
||||
futureDate,
|
||||
startFrom,
|
||||
{ addSuffix: true }
|
||||
)}`
|
||||
: 'Missing data';
|
||||
|
||||
return {
|
||||
label,
|
||||
isInFuture: isFuture(futureDate),
|
||||
};
|
||||
}
|
||||
|
||||
export default EpochMissingOverview;
|
||||
@@ -1,127 +0,0 @@
|
||||
import { useExplorerEpochQuery } from './__generated__/Epoch';
|
||||
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { BlockLink } from '../links';
|
||||
import { Time } from '../time';
|
||||
import { TimeAgo } from '../time-ago';
|
||||
import EpochMissingOverview from './epoch-missing';
|
||||
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import type { IconProps } from '@vegaprotocol/ui-toolkit';
|
||||
import isPast from 'date-fns/isPast';
|
||||
|
||||
const borderClass =
|
||||
'border-solid border-2 border-vega-dark-200 border-collapse';
|
||||
|
||||
export type EpochOverviewProps = {
|
||||
id?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Displays detailed information about an epoch, given an ID. This
|
||||
* works for past epochs and current epochs - future epochs, and a
|
||||
* few other situations (see epoch-missing) will not return us
|
||||
* enough information to render this.
|
||||
*
|
||||
* The details are hidden in a tooltip, behind the epoch number
|
||||
*/
|
||||
const EpochOverview = ({ id }: EpochOverviewProps) => {
|
||||
const { data, error, loading } = useExplorerEpochQuery({
|
||||
variables: { id: id || '' },
|
||||
});
|
||||
|
||||
const ti = data?.epoch.timestamps;
|
||||
if (
|
||||
error?.message &&
|
||||
error.message.includes('no resource corresponding to this id')
|
||||
) {
|
||||
return <EpochMissingOverview missingEpochId={id} />;
|
||||
}
|
||||
|
||||
if (!ti || loading || error) {
|
||||
return <span>{id}</span>;
|
||||
}
|
||||
|
||||
const description = (
|
||||
<table className="text-xs m-2">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th className={`text-center ${borderClass}`}>{t('Block')}</th>
|
||||
<th className={`text-center ${borderClass}`}>{t('Time')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th className={`px-2 ${borderClass}`}>{t('Start')}</th>
|
||||
<td className={`px-2 ${borderClass}`}>
|
||||
{ti.firstBlock ? <BlockLink height={ti.firstBlock} /> : '-'}
|
||||
</td>
|
||||
<td className={`px-2 ${borderClass}`}>
|
||||
<Time date={ti.start} />
|
||||
<br />
|
||||
<TimeAgo date={ti.start} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className={`px-2 ${borderClass}`}>{t('End')}</th>
|
||||
<td className={`px-2 ${borderClass}`}>
|
||||
{ti.lastBlock ? (
|
||||
<BlockLink height={ti.lastBlock} />
|
||||
) : (
|
||||
t('In progress')
|
||||
)}
|
||||
</td>
|
||||
<td className={`px-2 ${borderClass}`}>
|
||||
{ti.end ? (
|
||||
<>
|
||||
<Time date={ti.end} />
|
||||
<br />
|
||||
<TimeAgo date={ti.end} />
|
||||
</>
|
||||
) : (
|
||||
<span>{t('-')}</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip description={description}>
|
||||
<p>
|
||||
<IconForEpoch start={ti.start} end={ti.end} />
|
||||
{id}
|
||||
</p>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export type IconForEpochProps = {
|
||||
start: string;
|
||||
end: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Chooses an icon to display next to the epoch number, representing
|
||||
* when the epoch is relative to now (i.e. not yet started, started,
|
||||
* finished)
|
||||
*/
|
||||
export function IconForEpoch({ start, end }: IconForEpochProps) {
|
||||
const startHasPassed = start ? isPast(new Date(start)) : false;
|
||||
const endHasPassed = end ? isPast(new Date(end)) : false;
|
||||
|
||||
let i: IconProps['name'] = 'calendar';
|
||||
|
||||
if (!startHasPassed && !endHasPassed) {
|
||||
i = 'calendar';
|
||||
} else if (startHasPassed && !endHasPassed) {
|
||||
i = 'circle';
|
||||
} else if (startHasPassed && endHasPassed) {
|
||||
i = 'tick-circle';
|
||||
}
|
||||
|
||||
return <Icon name={i} className="mr-2" />;
|
||||
}
|
||||
|
||||
export default EpochOverview;
|
||||
@@ -1,59 +1,39 @@
|
||||
import { NodeSwitcherDialog, useEnvironment } from '@vegaprotocol/environment';
|
||||
import { t, useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||
import { ExternalLink, Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { ENV } from '../../config/env';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { Link } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const Footer = () => {
|
||||
const { VEGA_URL, GIT_COMMIT_HASH, GIT_ORIGIN_URL } = useEnvironment();
|
||||
const [nodeSwitcherOpen, setNodeSwitcherOpen] = useState(false);
|
||||
const { screenSize } = useScreenDimensions();
|
||||
const showFullFeedbackLabel = useMemo(
|
||||
() => ['lg', 'xl'].includes(screenSize),
|
||||
[screenSize]
|
||||
);
|
||||
|
||||
const { VEGA_URL, GIT_COMMIT_HASH, GIT_ORIGIN_URL, setNodeSwitcherOpen } =
|
||||
useEnvironment();
|
||||
return (
|
||||
<>
|
||||
<footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-xs md:text-md md:flex md:col-span-2 px-4 py-2 gap-4 border-t border-neutral-700 dark:border-neutral-300">
|
||||
<div className="flex justify-between gap-2 align-middle">
|
||||
<footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-xs md:text-md md:flex md:col-span-2 px-4 py-2 gap-4 border-t border-neutral-700 dark:border-neutral-300">
|
||||
<div className="flex justify-between gap-2 align-middle">
|
||||
<div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4">
|
||||
{GIT_COMMIT_HASH && (
|
||||
<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>
|
||||
<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 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>
|
||||
</footer>
|
||||
<NodeSwitcherDialog
|
||||
open={nodeSwitcherOpen}
|
||||
setOpen={setNodeSwitcherOpen}
|
||||
/>
|
||||
</>
|
||||
|
||||
<div className="flex pl-2 content-center">
|
||||
{VEGA_URL && <NodeUrl url={VEGA_URL} />}
|
||||
<Link className="ml-2" onClick={setNodeSwitcherOpen}>
|
||||
{t('Change')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ jest.mock('../search', () => ({
|
||||
|
||||
const renderComponent = () => (
|
||||
<MemoryRouter>
|
||||
<Header />
|
||||
<Header menuOpen={false} setMenuOpen={jest.fn()} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
|
||||
@@ -4,11 +4,15 @@ import { ThemeSwitcher, Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { Search } from '../search';
|
||||
import { Routes } from '../../routes/route-names';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
import { NetworkSwitcher } from '@vegaprotocol/environment';
|
||||
import { useNavStore } from '../nav';
|
||||
|
||||
export const Header = () => {
|
||||
const [open, toggle] = useNavStore((state) => [state.open, state.toggle]);
|
||||
interface ThemeToggleProps {
|
||||
menuOpen: boolean;
|
||||
setMenuOpen: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export const Header = ({ menuOpen, setMenuOpen }: ThemeToggleProps) => {
|
||||
const headerClasses = classnames(
|
||||
'md:col-span-2',
|
||||
'grid grid-rows-2 md:grid-rows-1 grid-cols-[1fr_auto] md:grid-cols-[auto_1fr_auto] items-center',
|
||||
@@ -32,9 +36,9 @@ export const Header = () => {
|
||||
<button
|
||||
data-testid="open-menu"
|
||||
className="md:hidden text-white"
|
||||
onClick={() => toggle()}
|
||||
onClick={() => setMenuOpen(!menuOpen)}
|
||||
>
|
||||
<Icon name={open ? 'cross' : 'menu'} />
|
||||
<Icon name={menuOpen ? 'cross' : 'menu'} />
|
||||
</button>
|
||||
<Search />
|
||||
<ThemeSwitcher className="-my-4" />
|
||||
|
||||
@@ -11,14 +11,14 @@ export const InfoBlock = ({ title, subtitle, tooltipInfo }: InfoBlockProps) => {
|
||||
return (
|
||||
<div className="flex flex-col text-center ">
|
||||
<h3 className="text-4xl">{title}</h3>
|
||||
<p className="text-vega-dark-100 dark:text-vega-light-200">
|
||||
<p className="text-zinc-800 dark:text-zinc-300">
|
||||
{subtitle}
|
||||
{tooltipInfo ? (
|
||||
<Tooltip description={tooltipInfo} align="center">
|
||||
<span>
|
||||
<Icon
|
||||
name="info-sign"
|
||||
className="ml-2 text-vega-light-300 dark:text-vega-dark-300"
|
||||
className="ml-2 text-zinc-400 dark:text-zinc-600"
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
@@ -44,12 +44,12 @@ export const InfoPanel = ({
|
||||
text={id}
|
||||
startChars={visibleChars}
|
||||
endChars={visibleChars}
|
||||
className="text-vega-dark-100 dark:text-vega-light-200"
|
||||
className="text-black dark:text-zinc-200"
|
||||
/>
|
||||
) : (
|
||||
<p
|
||||
title={id}
|
||||
className="text-vega-dark-100 dark:text-vega-light-200 truncate ..."
|
||||
className="text-black dark:text-zinc-200 truncate ..."
|
||||
>
|
||||
{id}
|
||||
</p>
|
||||
@@ -70,7 +70,7 @@ export const InfoPanel = ({
|
||||
</div>
|
||||
{copy && (
|
||||
<CopyWithTooltip text={id}>
|
||||
<button className="bg-vega-light-100 dark:bg-vega-dark-100 rounded-sm py-2 px-3">
|
||||
<button className="bg-zinc-100 dark:bg-zinc-900 rounded-sm py-2 px-3">
|
||||
<Icon name="duplicate" />
|
||||
</button>
|
||||
</CopyWithTooltip>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
query ExplorerAsset($id: ID!) {
|
||||
asset(id: $id) {
|
||||
id
|
||||
name
|
||||
status
|
||||
decimals
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type ExplorerAssetQueryVariables = Types.Exact<{
|
||||
id: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type ExplorerAssetQuery = { __typename?: 'Query', asset?: { __typename?: 'Asset', id: string, name: string, status: Types.AssetStatus, decimals: number } | null };
|
||||
|
||||
|
||||
export const ExplorerAssetDocument = gql`
|
||||
query ExplorerAsset($id: ID!) {
|
||||
asset(id: $id) {
|
||||
id
|
||||
name
|
||||
status
|
||||
decimals
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useExplorerAssetQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useExplorerAssetQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useExplorerAssetQuery` 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 } = useExplorerAssetQuery({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useExplorerAssetQuery(baseOptions: Apollo.QueryHookOptions<ExplorerAssetQuery, ExplorerAssetQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ExplorerAssetQuery, ExplorerAssetQueryVariables>(ExplorerAssetDocument, options);
|
||||
}
|
||||
export function useExplorerAssetLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerAssetQuery, ExplorerAssetQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ExplorerAssetQuery, ExplorerAssetQueryVariables>(ExplorerAssetDocument, options);
|
||||
}
|
||||
export type ExplorerAssetQueryHookResult = ReturnType<typeof useExplorerAssetQuery>;
|
||||
export type ExplorerAssetLazyQueryHookResult = ReturnType<typeof useExplorerAssetLazyQuery>;
|
||||
export type ExplorerAssetQueryResult = Apollo.QueryResult<ExplorerAssetQuery, ExplorerAssetQueryVariables>;
|
||||
@@ -1,37 +1,63 @@
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { AssetLink } from './asset-link';
|
||||
import { mockAssetA1 } from '../../../mocks/assets';
|
||||
import { render } from '@testing-library/react';
|
||||
import AssetLink from './asset-link';
|
||||
import { ExplorerAssetDocument } from './__generated__/Asset';
|
||||
|
||||
function renderComponent(id: string, mock: MockedResponse[]) {
|
||||
return (
|
||||
<MockedProvider mocks={mock} addTypename={false}>
|
||||
<MockedProvider mocks={mock}>
|
||||
<MemoryRouter>
|
||||
<AssetLink assetId={id} />
|
||||
<AssetLink id={id} />
|
||||
</MemoryRouter>
|
||||
</MockedProvider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('AssetLink', () => {
|
||||
it('renders the asset id when not found and makes the button disabled', async () => {
|
||||
describe('Asset link component', () => {
|
||||
it('Renders the ID at first', () => {
|
||||
const res = render(renderComponent('123', []));
|
||||
expect(res.getByText('123')).toBeInTheDocument();
|
||||
expect(await res.findByTestId('asset-link')).toBeDisabled();
|
||||
await waitFor(async () => {
|
||||
expect(await res.queryByText('A ONE')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders the asset name when found and make the button enabled', async () => {
|
||||
const res = render(renderComponent('123', [mockAssetA1]));
|
||||
expect(res.getByText('123')).toBeInTheDocument();
|
||||
it('Renders the asset name when the query returns a result', async () => {
|
||||
const mock = {
|
||||
request: {
|
||||
query: ExplorerAssetDocument,
|
||||
variables: {
|
||||
id: '123',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
asset: {
|
||||
id: '123',
|
||||
name: 'test-label',
|
||||
status: 'irrelevant-test-data',
|
||||
decimals: 18,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await waitFor(async () => {
|
||||
expect(await res.findByText('A ONE')).toBeInTheDocument();
|
||||
expect(await res.findByTestId('asset-link')).not.toBeDisabled();
|
||||
});
|
||||
const res = render(renderComponent('123', [mock]));
|
||||
expect(res.getByText('123')).toBeInTheDocument();
|
||||
expect(await res.findByText('test-label')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Leaves the asset id when the asset is not found', async () => {
|
||||
const mock = {
|
||||
request: {
|
||||
query: ExplorerAssetDocument,
|
||||
variables: {
|
||||
id: '123',
|
||||
},
|
||||
},
|
||||
error: new Error('No such asset'),
|
||||
};
|
||||
|
||||
const res = render(renderComponent('123', [mock]));
|
||||
expect(await res.findByText('123')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,54 +1,36 @@
|
||||
import React from 'react';
|
||||
import { Routes } from '../../../routes/route-names';
|
||||
import { useExplorerAssetQuery } from './__generated__/Asset';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import Hash from '../hash';
|
||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
useAssetDataProvider,
|
||||
useAssetDetailsDialogStore,
|
||||
} from '@vegaprotocol/assets';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Routes } from '../../../routes/route-names';
|
||||
|
||||
export type AssetLinkProps = Partial<ComponentProps<typeof ButtonLink>> & {
|
||||
assetId: string;
|
||||
asDialog?: boolean;
|
||||
showAssetSymbol?: boolean;
|
||||
export type AssetLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||
id: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given an asset ID, it will fetch the asset name and show that,
|
||||
* with a link to the assets modal. If the name does not come back
|
||||
* with a link to the assets list. If the name does not come back
|
||||
* it will use the ID instead.
|
||||
*/
|
||||
export const AssetLink = ({
|
||||
assetId,
|
||||
asDialog,
|
||||
showAssetSymbol = false,
|
||||
...props
|
||||
}: AssetLinkProps) => {
|
||||
const { data: asset } = useAssetDataProvider(assetId);
|
||||
const AssetLink = ({ id, ...props }: AssetLinkProps) => {
|
||||
const { data } = useExplorerAssetQuery({
|
||||
variables: { id },
|
||||
});
|
||||
|
||||
const open = useAssetDetailsDialogStore((state) => state.open);
|
||||
const navigate = useNavigate();
|
||||
const label = asset
|
||||
? showAssetSymbol
|
||||
? asset?.symbol
|
||||
: asset?.name
|
||||
: assetId;
|
||||
let label: string = id;
|
||||
|
||||
if (data?.asset?.name) {
|
||||
label = data.asset.name;
|
||||
}
|
||||
|
||||
return (
|
||||
<ButtonLink
|
||||
data-testid="asset-link"
|
||||
disabled={!asset}
|
||||
onClick={(e) => {
|
||||
if (asDialog) {
|
||||
open(assetId, e.target as HTMLElement);
|
||||
} else {
|
||||
navigate(`${Routes.ASSETS}/${asset?.id}`);
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<Link className="underline" {...props} to={`/${Routes.ASSETS}#${id}`}>
|
||||
<Hash text={label} />
|
||||
</ButtonLink>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssetLink;
|
||||
|
||||
@@ -2,4 +2,4 @@ export { default as BlockLink } from './block-link/block-link';
|
||||
export { default as PartyLink } from './party-link/party-link';
|
||||
export { default as NodeLink } from './node-link/node-link';
|
||||
export { default as MarketLink } from './market-link/market-link';
|
||||
export * from './asset-link/asset-link';
|
||||
export { default as AssetLink } from './asset-link/asset-link';
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import PartyLink from './party-link';
|
||||
|
||||
describe('PartyLink', () => {
|
||||
it('renders Network for 000.000 party', () => {
|
||||
const zeroes =
|
||||
'0000000000000000000000000000000000000000000000000000000000000000';
|
||||
const screen = render(<PartyLink id={zeroes} />);
|
||||
expect(screen.getByText('Network')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Network for network party', () => {
|
||||
const screen = render(<PartyLink id="network" />);
|
||||
expect(screen.getByText('Network')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders ID with no link for invalid party', () => {
|
||||
const screen = render(<PartyLink id="this-party-is-not-valid" />);
|
||||
expect(screen.getByTestId('invalid-party')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('links a valid party to the party page', () => {
|
||||
const aValidParty =
|
||||
'13464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6e';
|
||||
|
||||
const screen = render(
|
||||
<MemoryRouter>
|
||||
<PartyLink id={aValidParty} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
const el = screen.getByText(aValidParty);
|
||||
expect(el).toBeInTheDocument();
|
||||
// The text should be a link that points to the party's page
|
||||
expect(el.parentElement?.tagName).toEqual('A');
|
||||
expect(el.parentElement?.getAttribute('href')).toContain(aValidParty);
|
||||
});
|
||||
});
|
||||
@@ -3,47 +3,19 @@ import { Link } from 'react-router-dom';
|
||||
|
||||
import type { ComponentProps } from 'react';
|
||||
import Hash from '../hash';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { isValidPartyId } from '../../../routes/parties/id/components/party-id-error';
|
||||
import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const SPECIAL_CASE_NETWORK_ID =
|
||||
'0000000000000000000000000000000000000000000000000000000000000000';
|
||||
export const SPECIAL_CASE_NETWORK = 'network';
|
||||
|
||||
export type PartyLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||
id: string;
|
||||
truncate?: boolean;
|
||||
};
|
||||
|
||||
const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
|
||||
// Some transactions will involve the 'network' party, which is alias for '000...000'
|
||||
// The party page does not handle this nicely, so in this case we render the word 'Network'
|
||||
if (id === SPECIAL_CASE_NETWORK || id === SPECIAL_CASE_NETWORK_ID) {
|
||||
return (
|
||||
<span className="font-mono" data-testid="network">
|
||||
{t('Network')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// If the party doesn't look correct, there's no point in linking to id. Just render
|
||||
// the ID as it was given to us
|
||||
if (!isValidPartyId(id)) {
|
||||
return (
|
||||
<span className="font-mono" data-testid="invalid-party">
|
||||
{id}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const PartyLink = ({ id, ...props }: PartyLinkProps) => {
|
||||
return (
|
||||
<Link
|
||||
className="underline font-mono"
|
||||
{...props}
|
||||
to={`/${Routes.PARTIES}/${id}`}
|
||||
>
|
||||
<Hash text={truncate ? truncateMiddle(id) : id} />
|
||||
<Hash text={id} />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import { AppRouter } from '../../routes';
|
||||
|
||||
export const Main = () => {
|
||||
return (
|
||||
<main className="p-4">
|
||||
<main className="p-4 overflow-scroll">
|
||||
<AppRouter />
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -1,246 +0,0 @@
|
||||
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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,132 +0,0 @@
|
||||
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 +1,45 @@
|
||||
export * from './nav';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import routerConfig from '../../routes/router-config';
|
||||
import classnames from 'classnames';
|
||||
|
||||
interface NavProps {
|
||||
menuOpen: boolean;
|
||||
}
|
||||
|
||||
export const Nav = ({ menuOpen }: NavProps) => {
|
||||
return (
|
||||
<nav className="relative">
|
||||
<div
|
||||
className={classnames(
|
||||
'absolute top-0 z-50 md:static',
|
||||
'w-full p-4 md:border-r border-neutral-700 dark:border-neutral-300',
|
||||
'bg-white dark:bg-black',
|
||||
'transition-[right]',
|
||||
{
|
||||
'right-0 h-[100vh]': menuOpen,
|
||||
'right-[200vw] h-full': !menuOpen,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{routerConfig.map((r) => (
|
||||
<NavLink
|
||||
key={r.name}
|
||||
to={r.path}
|
||||
className={({ isActive }) =>
|
||||
classnames(
|
||||
'block mb-2 px-2',
|
||||
'text-lg hover:bg-vega-pink dark:hover:bg-vega-yellow hover:text-white dark:hover:text-black',
|
||||
{
|
||||
'bg-vega-pink text-white dark:bg-vega-yellow dark:text-black':
|
||||
isActive,
|
||||
}
|
||||
)
|
||||
}
|
||||
>
|
||||
{r.text}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,181 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -13,7 +13,7 @@ export interface DeterministicOrderDetailsProps {
|
||||
}
|
||||
|
||||
export const wrapperClasses =
|
||||
'grid lg:grid-cols-1 flex items-center max-w-xl border border-vega-light-200 dark:border-vega-dark-150 rounded-md pv-2 ph-5 mb-5';
|
||||
'grid lg:grid-cols-1 flex items-center max-w-xl border border-zinc-200 dark:border-zinc-800 rounded-md pv-2 ph-5 mb-5';
|
||||
|
||||
/**
|
||||
* This component renders the *current* details for an order
|
||||
@@ -42,7 +42,7 @@ const DeterministicOrderDetails = ({
|
||||
<h2 className="text-3xl font-bold mb-4 display-5">
|
||||
{t('Order not found')}
|
||||
</h2>
|
||||
<p className="text-vega-light-400 mb-12">
|
||||
<p className="text-gray-500 mb-12">
|
||||
{t('No order created from this transaction')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useExplorerDeterministicOrderQuery } from '../order-details/__generated__/Order';
|
||||
import PriceInMarket from '../price-in-market/price-in-market';
|
||||
import { sideText } from '../order-details/lib/order-labels';
|
||||
import SizeInMarket from '../size-in-market/size-in-market';
|
||||
|
||||
// Note: Edited has no style currently
|
||||
export type OrderSummaryModifier = 'cancelled' | 'edited';
|
||||
@@ -42,8 +41,7 @@ const OrderSummary = ({ id, modifier }: OrderSummaryProps) => {
|
||||
return (
|
||||
<div data-testid="order-summary" className={getClassName(modifier)}>
|
||||
<span>{sideText[order.side]}</span>
|
||||
<SizeInMarket marketId={order.market.id} size={order.size} />
|
||||
<i>@</i>
|
||||
<span>{order.size}</span> <i>@</i>
|
||||
<PriceInMarket marketId={order.market.id} price={order.price} />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -112,14 +112,13 @@ describe('Order TX Summary component', () => {
|
||||
const res = renderComponent(o, [mock]);
|
||||
expect(res.queryByTestId('order-summary')).toBeInTheDocument();
|
||||
expect(res.getByText('Buy')).toBeInTheDocument();
|
||||
|
||||
// Initially renders price and size unformatted
|
||||
expect(res.getByText('333')).toBeInTheDocument();
|
||||
expect(res.getByText('10')).toBeInTheDocument();
|
||||
|
||||
// Initially renders price alone
|
||||
expect(res.getByText('333')).toBeInTheDocument();
|
||||
|
||||
// After fetch renders formatted price and asset quotename
|
||||
expect(await res.findByText('3.33')).toBeInTheDocument();
|
||||
expect(await res.findByText('TEST')).toBeInTheDocument();
|
||||
expect(await res.getByText('0.10')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { components } from '../../../types/explorer';
|
||||
|
||||
import PriceInMarket from '../price-in-market/price-in-market';
|
||||
import { sideText } from '../order-details/lib/order-labels';
|
||||
import SizeInMarket from '../size-in-market/size-in-market';
|
||||
|
||||
export type OrderSummaryProps = {
|
||||
order: components['schemas']['v1OrderSubmission'];
|
||||
@@ -30,12 +29,7 @@ const OrderTxSummary = ({ order }: OrderSummaryProps) => {
|
||||
return (
|
||||
<div data-testid="order-summary">
|
||||
<span>{sideText[order.side]}</span>
|
||||
{order.size ? (
|
||||
<SizeInMarket size={order.size} marketId={order.marketId} />
|
||||
) : (
|
||||
'-'
|
||||
)}
|
||||
<i className="text-xs">@</i>
|
||||
<span>{order.size}</span> <i className="text-xs">@</i>
|
||||
<PriceInMarket
|
||||
marketId={order.marketId}
|
||||
price={order.price}
|
||||
|
||||
@@ -38,7 +38,7 @@ export const PageHeader = ({
|
||||
</h2>
|
||||
{copy && (
|
||||
<CopyWithTooltip data-testid="copy-to-clipboard" text={title}>
|
||||
<button className="bg-vega-light-100 dark:bg-vega-dark-100 rounded-sm py-2 px-3">
|
||||
<button className="bg-zinc-100 dark:bg-zinc-900 rounded-sm py-2 px-3">
|
||||
<Icon name="duplicate" className="" />
|
||||
</button>
|
||||
</CopyWithTooltip>
|
||||
|
||||
@@ -8,7 +8,7 @@ interface PanelProps {
|
||||
export const Panel = ({ children, className }: PanelProps) => (
|
||||
<div
|
||||
className={classNames(
|
||||
'border border-vega-light-150 dark:border-vega-dark-150 rounded-md p-5 mb-5',
|
||||
'border border-zinc-200 dark:border-zinc-800 rounded-md p-5 mb-5',
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
import type { ProposalListFieldsFragment } from '@vegaprotocol/governance';
|
||||
import { VoteProgress } from '@vegaprotocol/governance';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
import type {
|
||||
VegaICellRendererParams,
|
||||
VegaValueFormatterParams,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { RowClickedEvent } from 'ag-grid-community';
|
||||
import {
|
||||
getDateTimeFormat,
|
||||
NetworkParams,
|
||||
t,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { ProposalStateMapping } from '@vegaprotocol/types';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
|
||||
import { BREAKPOINT_MD } from '../../config/breakpoints';
|
||||
import { JsonViewerDialog } from '../dialogs/json-viewer-dialog';
|
||||
|
||||
type ProposalTermsDialog = {
|
||||
open: boolean;
|
||||
title: string;
|
||||
content: unknown;
|
||||
};
|
||||
type ProposalsTableProps = {
|
||||
data: ProposalListFieldsFragment[] | null;
|
||||
};
|
||||
export const ProposalsTable = ({ data }: ProposalsTableProps) => {
|
||||
const { params } = useNetworkParams([
|
||||
NetworkParams.governance_proposal_market_requiredMajority,
|
||||
]);
|
||||
const tokenLink = useLinks(DApp.Token);
|
||||
const requiredMajorityPercentage = useMemo(() => {
|
||||
const requiredMajority =
|
||||
params?.governance_proposal_market_requiredMajority ?? 1;
|
||||
return new BigNumber(requiredMajority).times(100);
|
||||
}, [params?.governance_proposal_market_requiredMajority]);
|
||||
|
||||
const gridRef = useRef<AgGridReact>(null);
|
||||
useLayoutEffect(() => {
|
||||
const showColumnsOnDesktop = () => {
|
||||
gridRef.current?.columnApi.setColumnsVisible(
|
||||
['voting', 'cDate', 'eDate', 'type'],
|
||||
window.innerWidth > BREAKPOINT_MD
|
||||
);
|
||||
gridRef.current?.columnApi.setColumnWidth(
|
||||
'actions',
|
||||
window.innerWidth > BREAKPOINT_MD ? 221 : 80
|
||||
);
|
||||
};
|
||||
window.addEventListener('resize', showColumnsOnDesktop);
|
||||
return () => {
|
||||
window.removeEventListener('resize', showColumnsOnDesktop);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [dialog, setDialog] = useState<ProposalTermsDialog>({
|
||||
open: false,
|
||||
title: '',
|
||||
content: null,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<AgGrid
|
||||
ref={gridRef}
|
||||
rowData={data}
|
||||
getRowId={({ data }: { data: ProposalListFieldsFragment }) =>
|
||||
data.id || data.rationale.title
|
||||
}
|
||||
overlayNoRowsTemplate={t('This chain has no markets')}
|
||||
domLayout="autoHeight"
|
||||
defaultColDef={{
|
||||
flex: 1,
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
filter: true,
|
||||
filterParams: { buttons: ['reset'] },
|
||||
autoHeight: true,
|
||||
}}
|
||||
suppressCellFocus={true}
|
||||
onRowClicked={({ data, event }: RowClickedEvent) => {
|
||||
if (
|
||||
(event?.target as HTMLElement).tagName.toUpperCase() !== 'BUTTON'
|
||||
) {
|
||||
const proposalPage = tokenLink(
|
||||
TOKEN_PROPOSAL.replace(':id', data.id)
|
||||
);
|
||||
window.open(proposalPage, '_blank');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AgGridColumn
|
||||
colId="title"
|
||||
headerName={t('Title')}
|
||||
field="rationale.title"
|
||||
flex={2}
|
||||
wrapText={true}
|
||||
/>
|
||||
<AgGridColumn
|
||||
colId="type"
|
||||
maxWidth={180}
|
||||
hide={window.innerWidth <= BREAKPOINT_MD}
|
||||
headerName={t('Type')}
|
||||
field="terms.change.__typename"
|
||||
/>
|
||||
<AgGridColumn
|
||||
maxWidth={100}
|
||||
headerName={t('State')}
|
||||
field="state"
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<ProposalListFieldsFragment, 'state'>) => {
|
||||
return value ? ProposalStateMapping[value] : '-';
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
colId="voting"
|
||||
maxWidth={100}
|
||||
hide={window.innerWidth <= BREAKPOINT_MD}
|
||||
headerName={t('Voting')}
|
||||
cellRenderer={({
|
||||
data,
|
||||
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
|
||||
if (data) {
|
||||
const yesTokens = new BigNumber(data.votes.yes.totalTokens);
|
||||
const noTokens = new BigNumber(data.votes.no.totalTokens);
|
||||
const totalTokensVoted = yesTokens.plus(noTokens);
|
||||
const yesPercentage = totalTokensVoted.isZero()
|
||||
? new BigNumber(0)
|
||||
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
|
||||
return (
|
||||
<div className="uppercase flex h-full items-center justify-center pt-2">
|
||||
<VoteProgress
|
||||
threshold={requiredMajorityPercentage}
|
||||
progress={yesPercentage}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return '-';
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
colId="cDate"
|
||||
maxWidth={150}
|
||||
hide={window.innerWidth <= BREAKPOINT_MD}
|
||||
headerName={t('Closing date')}
|
||||
field="terms.closingDatetime"
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<
|
||||
ProposalListFieldsFragment,
|
||||
'terms.closingDatetime'
|
||||
>) => {
|
||||
return value ? getDateTimeFormat().format(new Date(value)) : '-';
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
colId="eDate"
|
||||
maxWidth={150}
|
||||
hide={window.innerWidth <= BREAKPOINT_MD}
|
||||
headerName={t('Enactment date')}
|
||||
field="terms.enactmentDatetime"
|
||||
valueFormatter={({
|
||||
value,
|
||||
}: VegaValueFormatterParams<
|
||||
ProposalListFieldsFragment,
|
||||
'terms.enactmentDatetime'
|
||||
>) => {
|
||||
return value ? getDateTimeFormat().format(new Date(value)) : '-';
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
colId="actions"
|
||||
minWidth={window.innerWidth > BREAKPOINT_MD ? 221 : 80}
|
||||
maxWidth={221}
|
||||
sortable={false}
|
||||
filter={false}
|
||||
resizable={false}
|
||||
cellRenderer={({
|
||||
data,
|
||||
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
|
||||
const proposalPage = tokenLink(
|
||||
TOKEN_PROPOSAL.replace(':id', data?.id || '')
|
||||
);
|
||||
const openDialog = () => {
|
||||
if (!data) return;
|
||||
setDialog({
|
||||
open: true,
|
||||
title: data.rationale.title,
|
||||
content: data.terms,
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div className="pb-1">
|
||||
<button
|
||||
className="underline max-md:hidden"
|
||||
onClick={openDialog}
|
||||
>
|
||||
{t('View terms')}
|
||||
</button>{' '}
|
||||
<ExternalLink className="max-md:hidden" href={proposalPage}>
|
||||
{t('Open in Governance')}
|
||||
</ExternalLink>
|
||||
<ExternalLink className="md:hidden" href={proposalPage}>
|
||||
{t('Open')}
|
||||
</ExternalLink>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</AgGrid>
|
||||
<JsonViewerDialog
|
||||
open={dialog.open}
|
||||
onChange={(isOpen) => setDialog({ ...dialog, open: isOpen })}
|
||||
title={dialog.title}
|
||||
content={dialog.content}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
import { useAssetDataProvider } from '@vegaprotocol/assets';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/react-helpers';
|
||||
import { AssetLink } from '../links';
|
||||
|
||||
export type DecimalSource = 'ASSET';
|
||||
|
||||
export type SizeInAssetProps = {
|
||||
assetId: string;
|
||||
size?: string | number;
|
||||
decimalSource?: DecimalSource;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a market ID and an order size it will fetch the market
|
||||
* order size, and format the size accordingly
|
||||
*/
|
||||
const SizeInAsset = ({
|
||||
assetId,
|
||||
size,
|
||||
decimalSource = 'ASSET',
|
||||
}: SizeInAssetProps) => {
|
||||
const { data } = useAssetDataProvider(assetId);
|
||||
if (!size) {
|
||||
return <span>-</span>;
|
||||
}
|
||||
|
||||
let label = size;
|
||||
|
||||
if (data) {
|
||||
if (decimalSource === 'ASSET' && data.decimals) {
|
||||
label = addDecimalsFormatNumber(size, data.decimals);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<p>
|
||||
<span>{label}</span>
|
||||
<AssetLink assetId={assetId} showAssetSymbol={true} asDialog={true} />
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export default SizeInAsset;
|
||||
@@ -7,7 +7,7 @@ import type { DecimalSource } from './size-in-market';
|
||||
import { ExplorerMarketDocument } from '../links/market-link/__generated__/Market';
|
||||
|
||||
function renderComponent(
|
||||
size: string | undefined,
|
||||
price: string,
|
||||
marketId: string,
|
||||
mocks: MockedResponse[],
|
||||
decimalSource: DecimalSource = 'MARKET'
|
||||
@@ -17,7 +17,7 @@ function renderComponent(
|
||||
<MemoryRouter>
|
||||
<SizeInMarket
|
||||
marketId={marketId}
|
||||
size={size}
|
||||
size={price}
|
||||
decimalSource={decimalSource}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
@@ -57,11 +57,6 @@ const fullMock = {
|
||||
};
|
||||
|
||||
describe('Size in Market component', () => {
|
||||
it('Renders a dash size when there is no size', () => {
|
||||
const res = render(renderComponent(undefined, '123', []));
|
||||
expect(res.getByText('-')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Renders the raw size when there is no market data', () => {
|
||||
const res = render(renderComponent('100', '123', []));
|
||||
expect(res.getByText('100')).toBeInTheDocument();
|
||||
|
||||
@@ -3,9 +3,9 @@ import { useExplorerMarketQuery } from '../links/market-link/__generated__/Marke
|
||||
|
||||
export type DecimalSource = 'MARKET';
|
||||
|
||||
export type SizeInMarketProps = {
|
||||
export type PriceInMarketProps = {
|
||||
marketId: string;
|
||||
size?: string | number;
|
||||
size: string | number;
|
||||
decimalSource?: DecimalSource;
|
||||
};
|
||||
|
||||
@@ -17,14 +17,11 @@ const SizeInMarket = ({
|
||||
marketId,
|
||||
size,
|
||||
decimalSource = 'MARKET',
|
||||
}: SizeInMarketProps) => {
|
||||
}: PriceInMarketProps) => {
|
||||
const { data } = useExplorerMarketQuery({
|
||||
variables: { id: marketId },
|
||||
fetchPolicy: 'cache-first',
|
||||
});
|
||||
if (!size) {
|
||||
return <span>-</span>;
|
||||
}
|
||||
|
||||
let label = size;
|
||||
|
||||
|
||||
+5
-3
@@ -10,7 +10,7 @@ import { MemoryRouter } from 'react-router-dom';
|
||||
type Deposit = components['schemas']['vegaBuiltinAssetDeposit'];
|
||||
|
||||
const fullMock: Deposit = {
|
||||
partyId: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
partyId: 'party123',
|
||||
vegaAssetId: 'asset123',
|
||||
amount: 'amount123',
|
||||
};
|
||||
@@ -76,7 +76,9 @@ describe('Chain Event: Builtin asset deposit', () => {
|
||||
if (!assetLink.parentElement) {
|
||||
throw new Error('Asset link does not exist');
|
||||
}
|
||||
expect(assetLink.parentElement.tagName).toEqual('BUTTON');
|
||||
expect(assetLink.parentElement.textContent).toEqual(fullMock.vegaAssetId);
|
||||
expect(assetLink.parentElement.tagName).toEqual('A');
|
||||
expect(assetLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/assets#${fullMock.vegaAssetId}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ export const TxDetailsChainEventBuiltinDeposit = ({
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Asset')}</TableCell>
|
||||
<TableCell>
|
||||
<AssetLink assetId={deposit.vegaAssetId} /> ({t('built in asset')})
|
||||
<AssetLink id={deposit.vegaAssetId} /> ({t('built in asset')})
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
|
||||
+6
-5
@@ -10,7 +10,7 @@ import { MemoryRouter } from 'react-router-dom';
|
||||
type Withdrawal = components['schemas']['vegaBuiltinAssetWithdrawal'];
|
||||
|
||||
const fullMock: Withdrawal = {
|
||||
partyId: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
partyId: 'party123',
|
||||
vegaAssetId: 'asset123',
|
||||
amount: 'amount123',
|
||||
};
|
||||
@@ -67,12 +67,11 @@ describe('Chain Event: Builtin asset withdrawal', () => {
|
||||
expect(screen.getByText(`${fullMock.amount}`)).toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText(t('Recipient'))).toBeInTheDocument();
|
||||
|
||||
const partyLink = screen.getByText(`${fullMock.partyId}`);
|
||||
expect(partyLink).toBeInTheDocument();
|
||||
if (!partyLink.parentElement) {
|
||||
throw new Error('Party link does not exist');
|
||||
}
|
||||
|
||||
expect(partyLink.parentElement.tagName).toEqual('A');
|
||||
expect(partyLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/parties/${fullMock.partyId}`
|
||||
@@ -83,7 +82,9 @@ describe('Chain Event: Builtin asset withdrawal', () => {
|
||||
if (!assetLink.parentElement) {
|
||||
throw new Error('Asset link does not exist');
|
||||
}
|
||||
expect(assetLink.parentElement.tagName).toEqual('BUTTON');
|
||||
expect(assetLink.parentElement.textContent).toEqual(fullMock.vegaAssetId);
|
||||
expect(assetLink.parentElement.tagName).toEqual('A');
|
||||
expect(assetLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/assets#${fullMock.vegaAssetId}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,8 +39,8 @@ export const TxDetailsChainEventBuiltinWithdrawal = ({
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Asset')}</TableCell>
|
||||
<TableCell>
|
||||
<AssetLink assetId={withdrawal.vegaAssetId || ''} /> (
|
||||
{t('built in asset')})
|
||||
<AssetLink id={withdrawal.vegaAssetId || ''} /> ({t('built in asset')}
|
||||
)
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
|
||||
+4
-2
@@ -63,7 +63,9 @@ describe('Chain Event: ERC20 Asset Delist', () => {
|
||||
if (!assetLink.parentElement) {
|
||||
throw new Error('Asset link does not exist');
|
||||
}
|
||||
expect(assetLink.parentElement.tagName).toEqual('BUTTON');
|
||||
expect(assetLink.parentElement.textContent).toEqual(fullMock.vegaAssetId);
|
||||
expect(assetLink.parentElement.tagName).toEqual('A');
|
||||
expect(assetLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/assets#${fullMock.vegaAssetId}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ export const TxDetailsChainEventErc20AssetDelist = ({
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Removed Vega asset')}</TableCell>
|
||||
<TableCell>
|
||||
<AssetLink assetId={assetDelist.vegaAssetId || ''} />
|
||||
<AssetLink id={assetDelist.vegaAssetId || ''} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
|
||||
+4
-2
@@ -79,8 +79,10 @@ describe('Chain Event: ERC20 Asset limits updated', () => {
|
||||
if (!assetLink.parentElement) {
|
||||
throw new Error('Asset link does not exist');
|
||||
}
|
||||
expect(assetLink.parentElement.tagName).toEqual('BUTTON');
|
||||
expect(assetLink.parentElement.textContent).toEqual(fullMock.vegaAssetId);
|
||||
expect(assetLink.parentElement.tagName).toEqual('A');
|
||||
expect(assetLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/assets#${fullMock.vegaAssetId}`
|
||||
);
|
||||
|
||||
expect(screen.getByText(t('ERC20 asset'))).toBeInTheDocument();
|
||||
const ethLink = screen.getByText(`${fullMock.sourceEthereumAddress}`);
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ export const TxDetailsChainEventErc20AssetLimitsUpdated = ({
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Vega asset')}</TableCell>
|
||||
<TableCell>
|
||||
<AssetLink assetId={assetLimitsUpdated.vegaAssetId} />
|
||||
<AssetLink id={assetLimitsUpdated.vegaAssetId} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
|
||||
+4
-2
@@ -65,8 +65,10 @@ describe('Chain Event: ERC20 Asset List', () => {
|
||||
if (!assetLink.parentElement) {
|
||||
throw new Error('Asset link does not exist');
|
||||
}
|
||||
expect(assetLink.parentElement.tagName).toEqual('BUTTON');
|
||||
expect(assetLink.parentElement.textContent).toEqual(fullMock.vegaAssetId);
|
||||
expect(assetLink.parentElement.tagName).toEqual('A');
|
||||
expect(assetLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/assets#${fullMock.vegaAssetId}`
|
||||
);
|
||||
|
||||
expect(screen.getByText(t('Source'))).toBeInTheDocument();
|
||||
const ethLink = screen.getByText(`${fullMock.assetSource}`);
|
||||
|
||||
@@ -41,7 +41,7 @@ export const TxDetailsChainEventErc20AssetList = ({
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Added Vega asset')}</TableCell>
|
||||
<TableCell>
|
||||
<AssetLink assetId={assetList.vegaAssetId} />
|
||||
<AssetLink id={assetList.vegaAssetId} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
|
||||
@@ -10,12 +10,10 @@ import { TxDetailsChainEventDeposit } from './tx-erc20-deposit';
|
||||
type Deposit = components['schemas']['vegaERC20Deposit'];
|
||||
|
||||
const fullMock: Deposit = {
|
||||
vegaAssetId:
|
||||
'0000000000000000000000000000000000000000000000000000000000000002',
|
||||
vegaAssetId: 'asset123',
|
||||
amount: 'amount123',
|
||||
sourceEthereumAddress: 'eth123',
|
||||
targetPartyId:
|
||||
'0000000000000000000000000000000000000000000000000000000000000001',
|
||||
targetPartyId: 'vega123',
|
||||
};
|
||||
|
||||
describe('Chain Event: ERC20 asset deposit', () => {
|
||||
@@ -77,8 +75,10 @@ describe('Chain Event: ERC20 asset deposit', () => {
|
||||
if (!assetLink.parentElement) {
|
||||
throw new Error('Asset link does not exist');
|
||||
}
|
||||
expect(assetLink.parentElement.tagName).toEqual('BUTTON');
|
||||
expect(assetLink.parentElement.textContent).toEqual(fullMock.vegaAssetId);
|
||||
expect(assetLink.parentElement.tagName).toEqual('A');
|
||||
expect(assetLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/assets#${fullMock.vegaAssetId}`
|
||||
);
|
||||
|
||||
expect(screen.getByText(t('Source'))).toBeInTheDocument();
|
||||
const ethLink = screen.getByText(`${fullMock.sourceEthereumAddress}`);
|
||||
|
||||
@@ -51,7 +51,7 @@ export const TxDetailsChainEventDeposit = ({
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Asset')}</TableCell>
|
||||
<TableCell>
|
||||
<AssetLink assetId={deposit.vegaAssetId} />
|
||||
<AssetLink id={deposit.vegaAssetId} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
|
||||
+4
-2
@@ -60,8 +60,10 @@ describe('Chain Event: ERC20 asset deposit', () => {
|
||||
if (!assetLink.parentElement) {
|
||||
throw new Error('Asset link does not exist');
|
||||
}
|
||||
expect(assetLink.parentElement.tagName).toEqual('BUTTON');
|
||||
expect(assetLink.parentElement.textContent).toEqual(fullMock.vegaAssetId);
|
||||
expect(assetLink.parentElement.tagName).toEqual('A');
|
||||
expect(assetLink.parentElement.getAttribute('href')).toEqual(
|
||||
`/assets#${fullMock.vegaAssetId}`
|
||||
);
|
||||
|
||||
expect(screen.getByText(t('Recipient'))).toBeInTheDocument();
|
||||
const ethLink = screen.getByText(`${fullMock.targetEthereumAddress}`);
|
||||
|
||||
@@ -45,7 +45,7 @@ export const TxDetailsChainEventWithdrawal = ({
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Asset')}</TableCell>
|
||||
<TableCell>
|
||||
<AssetLink assetId={withdrawal.vegaAssetId} />
|
||||
<AssetLink id={withdrawal.vegaAssetId} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
|
||||
@@ -13,8 +13,7 @@ const fullMock: Deposit = {
|
||||
amount: 'amount123',
|
||||
blockTime: 'block123',
|
||||
ethereumAddress: 'eth123',
|
||||
vegaPublicKey:
|
||||
'0000000000000000000000000000000000000000000000000000000000000001',
|
||||
vegaPublicKey: 'vega123',
|
||||
};
|
||||
|
||||
describe('Chain Event: Stake deposit', () => {
|
||||
|
||||
@@ -13,8 +13,7 @@ const fullMock: Remove = {
|
||||
amount: 'amount123',
|
||||
blockTime: 'block123',
|
||||
ethereumAddress: 'eth123',
|
||||
vegaPublicKey:
|
||||
'0000000000000000000000000000000000000000000000000000000000000001',
|
||||
vegaPublicKey: 'vega123',
|
||||
};
|
||||
|
||||
describe('Chain Event: Stake remove', () => {
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ export const ChainResponseCode = ({
|
||||
error && error.length > 100 ? error.replace(/,/g, ',\r\n') : error;
|
||||
|
||||
return (
|
||||
<div title={`Response code: ${code} - ${label}`} className="inline-block">
|
||||
<div title={`Response code: ${code} - ${label}`}>
|
||||
<span
|
||||
className="mr-2"
|
||||
aria-label={isSuccess ? 'Success' : 'Warning'}
|
||||
|
||||
@@ -14,14 +14,10 @@ interface TxDetailsSharedProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
pubKey: string | undefined;
|
||||
blockData: TendermintBlocksResponse | undefined;
|
||||
|
||||
// A transitional property used in some complex TX types to display more detailed type information
|
||||
// than the shared component can derive
|
||||
hideTypeRow?: boolean;
|
||||
}
|
||||
|
||||
// Applied to all header cells
|
||||
export const sharedHeaderProps = {
|
||||
const sharedHeaderProps = {
|
||||
// Ensures that multi line contents still have the header aligned to the first line
|
||||
className: 'align-top',
|
||||
};
|
||||
@@ -35,7 +31,6 @@ export const TxDetailsShared = ({
|
||||
txData,
|
||||
pubKey,
|
||||
blockData,
|
||||
hideTypeRow = false,
|
||||
}: TxDetailsSharedProps) => {
|
||||
if (!txData) {
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
@@ -46,12 +41,10 @@ export const TxDetailsShared = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{hideTypeRow === false ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
|
||||
<TableCell>{txData.type}</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
|
||||
<TableCell>{txData.type}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell {...sharedHeaderProps}>{t('Hash')}</TableCell>
|
||||
<TableCell>
|
||||
|
||||
-138
@@ -1,138 +0,0 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import { PartyLink } from '../../../../links';
|
||||
import {
|
||||
SPECIAL_CASE_NETWORK,
|
||||
SPECIAL_CASE_NETWORK_ID,
|
||||
} from '../../../../links/party-link/party-link';
|
||||
import SizeInAsset from '../../../../size-in-asset/size-in-asset';
|
||||
import { AccountTypeMapping } from '@vegaprotocol/types';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import { headerClasses, wrapperClasses } from '../transfer-details';
|
||||
import type { Transfer } from '../transfer-details';
|
||||
|
||||
interface TransferParticipantsProps {
|
||||
transfer: Transfer;
|
||||
from: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a box containing the To, From and amount of a
|
||||
* transfer. This is shown for all transfers, including
|
||||
* recurring and reward transfers.
|
||||
*
|
||||
* @param transfer A recurring transfer object
|
||||
* @param from The sender is not in the transaction, but comes from the Transaction submitter
|
||||
*/
|
||||
export function TransferParticipants({
|
||||
transfer,
|
||||
from,
|
||||
}: TransferParticipantsProps) {
|
||||
// This mapping is required as the global account types require a type to be set, while
|
||||
// the underlying protobufs allow for every field to be undefined.
|
||||
const fromAcct =
|
||||
transfer.fromAccountType &&
|
||||
transfer.fromAccountType !== 'ACCOUNT_TYPE_UNSPECIFIED'
|
||||
? AccountType[transfer.fromAccountType]
|
||||
: AccountType.ACCOUNT_TYPE_GENERAL;
|
||||
const fromAccountTypeLabel = transfer.fromAccountType
|
||||
? AccountTypeMapping[fromAcct]
|
||||
: 'Unknown';
|
||||
|
||||
const toAcct =
|
||||
transfer.toAccountType &&
|
||||
transfer.toAccountType !== 'ACCOUNT_TYPE_UNSPECIFIED'
|
||||
? AccountType[transfer.toAccountType]
|
||||
: AccountType.ACCOUNT_TYPE_GENERAL;
|
||||
const toAccountTypeLabel = transfer.fromAccountType
|
||||
? AccountTypeMapping[toAcct]
|
||||
: 'Unknown';
|
||||
|
||||
return (
|
||||
<div className={wrapperClasses}>
|
||||
<h2 className={headerClasses}>{t('Transfer')}</h2>
|
||||
<div className="relative block rounded-lg py-6 text-center">
|
||||
<PartyLink id={from} truncate={true} />
|
||||
<Tooltip
|
||||
description={
|
||||
<p>{`${t('From account')}: ${fromAccountTypeLabel}`}</p>
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<Icon className="ml-3" name={'bank-account'} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
<br />
|
||||
|
||||
{/* This block of divs is used to render the inset arrow containing the transfer amount */}
|
||||
<div className="bg-vega-light-200 dark:vega-dark-200 flex items-center justify-center my-4 relative">
|
||||
<div className="bg-vega-light-200 dark:bg-vega-dark-200 border w-full pt-5 pb-3 px-3 border-vega-light-200 dark:border-vega-dark-150 relative">
|
||||
<div className="text-xs z-20 relative leading-none">
|
||||
{transfer.asset ? (
|
||||
<SizeInAsset assetId={transfer.asset} size={transfer.amount} />
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{/* Empty divs for the top arrow and the bottom arrow of the transfer inset */}
|
||||
<div className="z-10 absolute top-[-1px] left-1/2 w-4 h-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 9"
|
||||
className="fill-vega-light-100 dark:fill-black"
|
||||
>
|
||||
<path d="M0,0L8,9l8,-9Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="z-10 absolute bottom-[-16px] left-1/2 w-4 h-4">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 9"
|
||||
className="fill-vega-light-100 dark:fill-vega-dark-200"
|
||||
>
|
||||
<path d="M0,0L8,9l8,-9Z" />
|
||||
</svg>
|
||||
</div>
|
||||
{/*
|
||||
<div className="z-10 absolute top-0 left-1/2 transform -translate-x-1/2 -translate-y-1/2 rotate-45 w-4 h-4 dark:border-vega-dark-200 border-vega-light-200 bg-white dark:bg-black border-r border-b"></div>
|
||||
<div className="z-10 absolute bottom-0 left-1/2 transform -translate-x-1/2 translate-y-1/2 rotate-45 w-4 h-4 border-vega-light-200 dark:border-vega-dark-200 bg-vega-light-200 dark:bg-vega-dark-200 border-r border-b"></div>
|
||||
*/}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TransferRecurringRecipient to={transfer.to} />
|
||||
<Tooltip
|
||||
description={<p>{`${t('To account')}: ${toAccountTypeLabel}`}</p>}
|
||||
>
|
||||
<span>
|
||||
<Icon className="ml-3" name={'bank-account'} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
<br />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface TransferRecurringRecipientProps {
|
||||
to?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the transfer is to 000...000, then this is a transfer to the
|
||||
* Rewards Pool rather than the network. This component saves this
|
||||
* logic from complicating the To section of the participants block
|
||||
*
|
||||
* @param markets String[] IDs of markets for this dispatch strategy
|
||||
*/
|
||||
export function TransferRecurringRecipient({
|
||||
to,
|
||||
}: TransferRecurringRecipientProps) {
|
||||
if (to === SPECIAL_CASE_NETWORK || to === SPECIAL_CASE_NETWORK_ID) {
|
||||
return <span>{t('Rewards pool')}</span>;
|
||||
} else if (to) {
|
||||
return <PartyLink id={to} truncate={true} />;
|
||||
}
|
||||
|
||||
// Fallback should not happen
|
||||
return null;
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import EpochOverview from '../../../../epoch-overview/epoch';
|
||||
import { useExplorerFutureEpochQuery } from '../../../../epoch-overview/__generated__/Epoch';
|
||||
import { headerClasses, wrapperClasses } from '../transfer-details';
|
||||
import type { IconProps } from '@vegaprotocol/ui-toolkit';
|
||||
import type { Recurring } from '../transfer-details';
|
||||
|
||||
interface TransferRepeatProps {
|
||||
recurring: Recurring;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderer for a transfer. These can vary quite
|
||||
* widely, essentially every field can be null.
|
||||
*
|
||||
* @param transfer A recurring transfer object
|
||||
*/
|
||||
export function TransferRepeat({ recurring }: TransferRepeatProps) {
|
||||
const { data } = useExplorerFutureEpochQuery();
|
||||
|
||||
if (!recurring) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={wrapperClasses}>
|
||||
<h2 className={headerClasses}>{t('Active epochs')}</h2>
|
||||
<div className="relative block rounded-lg py-6 text-center p-6">
|
||||
<p>
|
||||
<EpochOverview id={recurring.startEpoch} />
|
||||
</p>
|
||||
<p className="leading-10 my-2">
|
||||
<IconForEpoch
|
||||
start={recurring.startEpoch}
|
||||
end={recurring.endEpoch}
|
||||
current={data?.epoch.id}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
{recurring.endEpoch ? (
|
||||
<EpochOverview id={recurring.endEpoch} />
|
||||
) : (
|
||||
<span>{t('Forever')}</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export type IconForTransferProps = {
|
||||
current?: string;
|
||||
start?: string;
|
||||
end?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Pick an icon rto represent the state of the repetition for this recurring
|
||||
* transfer. It can be unstarted, in progress, or complete.
|
||||
*
|
||||
* @param start The epoch in which the transfer first occurs
|
||||
* @param end The last epoch in which the transfer occurs
|
||||
* @param current The current epoch
|
||||
*/
|
||||
function IconForEpoch({ start, end, current }: IconForTransferProps) {
|
||||
let i: IconProps['name'] = 'repeat';
|
||||
|
||||
if (current && start && end) {
|
||||
const startEpoch = parseInt(start);
|
||||
const endEpoch = parseInt(end);
|
||||
const currentEpoch = parseInt(current);
|
||||
|
||||
if (currentEpoch > endEpoch) {
|
||||
// If we've finished
|
||||
i = 'updated';
|
||||
} else if (startEpoch > currentEpoch) {
|
||||
// If we haven't yet started
|
||||
i = 'time';
|
||||
} else if (startEpoch < currentEpoch && endEpoch > currentEpoch) {
|
||||
i = 'repeat';
|
||||
}
|
||||
}
|
||||
|
||||
return <Icon name={i} className="mr-2" />;
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { AssetLink, MarketLink } from '../../../../links';
|
||||
import { headerClasses, wrapperClasses } from '../transfer-details';
|
||||
import type { components } from '../../../../../../types/explorer';
|
||||
import type { Recurring } from '../transfer-details';
|
||||
import { DispatchMetricLabels } from '@vegaprotocol/types';
|
||||
|
||||
export type Metric = components['schemas']['vegaDispatchMetric'];
|
||||
export type Strategy = components['schemas']['vegaDispatchStrategy'];
|
||||
|
||||
const metricLabels = {
|
||||
DISPATCH_METRIC_UNSPECIFIED: 'Unknown metric',
|
||||
...DispatchMetricLabels,
|
||||
};
|
||||
|
||||
interface TransferRewardsProps {
|
||||
recurring: Recurring;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderer for a transfer. These can vary quite
|
||||
* widely, essentially every field can be null.
|
||||
*
|
||||
* @param transfer A recurring transfer object
|
||||
*/
|
||||
export function TransferRewards({ recurring }: TransferRewardsProps) {
|
||||
const metric =
|
||||
recurring?.dispatchStrategy?.metric || 'DISPATCH_METRIC_UNSPECIFIED';
|
||||
|
||||
if (!recurring || !recurring.dispatchStrategy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={wrapperClasses}>
|
||||
<h2 className={headerClasses}>{t('Reward metrics')}</h2>
|
||||
<ul className="relative block rounded-lg py-6 text-center p-6">
|
||||
{recurring.dispatchStrategy.assetForMetric ? (
|
||||
<li>
|
||||
<strong>{t('Asset')}</strong>:{' '}
|
||||
<AssetLink assetId={recurring.dispatchStrategy.assetForMetric} />
|
||||
</li>
|
||||
) : null}
|
||||
<li>
|
||||
<strong>{t('Metric')}</strong>: {metricLabels[metric]}
|
||||
</li>
|
||||
{recurring.dispatchStrategy.markets &&
|
||||
recurring.dispatchStrategy.markets.length > 0 ? (
|
||||
<li>
|
||||
<strong>{t('Markets in scope')}</strong>:
|
||||
<ul>
|
||||
{recurring.dispatchStrategy.markets.map((m) => (
|
||||
<li key={m}>
|
||||
<MarketLink id={m} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
) : null}
|
||||
<li>
|
||||
<strong>{t('Factor')}</strong>: {recurring.factor}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface TransferRecurringStrategyProps {
|
||||
strategy: Strategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple renderer for a dispatch strategy in a recurring transfer
|
||||
*
|
||||
* @param strategy Dispatch strategy object
|
||||
*/
|
||||
export function TransferRecurringStrategy({
|
||||
strategy,
|
||||
}: TransferRecurringStrategyProps) {
|
||||
if (!strategy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{strategy.assetForMetric ? (
|
||||
<li>
|
||||
<strong>{t('Asset for metric')}</strong>:{' '}
|
||||
<AssetLink assetId={strategy.assetForMetric} />
|
||||
</li>
|
||||
) : null}
|
||||
<li>
|
||||
<strong>{t('Metric')}</strong>: {strategy.metric}
|
||||
</li>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import type { components } from '../../../../../types/explorer';
|
||||
import { TransferRepeat } from './blocks/transfer-repeat';
|
||||
import { TransferRewards } from './blocks/transfer-rewards';
|
||||
import { TransferParticipants } from './blocks/transfer-participants';
|
||||
|
||||
export type Recurring = components['schemas']['v1RecurringTransfer'];
|
||||
export type Metric = components['schemas']['vegaDispatchMetric'];
|
||||
|
||||
export const wrapperClasses =
|
||||
'border border-vega-light-150 dark:border-vega-dark-200 rounded-md pv-2 mb-5 w-full sm:w-1/4 min-w-[200px] ';
|
||||
export const headerClasses =
|
||||
'bg-solid bg-vega-light-150 dark:bg-vega-dark-150 border-vega-light-150 text-center text-xl py-2 font-alpha';
|
||||
|
||||
export type Transfer = components['schemas']['commandsv1Transfer'];
|
||||
|
||||
interface TransferDetailsProps {
|
||||
transfer: Transfer;
|
||||
from: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderer for a transfer. These can vary quite
|
||||
* widely, essentially every field can be null.
|
||||
*
|
||||
* @param transfer A recurring transfer object
|
||||
*/
|
||||
export function TransferDetails({ transfer, from }: TransferDetailsProps) {
|
||||
const recurring = transfer.recurring;
|
||||
|
||||
return (
|
||||
<div className="flex gap-5 flex-wrap">
|
||||
<TransferParticipants from={from} transfer={transfer} />
|
||||
{recurring ? <TransferRepeat recurring={transfer.recurring} /> : null}
|
||||
{recurring && recurring.dispatchStrategy ? (
|
||||
<TransferRewards recurring={transfer.recurring} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -21,9 +21,7 @@ import { TxDetailsDataSubmission } from './tx-data-submission';
|
||||
import { TxProposalVote } from './tx-proposal-vote';
|
||||
import { TxDetailsProtocolUpgrade } from './tx-details-protocol-upgrade';
|
||||
import { TxDetailsIssueSignatures } from './tx-issue-signatures';
|
||||
import { TxDetailsNodeAnnounce } from './tx-node-announce';
|
||||
import { TxDetailsStateVariable } from './tx-state-variable-proposal';
|
||||
import { TxDetailsTransfer } from './tx-transfer';
|
||||
|
||||
interface TxDetailsWrapperProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
@@ -71,8 +69,6 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
|
||||
|
||||
// These come from https://github.com/vegaprotocol/vega/blob/develop/core/txn/command.go#L72-L98
|
||||
switch (txData.type) {
|
||||
case 'Register new Node':
|
||||
return TxDetailsNodeAnnounce;
|
||||
case 'Issue Signatures':
|
||||
return TxDetailsIssueSignatures;
|
||||
case 'Submit Order':
|
||||
@@ -109,8 +105,6 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
|
||||
return TxDetailsUndelegate;
|
||||
case 'State Variable Proposal':
|
||||
return TxDetailsStateVariable;
|
||||
case 'Transfer Funds':
|
||||
return TxDetailsTransfer;
|
||||
default:
|
||||
return TxDetailsGeneric;
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
||||
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||
import { TxDetailsShared } from './shared/tx-details-shared';
|
||||
import { TableCell, TableRow, TableWithTbody } from '../../table';
|
||||
import type { components } from '../../../../types/explorer';
|
||||
import {
|
||||
EthExplorerLink,
|
||||
EthExplorerLinkTypes,
|
||||
} from '../../links/eth-explorer-link/eth-explorer-link';
|
||||
import { BlockLink } from '../../links';
|
||||
|
||||
type EthKeyRotate = components['schemas']['v1EthereumKeyRotateSubmission'];
|
||||
interface TxDetailsEthKeyRotateProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
pubKey: string | undefined;
|
||||
blockData: TendermintBlocksResponse | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* A node is changing ethereum key
|
||||
*/
|
||||
export const TxDetailsEthKeyRotate = ({
|
||||
txData,
|
||||
pubKey,
|
||||
blockData,
|
||||
}: TxDetailsEthKeyRotateProps) => {
|
||||
if (!txData) {
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
|
||||
const k: EthKeyRotate = txData.command;
|
||||
|
||||
return (
|
||||
<TableWithTbody className="mb-8">
|
||||
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
|
||||
{k.targetBlock ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Target block')}</TableCell>
|
||||
<TableCell>
|
||||
<BlockLink height={k.targetBlock} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{k.currentAddress ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Old Address')}</TableCell>
|
||||
<TableCell>
|
||||
<EthExplorerLink
|
||||
type={EthExplorerLinkTypes.address}
|
||||
id={k.currentAddress}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{k.newAddress ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('New Address')}</TableCell>
|
||||
<TableCell>
|
||||
<EthExplorerLink
|
||||
type={EthExplorerLinkTypes.address}
|
||||
id={k.newAddress}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{k.submitterAddress ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Submitter address')}</TableCell>
|
||||
<TableCell>
|
||||
<EthExplorerLink
|
||||
type={EthExplorerLinkTypes.address}
|
||||
id={k.submitterAddress}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
</TableWithTbody>
|
||||
);
|
||||
};
|
||||
@@ -1,67 +0,0 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
||||
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||
import { TxDetailsShared } from './shared/tx-details-shared';
|
||||
import { TableCell, TableRow, TableWithTbody } from '../../table';
|
||||
import type { components } from '../../../../types/explorer';
|
||||
import { BlockLink, PartyLink } from '../../links';
|
||||
|
||||
type KeyRotate = components['schemas']['v1KeyRotateSubmission'];
|
||||
interface TxDetailsKeyRotateProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
pubKey: string | undefined;
|
||||
blockData: TendermintBlocksResponse | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* A node is changing Vega key
|
||||
*/
|
||||
export const TxDetailsKeyRotate = ({
|
||||
txData,
|
||||
pubKey,
|
||||
blockData,
|
||||
}: TxDetailsKeyRotateProps) => {
|
||||
if (!txData) {
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
|
||||
const k: KeyRotate = txData.command;
|
||||
|
||||
return (
|
||||
<TableWithTbody className="mb-8">
|
||||
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
|
||||
{k.targetBlock ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Target block')}</TableCell>
|
||||
<TableCell>
|
||||
<BlockLink height={k.targetBlock} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{k.currentPubKeyHash ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Old Address')}</TableCell>
|
||||
<TableCell>
|
||||
<PartyLink id={k.currentPubKeyHash} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{k.currentPubKeyHash ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('New Address')}</TableCell>
|
||||
<TableCell>
|
||||
<PartyLink id={k.currentPubKeyHash} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{k.newPubKeyIndex ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Key index')}</TableCell>
|
||||
<TableCell>
|
||||
<code>{k.newPubKeyIndex}</code>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
</TableWithTbody>
|
||||
);
|
||||
};
|
||||
@@ -1,112 +0,0 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
||||
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||
import { TxDetailsShared } from './shared/tx-details-shared';
|
||||
import { TableRow, TableCell, TableWithTbody } from '../../table';
|
||||
|
||||
import type { components } from '../../../../types/explorer';
|
||||
import {
|
||||
EthExplorerLink,
|
||||
EthExplorerLinkTypes,
|
||||
} from '../../links/eth-explorer-link/eth-explorer-link';
|
||||
import { PartyLink } from '../../links';
|
||||
import Hash from '../../links/hash';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
type Command = components['schemas']['v1AnnounceNode'];
|
||||
|
||||
interface TxDetailsNodeAnnounceProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
pubKey: string | undefined;
|
||||
blockData: TendermintBlocksResponse | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* When a new potential validator node comes online, it announces
|
||||
* itself with this transaction.
|
||||
*
|
||||
* Design decisions:
|
||||
* - Signatures are not rendered. You can still access them via the
|
||||
* TX details. This is consistent with explorers for other chains
|
||||
* - The avatar icon is rendered as a link rather than embedding
|
||||
*/
|
||||
export const TxDetailsNodeAnnounce = ({
|
||||
txData,
|
||||
pubKey,
|
||||
blockData,
|
||||
}: TxDetailsNodeAnnounceProps) => {
|
||||
if (!txData) {
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
|
||||
const cmd: Command = txData.command.announceNode;
|
||||
|
||||
return (
|
||||
<TableWithTbody className="mb-8">
|
||||
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
|
||||
{cmd.name ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Name')}</TableCell>
|
||||
<TableCell>
|
||||
<span>{cmd.name}</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{cmd.id ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('ID')}</TableCell>
|
||||
<TableCell>
|
||||
<Hash text={cmd.id} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{cmd.chainPubKey ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Chain public key')}</TableCell>
|
||||
<TableCell>
|
||||
<Hash text={cmd.chainPubKey} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{cmd.ethereumAddress ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Ethereum Address')}</TableCell>
|
||||
<TableCell>
|
||||
<EthExplorerLink
|
||||
type={EthExplorerLinkTypes.address}
|
||||
id={cmd.ethereumAddress}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{cmd.vegaPubKey ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Vega public key')}</TableCell>
|
||||
<TableCell>
|
||||
<PartyLink id={cmd.vegaPubKey} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{cmd.avatarUrl ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Avatar URL')}</TableCell>
|
||||
<TableCell>
|
||||
<ExternalLink href={cmd.avatarUrl} rel="noreferrer noopener">
|
||||
{cmd.avatarUrl}
|
||||
</ExternalLink>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{cmd.infoUrl ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Info link')}</TableCell>
|
||||
<TableCell>
|
||||
<ExternalLink href={cmd.infoUrl} rel="noreferrer noopener">
|
||||
{cmd.infoUrl}
|
||||
</ExternalLink>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
</TableWithTbody>
|
||||
);
|
||||
};
|
||||
@@ -1,125 +0,0 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
||||
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||
import { sharedHeaderProps, TxDetailsShared } from './shared/tx-details-shared';
|
||||
import { TableRow, TableCell, TableWithTbody } from '../../table';
|
||||
|
||||
import type { components } from '../../../../types/explorer';
|
||||
import { PartyLink } from '../../links';
|
||||
import SizeInAsset from '../../size-in-asset/size-in-asset';
|
||||
import { TransferDetails } from './transfer/transfer-details';
|
||||
import {
|
||||
SPECIAL_CASE_NETWORK,
|
||||
SPECIAL_CASE_NETWORK_ID,
|
||||
} from '../../links/party-link/party-link';
|
||||
|
||||
type Transfer = components['schemas']['commandsv1Transfer'];
|
||||
|
||||
interface TxDetailsNodeAnnounceProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
pubKey: string | undefined;
|
||||
blockData: TendermintBlocksResponse | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the details of a transfer. Broadly there are three distinct
|
||||
* types of transfer, listed below in order of complexity:
|
||||
*
|
||||
* - A one off transfer
|
||||
* - A recurring transfer
|
||||
* - A recurring rewards pool transfer
|
||||
*
|
||||
* One off transfers are simple, really the important data is the amount
|
||||
* and who sent it to whom. This is rendered as one distinct box.
|
||||
*
|
||||
* A recurring transfer has two components - the same as above, and an
|
||||
* additional box that shows details about how it repeats. This is defined
|
||||
* as a start epoch and and end epoch. The Epoch/MissingEpoch components
|
||||
* render slightly differently depending on if the epoch is in the past,
|
||||
* current or in the future.
|
||||
*
|
||||
* Finally rewards pool transfers get the two boxes above, and an additional
|
||||
* one that describes how the reward is distributed.
|
||||
*
|
||||
* The information is split up in to three boxes to allow for the reuse across
|
||||
* all the types of transfer above.
|
||||
*/
|
||||
export const TxDetailsTransfer = ({
|
||||
txData,
|
||||
pubKey,
|
||||
blockData,
|
||||
}: TxDetailsNodeAnnounceProps) => {
|
||||
if (!txData) {
|
||||
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||
}
|
||||
|
||||
const transfer: Transfer = txData.command.transfer;
|
||||
if (!transfer) {
|
||||
return <>{t('Transfer data missing')}</>;
|
||||
}
|
||||
|
||||
const from = txData.submitter;
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableWithTbody className="mb-8" allowWrap={true}>
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
|
||||
<TableCell>{getTypeLabelForTransfer(transfer)}</TableCell>
|
||||
</TableRow>
|
||||
<TxDetailsShared
|
||||
txData={txData}
|
||||
pubKey={pubKey}
|
||||
blockData={blockData}
|
||||
hideTypeRow={true}
|
||||
/>
|
||||
{from ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('From')}</TableCell>
|
||||
<TableCell>
|
||||
<PartyLink id={from} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{transfer.to ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('To')}</TableCell>
|
||||
<TableCell>
|
||||
<PartyLink id={transfer.to} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
{transfer.asset && transfer.amount ? (
|
||||
<TableRow modifier="bordered">
|
||||
<TableCell>{t('Amount')}</TableCell>
|
||||
<TableCell>
|
||||
<SizeInAsset assetId={transfer.asset} size={transfer.amount} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : null}
|
||||
</TableWithTbody>
|
||||
<TransferDetails from={from} transfer={transfer} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets a string description of this transfer
|
||||
* @param txData A full transfer
|
||||
* @returns string Transfer label
|
||||
*/
|
||||
export function getTypeLabelForTransfer(tx: Transfer) {
|
||||
if (tx.to === SPECIAL_CASE_NETWORK || tx.to === SPECIAL_CASE_NETWORK_ID) {
|
||||
if (tx.recurring && tx.recurring.dispatchStrategy) {
|
||||
return 'Reward top up transfer';
|
||||
}
|
||||
// Else: we don't know that it's a reward transfer, so let's not guess
|
||||
} else if (tx.recurring) {
|
||||
return 'Recurring transfer';
|
||||
} else if (tx.oneOff) {
|
||||
// Currently redundant, but could be used to indicate something more specific
|
||||
return 'Transfer';
|
||||
}
|
||||
|
||||
return 'Transfer';
|
||||
}
|
||||
@@ -14,17 +14,14 @@ interface StringMap {
|
||||
// Using https://github.com/vegaprotocol/protos/blob/e0f646ce39aab1fc66a9200ceec0262306d3beb3/commands/transaction.go#L93 as a reference
|
||||
const displayString: StringMap = {
|
||||
OrderSubmission: 'Order Submission',
|
||||
'Submit Order': 'Order',
|
||||
OrderCancellation: 'Order Cancellation',
|
||||
OrderAmendment: 'Order Amendment',
|
||||
VoteSubmission: 'Vote Submission',
|
||||
WithdrawSubmission: 'Withdraw Submission',
|
||||
Withdraw: 'Withdraw Request',
|
||||
LiquidityProvisionSubmission: 'LP order',
|
||||
'Liquidity Provision Order': 'LP order',
|
||||
LiquidityProvisionCancellation: 'LP cancel',
|
||||
LiquidityProvisionAmendment: 'LP update',
|
||||
'Amend LiquidityProvision Order': 'Amend LP',
|
||||
LiquidityProvisionSubmission: 'Liquidity Provision',
|
||||
LiquidityProvisionCancellation: 'Liquidity Cancellation',
|
||||
LiquidityProvisionAmendment: 'Liquidity Amendment',
|
||||
ProposalSubmission: 'Governance Proposal',
|
||||
AnnounceNode: 'Node Announcement',
|
||||
NodeVote: 'Node Vote',
|
||||
@@ -34,11 +31,10 @@ const displayString: StringMap = {
|
||||
DelegateSubmission: 'Delegation',
|
||||
UndelegateSubmission: 'Undelegation',
|
||||
KeyRotateSubmission: 'Key Rotation',
|
||||
StateVariableProposal: 'State Variable',
|
||||
StateVariableProposal: 'State Variable Proposal',
|
||||
Transfer: 'Transfer',
|
||||
CancelTransfer: 'Cancel Transfer',
|
||||
ValidatorHeartbeat: 'Heartbeat',
|
||||
'Batch Market Instructions': 'Batch',
|
||||
ValidatorHeartbeat: 'Validator Heartbeat',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -136,8 +132,7 @@ export function getLabelForChainEvent(
|
||||
export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
|
||||
let type = displayString[orderType] || orderType;
|
||||
|
||||
let colours =
|
||||
'text-white dark:text-white bg-vega-dark-150 dark:bg-vega-dark-150';
|
||||
let colours = 'text-white dark:text-white bg-zinc-800 dark:bg-zinc-800';
|
||||
|
||||
// This will get unwieldy and should probably produce a different colour of tag
|
||||
if (type === 'Chain Event' && !!command?.chainEvent) {
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import { getTypeLabelForTransfer } from './details/tx-transfer';
|
||||
import type { components } from '../../../types/explorer';
|
||||
|
||||
type Transfer = components['schemas']['commandsv1Transfer'];
|
||||
|
||||
describe('TX: Transfer: getLabelForTransfer', () => {
|
||||
it('renders reward top up label if the TO party is 000', () => {
|
||||
const mock: Transfer = {
|
||||
to: '0000000000000000000000000000000000000000000000000000000000000000',
|
||||
recurring: {
|
||||
dispatchStrategy: {},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getTypeLabelForTransfer(mock)).toEqual('Reward top up transfer');
|
||||
});
|
||||
|
||||
it('renders reward top up label if the TO party is network', () => {
|
||||
const mock = {
|
||||
to: 'network',
|
||||
recurring: {
|
||||
dispatchStrategy: {},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getTypeLabelForTransfer(mock)).toEqual('Reward top up transfer');
|
||||
});
|
||||
|
||||
it('renders recurring label if the tx has a recurring property', () => {
|
||||
const mock: Transfer = {
|
||||
to: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
recurring: {
|
||||
startEpoch: '0',
|
||||
},
|
||||
};
|
||||
|
||||
expect(getTypeLabelForTransfer(mock)).toEqual('Recurring transfer');
|
||||
});
|
||||
|
||||
it('renders one off label if the tx has a oneOff property', () => {
|
||||
const mock: Transfer = {
|
||||
to: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
oneOff: {
|
||||
deliverOn: '0',
|
||||
},
|
||||
};
|
||||
|
||||
expect(getTypeLabelForTransfer(mock)).toEqual('Transfer');
|
||||
});
|
||||
|
||||
it('renders one off label otherwise', () => {
|
||||
const mock: Transfer = {
|
||||
to: '0000000000000000000000000000000000000000000000000000000000000001',
|
||||
};
|
||||
|
||||
expect(getTypeLabelForTransfer(mock)).toEqual('Transfer');
|
||||
});
|
||||
});
|
||||
@@ -37,9 +37,7 @@ export const TxsInfiniteListItem = ({
|
||||
className="text-sm col-span-10 xl:col-span-3 leading-none"
|
||||
data-testid="tx-hash"
|
||||
>
|
||||
<span className="xl:hidden uppercase text-vega-dark-300">
|
||||
ID:
|
||||
</span>
|
||||
<span className="xl:hidden uppercase text-zinc-500">ID: </span>
|
||||
<TruncatedLink
|
||||
to={`/${Routes.TX}/${toHex(hash)}`}
|
||||
text={hash}
|
||||
@@ -51,9 +49,7 @@ export const TxsInfiniteListItem = ({
|
||||
className="text-sm col-span-10 xl:col-span-3 leading-none"
|
||||
data-testid="pub-key"
|
||||
>
|
||||
<span className="xl:hidden uppercase text-vega-dark-300">
|
||||
By:
|
||||
</span>
|
||||
<span className="xl:hidden uppercase text-zinc-500">By: </span>
|
||||
<TruncatedLink
|
||||
to={`/${Routes.PARTIES}/${submitter}`}
|
||||
text={submitter}
|
||||
@@ -68,9 +64,7 @@ export const TxsInfiniteListItem = ({
|
||||
className="text-sm col-span-3 xl:col-span-1 leading-none flex items-center"
|
||||
data-testid="tx-block"
|
||||
>
|
||||
<span className="xl:hidden uppercase text-vega-dark-300">
|
||||
Block:
|
||||
</span>
|
||||
<span className="xl:hidden uppercase text-zinc-500">Block: </span>
|
||||
<TruncatedLink
|
||||
to={`/${Routes.BLOCKS}/${block}`}
|
||||
text={block}
|
||||
@@ -82,7 +76,7 @@ export const TxsInfiniteListItem = ({
|
||||
className="text-sm col-span-2 xl:col-span-1 leading-none flex items-center"
|
||||
data-testid="tx-success"
|
||||
>
|
||||
<span className="xl:hidden uppercase text-vega-dark-300">
|
||||
<span className="xl:hidden uppercase text-zinc-500">
|
||||
Success:
|
||||
</span>
|
||||
{isNumber(code) ? (
|
||||
|
||||
@@ -94,7 +94,7 @@ export const TxsInfiniteList = ({
|
||||
|
||||
return (
|
||||
<div className={className} data-testid="transactions-list">
|
||||
<div className="xl:grid grid-cols-10 w-full mb-3 hidden text-vega-dark-300 uppercase">
|
||||
<div className="xl:grid grid-cols-10 w-full mb-3 hidden text-zinc-500 uppercase">
|
||||
<div className="col-span-3">
|
||||
<span className="hidden xl:inline">Transaction </span>
|
||||
<span>ID</span>
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
/**
|
||||
* Equivalent of tailwind's `md` modifier
|
||||
*/
|
||||
export const BREAKPOINT_MD = 768;
|
||||
@@ -33,7 +33,4 @@ export const ENV = {
|
||||
parties: truthy.includes(windowOrDefault('NX_EXPLORER_PARTIES')),
|
||||
validators: truthy.includes(windowOrDefault('NX_EXPLORER_VALIDATORS')),
|
||||
},
|
||||
addresses: {
|
||||
feedback: windowOrDefault('NX_GITHUB_FEEDBACK_URL'),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
import { AssetDocument } from '@vegaprotocol/assets';
|
||||
import { AssetsDocument } from '@vegaprotocol/assets';
|
||||
import { AssetStatus } from '@vegaprotocol/types';
|
||||
|
||||
const A1: AssetFieldsFragment = {
|
||||
__typename: 'Asset',
|
||||
id: '123',
|
||||
name: 'A ONE',
|
||||
symbol: 'A1',
|
||||
decimals: 0,
|
||||
quantum: '',
|
||||
status: AssetStatus.STATUS_ENABLED,
|
||||
source: {
|
||||
__typename: 'BuiltinAsset',
|
||||
maxFaucetAmountMint: '',
|
||||
},
|
||||
infrastructureFeeAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
globalRewardPoolAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
lpFeeRewardAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
makerFeeRewardAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
marketProposerRewardAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
takerFeeRewardAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
};
|
||||
|
||||
const A2: AssetFieldsFragment = {
|
||||
__typename: 'Asset',
|
||||
id: '456',
|
||||
name: 'A TWO',
|
||||
symbol: 'A2',
|
||||
decimals: 0,
|
||||
quantum: '',
|
||||
status: AssetStatus.STATUS_ENABLED,
|
||||
source: {
|
||||
__typename: 'BuiltinAsset',
|
||||
maxFaucetAmountMint: '',
|
||||
},
|
||||
infrastructureFeeAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
globalRewardPoolAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
lpFeeRewardAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
makerFeeRewardAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
marketProposerRewardAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
takerFeeRewardAccount: {
|
||||
__typename: 'AccountBalance',
|
||||
balance: '',
|
||||
},
|
||||
};
|
||||
|
||||
export const assetsList = [A1, A2];
|
||||
|
||||
export const mockAssetsList = {
|
||||
request: {
|
||||
query: AssetsDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
assetsConnection: {
|
||||
__typename: 'AssetsConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'AssetEdge',
|
||||
node: A1,
|
||||
},
|
||||
{
|
||||
__typename: 'AssetEdge',
|
||||
node: A2,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockEmptyAssetsList = {
|
||||
request: {
|
||||
query: AssetsDocument,
|
||||
},
|
||||
result: { data: null },
|
||||
};
|
||||
|
||||
export const mockAssetA1 = {
|
||||
request: {
|
||||
query: AssetDocument,
|
||||
variables: {
|
||||
assetId: '123',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
assetsConnection: {
|
||||
__typename: 'AssetsConnection',
|
||||
edges: [
|
||||
{
|
||||
__typename: 'AssetEdge',
|
||||
node: A1,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
fragment AssetsFields on Asset {
|
||||
id
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
source {
|
||||
... on ERC20 {
|
||||
contractAddress
|
||||
}
|
||||
... on BuiltinAsset {
|
||||
maxFaucetAmountMint
|
||||
}
|
||||
}
|
||||
|
||||
infrastructureFeeAccount {
|
||||
type
|
||||
balance
|
||||
market {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query ExplorerAssets {
|
||||
assetsConnection {
|
||||
edges {
|
||||
node {
|
||||
...AssetsFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type AssetsFieldsFragment = { __typename?: 'Asset', id: string, name: string, symbol: string, decimals: number, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string }, infrastructureFeeAccount?: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, market?: { __typename?: 'Market', id: string } | null } | null };
|
||||
|
||||
export type ExplorerAssetsQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ExplorerAssetsQuery = { __typename?: 'Query', assetsConnection?: { __typename?: 'AssetsConnection', edges?: Array<{ __typename?: 'AssetEdge', node: { __typename?: 'Asset', id: string, name: string, symbol: string, decimals: number, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string }, infrastructureFeeAccount?: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, market?: { __typename?: 'Market', id: string } | null } | null } } | null> | null } | null };
|
||||
|
||||
export const AssetsFieldsFragmentDoc = gql`
|
||||
fragment AssetsFields on Asset {
|
||||
id
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
source {
|
||||
... on ERC20 {
|
||||
contractAddress
|
||||
}
|
||||
... on BuiltinAsset {
|
||||
maxFaucetAmountMint
|
||||
}
|
||||
}
|
||||
infrastructureFeeAccount {
|
||||
type
|
||||
balance
|
||||
market {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const ExplorerAssetsDocument = gql`
|
||||
query ExplorerAssets {
|
||||
assetsConnection {
|
||||
edges {
|
||||
node {
|
||||
...AssetsFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${AssetsFieldsFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useExplorerAssetsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useExplorerAssetsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useExplorerAssetsQuery` 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 } = useExplorerAssetsQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useExplorerAssetsQuery(baseOptions?: Apollo.QueryHookOptions<ExplorerAssetsQuery, ExplorerAssetsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ExplorerAssetsQuery, ExplorerAssetsQueryVariables>(ExplorerAssetsDocument, options);
|
||||
}
|
||||
export function useExplorerAssetsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerAssetsQuery, ExplorerAssetsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ExplorerAssetsQuery, ExplorerAssetsQueryVariables>(ExplorerAssetsDocument, options);
|
||||
}
|
||||
export type ExplorerAssetsQueryHookResult = ReturnType<typeof useExplorerAssetsQuery>;
|
||||
export type ExplorerAssetsLazyQueryHookResult = ReturnType<typeof useExplorerAssetsLazyQuery>;
|
||||
export type ExplorerAssetsQueryResult = Apollo.QueryResult<ExplorerAssetsQuery, ExplorerAssetsQueryVariables>;
|
||||
@@ -1,50 +0,0 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { useScrollToLocation } from '../../hooks/scroll-to-location';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { JsonViewerDialog } from '../../components/dialogs/json-viewer-dialog';
|
||||
import { useState } from 'react';
|
||||
|
||||
export const AssetPage = () => {
|
||||
useDocumentTitle(['Assets']);
|
||||
useScrollToLocation();
|
||||
|
||||
const { assetId } = useParams<{ assetId: string }>();
|
||||
const { data, loading, error } = useAssetDataProvider(assetId || '');
|
||||
|
||||
const title = data ? data.name : error ? t('Asset not found') : '';
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="relative">
|
||||
<RouteTitle data-testid="asset-header">{title}</RouteTitle>
|
||||
<AsyncRenderer
|
||||
noDataMessage={t('Asset not found')}
|
||||
data={data}
|
||||
loading={loading}
|
||||
error={error}
|
||||
>
|
||||
<div className="absolute top-0 right-0">
|
||||
<Button size="xs" onClick={() => setDialogOpen(true)}>
|
||||
{t('View JSON')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-full relative">
|
||||
<AssetDetailsTable asset={data as AssetFieldsFragment} />
|
||||
</div>
|
||||
</AsyncRenderer>
|
||||
</section>
|
||||
<JsonViewerDialog
|
||||
open={dialogOpen}
|
||||
onChange={(isOpen) => setDialogOpen(isOpen)}
|
||||
title={data?.name || ''}
|
||||
content={data}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { useScrollToLocation } from '../../hooks/scroll-to-location';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
import { useAssetsDataProvider } from '@vegaprotocol/assets';
|
||||
import { AssetsTable } from '../../components/assets/assets-table';
|
||||
|
||||
export const AssetsPage = () => {
|
||||
useDocumentTitle(['Assets']);
|
||||
useScrollToLocation();
|
||||
|
||||
const { data, loading, error } = useAssetsDataProvider();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<RouteTitle data-testid="assets-header">{t('Assets')}</RouteTitle>
|
||||
<AsyncRenderer
|
||||
noDataMessage={t('This chain has no assets')}
|
||||
data={data}
|
||||
loading={loading}
|
||||
error={error}
|
||||
>
|
||||
<div className="h-full relative">
|
||||
<AssetsTable data={data} />
|
||||
</div>
|
||||
</AsyncRenderer>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { render } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import Assets from './index';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { ExplorerAssetDocument } from '../../components/links/asset-link/__generated__/Asset';
|
||||
|
||||
function renderComponent(mock: MockedResponse[]) {
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<MockedProvider mocks={mock}>
|
||||
<Assets />
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
describe('Assets index', () => {
|
||||
it('Renders loader when loading', async () => {
|
||||
const mock = {
|
||||
request: {
|
||||
query: ExplorerAssetDocument,
|
||||
},
|
||||
result: {
|
||||
data: {},
|
||||
},
|
||||
};
|
||||
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: ExplorerAssetDocument,
|
||||
},
|
||||
result: {
|
||||
data: {},
|
||||
},
|
||||
};
|
||||
const res = render(renderComponent([mock]));
|
||||
expect(await res.findByTestId('emptylist')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,2 +1,53 @@
|
||||
export * from './assets-page';
|
||||
export * from './asset-page';
|
||||
import { getNodes, t } from '@vegaprotocol/react-helpers';
|
||||
import React from 'react';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import { SubHeading } from '../../components/sub-heading';
|
||||
import { Loader, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
|
||||
import { useExplorerAssetsQuery } from './__generated__/Assets';
|
||||
import type { AssetsFieldsFragment } from './__generated__/Assets';
|
||||
import { useScrollToLocation } from '../../hooks/scroll-to-location';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
import EmptyList from '../../components/empty-list/empty-list';
|
||||
|
||||
const Assets = () => {
|
||||
const { data, loading } = useExplorerAssetsQuery();
|
||||
useDocumentTitle(['Assets']);
|
||||
|
||||
useScrollToLocation();
|
||||
|
||||
const assets = getNodes<AssetsFieldsFragment>(data?.assetsConnection);
|
||||
|
||||
if (!assets || assets.length === 0) {
|
||||
if (!loading) {
|
||||
return (
|
||||
<section>
|
||||
<RouteTitle data-testid="assets-header">{t('Assets')}</RouteTitle>
|
||||
<EmptyList
|
||||
heading={t('This chain has no assets')}
|
||||
label={t('0 assets')}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
} else {
|
||||
return <Loader />;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
<RouteTitle data-testid="assets-header">{t('Assets')}</RouteTitle>
|
||||
{assets.map((a) => {
|
||||
return (
|
||||
<React.Fragment key={a.id}>
|
||||
<SubHeading data-testid="asset-header" id={a.id}>
|
||||
{a.name} ({a.symbol})
|
||||
</SubHeading>
|
||||
<SyntaxHighlighter data={a} />
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Assets;
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
query ExplorerProposals {
|
||||
proposalsConnection {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
rationale {
|
||||
title
|
||||
description
|
||||
}
|
||||
reference
|
||||
state
|
||||
datetime
|
||||
rejectionReason
|
||||
party {
|
||||
id
|
||||
}
|
||||
terms {
|
||||
closingDatetime
|
||||
enactmentDatetime
|
||||
change {
|
||||
... on NewMarket {
|
||||
instrument {
|
||||
name
|
||||
}
|
||||
}
|
||||
... on UpdateMarket {
|
||||
marketId
|
||||
}
|
||||
... on NewAsset {
|
||||
__typename
|
||||
symbol
|
||||
source {
|
||||
... on BuiltinAsset {
|
||||
maxFaucetAmountMint
|
||||
}
|
||||
... on ERC20 {
|
||||
contractAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
... on UpdateNetworkParameter {
|
||||
networkParameter {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
votes {
|
||||
yes {
|
||||
totalTokens
|
||||
totalNumber
|
||||
votes {
|
||||
value
|
||||
party {
|
||||
id
|
||||
stakingSummary {
|
||||
currentStakeAvailable
|
||||
}
|
||||
}
|
||||
datetime
|
||||
}
|
||||
}
|
||||
no {
|
||||
totalTokens
|
||||
totalNumber
|
||||
votes {
|
||||
value
|
||||
party {
|
||||
id
|
||||
stakingSummary {
|
||||
currentStakeAvailable
|
||||
}
|
||||
}
|
||||
datetime
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type ExplorerProposalsQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type ExplorerProposalsQuery = { __typename?: 'Query', proposalsConnection?: { __typename?: 'ProposalsConnection', edges?: Array<{ __typename?: 'ProposalEdge', node: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, party: { __typename?: 'Party', id: string }, terms: { __typename?: 'ProposalTerms', closingDatetime: any, enactmentDatetime?: any | null, change: { __typename: 'NewAsset', symbol: string, source: { __typename?: 'BuiltinAsset', maxFaucetAmountMint: string } | { __typename?: 'ERC20', contractAddress: string } } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket', instrument: { __typename?: 'InstrumentConfiguration', name: string } } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket', marketId: string } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } }, votes: { __typename?: 'ProposalVotes', yes: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, votes?: Array<{ __typename?: 'Vote', value: Types.VoteValue, datetime: any, party: { __typename?: 'Party', id: string, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string } } }> | null }, no: { __typename?: 'ProposalVoteSide', totalTokens: string, totalNumber: string, votes?: Array<{ __typename?: 'Vote', value: Types.VoteValue, datetime: any, party: { __typename?: 'Party', id: string, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string } } }> | null } } } } | null> | null } | null };
|
||||
|
||||
|
||||
export const ExplorerProposalsDocument = gql`
|
||||
query ExplorerProposals {
|
||||
proposalsConnection {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
rationale {
|
||||
title
|
||||
description
|
||||
}
|
||||
reference
|
||||
state
|
||||
datetime
|
||||
rejectionReason
|
||||
party {
|
||||
id
|
||||
}
|
||||
terms {
|
||||
closingDatetime
|
||||
enactmentDatetime
|
||||
change {
|
||||
... on NewMarket {
|
||||
instrument {
|
||||
name
|
||||
}
|
||||
}
|
||||
... on UpdateMarket {
|
||||
marketId
|
||||
}
|
||||
... on NewAsset {
|
||||
__typename
|
||||
symbol
|
||||
source {
|
||||
... on BuiltinAsset {
|
||||
maxFaucetAmountMint
|
||||
}
|
||||
... on ERC20 {
|
||||
contractAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
... on UpdateNetworkParameter {
|
||||
networkParameter {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
votes {
|
||||
yes {
|
||||
totalTokens
|
||||
totalNumber
|
||||
votes {
|
||||
value
|
||||
party {
|
||||
id
|
||||
stakingSummary {
|
||||
currentStakeAvailable
|
||||
}
|
||||
}
|
||||
datetime
|
||||
}
|
||||
}
|
||||
no {
|
||||
totalTokens
|
||||
totalNumber
|
||||
votes {
|
||||
value
|
||||
party {
|
||||
id
|
||||
stakingSummary {
|
||||
currentStakeAvailable
|
||||
}
|
||||
}
|
||||
datetime
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useExplorerProposalsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useExplorerProposalsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useExplorerProposalsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useExplorerProposalsQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useExplorerProposalsQuery(baseOptions?: Apollo.QueryHookOptions<ExplorerProposalsQuery, ExplorerProposalsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ExplorerProposalsQuery, ExplorerProposalsQueryVariables>(ExplorerProposalsDocument, options);
|
||||
}
|
||||
export function useExplorerProposalsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerProposalsQuery, ExplorerProposalsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ExplorerProposalsQuery, ExplorerProposalsQueryVariables>(ExplorerProposalsDocument, options);
|
||||
}
|
||||
export type ExplorerProposalsQueryHookResult = ReturnType<typeof useExplorerProposalsQuery>;
|
||||
export type ExplorerProposalsLazyQueryHookResult = ReturnType<typeof useExplorerProposalsLazyQuery>;
|
||||
export type ExplorerProposalsQueryResult = Apollo.QueryResult<ExplorerProposalsQuery, ExplorerProposalsQueryVariables>;
|
||||
@@ -1 +1,63 @@
|
||||
export * from './proposals-page';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import React from 'react';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import { SubHeading } from '../../components/sub-heading';
|
||||
import { Loader, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
|
||||
import { useExplorerProposalsQuery } from './__generated__/Proposals';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
import EmptyList from '../../components/empty-list/empty-list';
|
||||
|
||||
const Governance = () => {
|
||||
const { data, loading } = useExplorerProposalsQuery({
|
||||
errorPolicy: 'ignore',
|
||||
});
|
||||
|
||||
useDocumentTitle();
|
||||
|
||||
if (!data || !data.proposalsConnection || !data.proposalsConnection.edges) {
|
||||
if (!loading) {
|
||||
return (
|
||||
<section>
|
||||
<RouteTitle data-testid="governance-header">
|
||||
{t('Governance Proposals')}
|
||||
</RouteTitle>
|
||||
|
||||
<EmptyList
|
||||
heading={t('This chain has no proposals')}
|
||||
label={t('0 proposals')}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
} else {
|
||||
return <Loader />;
|
||||
}
|
||||
}
|
||||
|
||||
const proposals = data?.proposalsConnection?.edges.map((e) => {
|
||||
return e?.node;
|
||||
});
|
||||
|
||||
return (
|
||||
<section>
|
||||
<RouteTitle data-testid="governance-header">
|
||||
{t('Governance Proposals')}
|
||||
</RouteTitle>
|
||||
{proposals.map((p) => {
|
||||
if (!p || !p.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment key={p.id}>
|
||||
<SubHeading>
|
||||
{p.rationale.title || p.rationale.description}
|
||||
</SubHeading>
|
||||
<SyntaxHighlighter data={p} />
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Governance;
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import { proposalsDataProvider } from '@vegaprotocol/governance';
|
||||
import { t, useDataProvider } from '@vegaprotocol/react-helpers';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { ProposalsTable } from '../../components/proposals/proposals-table';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import { useScrollToLocation } from '../../hooks/scroll-to-location';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
|
||||
export const Proposals = () => {
|
||||
useScrollToLocation();
|
||||
|
||||
const { data, loading, error } = useDataProvider({
|
||||
dataProvider: proposalsDataProvider,
|
||||
});
|
||||
|
||||
useDocumentTitle([t('Governance Proposals')]);
|
||||
|
||||
return (
|
||||
<section>
|
||||
<RouteTitle data-testid="proposals-heading">
|
||||
{t('Governance proposals')}
|
||||
</RouteTitle>
|
||||
<AsyncRenderer
|
||||
noDataMessage={t('This chain has no proposals')}
|
||||
data={data}
|
||||
loading={loading}
|
||||
error={error}
|
||||
>
|
||||
<ProposalsTable data={data} />
|
||||
</AsyncRenderer>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
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>;
|
||||
@@ -0,0 +1,48 @@
|
||||
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,2 +1,44 @@
|
||||
export * from './markets-page';
|
||||
export * from './market-page';
|
||||
import React from 'react';
|
||||
import { Loader, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import { SubHeading } from '../../components/sub-heading';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { useExplorerMarketsQuery } from './__generated__/Markets';
|
||||
import { useScrollToLocation } from '../../hooks/scroll-to-location';
|
||||
import { useDocumentTitle } from '../../hooks/use-document-title';
|
||||
import EmptyList from '../../components/empty-list/empty-list';
|
||||
|
||||
const Markets = () => {
|
||||
const { data, loading } = useExplorerMarketsQuery();
|
||||
|
||||
useScrollToLocation();
|
||||
useDocumentTitle(['Markets']);
|
||||
|
||||
const m = data?.marketsConnection?.edges;
|
||||
|
||||
return (
|
||||
<section key="markets">
|
||||
<RouteTitle data-testid="markets-heading">{t('Markets')}</RouteTitle>
|
||||
|
||||
{m ? (
|
||||
m.map((e) => (
|
||||
<React.Fragment key={e.node.id}>
|
||||
<SubHeading data-testid="markets-header" id={e.node.id}>
|
||||
{e.node.tradableInstrument.instrument.name}
|
||||
</SubHeading>
|
||||
<SyntaxHighlighter data={e.node} />
|
||||
</React.Fragment>
|
||||
))
|
||||
) : loading ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<EmptyList
|
||||
heading={t('This chain has no markets')}
|
||||
label={t('0 markets')}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Markets;
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -71,7 +71,7 @@ export const PartyAccounts = ({ accounts }: PartyAccountsProps) => {
|
||||
/>
|
||||
</td>
|
||||
<td className="text-md">
|
||||
<AssetLink assetId={account.asset.id} />
|
||||
<AssetLink id={account.asset.id} />
|
||||
</td>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
@@ -68,7 +68,7 @@ const Party = () => {
|
||||
return (
|
||||
<section>
|
||||
<h1
|
||||
className="font-alpha uppercase font-xl mb-4 text-vega-dark-100 dark:text-vega-light-100"
|
||||
className="font-alpha uppercase font-xl mb-4 text-zinc-800 dark:text-zinc-200"
|
||||
data-testid="parties-header"
|
||||
>
|
||||
{t('Public key')}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { AssetPage, AssetsPage } from './assets';
|
||||
import Assets from './assets';
|
||||
import BlockPage from './blocks';
|
||||
import { Proposals } from './governance';
|
||||
import Governance from './governance';
|
||||
import Home from './home';
|
||||
import Markets from './markets';
|
||||
import OraclePage from './oracles';
|
||||
import Oracles from './oracles/home';
|
||||
import { Oracle } from './oracles/id';
|
||||
@@ -20,13 +21,8 @@ import flags from '../config/flags';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { Routes } from './route-names';
|
||||
import { NetworkParameters } from './network-parameters';
|
||||
import type { RouteObject } from 'react-router-dom';
|
||||
import { MarketPage, MarketsPage } from './markets';
|
||||
|
||||
export type Navigable = { path: string; name: string; text: string };
|
||||
type Route = RouteObject & Navigable;
|
||||
|
||||
const partiesRoutes: Route[] = flags.parties
|
||||
const partiesRoutes = flags.parties
|
||||
? [
|
||||
{
|
||||
path: Routes.PARTIES,
|
||||
@@ -47,27 +43,18 @@ const partiesRoutes: Route[] = flags.parties
|
||||
]
|
||||
: [];
|
||||
|
||||
const assetsRoutes: Route[] = flags.assets
|
||||
const assetsRoutes = flags.assets
|
||||
? [
|
||||
{
|
||||
path: Routes.ASSETS,
|
||||
text: t('Assets'),
|
||||
name: 'Assets',
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <AssetsPage />,
|
||||
},
|
||||
{
|
||||
path: ':assetId',
|
||||
element: <AssetPage />,
|
||||
},
|
||||
],
|
||||
element: <Assets />,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const genesisRoutes: Route[] = flags.genesis
|
||||
const genesisRoutes = flags.genesis
|
||||
? [
|
||||
{
|
||||
path: Routes.GENESIS,
|
||||
@@ -78,38 +65,29 @@ const genesisRoutes: Route[] = flags.genesis
|
||||
]
|
||||
: [];
|
||||
|
||||
const governanceRoutes: Route[] = flags.governance
|
||||
const governanceRoutes = flags.governance
|
||||
? [
|
||||
{
|
||||
path: Routes.GOVERNANCE,
|
||||
name: 'Governance proposals',
|
||||
text: t('Governance Proposals'),
|
||||
element: <Proposals />,
|
||||
element: <Governance />,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const marketsRoutes: Route[] = flags.markets
|
||||
const marketsRoutes = flags.markets
|
||||
? [
|
||||
{
|
||||
path: Routes.MARKETS,
|
||||
name: 'Markets',
|
||||
text: t('Markets'),
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <MarketsPage />,
|
||||
},
|
||||
{
|
||||
path: ':marketId',
|
||||
element: <MarketPage />,
|
||||
},
|
||||
],
|
||||
element: <Markets />,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const networkParametersRoutes: Route[] = flags.networkParameters
|
||||
const networkParametersRoutes = flags.networkParameters
|
||||
? [
|
||||
{
|
||||
path: Routes.NETWORK_PARAMETERS,
|
||||
@@ -119,7 +97,7 @@ const networkParametersRoutes: Route[] = flags.networkParameters
|
||||
},
|
||||
]
|
||||
: [];
|
||||
const validators: Route[] = flags.validators
|
||||
const validators = flags.validators
|
||||
? [
|
||||
{
|
||||
path: Routes.VALIDATORS,
|
||||
@@ -130,7 +108,7 @@ const validators: Route[] = flags.validators
|
||||
]
|
||||
: [];
|
||||
|
||||
const routerConfig: Route[] = [
|
||||
const routerConfig = [
|
||||
{
|
||||
path: Routes.HOME,
|
||||
name: 'Home',
|
||||
|
||||
+21
-62
@@ -15,7 +15,6 @@ type OneOf<T extends any[]> = T extends [infer Only]
|
||||
? OneOf<[XOR<A, B>, ...Rest]>
|
||||
: never;
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export interface paths {
|
||||
'/info': {
|
||||
/**
|
||||
@@ -41,6 +40,8 @@ export interface paths {
|
||||
};
|
||||
}
|
||||
|
||||
export type webhooks = Record<string, never>;
|
||||
|
||||
export interface components {
|
||||
schemas: {
|
||||
/**
|
||||
@@ -65,7 +66,7 @@ export interface components {
|
||||
| 'OPERATOR_LESS_THAN'
|
||||
| 'OPERATOR_LESS_THAN_OR_EQUAL';
|
||||
/**
|
||||
* The supported oracle sources
|
||||
* The supported Oracle sources
|
||||
* @description - ORACLE_SOURCE_UNSPECIFIED: The default value
|
||||
* - ORACLE_SOURCE_OPEN_ORACLE: Specifies that the payload will be base64 encoded JSON conforming to the Open Oracle standard
|
||||
* - ORACLE_SOURCE_JSON: Specifies that the payload will be base64 encoded JSON, but does not specify the shape of the data
|
||||
@@ -173,7 +174,7 @@ export interface components {
|
||||
readonly '@type'?: string;
|
||||
[key: string]: unknown | undefined;
|
||||
};
|
||||
/** Used to announce a node as a new pending validator */
|
||||
/** Used announce a node as a new pending validator */
|
||||
readonly v1AnnounceNode: {
|
||||
/** AvatarURL of the validator */
|
||||
readonly avatarUrl?: string;
|
||||
@@ -268,13 +269,13 @@ export interface components {
|
||||
readonly v1ETHAddress: {
|
||||
readonly address?: string;
|
||||
};
|
||||
/** A transaction to allow a validator to rotate their ethereum keys */
|
||||
/** A transaction to allow validator to rotate their ethereum keys */
|
||||
readonly v1EthereumKeyRotateSubmission: {
|
||||
/** Currently used public address */
|
||||
readonly currentAddress?: string;
|
||||
/** Signature that can be verified using the new ethereum address */
|
||||
readonly ethereumSignature?: components['schemas']['v1Signature'];
|
||||
/** The new address to rotate to */
|
||||
/** The new adress to rotate to */
|
||||
readonly newAddress?: string;
|
||||
/** Ethereum public key to use as a submitter to allow automatic signature generation */
|
||||
readonly submitterAddress?: string;
|
||||
@@ -308,9 +309,7 @@ export interface components {
|
||||
readonly version?: string;
|
||||
};
|
||||
readonly v1InputData: {
|
||||
/** A command used by a node operator to announce its node as a pending validator */
|
||||
readonly announceNode?: components['schemas']['v1AnnounceNode'];
|
||||
/** A command to submit a batch of order instructions to a market */
|
||||
readonly batchMarketInstructions?: components['schemas']['v1BatchMarketInstructions'];
|
||||
/**
|
||||
* Format: uint64
|
||||
@@ -324,35 +323,17 @@ export interface components {
|
||||
* `block_height` prevents replay attacks in conjunction with `nonce` (see above).
|
||||
*/
|
||||
readonly blockHeight?: string;
|
||||
/** A command to request cancelling a recurring transfer */
|
||||
readonly cancelTransfer?: components['schemas']['v1CancelTransfer'];
|
||||
/**
|
||||
* Command used by a validator to submit an event forwarded to the Vega network to provide information
|
||||
* on events happening on other networks, to be used by a foreign chain
|
||||
* to recognise a decision taken by the Vega network
|
||||
*/
|
||||
readonly chainEvent?: components['schemas']['v1ChainEvent'];
|
||||
/** Command to delegate tokens to a validator */
|
||||
readonly delegateSubmission?: components['schemas']['v1DelegateSubmission'];
|
||||
/** Command used by a validator to allow given validator to rotate their Ethereum keys */
|
||||
readonly ethereumKeyRotateSubmission?: components['schemas']['v1EthereumKeyRotateSubmission'];
|
||||
/** Command used by a validator to submit signatures to a smart contract */
|
||||
readonly issueSignatures?: components['schemas']['v1IssueSignatures'];
|
||||
/** Command used by a validator to allow given validator to rotate their Vega keys */
|
||||
readonly keyRotateSubmission?: components['schemas']['v1KeyRotateSubmission'];
|
||||
/** Command to request amending a liquidity commitment */
|
||||
readonly liquidityProvisionAmendment?: components['schemas']['v1LiquidityProvisionAmendment'];
|
||||
/** Command to request cancelling a liquidity commitment */
|
||||
readonly liquidityProvisionCancellation?: components['schemas']['v1LiquidityProvisionCancellation'];
|
||||
/** Command to submit a liquidity commitment */
|
||||
readonly liquidityProvisionSubmission?: components['schemas']['v1LiquidityProvisionSubmission'];
|
||||
/** Command used by a validator to submit a signature, to be used by a foreign chain to recognise a decision taken by the Vega network */
|
||||
readonly nodeSignature?: components['schemas']['v1NodeSignature'];
|
||||
/**
|
||||
* Validator commands
|
||||
* Command used by a validator when a node votes for validating that a given resource exists or is valid,
|
||||
* for example, an ERC20 deposit is valid and exists on ethereum
|
||||
*/
|
||||
/** Validator commands */
|
||||
readonly nodeVote?: components['schemas']['v1NodeVote'];
|
||||
/**
|
||||
* Format: uint64
|
||||
@@ -368,50 +349,30 @@ export interface components {
|
||||
* slightly differently, causing a different hash.
|
||||
*/
|
||||
readonly nonce?: string;
|
||||
/**
|
||||
* Oracles
|
||||
* Command to submit new oracle data from third party providers
|
||||
*/
|
||||
/** Oracles */
|
||||
readonly oracleDataSubmission?: components['schemas']['v1OracleDataSubmission'];
|
||||
/** Command to amend an order */
|
||||
readonly orderAmendment?: components['schemas']['v1OrderAmendment'];
|
||||
/**
|
||||
* User commands
|
||||
* Command to cancel an order
|
||||
*/
|
||||
readonly orderCancellation?: components['schemas']['v1OrderCancellation'];
|
||||
/** A command for submitting an order */
|
||||
/** User commands */
|
||||
readonly orderSubmission?: components['schemas']['v1OrderSubmission'];
|
||||
/** Command to submit a governance proposal */
|
||||
readonly proposalSubmission?: components['schemas']['v1ProposalSubmission'];
|
||||
/** Command used by a validator to propose a protocol upgrade */
|
||||
readonly protocolUpgradeProposal?: components['schemas']['v1ProtocolUpgradeProposal'];
|
||||
/** Command used by a validator to submit a floating point value */
|
||||
readonly stateVariableProposal?: components['schemas']['v1StateVariableProposal'];
|
||||
/** Command to submit a transfer */
|
||||
readonly transfer?: components['schemas']['commandsv1Transfer'];
|
||||
/** Command to remove tokens delegated to a validator */
|
||||
readonly undelegateSubmission?: components['schemas']['v1UndelegateSubmission'];
|
||||
/**
|
||||
* Command used by a validator to signal they are still online and validating blocks
|
||||
* or ready to validate blocks when they are still a pending validator
|
||||
*/
|
||||
readonly validatorHeartbeat?: components['schemas']['v1ValidatorHeartbeat'];
|
||||
/** Command to submit a vote on a governance proposal */
|
||||
readonly voteSubmission?: components['schemas']['v1VoteSubmission'];
|
||||
/** Command to submit a withdrawal */
|
||||
readonly withdrawSubmission?: components['schemas']['v1WithdrawSubmission'];
|
||||
};
|
||||
/** A transaction for a validator to submit signatures to a smart contract */
|
||||
readonly v1IssueSignatures: {
|
||||
/** The kind of signatures to generate, namely for whether a signer is being added or removed */
|
||||
readonly kind?: components['schemas']['v1NodeSignatureKind'];
|
||||
/** The ethereum address which will submit the signatures to the smart contract */
|
||||
/** The ethereum address which will submit the signatures to the smart-contract */
|
||||
readonly submitter?: string;
|
||||
/** The ID of the node that will be signed in or out of the smart contract */
|
||||
/** The ID of the node that will be signed in or out of the smartcontract */
|
||||
readonly validatorNodeId?: string;
|
||||
};
|
||||
/** A transaction to allow a validator to rotate their Vega keys */
|
||||
/** A transaction to allow validator to rotate their Vega keys */
|
||||
readonly v1KeyRotateSubmission: {
|
||||
/** Hash of currently used public key */
|
||||
readonly currentPubKeyHash?: string;
|
||||
@@ -535,8 +496,8 @@ export interface components {
|
||||
/** Specific details for a one off transfer */
|
||||
readonly v1OneOffTransfer: {
|
||||
/**
|
||||
* A unix timestamp in seconds. Time at which the
|
||||
* transfer should be delivered into the To account
|
||||
* A unix timestamp in second. Time at which the
|
||||
* transfer should be delivered in the to account
|
||||
* Format: int64
|
||||
*/
|
||||
readonly deliverOn?: string;
|
||||
@@ -551,7 +512,7 @@ export interface components {
|
||||
readonly payload?: string;
|
||||
/**
|
||||
* @description The source from which the data is coming from. Must be base64 encoded.
|
||||
* Oracle data is a type of external data source data.
|
||||
* Oracle data a type of external data source data.
|
||||
*/
|
||||
readonly source?: components['schemas']['OracleDataSubmissionOracleSource'];
|
||||
};
|
||||
@@ -642,7 +603,7 @@ export interface components {
|
||||
/** Type for the order, required field - See `Order.Type` */
|
||||
readonly type?: components['schemas']['vegaOrderType'];
|
||||
};
|
||||
/** @description PropertyKey describes the property key contained in data source data. */
|
||||
/** @description PropertyKey describes the property key contained in an data source data. */
|
||||
readonly v1PropertyKey: {
|
||||
/** @description name is the name of the property. */
|
||||
readonly name?: string;
|
||||
@@ -689,7 +650,6 @@ export interface components {
|
||||
/** Proposal configuration and the actual change that is meant to be executed when proposal is enacted */
|
||||
readonly terms?: components['schemas']['vegaProposalTerms'];
|
||||
};
|
||||
/** A transaction for a validator to suggest a protocol upgrade */
|
||||
readonly v1ProtocolUpgradeProposal: {
|
||||
/**
|
||||
* The block height at which to perform the upgrade
|
||||
@@ -748,7 +708,6 @@ export interface components {
|
||||
*/
|
||||
readonly pubKey?: components['schemas']['v1PubKey'];
|
||||
};
|
||||
/** A transaction for a validator to submit a floating point value */
|
||||
readonly v1StateVariableProposal: {
|
||||
/** The state value proposal details */
|
||||
readonly proposal?: components['schemas']['vegaStateValueProposal'];
|
||||
@@ -1010,7 +969,7 @@ export interface components {
|
||||
readonly sourceEthereumAddress?: string;
|
||||
/** The Vega network internal identifier of the asset */
|
||||
readonly vegaAssetId?: string;
|
||||
/** The updated withdrawal threshold */
|
||||
/** The updated withdraw threshold */
|
||||
readonly withdrawThreshold?: string;
|
||||
};
|
||||
/** An asset allow-listing for an ERC20 token */
|
||||
@@ -1105,7 +1064,7 @@ export interface components {
|
||||
/** The ethereum address of the old signer */
|
||||
readonly oldSigner?: string;
|
||||
};
|
||||
/** The threshold has been updated on the multisig control */
|
||||
/** The threshold have been updated on the multisigcontrol */
|
||||
readonly vegaERC20ThresholdSet: {
|
||||
/**
|
||||
* Format: int64
|
||||
@@ -1119,13 +1078,13 @@ export interface components {
|
||||
* Format: int64
|
||||
*/
|
||||
readonly newThreshold?: number;
|
||||
/** The nonce created by the Vega network */
|
||||
/** The nonce create by the vega network */
|
||||
readonly nonce?: string;
|
||||
};
|
||||
readonly vegaERC20Update: {
|
||||
/**
|
||||
* The lifetime limits deposit per address.
|
||||
* This will be interpreted against the asset decimals.
|
||||
* This is will be interpreted against the asset decimals.
|
||||
* note: this is a temporary measure that can be changed by governance
|
||||
*/
|
||||
readonly lifetimeLimit?: string;
|
||||
@@ -1278,7 +1237,7 @@ export interface components {
|
||||
* price levels over which automated liquidity provision orders will be deployed
|
||||
*/
|
||||
readonly lpPriceRange?: string;
|
||||
/** Optional new market metadata, tags */
|
||||
/** Optional new market meta data, tags */
|
||||
readonly metadata?: readonly string[];
|
||||
/**
|
||||
* Decimal places for order sizes, sets what size the smallest order / position on the market can be
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
const { join } = require('path');
|
||||
const { createGlobPatternsForDependencies } = require('@nrwl/next/tailwind');
|
||||
const theme = require('../../libs/tailwindcss-config/src/theme');
|
||||
const {
|
||||
VegaColours,
|
||||
} = require('../../libs/tailwindcss-config/src/vega-colours');
|
||||
const vegaCustomClasses = require('../../libs/tailwindcss-config/src/vega-custom-classes');
|
||||
|
||||
module.exports = {
|
||||
@@ -11,7 +14,12 @@ module.exports = {
|
||||
],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: theme,
|
||||
extend: {
|
||||
...theme,
|
||||
colors: {
|
||||
vega: VegaColours,
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [vegaCustomClasses],
|
||||
};
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { InMemoryCacheConfig } from '@apollo/client';
|
||||
import { NetworkLoader, useInitializeEnv } from '@vegaprotocol/environment';
|
||||
import { useRoutes } from 'react-router-dom';
|
||||
|
||||
import '../styles.scss';
|
||||
@@ -7,40 +5,14 @@ import { Navbar } from './components/navbar';
|
||||
|
||||
import { routerConfig } from './routes/router-config';
|
||||
|
||||
const cache: InMemoryCacheConfig = {
|
||||
typePolicies: {
|
||||
Market: {
|
||||
merge: true,
|
||||
},
|
||||
Party: {
|
||||
merge: true,
|
||||
},
|
||||
Query: {},
|
||||
Account: {
|
||||
keyFields: false,
|
||||
fields: {
|
||||
balanceFormatted: {},
|
||||
},
|
||||
},
|
||||
Node: {
|
||||
keyFields: false,
|
||||
},
|
||||
Instrument: {
|
||||
keyFields: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
const AppRouter = () => useRoutes(routerConfig);
|
||||
|
||||
export function App() {
|
||||
useInitializeEnv();
|
||||
return (
|
||||
<NetworkLoader cache={cache}>
|
||||
<div className="max-h-full min-h-full bg-white">
|
||||
<Navbar />
|
||||
<AppRouter />
|
||||
</div>
|
||||
</NetworkLoader>
|
||||
<div className="max-h-full min-h-full bg-white">
|
||||
<Navbar />
|
||||
<AppRouter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user