Compare commits
61
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5eb25db969 | ||
|
|
d32f27fcb1 | ||
|
|
d05cd6a2ed | ||
|
|
c003e5fa30 | ||
|
|
f62d3289ab | ||
|
|
a3d3d18c5c | ||
|
|
77ca101781 | ||
|
|
d238c37d0c | ||
|
|
79feb485f6 | ||
|
|
6aa5c3b6e3 | ||
|
|
82abc13fda | ||
|
|
933f07cddf | ||
|
|
c8067669c2 | ||
|
|
78f5a9c520 | ||
|
|
462066959d | ||
|
|
f12f5ab961 | ||
|
|
98b2460d8b | ||
|
|
67c182f3de | ||
|
|
68e0009ab0 | ||
|
|
ad74b12908 | ||
|
|
7fff33e493 | ||
|
|
e7cb2c8276 | ||
|
|
4adaeea40a | ||
|
|
d47d894147 | ||
|
|
515932d401 | ||
|
|
723ff2805d | ||
|
|
424cced4be | ||
|
|
b2c7ee82c5 | ||
|
|
4cc63be4f9 | ||
|
|
9fbb7a13e6 | ||
|
|
1874095222 | ||
|
|
e459e92127 | ||
|
|
4dd65b4923 | ||
|
|
5d792d2458 | ||
|
|
59b2f75b13 | ||
|
|
95775679ca | ||
|
|
4cffee29f8 | ||
|
|
bd70b0c233 | ||
|
|
145792f216 | ||
|
|
10add7c236 | ||
|
|
362a2031c7 | ||
|
|
5aaeb87059 | ||
|
|
d240dddab5 | ||
|
|
0af1d0ba82 | ||
|
|
665ab6693a | ||
|
|
902ee2696c | ||
|
|
3beeb0140c | ||
|
|
4b208e90bf | ||
|
|
ec837854cf | ||
|
|
8b94a75ba4 | ||
|
|
08c57b6759 | ||
|
|
bb2184498f | ||
|
|
8b07ce3024 | ||
|
|
8a110584dd | ||
|
|
b82615a3d8 | ||
|
|
f178b85846 | ||
|
|
0796f2b31f | ||
|
|
67be224138 | ||
|
|
51c426ef4b | ||
|
|
fa28d31ef3 | ||
|
|
201a586b05 |
@@ -1,3 +1,4 @@
|
||||
* text eol=lf
|
||||
*.png binary
|
||||
*.ico binary
|
||||
*.woff2 binary
|
||||
|
||||
@@ -12,7 +12,8 @@ import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-web
|
||||
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import { router } from './routes/router-config';
|
||||
import { useRouterConfig } from './routes/router-config';
|
||||
import { createBrowserRouter } from 'react-router-dom';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
@@ -38,7 +39,10 @@ function App() {
|
||||
}
|
||||
>
|
||||
<Suspense fallback={splashLoading}>
|
||||
<RouterProvider router={router} fallbackElement={splashLoading} />
|
||||
<RouterProvider
|
||||
router={createBrowserRouter(useRouterConfig())}
|
||||
fallbackElement={splashLoading}
|
||||
/>
|
||||
</Suspense>
|
||||
</NodeGuard>
|
||||
<NodeSwitcherDialog
|
||||
|
||||
@@ -24,7 +24,7 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
|
||||
const navigate = useNavigate();
|
||||
const ref = useRef<AgGridReact>(null);
|
||||
const showColumnsOnDesktop = () => {
|
||||
ref.current?.columnApi.setColumnsVisible(
|
||||
ref.current?.api.setColumnsVisible(
|
||||
['id', 'type', 'status'],
|
||||
window.innerWidth > BREAKPOINT_MD
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Routes } from '../../routes/route-names';
|
||||
import { NetworkSwitcher } from '@vegaprotocol/environment';
|
||||
import type { Navigable } from '../../routes/router-config';
|
||||
import { isNavigable } from '../../routes/router-config';
|
||||
import { routerConfig } from '../../routes/router-config';
|
||||
import { useRouterConfig } from '../../routes/router-config';
|
||||
import { useMemo } from 'react';
|
||||
import compact from 'lodash/compact';
|
||||
import { Search } from '../search';
|
||||
@@ -26,6 +26,7 @@ const routeToNavigationItem = (r: Navigable) => (
|
||||
);
|
||||
|
||||
export const Header = () => {
|
||||
const routerConfig = useRouterConfig();
|
||||
const isHome = Boolean(useMatch(Routes.HOME));
|
||||
const pages = routerConfig[0].children || [];
|
||||
const mainItems = compact(
|
||||
|
||||
@@ -60,7 +60,7 @@ const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="whitespace-nowrap">
|
||||
<span>
|
||||
{useName && <Icon size={4} name="cube" className="mr-2" />}
|
||||
<Link
|
||||
className="underline font-mono"
|
||||
|
||||
@@ -29,7 +29,7 @@ export const MarketsTable = ({ data }: MarketsTableProps) => {
|
||||
const gridRef = useRef<AgGridReact>(null);
|
||||
useLayoutEffect(() => {
|
||||
const showColumnsOnDesktop = () => {
|
||||
gridRef.current?.columnApi.setColumnsVisible(
|
||||
gridRef.current?.api.setColumnsVisible(
|
||||
['id', 'state', 'asset'],
|
||||
window.innerWidth > BREAKPOINT_MD
|
||||
);
|
||||
|
||||
@@ -60,7 +60,7 @@ const DeterministicOrderDetails = ({
|
||||
const o = data.orderByID;
|
||||
return (
|
||||
<div className={wrapperClasses}>
|
||||
<div className="mb-12 lg:mb-0">
|
||||
<div className="mb-0">
|
||||
<div className="relative block px-3 py-6 md:px-6 lg:-mr-7">
|
||||
<h2 className="text-3xl font-bold mb-4 display-5">
|
||||
<abbr title={tifFull[o.timeInForce]} className="bb-dotted mr-2">
|
||||
@@ -89,9 +89,9 @@ const DeterministicOrderDetails = ({
|
||||
<span>{t('Reference')}</span>: {o.reference}
|
||||
</p>
|
||||
) : null}
|
||||
<div className="grid md:grid-cols-5 gap-x-6 mt-4">
|
||||
<div className="mb-12 md:mb-0">
|
||||
<h2 className="text-2xl font-bold text-dark mb-4">
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-x-6 mt-4">
|
||||
<div className="mb-6 md:mb-0">
|
||||
<h2 className="text-2xl font-bold text-dark mb-0 md:mb-4">
|
||||
{t('Status')}
|
||||
</h2>
|
||||
<h5 className="text-lg font-medium text-gray-500 mb-0 capitalize">
|
||||
@@ -99,15 +99,17 @@ const DeterministicOrderDetails = ({
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div className="mb-12 md:mb-0">
|
||||
<h2 className="text-2xl font-bold text-dark mb-4">{t('Size')}</h2>
|
||||
<div className="mb-6 md:mb-0">
|
||||
<h2 className="text-2xl font-bold text-dark mb-0 md:mb-4">
|
||||
{t('Size')}
|
||||
</h2>
|
||||
<h5 className="text-lg font-medium text-gray-500 mb-0">
|
||||
<SizeInMarket size={o.size} marketId={o.market.id} />
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div className="">
|
||||
<h2 className="text-2xl font-bold text-dark mb-4">
|
||||
<div className="mb-6 md:mb-0">
|
||||
<h2 className="text-2xl font-bold text-dark mb-0 md:mb-4">
|
||||
{t('Version')}
|
||||
</h2>
|
||||
<h5 className="text-lg font-medium text-gray-500 mb-0">
|
||||
@@ -115,8 +117,8 @@ const DeterministicOrderDetails = ({
|
||||
</h5>
|
||||
</div>
|
||||
{o.type ? (
|
||||
<div className="">
|
||||
<h2 className="text-2xl font-bold text-dark mb-4">
|
||||
<div className="mb-6 md:mb-0">
|
||||
<h2 className="text-2xl font-bold text-dark mb-0 md:mb-4">
|
||||
{t('Type')}
|
||||
</h2>
|
||||
<h5 className="text-lg font-medium text-gray-500 mb-0">
|
||||
|
||||
@@ -44,11 +44,11 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
|
||||
const gridRef = useRef<AgGridReact>(null);
|
||||
useLayoutEffect(() => {
|
||||
const showColumnsOnDesktop = () => {
|
||||
gridRef.current?.columnApi.setColumnsVisible(
|
||||
gridRef.current?.api.setColumnsVisible(
|
||||
['voting', 'cDate', 'eDate', 'type'],
|
||||
window.innerWidth > BREAKPOINT_MD
|
||||
);
|
||||
gridRef.current?.columnApi.setColumnWidth(
|
||||
gridRef.current?.api.setColumnWidth(
|
||||
'actions',
|
||||
window.innerWidth > BREAKPOINT_MD ? 221 : 80
|
||||
);
|
||||
|
||||
@@ -30,12 +30,12 @@ export const Signature = ({ signature }: SignatureProps) => {
|
||||
|
||||
return (
|
||||
<div className="inline-flex border rounded signature-component relative pr-[20px]">
|
||||
<span
|
||||
<div
|
||||
className="bg-gray-100 px-2.5 py-0.5 text-xs text-gray-500 select-none cursor-default"
|
||||
title={`Version ${signature.version}`}
|
||||
title={`${signature.algo}`}
|
||||
>
|
||||
{signature.algo}
|
||||
</span>
|
||||
<span>v{signature.version}</span>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
isOpen
|
||||
|
||||
@@ -54,7 +54,7 @@ const Block = () => {
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
{blockData && (
|
||||
{blockData && 'result' in blockData && (
|
||||
<>
|
||||
<TableWithTbody className="mb-8">
|
||||
<TableRow modifier="bordered">
|
||||
|
||||
@@ -73,7 +73,7 @@ export const Layout = () => {
|
||||
<ProtocolUpgradeInProgressNotification />
|
||||
</div>
|
||||
<div className={fixedWidthClasses}>
|
||||
<main className="p-4">
|
||||
<main className="md:p-4">
|
||||
{!isHome && <BreadcrumbsContainer className="mb-4" />}
|
||||
<Outlet />
|
||||
</main>
|
||||
|
||||
@@ -18,7 +18,6 @@ import { t } from '@vegaprotocol/i18n';
|
||||
import { Routes } from './route-names';
|
||||
import { NetworkParameters } from './network-parameters';
|
||||
import type { Params, RouteObject } from 'react-router-dom';
|
||||
import { createBrowserRouter } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { MarketPage, MarketsPage } from './markets';
|
||||
import type { ReactNode } from 'react';
|
||||
@@ -29,7 +28,7 @@ import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
|
||||
import { remove0x } from '@vegaprotocol/utils';
|
||||
import { PartyAccountsByAsset } from './parties/id/accounts';
|
||||
import { Disclaimer } from './pages/disclaimer';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||
import RestrictedPage from './restricted';
|
||||
|
||||
export type Navigable = {
|
||||
@@ -60,311 +59,315 @@ type Route = RouteItem & {
|
||||
children?: RouteItem[];
|
||||
};
|
||||
|
||||
const partiesRoutes: Route[] = FLAGS.EXPLORER_PARTIES
|
||||
? [
|
||||
{
|
||||
path: Routes.PARTIES,
|
||||
element: <Party />,
|
||||
handle: {
|
||||
name: t('Parties'),
|
||||
text: t('Parties'),
|
||||
breadcrumb: () => <Link to={Routes.PARTIES}>{t('Parties')}</Link>,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Parties />,
|
||||
},
|
||||
{
|
||||
path: ':party',
|
||||
element: <Party />,
|
||||
export const useRouterConfig = () => {
|
||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <PartySingle />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<Link to={linkTo(Routes.PARTIES, params.party)}>
|
||||
{truncateMiddle(params.party as string)}
|
||||
</Link>
|
||||
),
|
||||
const partiesRoutes: Route[] = featureFlags.EXPLORER_PARTIES
|
||||
? [
|
||||
{
|
||||
path: Routes.PARTIES,
|
||||
element: <Party />,
|
||||
handle: {
|
||||
name: t('Parties'),
|
||||
text: t('Parties'),
|
||||
breadcrumb: () => <Link to={Routes.PARTIES}>{t('Parties')}</Link>,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Parties />,
|
||||
},
|
||||
{
|
||||
path: ':party',
|
||||
element: <Party />,
|
||||
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <PartySingle />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<Link to={linkTo(Routes.PARTIES, params.party)}>
|
||||
{truncateMiddle(params.party as string)}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'assets',
|
||||
element: <Party />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<Link to={linkTo(Routes.PARTIES, params.party)}>
|
||||
{truncateMiddle(params.party as string)}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <PartyAccountsByAsset />,
|
||||
handle: {
|
||||
breadcrumb: () => {
|
||||
return t('Assets');
|
||||
{
|
||||
path: 'assets',
|
||||
element: <Party />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<Link to={linkTo(Routes.PARTIES, params.party)}>
|
||||
{truncateMiddle(params.party as string)}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <PartyAccountsByAsset />,
|
||||
handle: {
|
||||
breadcrumb: () => {
|
||||
return t('Assets');
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const assetsRoutes: Route[] = featureFlags.EXPLORER_ASSETS
|
||||
? [
|
||||
{
|
||||
path: Routes.ASSETS,
|
||||
handle: {
|
||||
name: t('Assets'),
|
||||
text: t('Assets'),
|
||||
breadcrumb: () => <Link to={Routes.ASSETS}>{t('Assets')}</Link>,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <AssetsPage />,
|
||||
},
|
||||
{
|
||||
path: ':assetId',
|
||||
element: <AssetPage />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<AssetLink assetId={params.assetId as string} />
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const assetsRoutes: Route[] = FLAGS.EXPLORER_ASSETS
|
||||
? [
|
||||
{
|
||||
path: Routes.ASSETS,
|
||||
handle: {
|
||||
name: t('Assets'),
|
||||
text: t('Assets'),
|
||||
breadcrumb: () => <Link to={Routes.ASSETS}>{t('Assets')}</Link>,
|
||||
},
|
||||
],
|
||||
},
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <AssetsPage />,
|
||||
]
|
||||
: [];
|
||||
|
||||
const genesisRoutes: Route[] = featureFlags.EXPLORER_GENESIS
|
||||
? [
|
||||
{
|
||||
path: Routes.GENESIS,
|
||||
handle: {
|
||||
name: t('Genesis'),
|
||||
text: t('Genesis Parameters'),
|
||||
breadcrumb: () => (
|
||||
<Link to={Routes.GENESIS}>{t('Genesis Parameters')}</Link>
|
||||
),
|
||||
},
|
||||
element: <Genesis />,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const governanceRoutes: Route[] = featureFlags.EXPLORER_GOVERNANCE
|
||||
? [
|
||||
{
|
||||
path: Routes.GOVERNANCE,
|
||||
handle: {
|
||||
name: t('Governance proposals'),
|
||||
text: t('Governance Proposals'),
|
||||
breadcrumb: () => (
|
||||
<Link to={Routes.GOVERNANCE}>{t('Governance Proposals')}</Link>
|
||||
),
|
||||
},
|
||||
element: <Proposals />,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const marketsRoutes: Route[] = featureFlags.EXPLORER_MARKETS
|
||||
? [
|
||||
{
|
||||
path: Routes.MARKETS,
|
||||
handle: {
|
||||
name: t('Markets'),
|
||||
text: t('Markets'),
|
||||
breadcrumb: () => <Link to={Routes.MARKETS}>{t('Markets')}</Link>,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <MarketsPage />,
|
||||
},
|
||||
{
|
||||
path: ':marketId',
|
||||
element: <MarketPage />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<MarketLink id={params.marketId as string} />
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const networkParametersRoutes: Route[] =
|
||||
featureFlags.EXPLORER_NETWORK_PARAMETERS
|
||||
? [
|
||||
{
|
||||
path: ':assetId',
|
||||
element: <AssetPage />,
|
||||
path: Routes.NETWORK_PARAMETERS,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<AssetLink assetId={params.assetId as string} />
|
||||
name: t('NetworkParameters'),
|
||||
text: t('Network Parameters'),
|
||||
breadcrumb: () => (
|
||||
<Link to={Routes.NETWORK_PARAMETERS}>
|
||||
{t('Network Parameters')}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
element: <NetworkParameters />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: [];
|
||||
]
|
||||
: [];
|
||||
|
||||
const genesisRoutes: Route[] = FLAGS.EXPLORER_GENESIS
|
||||
? [
|
||||
{
|
||||
path: Routes.GENESIS,
|
||||
handle: {
|
||||
name: t('Genesis'),
|
||||
text: t('Genesis Parameters'),
|
||||
breadcrumb: () => (
|
||||
<Link to={Routes.GENESIS}>{t('Genesis Parameters')}</Link>
|
||||
),
|
||||
},
|
||||
element: <Genesis />,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const governanceRoutes: Route[] = FLAGS.EXPLORER_GOVERNANCE
|
||||
? [
|
||||
{
|
||||
path: Routes.GOVERNANCE,
|
||||
handle: {
|
||||
name: t('Governance proposals'),
|
||||
text: t('Governance Proposals'),
|
||||
breadcrumb: () => (
|
||||
<Link to={Routes.GOVERNANCE}>{t('Governance Proposals')}</Link>
|
||||
),
|
||||
},
|
||||
element: <Proposals />,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const marketsRoutes: Route[] = FLAGS.EXPLORER_MARKETS
|
||||
? [
|
||||
{
|
||||
path: Routes.MARKETS,
|
||||
handle: {
|
||||
name: t('Markets'),
|
||||
text: t('Markets'),
|
||||
breadcrumb: () => <Link to={Routes.MARKETS}>{t('Markets')}</Link>,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <MarketsPage />,
|
||||
const validators: Route[] = featureFlags.EXPLORER_VALIDATORS
|
||||
? [
|
||||
{
|
||||
path: Routes.VALIDATORS,
|
||||
handle: {
|
||||
name: t('Validators'),
|
||||
text: t('Validators'),
|
||||
breadcrumb: () => (
|
||||
<Link to={Routes.VALIDATORS}>{t('Validators')}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: ':marketId',
|
||||
element: <MarketPage />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<MarketLink id={params.marketId as string} />
|
||||
),
|
||||
element: <ValidatorsPage />,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const linkTo = (...segments: (string | undefined)[]) =>
|
||||
compact(segments).join('/');
|
||||
|
||||
const routerConfig: Route[] = [
|
||||
{
|
||||
path: Routes.HOME,
|
||||
element: <Layout />,
|
||||
handle: {
|
||||
name: t('Home'),
|
||||
text: t('Home'),
|
||||
breadcrumb: () => <Link to={Routes.HOME}>{t('Home')}</Link>,
|
||||
},
|
||||
errorElement: <ErrorBoundary />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Home />,
|
||||
},
|
||||
{
|
||||
path: Routes.TX,
|
||||
handle: {
|
||||
name: t('Txs'),
|
||||
text: t('Transactions'),
|
||||
breadcrumb: () => <Link to={Routes.TX}>{t('Transactions')}</Link>,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: ':txHash',
|
||||
element: <Tx />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<Link to={linkTo(Routes.TX, params.txHash)}>
|
||||
{truncateMiddle(remove0x(params.txHash as string))}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
index: true,
|
||||
element: <TxsList />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: Routes.BLOCKS,
|
||||
handle: {
|
||||
name: t('Blocks'),
|
||||
text: t('Blocks'),
|
||||
breadcrumb: () => <Link to={Routes.BLOCKS}>{t('Blocks')}</Link>,
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const networkParametersRoutes: Route[] = FLAGS.EXPLORER_NETWORK_PARAMETERS
|
||||
? [
|
||||
{
|
||||
path: Routes.NETWORK_PARAMETERS,
|
||||
handle: {
|
||||
name: t('NetworkParameters'),
|
||||
text: t('Network Parameters'),
|
||||
breadcrumb: () => (
|
||||
<Link to={Routes.NETWORK_PARAMETERS}>
|
||||
{t('Network Parameters')}
|
||||
</Link>
|
||||
),
|
||||
element: <BlockPage />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Blocks />,
|
||||
},
|
||||
{
|
||||
path: ':block',
|
||||
element: <Block />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<Link to={linkTo(Routes.BLOCKS, params.block)}>
|
||||
{params.block}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
element: <NetworkParameters />,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const validators: Route[] = FLAGS.EXPLORER_VALIDATORS
|
||||
? [
|
||||
{
|
||||
path: Routes.VALIDATORS,
|
||||
handle: {
|
||||
name: t('Validators'),
|
||||
text: t('Validators'),
|
||||
breadcrumb: () => (
|
||||
<Link to={Routes.VALIDATORS}>{t('Validators')}</Link>
|
||||
),
|
||||
{
|
||||
path: Routes.ORACLES,
|
||||
handle: {
|
||||
name: t('Oracles'),
|
||||
text: t('Oracles'),
|
||||
breadcrumb: () => <Link to={Routes.ORACLES}>{t('Oracles')}</Link>,
|
||||
},
|
||||
element: <OraclePage />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Oracles />,
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
element: <Oracle />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<Link to={linkTo(Routes.ORACLES, params.id)}>
|
||||
{truncateMiddle(params.id as string)}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
element: <ValidatorsPage />,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const linkTo = (...segments: (string | undefined)[]) =>
|
||||
compact(segments).join('/');
|
||||
|
||||
export const routerConfig: Route[] = [
|
||||
{
|
||||
path: Routes.HOME,
|
||||
element: <Layout />,
|
||||
handle: {
|
||||
name: t('Home'),
|
||||
text: t('Home'),
|
||||
breadcrumb: () => <Link to={Routes.HOME}>{t('Home')}</Link>,
|
||||
{
|
||||
path: Routes.DISCLAIMER,
|
||||
element: <Disclaimer />,
|
||||
handle: {
|
||||
name: t('Disclaimer'),
|
||||
text: t('Disclaimer'),
|
||||
breadcrumb: () => (
|
||||
<Link to={Routes.DISCLAIMER}>{t('Disclaimer')}</Link>
|
||||
),
|
||||
},
|
||||
},
|
||||
...partiesRoutes,
|
||||
...assetsRoutes,
|
||||
...genesisRoutes,
|
||||
...governanceRoutes,
|
||||
...marketsRoutes,
|
||||
...networkParametersRoutes,
|
||||
...validators,
|
||||
],
|
||||
},
|
||||
errorElement: <ErrorBoundary />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Home />,
|
||||
{
|
||||
path: Routes.RESTRICTED,
|
||||
element: <RestrictedPage />,
|
||||
handle: {
|
||||
name: t('Restricted'),
|
||||
text: t('Restricted'),
|
||||
},
|
||||
{
|
||||
path: Routes.TX,
|
||||
handle: {
|
||||
name: t('Txs'),
|
||||
text: t('Transactions'),
|
||||
breadcrumb: () => <Link to={Routes.TX}>{t('Transactions')}</Link>,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: ':txHash',
|
||||
element: <Tx />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<Link to={linkTo(Routes.TX, params.txHash)}>
|
||||
{truncateMiddle(remove0x(params.txHash as string))}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
index: true,
|
||||
element: <TxsList />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: Routes.BLOCKS,
|
||||
handle: {
|
||||
name: t('Blocks'),
|
||||
text: t('Blocks'),
|
||||
breadcrumb: () => <Link to={Routes.BLOCKS}>{t('Blocks')}</Link>,
|
||||
},
|
||||
element: <BlockPage />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Blocks />,
|
||||
},
|
||||
{
|
||||
path: ':block',
|
||||
element: <Block />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<Link to={linkTo(Routes.BLOCKS, params.block)}>
|
||||
{params.block}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: Routes.ORACLES,
|
||||
handle: {
|
||||
name: t('Oracles'),
|
||||
text: t('Oracles'),
|
||||
breadcrumb: () => <Link to={Routes.ORACLES}>{t('Oracles')}</Link>,
|
||||
},
|
||||
element: <OraclePage />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Oracles />,
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
element: <Oracle />,
|
||||
handle: {
|
||||
breadcrumb: (params: Params<string>) => (
|
||||
<Link to={linkTo(Routes.ORACLES, params.id)}>
|
||||
{truncateMiddle(params.id as string)}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: Routes.DISCLAIMER,
|
||||
element: <Disclaimer />,
|
||||
handle: {
|
||||
name: t('Disclaimer'),
|
||||
text: t('Disclaimer'),
|
||||
breadcrumb: () => (
|
||||
<Link to={Routes.DISCLAIMER}>{t('Disclaimer')}</Link>
|
||||
),
|
||||
},
|
||||
},
|
||||
...partiesRoutes,
|
||||
...assetsRoutes,
|
||||
...genesisRoutes,
|
||||
...governanceRoutes,
|
||||
...marketsRoutes,
|
||||
...networkParametersRoutes,
|
||||
...validators,
|
||||
],
|
||||
},
|
||||
{
|
||||
path: Routes.RESTRICTED,
|
||||
element: <RestrictedPage />,
|
||||
handle: {
|
||||
name: t('Restricted'),
|
||||
text: t('Restricted'),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const router = createBrowserRouter(routerConfig);
|
||||
];
|
||||
return routerConfig;
|
||||
};
|
||||
|
||||
@@ -32,7 +32,7 @@ CYPRESS_FAIRGROUND=false
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
NX_METAMASK_SNAPS=false
|
||||
NX_METAMASK_SNAPS=true
|
||||
NX_PRODUCT_PERPETUALS=true
|
||||
NX_UPDATE_MARKET_STATE=true
|
||||
NX_REFERRALS=true
|
||||
|
||||
@@ -23,7 +23,7 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
NX_METAMASK_SNAPS=false
|
||||
NX_METAMASK_SNAPS=true
|
||||
NX_PRODUCT_PERPETUALS=true
|
||||
NX_UPDATE_MARKET_STATE=true
|
||||
NX_REFERRALS=true
|
||||
|
||||
@@ -22,7 +22,7 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
NX_METAMASK_SNAPS=false
|
||||
NX_METAMASK_SNAPS=true
|
||||
NX_PRODUCT_PERPETUALS=true
|
||||
NX_UPDATE_MARKET_STATE=true
|
||||
NX_REFERRALS=true
|
||||
|
||||
@@ -21,7 +21,7 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.
|
||||
|
||||
# Cosmic elevator flags
|
||||
NX_SUCCESSOR_MARKETS=true
|
||||
NX_METAMASK_SNAPS=false
|
||||
NX_METAMASK_SNAPS=true
|
||||
NX_PRODUCT_PERPETUALS=true
|
||||
NX_UPDATE_MARKET_STATE=true
|
||||
NX_REFERRALS=true
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as Sentry from '@sentry/react';
|
||||
import { toBigNum } from '@vegaprotocol/utils';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet, useEagerConnect } from '@vegaprotocol/wallet';
|
||||
import { FLAGS, useEnvironment } from '@vegaprotocol/environment';
|
||||
import { useFeatureFlags, useEnvironment } from '@vegaprotocol/environment';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import React, { Suspense } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -15,21 +15,23 @@ import {
|
||||
} from './contexts/app-state/app-state-context';
|
||||
import { useContracts } from './contexts/contracts/contracts-context';
|
||||
import { useRefreshAssociatedBalances } from './hooks/use-refresh-associated-balances';
|
||||
import { Connectors } from './lib/vega-connectors';
|
||||
import { useConnectors } from './lib/vega-connectors';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
const useVegaWalletEagerConnect = () => {
|
||||
const vegaConnecting = useEagerConnect(Connectors);
|
||||
const connectors = useConnectors();
|
||||
const vegaConnecting = useEagerConnect(connectors);
|
||||
const { pubKey, connect } = useVegaWallet();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [query] = React.useState(searchParams.get('address'));
|
||||
if (query && !pubKey) {
|
||||
connect(Connectors['view']);
|
||||
connect(connectors.view);
|
||||
}
|
||||
return vegaConnecting;
|
||||
};
|
||||
|
||||
export const AppLoader = ({ children }: { children: React.ReactElement }) => {
|
||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||
const { t } = useTranslation();
|
||||
const { account } = useWeb3React();
|
||||
const { VEGA_URL } = useEnvironment();
|
||||
@@ -79,10 +81,16 @@ export const AppLoader = ({ children }: { children: React.ReactElement }) => {
|
||||
}
|
||||
};
|
||||
|
||||
if (!FLAGS.GOVERNANCE_NETWORK_DOWN) {
|
||||
if (!featureFlags.GOVERNANCE_NETWORK_DOWN) {
|
||||
run();
|
||||
}
|
||||
}, [token, appDispatch, staking, vesting]);
|
||||
}, [
|
||||
token,
|
||||
appDispatch,
|
||||
staking,
|
||||
vesting,
|
||||
featureFlags.GOVERNANCE_NETWORK_DOWN,
|
||||
]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (account && pubKey) {
|
||||
@@ -147,16 +155,16 @@ export const AppLoader = ({ children }: { children: React.ReactElement }) => {
|
||||
};
|
||||
|
||||
// Only begin polling if network limits flag is set, as this is a new API not yet on mainnet 7/3/22
|
||||
if (FLAGS.GOVERNANCE_NETWORK_LIMITS) {
|
||||
if (featureFlags.GOVERNANCE_NETWORK_LIMITS) {
|
||||
getNetworkLimits();
|
||||
}
|
||||
|
||||
return () => {
|
||||
stopPoll();
|
||||
};
|
||||
}, [appDispatch, VEGA_URL, t]);
|
||||
}, [appDispatch, VEGA_URL, t, featureFlags.GOVERNANCE_NETWORK_LIMITS]);
|
||||
|
||||
if (FLAGS.GOVERNANCE_NETWORK_DOWN) {
|
||||
if (featureFlags.GOVERNANCE_NETWORK_DOWN) {
|
||||
return (
|
||||
<Splash>
|
||||
<SplashError />
|
||||
|
||||
@@ -7,16 +7,16 @@ import {
|
||||
AppStateActionType,
|
||||
useAppState,
|
||||
} from '../../contexts/app-state/app-state-context';
|
||||
import { Connectors } from '../../lib/vega-connectors';
|
||||
import { useConnectors } from '../../lib/vega-connectors';
|
||||
import { RiskMessage } from './risk-message';
|
||||
|
||||
export const VegaWalletDialogs = () => {
|
||||
const { appState, appDispatch } = useAppState();
|
||||
|
||||
const connectors = useConnectors();
|
||||
return (
|
||||
<>
|
||||
<VegaConnectDialog
|
||||
connectors={Connectors}
|
||||
connectors={connectors}
|
||||
riskMessage={<RiskMessage />}
|
||||
/>
|
||||
|
||||
@@ -30,7 +30,7 @@ export const VegaWalletDialogs = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
<ViewAsDialog connector={Connectors.view} />
|
||||
<ViewAsDialog connector={connectors.view} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
JsonRpcConnector,
|
||||
ViewConnector,
|
||||
@@ -13,13 +14,17 @@ export const jsonRpc = new JsonRpcConnector();
|
||||
export const injected = new InjectedConnector();
|
||||
export const view = new ViewConnector(urlParams.get('address'));
|
||||
|
||||
export const snap = FLAGS.METAMASK_SNAPS
|
||||
? new SnapConnector(DEFAULT_SNAP_ID)
|
||||
: undefined;
|
||||
export const snap = new SnapConnector(DEFAULT_SNAP_ID);
|
||||
|
||||
export const Connectors = {
|
||||
injected,
|
||||
jsonRpc,
|
||||
view,
|
||||
snap,
|
||||
export const useConnectors = () => {
|
||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||
return useMemo(
|
||||
() => ({
|
||||
injected,
|
||||
jsonRpc,
|
||||
view,
|
||||
snap: featureFlags.METAMASK_SNAPS ? snap : undefined,
|
||||
}),
|
||||
[featureFlags.METAMASK_SNAPS]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useRefreshAfterEpoch } from '../../hooks/use-refresh-after-epoch';
|
||||
import { ProposalsListItem } from '../proposals/components/proposals-list-item';
|
||||
import { ProtocolUpgradeProposalsListItem } from '../proposals/components/protocol-upgrade-proposals-list-item/protocol-upgrade-proposals-list-item';
|
||||
import Routes from '../routes';
|
||||
import { ExternalLinks, FLAGS } from '@vegaprotocol/environment';
|
||||
import { ExternalLinks, useFeatureFlags } from '@vegaprotocol/environment';
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import { useNodesQuery } from '../staking/home/__generated__/Nodes';
|
||||
import { useProposalsQuery } from '../proposals/proposals/__generated__/Proposals';
|
||||
@@ -175,6 +175,7 @@ export const ValidatorDetailsLink = ({
|
||||
};
|
||||
|
||||
const GovernanceHome = ({ name }: RouteChildProps) => {
|
||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||
useDocumentTitle(name);
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
@@ -186,9 +187,9 @@ const GovernanceHome = ({ name }: RouteChildProps) => {
|
||||
fetchPolicy: 'network-only',
|
||||
errorPolicy: 'ignore',
|
||||
variables: {
|
||||
includeNewMarketProductFields: !!FLAGS.PRODUCT_PERPETUALS,
|
||||
includeUpdateMarketStates: !!FLAGS.UPDATE_MARKET_STATE,
|
||||
includeUpdateReferralPrograms: !!FLAGS.REFERRALS,
|
||||
includeNewMarketProductFields: !!featureFlags.PRODUCT_PERPETUALS,
|
||||
includeUpdateMarketStates: !!featureFlags.UPDATE_MARKET_STATE,
|
||||
includeUpdateReferralPrograms: !!featureFlags.REFERRALS,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+2
-3
@@ -19,7 +19,7 @@ import {
|
||||
mockWalletContext,
|
||||
createUserVoteQueryMock,
|
||||
} from '../../test-helpers/mocks';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { VoteState } from '../vote-details/use-user-vote';
|
||||
import { useNewTransferProposalDetails } from '@vegaprotocol/proposals';
|
||||
@@ -62,8 +62,7 @@ describe('Proposal header', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
it('Renders New market proposal', () => {
|
||||
const mockedFlags = jest.mocked(FLAGS);
|
||||
mockedFlags.SUCCESSOR_MARKETS = true;
|
||||
useFeatureFlags.setState({ flags: { SUCCESSOR_MARKETS: true } });
|
||||
renderComponent(
|
||||
generateProposal({
|
||||
rationale: {
|
||||
|
||||
+9
-7
@@ -12,7 +12,7 @@ import {
|
||||
useNewTransferProposalDetails,
|
||||
useSuccessorMarketProposalDetails,
|
||||
} from '@vegaprotocol/proposals';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||
import Routes from '../../../routes';
|
||||
import { Link } from 'react-router-dom';
|
||||
import type { VoteState } from '../vote-details/use-user-vote';
|
||||
@@ -28,6 +28,7 @@ export const ProposalHeader = ({
|
||||
isListItem?: boolean;
|
||||
voteState?: VoteState | null;
|
||||
}) => {
|
||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||
const { t } = useTranslation();
|
||||
const change = proposal?.terms.change;
|
||||
|
||||
@@ -54,13 +55,14 @@ export const ProposalHeader = ({
|
||||
switch (change?.__typename) {
|
||||
case 'NewMarket': {
|
||||
proposalType =
|
||||
FLAGS.PRODUCT_PERPETUALS && change?.instrument?.product?.__typename
|
||||
featureFlags.PRODUCT_PERPETUALS &&
|
||||
change?.instrument?.product?.__typename
|
||||
? `NewMarket${change?.instrument?.product?.__typename}`
|
||||
: 'NewMarket';
|
||||
fallbackTitle = t('NewMarketProposal');
|
||||
details = (
|
||||
<>
|
||||
{FLAGS.SUCCESSOR_MARKETS && (
|
||||
{featureFlags.SUCCESSOR_MARKETS && (
|
||||
<SuccessorCode proposalId={proposal?.id} />
|
||||
)}
|
||||
<span>
|
||||
@@ -82,13 +84,13 @@ export const ProposalHeader = ({
|
||||
}
|
||||
case 'UpdateMarketState': {
|
||||
proposalType =
|
||||
FLAGS.UPDATE_MARKET_STATE && change?.updateType
|
||||
featureFlags.UPDATE_MARKET_STATE && change?.updateType
|
||||
? t(change.updateType)
|
||||
: 'UpdateMarketState';
|
||||
fallbackTitle = t('UpdateMarketStateProposal');
|
||||
details = (
|
||||
<span>
|
||||
{FLAGS.UPDATE_MARKET_STATE &&
|
||||
{featureFlags.UPDATE_MARKET_STATE &&
|
||||
change?.market?.id &&
|
||||
change.updateType ? (
|
||||
<>
|
||||
@@ -177,14 +179,14 @@ export const ProposalHeader = ({
|
||||
case 'NewTransfer':
|
||||
proposalType = 'NewTransfer';
|
||||
fallbackTitle = t('NewTransferProposal');
|
||||
details = FLAGS.GOVERNANCE_TRANSFERS ? (
|
||||
details = featureFlags.GOVERNANCE_TRANSFERS ? (
|
||||
<NewTransferSummary proposalId={proposal?.id} />
|
||||
) : null;
|
||||
break;
|
||||
case 'CancelTransfer':
|
||||
proposalType = 'CancelTransfer';
|
||||
fallbackTitle = t('CancelTransferProposal');
|
||||
details = FLAGS.GOVERNANCE_TRANSFERS ? (
|
||||
details = featureFlags.GOVERNANCE_TRANSFERS ? (
|
||||
<CancelTransferSummary proposalId={proposal?.id} />
|
||||
) : null;
|
||||
break;
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
ProposalCancelTransferDetails,
|
||||
ProposalTransferDetails,
|
||||
} from '../proposal-transfer';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||
import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers';
|
||||
|
||||
export interface ProposalProps {
|
||||
@@ -53,6 +53,7 @@ export const Proposal = ({
|
||||
originalMarketProposalRestData,
|
||||
mostRecentlyEnactedAssociatedMarketProposal,
|
||||
}: ProposalProps) => {
|
||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||
const { t } = useTranslation();
|
||||
const { submit, Dialog, finalizedVote, transaction } = useVoteSubmit();
|
||||
const { voteState, voteDatetime } = useUserVote(proposal?.id, finalizedVote);
|
||||
@@ -132,7 +133,7 @@ export const Proposal = ({
|
||||
}
|
||||
|
||||
// Show governance transfer details only if the GOVERNANCE_TRANSFERS flag is on.
|
||||
const governanceTransferDetails = FLAGS.GOVERNANCE_TRANSFERS && (
|
||||
const governanceTransferDetails = featureFlags.GOVERNANCE_TRANSFERS && (
|
||||
<>
|
||||
{proposal.terms.change.__typename === 'NewTransfer' && (
|
||||
/** Governance New Transfer Details */
|
||||
|
||||
+2
-2
@@ -281,8 +281,8 @@ describe('VoteBreakdown', () => {
|
||||
});
|
||||
|
||||
it('Progress bar displays status - LP majority', () => {
|
||||
const yesVotesLP = 800;
|
||||
const noVotesLP = 200;
|
||||
const yesVotesLP = 0.8;
|
||||
const noVotesLP = 0.2;
|
||||
const expectedProgress = (yesVotesLP / (yesVotesLP + noVotesLP)) * 100; // 80%
|
||||
|
||||
renderComponent(
|
||||
|
||||
@@ -105,8 +105,6 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
|
||||
yesLPPercentage,
|
||||
yesTokens,
|
||||
noTokens,
|
||||
yesEquityLikeShareWeight,
|
||||
noEquityLikeShareWeight,
|
||||
totalEquityLikeShareWeight,
|
||||
requiredMajorityPercentage,
|
||||
requiredMajorityLPPercentage,
|
||||
@@ -135,6 +133,7 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
|
||||
.multipliedBy(100),
|
||||
new BigNumber(100)
|
||||
);
|
||||
|
||||
const willPass = willPassByTokenVote || willPassByLPVote;
|
||||
const updateMarketVotePassMethod = willPassByTokenVote
|
||||
? t('byTokenVote')
|
||||
@@ -202,50 +201,24 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
|
||||
<div className="flex items-center gap-1">
|
||||
<span>{t('liquidityProviderVotesFor')}:</span>
|
||||
<Tooltip
|
||||
description={formatNumber(
|
||||
yesEquityLikeShareWeight,
|
||||
defaultDP
|
||||
)}
|
||||
description={
|
||||
<span>{yesLPPercentage.toFixed(defaultDP)}%</span>
|
||||
}
|
||||
>
|
||||
<button>
|
||||
<CompactVotes number={yesEquityLikeShareWeight} />
|
||||
</button>
|
||||
<button>{yesLPPercentage.toFixed(1)}%</button>
|
||||
</Tooltip>
|
||||
<span>
|
||||
(
|
||||
<Tooltip
|
||||
description={
|
||||
<span>{yesLPPercentage.toFixed(defaultDP)}%</span>
|
||||
}
|
||||
>
|
||||
<button>{yesLPPercentage.toFixed(0)}%</button>
|
||||
</Tooltip>
|
||||
)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<span>{t('liquidityProviderVotesAgainst')}:</span>
|
||||
<Tooltip
|
||||
description={formatNumber(
|
||||
noEquityLikeShareWeight,
|
||||
defaultDP
|
||||
)}
|
||||
>
|
||||
<button>
|
||||
<CompactVotes number={noEquityLikeShareWeight} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<span>
|
||||
(
|
||||
<Tooltip
|
||||
description={
|
||||
<span>{noLPPercentage.toFixed(defaultDP)}%</span>
|
||||
}
|
||||
>
|
||||
<button>{noLPPercentage.toFixed(0)}%</button>
|
||||
<button>{noLPPercentage.toFixed(1)}%</button>
|
||||
</Tooltip>
|
||||
)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -282,13 +255,8 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
|
||||
defaultDP
|
||||
)}
|
||||
>
|
||||
<button>
|
||||
<CompactVotes number={totalEquityLikeShareWeight} />
|
||||
</button>
|
||||
<span>{totalEquityLikeShareWeight.toFixed(1)}%</span>
|
||||
</Tooltip>
|
||||
<span>
|
||||
({totalEquityLikeShareWeight.toFixed(defaultDP)}%)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -54,8 +54,8 @@ describe('use-vote-information', () => {
|
||||
it('returns all required vote information', () => {
|
||||
const yesVotes = 40;
|
||||
const noVotes = 60;
|
||||
const yesEquityLikeShareWeight = '30';
|
||||
const noEquityLikeShareWeight = '70';
|
||||
const yesEquityLikeShareWeight = '0.30';
|
||||
const noEquityLikeShareWeight = '0.70';
|
||||
// Note - giving a fixedTokenValue of 1 means a ratio of 1:1 votes to tokens, making sums easier :)
|
||||
const fixedTokenValue = 1000000000000000000;
|
||||
|
||||
@@ -195,10 +195,10 @@ describe('use-vote-information', () => {
|
||||
});
|
||||
|
||||
it('correctly shows whether an update market proposal will pass by token or LP vote - both failing', () => {
|
||||
const yesVotes = 20;
|
||||
const noVotes = 70;
|
||||
const yesEquityLikeShareWeight = '30';
|
||||
const noEquityLikeShareWeight = '60';
|
||||
const yesVotes = 0.2;
|
||||
const noVotes = 0.7;
|
||||
const yesEquityLikeShareWeight = '0.30';
|
||||
const noEquityLikeShareWeight = '0.60';
|
||||
const fixedTokenValue = 1000000000000000000;
|
||||
|
||||
const proposal = generateProposal({
|
||||
|
||||
@@ -61,7 +61,7 @@ export const useVoteInformation = ({
|
||||
const noEquityLikeShareWeight = !proposal?.votes.no
|
||||
.totalEquityLikeShareWeight
|
||||
? new BigNumber(0)
|
||||
: new BigNumber(proposal.votes.no.totalEquityLikeShareWeight);
|
||||
: new BigNumber(proposal.votes.no.totalEquityLikeShareWeight).times(100);
|
||||
|
||||
const yesTokens = new BigNumber(
|
||||
addDecimal(proposal?.votes.yes.totalTokens ?? 0, decimals)
|
||||
@@ -70,7 +70,7 @@ export const useVoteInformation = ({
|
||||
const yesEquityLikeShareWeight = !proposal?.votes.yes
|
||||
.totalEquityLikeShareWeight
|
||||
? new BigNumber(0)
|
||||
: new BigNumber(proposal.votes.yes.totalEquityLikeShareWeight);
|
||||
: new BigNumber(proposal.votes.yes.totalEquityLikeShareWeight).times(100);
|
||||
|
||||
const totalTokensVoted = yesTokens.plus(noTokens);
|
||||
|
||||
@@ -81,12 +81,7 @@ export const useVoteInformation = ({
|
||||
const yesPercentage = totalTokensVoted.isZero()
|
||||
? new BigNumber(0)
|
||||
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
|
||||
|
||||
const yesLPPercentage = totalEquityLikeShareWeight.isZero()
|
||||
? new BigNumber(0)
|
||||
: yesEquityLikeShareWeight
|
||||
.multipliedBy(100)
|
||||
.dividedBy(totalEquityLikeShareWeight);
|
||||
const yesLPPercentage = yesEquityLikeShareWeight;
|
||||
|
||||
const noPercentage = totalTokensVoted.isZero()
|
||||
? new BigNumber(0)
|
||||
@@ -103,9 +98,7 @@ export const useVoteInformation = ({
|
||||
);
|
||||
|
||||
const participationLPMet = requiredParticipationLP
|
||||
? totalEquityLikeShareWeight.isGreaterThan(
|
||||
totalSupply.multipliedBy(requiredParticipationLP)
|
||||
)
|
||||
? totalEquityLikeShareWeight.isGreaterThan(requiredParticipationLP)
|
||||
: false;
|
||||
|
||||
const majorityMet = yesPercentage.isGreaterThanOrEqualTo(
|
||||
@@ -120,9 +113,7 @@ export const useVoteInformation = ({
|
||||
.multipliedBy(100)
|
||||
.dividedBy(totalSupply);
|
||||
|
||||
const totalLPTokensPercentage = totalEquityLikeShareWeight
|
||||
.multipliedBy(100)
|
||||
.dividedBy(totalSupply);
|
||||
const totalLPTokensPercentage = totalEquityLikeShareWeight;
|
||||
|
||||
const willPassByTokenVote =
|
||||
participationMet &&
|
||||
|
||||
@@ -15,10 +15,11 @@ import {
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
import { useParentMarketIdQuery } from '@vegaprotocol/markets';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||
import { useSuccessorMarketProposalDetails } from '@vegaprotocol/proposals';
|
||||
|
||||
export const ProposalContainer = () => {
|
||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||
const [
|
||||
mostRecentlyEnactedAssociatedMarketProposal,
|
||||
setMostRecentlyEnactedAssociatedMarketProposal,
|
||||
@@ -59,9 +60,9 @@ export const ProposalContainer = () => {
|
||||
errorPolicy: 'ignore',
|
||||
variables: {
|
||||
proposalId: params.proposalId || '',
|
||||
includeNewMarketProductField: !!FLAGS.PRODUCT_PERPETUALS,
|
||||
includeUpdateMarketState: !!FLAGS.UPDATE_MARKET_STATE,
|
||||
includeUpdateReferralProgram: !!FLAGS.REFERRALS,
|
||||
includeNewMarketProductField: !!featureFlags.PRODUCT_PERPETUALS,
|
||||
includeUpdateMarketState: !!featureFlags.UPDATE_MARKET_STATE,
|
||||
includeUpdateReferralProgram: !!featureFlags.REFERRALS,
|
||||
},
|
||||
skip: !params.proposalId,
|
||||
});
|
||||
@@ -120,7 +121,7 @@ export const ProposalContainer = () => {
|
||||
variables: {
|
||||
marketId: marketData?.id || '',
|
||||
},
|
||||
skip: !FLAGS.SUCCESSOR_MARKETS || !isSuccessor || !marketData?.id,
|
||||
skip: !featureFlags.SUCCESSOR_MARKETS || !isSuccessor || !marketData?.id,
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -133,7 +134,7 @@ export const ProposalContainer = () => {
|
||||
variables: {
|
||||
marketId: parentMarketId?.market?.parentMarketID || '',
|
||||
skip:
|
||||
!FLAGS.SUCCESSOR_MARKETS ||
|
||||
!featureFlags.SUCCESSOR_MARKETS ||
|
||||
!isSuccessor ||
|
||||
!parentMarketId?.market?.parentMarketID,
|
||||
},
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from './__generated__/Proposals';
|
||||
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { useProtocolUpgradeProposalsQuery } from '@vegaprotocol/proposals';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||
|
||||
export function getNotRejectedProposals(data?: ProposalFieldsFragment[]) {
|
||||
return flow([
|
||||
@@ -43,15 +43,16 @@ export function getNotRejectedProtocolUpgradeProposals<
|
||||
}
|
||||
|
||||
export const ProposalsContainer = () => {
|
||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||
const { t } = useTranslation();
|
||||
const { data, loading, error } = useProposalsQuery({
|
||||
pollInterval: 5000,
|
||||
fetchPolicy: 'network-only',
|
||||
errorPolicy: 'ignore',
|
||||
variables: {
|
||||
includeNewMarketProductFields: !!FLAGS.PRODUCT_PERPETUALS,
|
||||
includeUpdateMarketStates: !!FLAGS.UPDATE_MARKET_STATE,
|
||||
includeUpdateReferralPrograms: !!FLAGS.REFERRALS,
|
||||
includeNewMarketProductFields: !!featureFlags.PRODUCT_PERPETUALS,
|
||||
includeUpdateMarketStates: !!featureFlags.UPDATE_MARKET_STATE,
|
||||
includeUpdateReferralPrograms: !!featureFlags.REFERRALS,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ export const ProtocolUpgradeProposalContainer = () => {
|
||||
time={
|
||||
pending && time ? (
|
||||
convertToCountdownString(time, '0:00:00:00')
|
||||
) : blockInfo?.result ? (
|
||||
) : blockInfo && 'result' in blockInfo && blockInfo?.result ? (
|
||||
<span title={blockInfo.result.block.header.time}>
|
||||
{formatDateWithLocalTimezone(
|
||||
new Date(blockInfo.result.block.header.time)
|
||||
|
||||
@@ -10,7 +10,7 @@ import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import flow from 'lodash/flow';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||
|
||||
const orderByDate = (arr: ProposalFieldsFragment[]) =>
|
||||
orderBy(
|
||||
@@ -33,15 +33,16 @@ export function getRejectedProposals(data?: ProposalFieldsFragment[] | null) {
|
||||
}
|
||||
|
||||
export const RejectedProposalsContainer = () => {
|
||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||
const { t } = useTranslation();
|
||||
const { data, loading, error } = useProposalsQuery({
|
||||
pollInterval: 5000,
|
||||
fetchPolicy: 'network-only',
|
||||
errorPolicy: 'ignore',
|
||||
variables: {
|
||||
includeNewMarketProductFields: !!FLAGS.PRODUCT_PERPETUALS,
|
||||
includeUpdateMarketStates: !!FLAGS.UPDATE_MARKET_STATE,
|
||||
includeUpdateReferralPrograms: !!FLAGS.REFERRALS,
|
||||
includeNewMarketProductFields: !!featureFlags.PRODUCT_PERPETUALS,
|
||||
includeUpdateMarketStates: !!featureFlags.UPDATE_MARKET_STATE,
|
||||
includeUpdateReferralPrograms: !!featureFlags.REFERRALS,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -116,7 +116,8 @@ export const generateYesVotes = (
|
||||
fixedTokenValue?: number,
|
||||
totalEquityLikeShareWeight?: string
|
||||
): Votes => {
|
||||
const votes = Array.from(Array(numberOfVotes)).map(() => {
|
||||
const votes = [];
|
||||
for (let i = 0; i < numberOfVotes; i++) {
|
||||
const vote: Vote = {
|
||||
__typename: 'Vote',
|
||||
value: Schema.VoteValue.VALUE_YES,
|
||||
@@ -152,8 +153,9 @@ export const generateYesVotes = (
|
||||
datetime: faker.date.past().toISOString(),
|
||||
};
|
||||
|
||||
return vote;
|
||||
});
|
||||
votes.push(vote);
|
||||
}
|
||||
|
||||
return {
|
||||
__typename: 'ProposalVoteSide',
|
||||
totalNumber: votes.length.toString(),
|
||||
@@ -172,7 +174,8 @@ export const generateNoVotes = (
|
||||
fixedTokenValue?: number,
|
||||
totalEquityLikeShareWeight?: string
|
||||
): Votes => {
|
||||
const votes = Array.from(Array(numberOfVotes)).map(() => {
|
||||
const votes = [];
|
||||
for (let i = 0; i < numberOfVotes; i++) {
|
||||
const vote: Vote = {
|
||||
__typename: 'Vote',
|
||||
value: Schema.VoteValue.VALUE_NO,
|
||||
@@ -207,8 +210,9 @@ export const generateNoVotes = (
|
||||
},
|
||||
datetime: faker.date.past().toISOString(),
|
||||
};
|
||||
return vote;
|
||||
});
|
||||
votes.push(vote);
|
||||
}
|
||||
|
||||
return {
|
||||
__typename: 'ProposalVoteSide',
|
||||
totalNumber: votes.length.toString(),
|
||||
|
||||
@@ -25,3 +25,4 @@ NX_ICEBERG_ORDERS=true
|
||||
# NX_PRODUCT_PERPETUALS
|
||||
NX_METAMASK_SNAPS=true
|
||||
NX_REFERRALS=true
|
||||
NX_TEAM_COMPETITION=true
|
||||
|
||||
@@ -24,7 +24,7 @@ NX_STOP_ORDERS=true
|
||||
NX_ICEBERG_ORDERS=true
|
||||
NX_METAMASK_SNAPS=true
|
||||
NX_REFERRALS=true
|
||||
NX_DISABLE_CLOSE_POSITION=true
|
||||
# NX_DISABLE_CLOSE_POSITION=false
|
||||
|
||||
NX_TENDERMINT_URL=https://be.vega.community
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
|
||||
|
||||
@@ -24,4 +24,7 @@ NX_STOP_ORDERS=true
|
||||
NX_ICEBERG_ORDERS=true
|
||||
# NX_PRODUCT_PERPETUALS
|
||||
NX_METAMASK_SNAPS=true
|
||||
NX_REFERRALS=true
|
||||
NX_REFERRALS=true
|
||||
|
||||
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
|
||||
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
|
||||
|
||||
@@ -28,3 +28,6 @@ NX_REFERRALS=true
|
||||
|
||||
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
|
||||
|
||||
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
|
||||
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
|
||||
|
||||
@@ -54,3 +54,5 @@ To run the UI automation tests with a mocked API, run:
|
||||
```bash
|
||||
yarn nx run trading-e2e:e2e
|
||||
```
|
||||
|
||||
To run tests with market sim please read [the readme](e2e/README.md).
|
||||
|
||||
@@ -188,12 +188,11 @@ const useNow = () => {
|
||||
return now;
|
||||
};
|
||||
|
||||
const useEvery = (marketId: string) => {
|
||||
const { data: marketTradingMode } = useMarketTradingMode(marketId);
|
||||
const useEvery = (marketId: string, skip: boolean) => {
|
||||
const { data: marketInfo } = useDataProvider({
|
||||
dataProvider: marketInfoProvider,
|
||||
variables: { marketId },
|
||||
skip: !marketTradingMode || isMarketInAuction(marketTradingMode),
|
||||
skip,
|
||||
});
|
||||
let every: number | undefined = undefined;
|
||||
const sourceType =
|
||||
@@ -211,8 +210,10 @@ const useEvery = (marketId: string) => {
|
||||
return every;
|
||||
};
|
||||
|
||||
const useStartTime = (marketId: string) => {
|
||||
const useStartTime = (marketId: string, skip: boolean) => {
|
||||
const { data: fundingPeriods } = useFundingPeriodsQuery({
|
||||
pollInterval: 5000,
|
||||
skip,
|
||||
variables: {
|
||||
marketId: marketId,
|
||||
pagination: { first: 1 },
|
||||
@@ -246,8 +247,10 @@ const useFormatCountdown = (
|
||||
|
||||
export const FundingCountdown = ({ marketId }: { marketId: string }) => {
|
||||
const now = useNow();
|
||||
const startTime = useStartTime(marketId);
|
||||
const every = useEvery(marketId);
|
||||
const { data: marketTradingMode } = useMarketTradingMode(marketId);
|
||||
const skip = !marketTradingMode || isMarketInAuction(marketTradingMode);
|
||||
const startTime = useStartTime(marketId, skip);
|
||||
const every = useEvery(marketId, skip);
|
||||
|
||||
return (
|
||||
<div data-testid="funding-countdown">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { addDecimalsFormatNumber, titlefy } from '@vegaprotocol/utils';
|
||||
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||
import { useThrottledDataProvider } from '@vegaprotocol/data-provider';
|
||||
@@ -9,8 +9,6 @@ import { TradeGrid } from './trade-grid';
|
||||
import { TradePanels } from './trade-panels';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { Links } from '../../lib/links';
|
||||
import { ViewType, useSidebar } from '../../components/sidebar';
|
||||
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
||||
import { useT, ns } from '../../lib/use-t';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
@@ -61,9 +59,6 @@ export const MarketPage = () => {
|
||||
const t = useT();
|
||||
const { marketId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const currentRouteId = useGetCurrentRouteId();
|
||||
const { setViews, getView } = useSidebar();
|
||||
const view = getView(currentRouteId);
|
||||
const { screenSize } = useScreenDimensions();
|
||||
const largeScreen = ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize);
|
||||
const update = useGlobalStore((store) => store.update);
|
||||
@@ -72,31 +67,11 @@ export const MarketPage = () => {
|
||||
const { data, loading } = useMarket(marketId);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.id && data.id !== lastMarketId && !closed) {
|
||||
if (data?.id && data.id !== lastMarketId) {
|
||||
update({ marketId: data.id });
|
||||
}
|
||||
}, [update, lastMarketId, data?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (largeScreen && view === undefined) {
|
||||
setViews(
|
||||
{ type: closed ? ViewType.Info : ViewType.Order },
|
||||
currentRouteId
|
||||
);
|
||||
}
|
||||
}, [setViews, view, currentRouteId, largeScreen]);
|
||||
|
||||
const pinnedAsset = data && getAsset(data);
|
||||
|
||||
const tradeView = useMemo(() => {
|
||||
if (pinnedAsset) {
|
||||
if (largeScreen) {
|
||||
return <TradeGrid market={data} pinnedAsset={pinnedAsset} />;
|
||||
}
|
||||
return <TradePanels market={data} pinnedAsset={pinnedAsset} />;
|
||||
}
|
||||
}, [largeScreen, data, pinnedAsset]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Splash>
|
||||
@@ -132,6 +107,8 @@ export const MarketPage = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const pinnedAsset = data && getAsset(data);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleUpdater
|
||||
@@ -139,7 +116,11 @@ export const MarketPage = () => {
|
||||
marketName={data?.tradableInstrument.instrument.name}
|
||||
decimalPlaces={data?.decimalPlaces}
|
||||
/>
|
||||
{tradeView}
|
||||
{largeScreen ? (
|
||||
<TradeGrid market={data} pinnedAsset={pinnedAsset} />
|
||||
) : (
|
||||
<TradePanels market={data} pinnedAsset={pinnedAsset} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { memo } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { memo, type ReactNode } from 'react';
|
||||
import { LayoutPriority } from 'allotment';
|
||||
import classNames from 'classnames';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import type { PinnedAsset } from '@vegaprotocol/accounts';
|
||||
import { OracleBanner, useMarket } from '@vegaprotocol/markets';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||
import { type PinnedAsset } from '@vegaprotocol/accounts';
|
||||
import { type Market } from '@vegaprotocol/markets';
|
||||
import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
ResizableGrid,
|
||||
@@ -13,30 +12,19 @@ import {
|
||||
usePaneLayout,
|
||||
} from '../../components/resizable-grid';
|
||||
import { TradingViews } from './trade-views';
|
||||
import {
|
||||
MarketSuccessorBanner,
|
||||
MarketSuccessorProposalBanner,
|
||||
MarketTerminationBanner,
|
||||
} from '../../components/market-banner';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { ErrorBoundary } from '../../components/error-boundary';
|
||||
import { MarketBanner } from '../../components/market-banner';
|
||||
|
||||
interface TradeGridProps {
|
||||
market: Market | null;
|
||||
pinnedAsset?: PinnedAsset;
|
||||
market: Market;
|
||||
pinnedAsset?: PinnedAsset | undefined;
|
||||
}
|
||||
|
||||
const MainGrid = memo(
|
||||
({
|
||||
marketId,
|
||||
pinnedAsset,
|
||||
}: {
|
||||
marketId: string;
|
||||
pinnedAsset?: PinnedAsset;
|
||||
}) => {
|
||||
({ market, pinnedAsset }: { market: Market; pinnedAsset?: PinnedAsset }) => {
|
||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||
const t = useT();
|
||||
const { data: market } = useMarket(marketId);
|
||||
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'top' });
|
||||
const [sizesMiddle, handleOnMiddleLayoutChange] = usePaneLayout({
|
||||
id: 'middle-1',
|
||||
@@ -61,20 +49,20 @@ const MainGrid = memo(
|
||||
id="chart"
|
||||
overflowHidden
|
||||
name={t('Chart')}
|
||||
menu={<TradingViews.candles.menu />}
|
||||
menu={<TradingViews.chart.menu />}
|
||||
>
|
||||
<ErrorBoundary feature="chart">
|
||||
<TradingViews.candles.component marketId={marketId} />
|
||||
<TradingViews.chart.component marketId={market.id} />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="depth" name={t('Depth')}>
|
||||
<ErrorBoundary feature="depth">
|
||||
<TradingViews.depth.component marketId={marketId} />
|
||||
<TradingViews.depth.component marketId={market.id} />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="liquidity" name={t('Liquidity')}>
|
||||
<ErrorBoundary feature="liquidity">
|
||||
<TradingViews.liquidity.component marketId={marketId} />
|
||||
<TradingViews.liquidity.component marketId={market.id} />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
{market &&
|
||||
@@ -82,17 +70,21 @@ const MainGrid = memo(
|
||||
'Perpetual' ? (
|
||||
<Tab id="funding-history" name={t('Funding history')}>
|
||||
<ErrorBoundary feature="funding-history">
|
||||
<TradingViews.funding.component marketId={marketId} />
|
||||
<TradingViews.funding.component marketId={market.id} />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
) : null}
|
||||
{market &&
|
||||
market.tradableInstrument.instrument.product.__typename ===
|
||||
'Perpetual' ? (
|
||||
<Tab id="funding-payments" name={t('Funding payments')}>
|
||||
<Tab
|
||||
id="funding-payments"
|
||||
name={t('Funding payments')}
|
||||
settings={<TradingViews.fundingPayments.settings />}
|
||||
>
|
||||
<ErrorBoundary feature="funding-payments">
|
||||
<TradingViews.fundingPayments.component
|
||||
marketId={marketId}
|
||||
marketId={market.id}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
@@ -108,12 +100,16 @@ const MainGrid = memo(
|
||||
<Tabs storageKey="console-trade-grid-main-right">
|
||||
<Tab id="orderbook" name={t('Orderbook')}>
|
||||
<ErrorBoundary feature="orderbook">
|
||||
<TradingViews.orderbook.component marketId={marketId} />
|
||||
<TradingViews.orderbook.component marketId={market.id} />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="trades" name={t('Trades')}>
|
||||
<Tab
|
||||
id="trades"
|
||||
name={t('Trades')}
|
||||
settings={<TradingViews.trades.settings />}
|
||||
>
|
||||
<ErrorBoundary feature="trades">
|
||||
<TradingViews.trades.component marketId={marketId} />
|
||||
<TradingViews.trades.component marketId={market.id} />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
@@ -132,6 +128,7 @@ const MainGrid = memo(
|
||||
id="positions"
|
||||
name={t('Positions')}
|
||||
menu={<TradingViews.positions.menu />}
|
||||
settings={<TradingViews.positions.settings />}
|
||||
>
|
||||
<ErrorBoundary feature="positions">
|
||||
<TradingViews.positions.component />
|
||||
@@ -141,17 +138,26 @@ const MainGrid = memo(
|
||||
id="open-orders"
|
||||
name={t('Open')}
|
||||
menu={<TradingViews.activeOrders.menu />}
|
||||
settings={<TradingViews.activeOrders.settings />}
|
||||
>
|
||||
<ErrorBoundary feature="activeOrders">
|
||||
<TradingViews.activeOrders.component />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="closed-orders" name={t('Closed')}>
|
||||
<Tab
|
||||
id="closed-orders"
|
||||
name={t('Closed')}
|
||||
settings={<TradingViews.closedOrders.settings />}
|
||||
>
|
||||
<ErrorBoundary feature="closedOrders">
|
||||
<TradingViews.closedOrders.component />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="rejected-orders" name={t('Rejected')}>
|
||||
<Tab
|
||||
id="rejected-orders"
|
||||
name={t('Rejected')}
|
||||
settings={<TradingViews.rejectedOrders.settings />}
|
||||
>
|
||||
<ErrorBoundary feature="rejectedOrders">
|
||||
<TradingViews.rejectedOrders.component />
|
||||
</ErrorBoundary>
|
||||
@@ -160,25 +166,35 @@ const MainGrid = memo(
|
||||
id="orders"
|
||||
name={t('All')}
|
||||
menu={<TradingViews.orders.menu />}
|
||||
settings={<TradingViews.orders.settings />}
|
||||
>
|
||||
<ErrorBoundary feature="orders">
|
||||
<TradingViews.orders.component />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
{FLAGS.STOP_ORDERS ? (
|
||||
<Tab id="stop-orders" name={t('Stop orders')}>
|
||||
{featureFlags.STOP_ORDERS ? (
|
||||
<Tab
|
||||
id="stop-orders"
|
||||
name={t('Stop orders')}
|
||||
settings={<TradingViews.stopOrders.settings />}
|
||||
>
|
||||
<ErrorBoundary feature="stop-orders">
|
||||
<TradingViews.stopOrders.component />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
) : null}
|
||||
<Tab id="fills" name={t('Fills')}>
|
||||
<Tab
|
||||
id="fills"
|
||||
name={t('Fills')}
|
||||
settings={<TradingViews.fills.settings />}
|
||||
>
|
||||
<TradingViews.fills.component />
|
||||
</Tab>
|
||||
<Tab
|
||||
id="accounts"
|
||||
name={t('Collateral')}
|
||||
menu={<TradingViews.collateral.menu />}
|
||||
settings={<TradingViews.collateral.settings />}
|
||||
>
|
||||
<ErrorBoundary feature="collateral">
|
||||
<TradingViews.collateral.component
|
||||
@@ -204,17 +220,10 @@ export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
|
||||
return (
|
||||
<div className={wrapperClasses}>
|
||||
<div>
|
||||
{FLAGS.SUCCESSOR_MARKETS && (
|
||||
<>
|
||||
<MarketSuccessorBanner market={market} />
|
||||
<MarketSuccessorProposalBanner marketId={market?.id} />
|
||||
</>
|
||||
)}
|
||||
<MarketTerminationBanner market={market} />
|
||||
<OracleBanner marketId={market?.id || ''} />
|
||||
<MarketBanner market={market} />
|
||||
</div>
|
||||
<div className="min-h-0 p-0.5">
|
||||
<MainGrid marketId={market?.id || ''} pinnedAsset={pinnedAsset} />
|
||||
<MainGrid market={market} pinnedAsset={pinnedAsset} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
import { type PinnedAsset } from '@vegaprotocol/accounts';
|
||||
import { type Market } from '@vegaprotocol/markets';
|
||||
import { OracleBanner } from '@vegaprotocol/markets';
|
||||
// TODO: handle oracle banner
|
||||
// import { OracleBanner } from '@vegaprotocol/markets';
|
||||
import { useState } from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import classNames from 'classnames';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import {
|
||||
MarketSuccessorBanner,
|
||||
MarketSuccessorProposalBanner,
|
||||
MarketTerminationBanner,
|
||||
} from '../../components/market-banner';
|
||||
Popover,
|
||||
Splash,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { MarketBanner } from '../../components/market-banner';
|
||||
import { ErrorBoundary } from '../../components/error-boundary';
|
||||
import { type TradingView } from './trade-views';
|
||||
import { TradingViews } from './trade-views';
|
||||
|
||||
interface TradePanelsProps {
|
||||
market: Market | null;
|
||||
market: Market;
|
||||
pinnedAsset?: PinnedAsset;
|
||||
}
|
||||
|
||||
export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
||||
const [view, setView] = useState<TradingView>('candles');
|
||||
const [view, setView] = useState<TradingView>('chart');
|
||||
const viewCfg = TradingViews[view];
|
||||
|
||||
const renderView = () => {
|
||||
const Component = TradingViews[view].component;
|
||||
@@ -43,35 +45,36 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
||||
};
|
||||
|
||||
const renderMenu = () => {
|
||||
const viewCfg = TradingViews[view];
|
||||
|
||||
if ('menu' in viewCfg) {
|
||||
const Menu = viewCfg.menu;
|
||||
|
||||
if ('menu' in viewCfg || 'settings' in viewCfg) {
|
||||
return (
|
||||
<div className="flex gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default">
|
||||
<Menu />
|
||||
<div className="flex items-center justify-end gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default">
|
||||
{'menu' in viewCfg ? <viewCfg.menu /> : null}
|
||||
{'settings' in viewCfg ? (
|
||||
<Popover
|
||||
align="end"
|
||||
trigger={
|
||||
<span className="ml-1 flex items-center justify-center h-6 w-6">
|
||||
<VegaIcon name={VegaIconNames.COG} size={16} />
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<div className="p-4 flex justify-end">
|
||||
<viewCfg.settings />
|
||||
</div>
|
||||
</Popover>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full grid grid-rows-[min-content_min-content_1fr_min-content]">
|
||||
<div>
|
||||
{FLAGS.SUCCESSOR_MARKETS && (
|
||||
<>
|
||||
<MarketSuccessorBanner market={market} />
|
||||
<MarketSuccessorProposalBanner marketId={market?.id} />
|
||||
</>
|
||||
)}
|
||||
<MarketTerminationBanner market={market} />
|
||||
<OracleBanner marketId={market?.id || ''} />
|
||||
<MarketBanner market={market} />
|
||||
</div>
|
||||
<div>{renderMenu()}</div>
|
||||
<div className="h-full">
|
||||
<div className="h-full relative">
|
||||
<AutoSizer>
|
||||
{({ width, height }) => (
|
||||
<div style={{ width, height }} className="overflow-auto">
|
||||
@@ -109,7 +112,9 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
||||
key={key}
|
||||
view={key}
|
||||
isActive={isActive}
|
||||
onClick={() => setView(key)}
|
||||
onClick={() => {
|
||||
setView(key);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -148,7 +153,7 @@ const useViewLabel = (view: TradingView) => {
|
||||
const t = useT();
|
||||
|
||||
const labels = {
|
||||
candles: t('Candles'),
|
||||
chart: t('Chart'),
|
||||
depth: t('Depth'),
|
||||
liquidity: t('Liquidity'),
|
||||
funding: t('Funding'),
|
||||
|
||||
@@ -1,28 +1,46 @@
|
||||
import { DepthChartContainer } from '@vegaprotocol/market-depth';
|
||||
import {
|
||||
CandlesChartContainer,
|
||||
CandlesMenu,
|
||||
} from '@vegaprotocol/candles-chart';
|
||||
import { Filter, OpenOrdersMenu } from '@vegaprotocol/orders';
|
||||
import { TradesContainer } from '../../components/trades-container';
|
||||
import {
|
||||
TradesContainer,
|
||||
TradesSettings,
|
||||
} from '../../components/trades-container';
|
||||
import { OrderbookContainer } from '../../components/orderbook-container';
|
||||
import { FillsContainer } from '../../components/fills-container';
|
||||
import { PositionsContainer } from '../../components/positions-container';
|
||||
import { AccountsContainer } from '../../components/accounts-container';
|
||||
import {
|
||||
FillsContainer,
|
||||
FillsSettings,
|
||||
} from '../../components/fills-container';
|
||||
import {
|
||||
PositionsContainer,
|
||||
PositionsSettings,
|
||||
} from '../../components/positions-container';
|
||||
import {
|
||||
AccountsContainer,
|
||||
AccountsSettings,
|
||||
} from '../../components/accounts-container';
|
||||
import { LiquidityContainer } from '../../components/liquidity-container';
|
||||
import { FundingContainer } from '../../components/funding-container';
|
||||
import { FundingPaymentsContainer } from '../../components/funding-payments-container';
|
||||
import { OrdersContainer } from '../../components/orders-container';
|
||||
import { StopOrdersContainer } from '../../components/stop-orders-container';
|
||||
import {
|
||||
FundingPaymentsContainer,
|
||||
FundingPaymentsSettings,
|
||||
} from '../../components/funding-payments-container';
|
||||
import {
|
||||
OrdersContainer,
|
||||
OrdersSettings,
|
||||
} from '../../components/orders-container';
|
||||
import {
|
||||
StopOrdersContainer,
|
||||
StopOrdersSettings,
|
||||
} from '../../components/stop-orders-container';
|
||||
import { AccountsMenu } from '../../components/accounts-menu';
|
||||
import { PositionsMenu } from '../../components/positions-menu';
|
||||
import { ChartContainer, ChartMenu } from '../../components/chart-container';
|
||||
|
||||
export type TradingView = keyof typeof TradingViews;
|
||||
|
||||
export const TradingViews = {
|
||||
candles: {
|
||||
component: CandlesChartContainer,
|
||||
menu: CandlesMenu,
|
||||
chart: {
|
||||
component: ChartContainer,
|
||||
menu: ChartMenu,
|
||||
},
|
||||
depth: {
|
||||
component: DepthChartContainer,
|
||||
@@ -35,37 +53,46 @@ export const TradingViews = {
|
||||
},
|
||||
fundingPayments: {
|
||||
component: FundingPaymentsContainer,
|
||||
settings: FundingPaymentsSettings,
|
||||
},
|
||||
orderbook: {
|
||||
component: OrderbookContainer,
|
||||
},
|
||||
trades: {
|
||||
component: TradesContainer,
|
||||
settings: TradesSettings,
|
||||
},
|
||||
positions: {
|
||||
component: PositionsContainer,
|
||||
menu: PositionsMenu,
|
||||
settings: PositionsSettings,
|
||||
},
|
||||
activeOrders: {
|
||||
component: () => <OrdersContainer filter={Filter.Open} />,
|
||||
menu: OpenOrdersMenu,
|
||||
settings: () => <OrdersSettings filter={Filter.Open} />,
|
||||
},
|
||||
closedOrders: {
|
||||
component: () => <OrdersContainer filter={Filter.Closed} />,
|
||||
settings: () => <OrdersSettings filter={Filter.Closed} />,
|
||||
},
|
||||
rejectedOrders: {
|
||||
component: () => <OrdersContainer filter={Filter.Rejected} />,
|
||||
settings: () => <OrdersSettings filter={Filter.Rejected} />,
|
||||
},
|
||||
orders: {
|
||||
component: OrdersContainer,
|
||||
menu: OpenOrdersMenu,
|
||||
settings: OrdersSettings,
|
||||
},
|
||||
stopOrders: {
|
||||
component: StopOrdersContainer,
|
||||
settings: StopOrdersSettings,
|
||||
},
|
||||
collateral: {
|
||||
component: AccountsContainer,
|
||||
menu: AccountsMenu,
|
||||
settings: AccountsSettings,
|
||||
},
|
||||
fills: { component: FillsContainer },
|
||||
fills: { component: FillsContainer, settings: FillsSettings },
|
||||
} as const;
|
||||
|
||||
@@ -42,7 +42,7 @@ export const createDataGridSlice: StateCreator<DataGridSlice> = (set) => ({
|
||||
},
|
||||
});
|
||||
|
||||
const useMarketsStore = create<DataGridSlice>()(
|
||||
export const useMarketsStore = create<DataGridSlice>()(
|
||||
persist(createDataGridSlice, {
|
||||
name: 'vega_market_list_store',
|
||||
})
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from '@vegaprotocol/environment';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { ErrorBoundary } from '../../components/error-boundary';
|
||||
import { MarketsSettings } from './markets-settings';
|
||||
|
||||
export const MarketsPage = () => {
|
||||
const t = useT();
|
||||
@@ -34,7 +35,11 @@ export const MarketsPage = () => {
|
||||
<div className="h-full pt-0.5 pb-3 px-1.5">
|
||||
<div className="h-full my-1 border rounded-sm border-default">
|
||||
<Tabs storageKey="console-markets">
|
||||
<Tab id="open-markets" name={t('Open markets')}>
|
||||
<Tab
|
||||
id="open-markets"
|
||||
name={t('Open markets')}
|
||||
settings={<MarketsSettings />}
|
||||
>
|
||||
<ErrorBoundary feature="markets-open">
|
||||
<OpenMarkets />
|
||||
</ErrorBoundary>
|
||||
@@ -42,6 +47,7 @@ export const MarketsPage = () => {
|
||||
<Tab
|
||||
id="proposed-markets"
|
||||
name={t('Proposed markets')}
|
||||
settings={<MarketsSettings />}
|
||||
menu={
|
||||
<TradingAnchorButton
|
||||
size="extra-small"
|
||||
@@ -56,7 +62,11 @@ export const MarketsPage = () => {
|
||||
<Proposed />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="closed-markets" name={t('Closed markets')}>
|
||||
<Tab
|
||||
id="closed-markets"
|
||||
name={t('Closed markets')}
|
||||
settings={<MarketsSettings />}
|
||||
>
|
||||
<ErrorBoundary feature="markets-closed">
|
||||
<Closed />
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { GridSettings } from '../../components/grid-settings/grid-settings';
|
||||
import { useMarketsStore } from './market-list-table';
|
||||
|
||||
export const MarketsSettings = () => (
|
||||
<GridSettings
|
||||
updateGridStore={useMarketsStore((store) => store.updateGridStore)}
|
||||
/>
|
||||
);
|
||||
@@ -4,9 +4,26 @@ import {
|
||||
SidebarButton,
|
||||
SidebarDivider,
|
||||
ViewType,
|
||||
useSidebar,
|
||||
} from '../../components/sidebar';
|
||||
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const ViewInitializer = () => {
|
||||
const currentRouteId = useGetCurrentRouteId();
|
||||
const { setViews, getView } = useSidebar();
|
||||
const view = getView(currentRouteId);
|
||||
const { screenSize } = useScreenDimensions();
|
||||
const largeScreen = ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize);
|
||||
useEffect(() => {
|
||||
if (largeScreen && view === undefined) {
|
||||
setViews({ type: ViewType.Order }, currentRouteId);
|
||||
}
|
||||
}, [setViews, view, currentRouteId, largeScreen]);
|
||||
return null;
|
||||
};
|
||||
|
||||
export const MarketsSidebar = () => {
|
||||
const t = useT();
|
||||
@@ -37,6 +54,7 @@ export const MarketsSidebar = () => {
|
||||
path=":marketId"
|
||||
element={
|
||||
<>
|
||||
<ViewInitializer />
|
||||
<SidebarDivider />
|
||||
<SidebarButton
|
||||
view={ViewType.Order}
|
||||
|
||||
@@ -5,14 +5,29 @@ import { titlefy } from '@vegaprotocol/utils';
|
||||
import { useIncompleteWithdrawals } from '@vegaprotocol/withdraws';
|
||||
import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit';
|
||||
import { usePageTitleStore } from '../../stores';
|
||||
import { AccountsContainer } from '../../components/accounts-container';
|
||||
import {
|
||||
AccountsContainer,
|
||||
AccountsSettings,
|
||||
} from '../../components/accounts-container';
|
||||
import { DepositsContainer } from '../../components/deposits-container';
|
||||
import { FillsContainer } from '../../components/fills-container';
|
||||
import { FundingPaymentsContainer } from '../../components/funding-payments-container';
|
||||
import { PositionsContainer } from '../../components/positions-container';
|
||||
import {
|
||||
FillsContainer,
|
||||
FillsSettings,
|
||||
} from '../../components/fills-container';
|
||||
import {
|
||||
FundingPaymentsContainer,
|
||||
FundingPaymentsSettings,
|
||||
} from '../../components/funding-payments-container';
|
||||
import {
|
||||
PositionsContainer,
|
||||
PositionsSettings,
|
||||
} from '../../components/positions-container';
|
||||
import { PositionsMenu } from '../../components/positions-menu';
|
||||
import { WithdrawalsContainer } from '../../components/withdrawals-container';
|
||||
import { OrdersContainer } from '../../components/orders-container';
|
||||
import {
|
||||
OrdersContainer,
|
||||
OrdersSettings,
|
||||
} from '../../components/orders-container';
|
||||
import { LedgerContainer } from '../../components/ledger-container';
|
||||
import {
|
||||
ResizableGrid,
|
||||
@@ -39,11 +54,21 @@ const WithdrawalsIndicator = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Portfolio = () => {
|
||||
const t = useT();
|
||||
const SidebarViewInitializer = () => {
|
||||
const currentRouteId = useGetCurrentRouteId();
|
||||
const { getView, setViews } = useSidebar();
|
||||
const view = getView(currentRouteId);
|
||||
// Make transfer sidebar open by default
|
||||
useEffect(() => {
|
||||
if (view === undefined) {
|
||||
setViews({ type: ViewType.Transfer }, currentRouteId);
|
||||
}
|
||||
}, [view, setViews, currentRouteId]);
|
||||
return null;
|
||||
};
|
||||
|
||||
export const Portfolio = () => {
|
||||
const t = useT();
|
||||
|
||||
const { updateTitle } = usePageTitleStore((store) => ({
|
||||
updateTitle: store.updateTitle,
|
||||
@@ -53,17 +78,11 @@ export const Portfolio = () => {
|
||||
updateTitle(titlefy([t('Portfolio')]));
|
||||
}, [updateTitle, t]);
|
||||
|
||||
// Make transfer sidebar open by default
|
||||
useEffect(() => {
|
||||
if (view === undefined) {
|
||||
setViews({ type: ViewType.Transfer }, currentRouteId);
|
||||
}
|
||||
}, [view, setViews, currentRouteId]);
|
||||
|
||||
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'portfolio' });
|
||||
const wrapperClasses = 'p-0.5 h-full max-h-full flex flex-col';
|
||||
return (
|
||||
<div className={wrapperClasses}>
|
||||
<SidebarViewInitializer />
|
||||
<ResizableGrid vertical onChange={handleOnLayoutChange}>
|
||||
<ResizableGridPanel minSize={75}>
|
||||
<PortfolioGridChild>
|
||||
@@ -72,22 +91,27 @@ export const Portfolio = () => {
|
||||
id="positions"
|
||||
name={t('Positions')}
|
||||
menu={<PositionsMenu />}
|
||||
settings={<PositionsSettings />}
|
||||
>
|
||||
<ErrorBoundary feature="portfolio-positions">
|
||||
<PositionsContainer allKeys />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="orders" name={t('Orders')}>
|
||||
<Tab id="orders" name={t('Orders')} settings={<OrdersSettings />}>
|
||||
<ErrorBoundary feature="portfolio-orders">
|
||||
<OrdersContainer />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="fills" name={t('Fills')}>
|
||||
<Tab id="fills" name={t('Fills')} settings={<FillsSettings />}>
|
||||
<ErrorBoundary feature="portfolio-fills">
|
||||
<FillsContainer />
|
||||
</ErrorBoundary>
|
||||
</Tab>
|
||||
<Tab id="funding-payments" name={t('Funding payments')}>
|
||||
<Tab
|
||||
id="funding-payments"
|
||||
name={t('Funding payments')}
|
||||
settings={<FundingPaymentsSettings />}
|
||||
>
|
||||
<ErrorBoundary feature="portfolio-funding-payments">
|
||||
<FundingPaymentsContainer />
|
||||
</ErrorBoundary>
|
||||
@@ -110,6 +134,7 @@ export const Portfolio = () => {
|
||||
<Tab
|
||||
id="collateral"
|
||||
name={t('Collateral')}
|
||||
settings={<AccountsSettings />}
|
||||
menu={<AccountsMenu />}
|
||||
>
|
||||
<ErrorBoundary feature="portfolio-accounts">
|
||||
|
||||
@@ -296,7 +296,7 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
|
||||
<>
|
||||
<div
|
||||
data-testid="referral-apply-code-form"
|
||||
className="bg-vega-clight-800 dark:bg-vega-cdark-800 mx-auto w-2/3 max-w-md rounded-lg p-8"
|
||||
className="bg-vega-clight-800 dark:bg-vega-cdark-800 mx-auto md:w-2/3 max-w-md rounded-lg p-8"
|
||||
>
|
||||
<h3 className="calt mb-4 text-center text-2xl">
|
||||
{t('Apply a referral code')}
|
||||
|
||||
@@ -3,7 +3,7 @@ export const GRADIENT =
|
||||
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
|
||||
|
||||
export const SKY_BACKGROUND =
|
||||
'bg-[url(/sky-light.png)] dark:bg-[url(/sky-dark.png)] bg-[40%_0px] bg-[length:1440px] bg-no-repeat bg-local';
|
||||
'bg-[url(/sky-light.png)] dark:bg-[url(/sky-dark.png)] bg-[37%_0px] bg-[length:1440px] bg-no-repeat bg-local';
|
||||
|
||||
// TODO: Update the links to use the correct referral related pages
|
||||
export const REFERRAL_DOCS_LINK =
|
||||
|
||||
@@ -49,7 +49,7 @@ export const CreateCodeForm = () => {
|
||||
return (
|
||||
<div
|
||||
data-testid="referral-create-code-form"
|
||||
className="w-2/3 max-w-md mx-auto bg-vega-clight-800 dark:bg-vega-cdark-800 p-8 rounded-lg"
|
||||
className="md:w-2/3 max-w-md mx-auto bg-vega-clight-800 dark:bg-vega-cdark-800 p-8 rounded-lg"
|
||||
>
|
||||
<h3 className="mb-4 text-2xl text-center calt">
|
||||
{t('Create a referral code')}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import { useCallback } from 'react';
|
||||
import { useRefereesQuery } from './__generated__/Referees';
|
||||
import compact from 'lodash/compact';
|
||||
import pick from 'lodash/pick';
|
||||
import type {
|
||||
ReferralSetsQuery,
|
||||
ReferralSetsQueryVariables,
|
||||
@@ -122,6 +123,66 @@ export const useReferral = (args: UseReferralArgs) => {
|
||||
};
|
||||
};
|
||||
|
||||
type Referee = NonNullable<
|
||||
NonNullable<ReturnType<typeof useReferral>['data']>['referee']
|
||||
>;
|
||||
|
||||
type RefereeProperties = (keyof Referee)[];
|
||||
|
||||
const findReferee = (referee: Referee, referees: Referee[]) =>
|
||||
referees.find((r) => r.refereeId === referee?.refereeId) || referee;
|
||||
|
||||
const updateReferee = (
|
||||
referee: Referee,
|
||||
referees: Referee[],
|
||||
properties: RefereeProperties
|
||||
) => ({
|
||||
...referee,
|
||||
...pick(findReferee(referee, referees), properties),
|
||||
});
|
||||
|
||||
export const useUpdateReferees = (
|
||||
referral: ReturnType<typeof useReferral>,
|
||||
aggregationEpochs: number,
|
||||
properties: RefereeProperties,
|
||||
skip?: boolean
|
||||
): ReturnType<typeof useReferral> => {
|
||||
const { data, loading, error, refetch } = useRefereesQuery({
|
||||
variables: {
|
||||
code: referral?.data?.code as string,
|
||||
aggregationEpochs,
|
||||
},
|
||||
skip: skip || !referral?.data?.code,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
context: { isEnlargedTimeout: true },
|
||||
});
|
||||
const refetchAll = useCallback(() => {
|
||||
refetch();
|
||||
referral.refetch();
|
||||
}, [refetch, referral]);
|
||||
if (!referral.data || skip) {
|
||||
return referral;
|
||||
}
|
||||
const referees = compact(
|
||||
removePaginationWrapper(data?.referralSetReferees.edges)
|
||||
);
|
||||
|
||||
return {
|
||||
data: data && {
|
||||
...referral.data,
|
||||
referees: referral.data.referees.map((referee) =>
|
||||
updateReferee(referee, referees, properties)
|
||||
),
|
||||
referee:
|
||||
referral.data.referee &&
|
||||
updateReferee(referral.data.referee, referees, properties),
|
||||
},
|
||||
loading: loading || referral.loading,
|
||||
error: error || referral.error,
|
||||
refetch: refetchAll,
|
||||
};
|
||||
};
|
||||
|
||||
const retrieveReferralSetData = (data: ReferralSetsQuery | undefined) =>
|
||||
data?.referralSets.edges && data.referralSets.edges.length > 0
|
||||
? data.referralSets.edges[0]?.node
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { Table } from './table';
|
||||
import { Table } from '../../components/table';
|
||||
|
||||
export const HowItWorksTable = () => {
|
||||
const t = useT();
|
||||
|
||||
@@ -5,16 +5,16 @@ import { useT } from '../../lib/use-t';
|
||||
export const LandingBanner = () => {
|
||||
const t = useT();
|
||||
return (
|
||||
<div className={classNames('relative mb-20')}>
|
||||
<div className={classNames('relative mb-10 lg:mb-20')}>
|
||||
<div className="">
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute top-20 right-[120px] md:right-[240px] max-sm:hidden"
|
||||
className="absolute top-20 right-[220px] md:right-[240px] max-sm:hidden"
|
||||
>
|
||||
<AnimatedDudeWithWire />
|
||||
</div>
|
||||
<div className="pt-20 sm:w-[50%]">
|
||||
<h1 className="text-6xl font-alpha calt mb-10">
|
||||
<div className="pt-10 lg:pt-20 sm:w-[50%]">
|
||||
<h1 className="text-3xl _text-[6vw] lg:!text-6xl leading-[1em] font-alpha calt mb-10">
|
||||
{t('Vega community referrals')}
|
||||
</h1>
|
||||
<p className="text-lg mb-1">
|
||||
@@ -22,7 +22,7 @@ export const LandingBanner = () => {
|
||||
'Referral programs can be proposed and created via community governance.'
|
||||
)}
|
||||
</p>
|
||||
<p className="text-lg mb-10">
|
||||
<p className="text-lg mb-1">
|
||||
{t(
|
||||
'Once live, users can generate referral codes to share with their friends and earn commission on their trades, while referred traders can access fee discounts based on the running volume of the group.'
|
||||
)}
|
||||
|
||||
@@ -13,7 +13,7 @@ export const Layout = ({
|
||||
<div
|
||||
className={classNames(
|
||||
'max-w-[1440px]',
|
||||
'mx-auto px-16 md:px-32 pb-32',
|
||||
'mx-auto px-4 lg:px-32 pb-32',
|
||||
'relative z-0',
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -151,6 +151,27 @@ const MOCK_REFEREES: RefereesQuery = {
|
||||
},
|
||||
};
|
||||
|
||||
const MOCK_REFEREES_30: RefereesQuery = {
|
||||
referralSetReferees: {
|
||||
__typename: 'ReferralSetRefereeConnection',
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
atEpoch: 1,
|
||||
joinedAt: '2023-11-21T14:17:09.257235Z',
|
||||
refereeId:
|
||||
'0987654321098765432109876543210987654321098765432109876543219876',
|
||||
referralSetId:
|
||||
'3772e570fbab89e50e563036b01dd949c554e5b5fe7908449672dfce9a8adffa',
|
||||
totalRefereeGeneratedRewards: '12340',
|
||||
totalRefereeNotionalTakerVolume: '56780',
|
||||
__typename: 'ReferralSetReferee',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const programMock: MockedResponse<ReferralProgramQuery> = {
|
||||
request: {
|
||||
query: ReferralProgramDocument,
|
||||
@@ -262,6 +283,19 @@ const refereesMock: MockedResponse<RefereesQuery, RefereesQueryVariables> = {
|
||||
},
|
||||
};
|
||||
|
||||
const refereesMock30: MockedResponse<RefereesQuery, RefereesQueryVariables> = {
|
||||
request: {
|
||||
query: RefereesDocument,
|
||||
variables: {
|
||||
code: MOCK_REFERRER_SET.referralSets.edges[0]?.node.id as string,
|
||||
aggregationEpochs: 30,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: MOCK_REFEREES_30,
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('@vegaprotocol/wallet', () => {
|
||||
return {
|
||||
...jest.requireActual('@vegaprotocol/wallet'),
|
||||
@@ -297,6 +331,7 @@ describe('ReferralStatistics', () => {
|
||||
noReferralSetAsRefereeMock,
|
||||
stakeAvailableMock,
|
||||
refereesMock,
|
||||
refereesMock30,
|
||||
]}
|
||||
showWarnings={false}
|
||||
>
|
||||
@@ -313,6 +348,10 @@ describe('ReferralStatistics', () => {
|
||||
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
|
||||
'referrer'
|
||||
);
|
||||
// gets commision from 30 epochs query
|
||||
expect(queryByTestId('total-commission-value')).toHaveTextContent(
|
||||
'12,340'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,12 +4,17 @@ import {
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
truncateMiddle,
|
||||
TextChildrenTooltip as Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { DEFAULT_AGGREGATION_DAYS, useReferral } from './hooks/use-referral';
|
||||
import {
|
||||
DEFAULT_AGGREGATION_DAYS,
|
||||
useReferral,
|
||||
useUpdateReferees,
|
||||
} from './hooks/use-referral';
|
||||
import classNames from 'classnames';
|
||||
import { Table } from './table';
|
||||
import { Table } from '../../components/table';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
getDateFormat,
|
||||
@@ -41,11 +46,17 @@ export const ReferralStatistics = () => {
|
||||
role: 'referee',
|
||||
aggregationEpochs: program.details?.windowLength,
|
||||
});
|
||||
const { data: referrer, refetch: referrerRefetch } = useReferral({
|
||||
pubKey,
|
||||
role: 'referrer',
|
||||
aggregationEpochs: program.details?.windowLength,
|
||||
});
|
||||
|
||||
const { data: referrer, refetch: referrerRefetch } = useUpdateReferees(
|
||||
useReferral({
|
||||
pubKey,
|
||||
role: 'referrer',
|
||||
aggregationEpochs: program.details?.windowLength,
|
||||
}),
|
||||
DEFAULT_AGGREGATION_DAYS,
|
||||
['totalRefereeGeneratedRewards'],
|
||||
DEFAULT_AGGREGATION_DAYS === program.details?.windowLength
|
||||
);
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
refereeRefetch();
|
||||
@@ -76,11 +87,9 @@ export const ReferralStatistics = () => {
|
||||
export const useStats = ({
|
||||
data,
|
||||
program,
|
||||
as,
|
||||
}: {
|
||||
data?: NonNullable<ReturnType<typeof useReferral>['data']>;
|
||||
program: ReturnType<typeof useReferralProgram>;
|
||||
as?: 'referrer' | 'referee';
|
||||
}) => {
|
||||
const { benefitTiers } = program;
|
||||
const { data: epochData } = useCurrentEpochInfoQuery({
|
||||
@@ -181,7 +190,7 @@ export const Statistics = ({
|
||||
nextBenefitTierValue,
|
||||
nextBenefitTierVolumeValue,
|
||||
nextBenefitTierEpochsValue,
|
||||
} = useStats({ data, program, as });
|
||||
} = useStats({ data, program });
|
||||
|
||||
const isApplyCodePreview = useMemo(
|
||||
() => data.referee === null,
|
||||
@@ -212,6 +221,7 @@ export const Statistics = ({
|
||||
).toString(),
|
||||
}
|
||||
)}
|
||||
testId="base-commission-rate"
|
||||
overrideWithNoProgram={!details}
|
||||
>
|
||||
{baseCommissionValue * 100}%
|
||||
@@ -221,6 +231,7 @@ export const Statistics = ({
|
||||
const stakingMultiplierTile = (
|
||||
<StatTile
|
||||
title={t('Staking multiplier')}
|
||||
testId="staking-multiplier"
|
||||
description={
|
||||
<span
|
||||
className={classNames({
|
||||
@@ -254,6 +265,7 @@ export const Statistics = ({
|
||||
? `(${baseCommissionFormatted}% ⨉ ${multiplier} = ${finalCommissionFormatted}%)`
|
||||
: undefined
|
||||
}
|
||||
testId="final-commission-rate"
|
||||
overrideWithNoProgram={!details}
|
||||
>
|
||||
{finalCommissionFormatted}%
|
||||
@@ -261,7 +273,9 @@ export const Statistics = ({
|
||||
);
|
||||
const numberOfTradersValue = data.referees.length;
|
||||
const numberOfTradersTile = (
|
||||
<StatTile title={t('Number of traders')}>{numberOfTradersValue}</StatTile>
|
||||
<StatTile title={t('Number of traders')} testId="number-of-traders">
|
||||
{numberOfTradersValue}
|
||||
</StatTile>
|
||||
);
|
||||
|
||||
const codeTile = (
|
||||
@@ -276,6 +290,7 @@ export const Statistics = ({
|
||||
title={t('myVolume', 'My volume (last {{count}} epochs)', {
|
||||
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
|
||||
})}
|
||||
testId="my-volume"
|
||||
overrideWithNoProgram={!details}
|
||||
>
|
||||
{compactNumFormat.format(referrerVolumeValue)}
|
||||
@@ -287,9 +302,26 @@ export const Statistics = ({
|
||||
.reduce((all, r) => all.plus(r), new BigNumber(0));
|
||||
const totalCommissionTile = (
|
||||
<StatTile
|
||||
title={t('totalCommission', 'Total commission (last {{count}} epochs)', {
|
||||
count: details?.windowLength || DEFAULT_AGGREGATION_DAYS,
|
||||
})}
|
||||
testId="total-commission"
|
||||
title={
|
||||
<Trans
|
||||
i18nKey="totalCommission"
|
||||
defaults="Total commission (<0>last {{count}} epochs</0>)"
|
||||
values={{
|
||||
count: DEFAULT_AGGREGATION_DAYS,
|
||||
}}
|
||||
components={[
|
||||
<Tooltip
|
||||
key="1"
|
||||
description={t(
|
||||
'Depending on data node retention you may not be able see the full 30 days'
|
||||
)}
|
||||
>
|
||||
last 30 epochs
|
||||
</Tooltip>,
|
||||
]}
|
||||
/>
|
||||
}
|
||||
description={<QUSDTooltip />}
|
||||
>
|
||||
{getNumberFormat(0).format(Number(totalCommissionValue))}
|
||||
@@ -316,6 +348,7 @@ export const Statistics = ({
|
||||
const currentBenefitTierTile = (
|
||||
<StatTile
|
||||
title={t('Current tier')}
|
||||
testId="current-tier"
|
||||
description={
|
||||
nextBenefitTierValue?.tier
|
||||
? t('(Next tier: {{nextTier}})', {
|
||||
@@ -331,7 +364,11 @@ export const Statistics = ({
|
||||
</StatTile>
|
||||
);
|
||||
const discountFactorTile = (
|
||||
<StatTile title={t('Discount')} overrideWithNoProgram={!details}>
|
||||
<StatTile
|
||||
title={t('Discount')}
|
||||
testId="discount"
|
||||
overrideWithNoProgram={!details}
|
||||
>
|
||||
{isApplyCodePreview && benefitTiers.length >= 1
|
||||
? benefitTiers[0].discountFactor * 100
|
||||
: discountFactorValue * 100}
|
||||
@@ -347,23 +384,34 @@ export const Statistics = ({
|
||||
count: details?.windowLength,
|
||||
}
|
||||
)}
|
||||
testId="combined-volume"
|
||||
overrideWithNoProgram={!details}
|
||||
>
|
||||
{compactNumFormat.format(runningVolumeValue)}
|
||||
</StatTile>
|
||||
);
|
||||
const epochsTile = (
|
||||
<StatTile title={t('Epochs in set')}>{epochsValue}</StatTile>
|
||||
<StatTile title={t('Epochs in set')} testId="epochs-in-set">
|
||||
{epochsValue}
|
||||
</StatTile>
|
||||
);
|
||||
const nextTierVolumeTile = (
|
||||
<StatTile title={t('Volume to next tier')} overrideWithNoProgram={!details}>
|
||||
<StatTile
|
||||
title={t('Volume to next tier')}
|
||||
testId="vol-to-next-tier"
|
||||
overrideWithNoProgram={!details}
|
||||
>
|
||||
{nextBenefitTierVolumeValue <= 0
|
||||
? '0'
|
||||
: compactNumFormat.format(nextBenefitTierVolumeValue)}
|
||||
</StatTile>
|
||||
);
|
||||
const nextTierEpochsTile = (
|
||||
<StatTile title={t('Epochs to next tier')} overrideWithNoProgram={!details}>
|
||||
<StatTile
|
||||
title={t('Epochs to next tier')}
|
||||
testId="epochs-to-next-tier"
|
||||
overrideWithNoProgram={!details}
|
||||
>
|
||||
{nextBenefitTierEpochsValue <= 0 ? '0' : nextBenefitTierEpochsValue}
|
||||
</StatTile>
|
||||
);
|
||||
@@ -480,12 +528,21 @@ export const RefereesTable = ({
|
||||
displayName: (
|
||||
<Trans
|
||||
i18nKey="referralStatisticsCommission"
|
||||
defaults="Commission earned in <0>qUSD</0> (last {{count}} epochs)"
|
||||
defaults="Commission earned in <0>qUSD</0> (<1>last {{count}} epochs</1>)"
|
||||
components={[
|
||||
<QUSDTooltip key="0" />,
|
||||
<Tooltip
|
||||
key="1"
|
||||
description={t(
|
||||
'Depending on data node retention you may not be able see the full 30 days'
|
||||
)}
|
||||
>
|
||||
last 30 epochs
|
||||
</Tooltip>,
|
||||
]}
|
||||
values={{
|
||||
count:
|
||||
details?.windowLength || DEFAULT_AGGREGATION_DAYS,
|
||||
count: DEFAULT_AGGREGATION_DAYS,
|
||||
}}
|
||||
components={[<QUSDTooltip key="qusd" />]}
|
||||
ns={ns}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -72,7 +72,7 @@ export const Referrals = () => {
|
||||
{showNav && <Nav />}
|
||||
<div
|
||||
className={classNames({
|
||||
'py-16': showNav,
|
||||
'py-8 lg:py-16': showNav,
|
||||
'h-[300px] relative': loading || error,
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -3,11 +3,12 @@ import {
|
||||
getDateTimeFormat,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { useReferralProgram } from './hooks/use-referral-program';
|
||||
import { Table } from './table';
|
||||
import { Table } from '../../components/table';
|
||||
import classNames from 'classnames';
|
||||
import { BORDER_COLOR, GRADIENT } from './constants';
|
||||
import { Tag } from './tag';
|
||||
import type { ComponentProps, ReactNode } from 'react';
|
||||
import { Tag } from '../../components/helpers/tag';
|
||||
import { getTierColor, getTierGradient } from '../../components/helpers/tiers';
|
||||
import type { ReactNode } from 'react';
|
||||
import { ExternalLink, truncateMiddle } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
DApp,
|
||||
@@ -19,25 +20,6 @@ import {
|
||||
import { useT, ns } from '../../lib/use-t';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
// rainbow-ish order
|
||||
const TIER_COLORS: Array<ComponentProps<typeof Tag>['color']> = [
|
||||
'pink',
|
||||
'orange',
|
||||
'yellow',
|
||||
'green',
|
||||
'blue',
|
||||
'purple',
|
||||
];
|
||||
|
||||
const getTierColor = (tier: number) => {
|
||||
const tiers = Object.keys(TIER_COLORS).length;
|
||||
let index = Math.abs(tier - 1);
|
||||
if (tier >= tiers) {
|
||||
index = index % tiers;
|
||||
}
|
||||
return TIER_COLORS[index];
|
||||
};
|
||||
|
||||
const Loading = ({ variant }: { variant: 'large' | 'inline' }) => (
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -53,10 +35,12 @@ const StakingTier = ({
|
||||
tier,
|
||||
referralRewardMultiplier,
|
||||
minimumStakedTokens,
|
||||
max,
|
||||
}: {
|
||||
tier: number;
|
||||
referralRewardMultiplier: string;
|
||||
minimumStakedTokens: string;
|
||||
max?: number;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const minimum = addDecimalsFormatNumber(minimumStakedTokens, 18);
|
||||
@@ -97,7 +81,7 @@ const StakingTier = ({
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<Tag color={getTierColor(tier)}>
|
||||
<Tag color={getTierColor(tier, max)}>
|
||||
{t('Multiplier')} {referralRewardMultiplier}x
|
||||
</Tag>
|
||||
<p className="mt-1 text-sm text-vega-clight-100 dark:text-vega-cdark-100">
|
||||
@@ -159,7 +143,9 @@ export const TiersContainer = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="text-3xl mt-10">{t('Current program details')}</h2>
|
||||
<h2 className="text-3xl mt-10 font-alpha calt">
|
||||
{t('Current program details')}
|
||||
</h2>
|
||||
{details?.id && (
|
||||
<p>
|
||||
<Trans
|
||||
@@ -198,7 +184,18 @@ export const TiersContainer = () => {
|
||||
</div>
|
||||
|
||||
{/* Container */}
|
||||
<div className="bg-vega-clight-800 dark:bg-vega-cdark-800 text-black dark:text-white rounded-lg p-6 mt-1 mb-20">
|
||||
<div
|
||||
className={classNames(
|
||||
'md:bg-vega-clight-800',
|
||||
'md:dark:bg-vega-cdark-800',
|
||||
'md:text-black',
|
||||
'md:dark:text-white',
|
||||
'md:rounded-lg',
|
||||
'md:p-6',
|
||||
'mt-1',
|
||||
'mb-20'
|
||||
)}
|
||||
>
|
||||
{/* Benefit tiers */}
|
||||
<div className="flex flex-col mb-5">
|
||||
<h3 className="text-2xl calt">{t('Benefit tiers')}</h3>
|
||||
@@ -261,6 +258,7 @@ const StakingTiers = ({
|
||||
<StakingTier
|
||||
key={i}
|
||||
tier={tier}
|
||||
max={data.length}
|
||||
referralRewardMultiplier={referralRewardMultiplier}
|
||||
minimumStakedTokens={minimumStakedTokens}
|
||||
/>
|
||||
@@ -322,22 +320,7 @@ const TiersTable = ({
|
||||
className="bg-white dark:bg-vega-cdark-900"
|
||||
data={data.map((d) => ({
|
||||
...d,
|
||||
className: classNames({
|
||||
'from-vega-yellow-400 dark:from-vega-yellow-600 to-20% bg-highlight':
|
||||
'yellow' === getTierColor(d.tier),
|
||||
'from-vega-green-400 dark:from-vega-green-600 to-20% bg-highlight':
|
||||
'green' === getTierColor(d.tier),
|
||||
'from-vega-blue-400 dark:from-vega-blue-600 to-20% bg-highlight':
|
||||
'blue' === getTierColor(d.tier),
|
||||
'from-vega-purple-400 dark:from-vega-purple-600 to-20% bg-highlight':
|
||||
'purple' === getTierColor(d.tier),
|
||||
'from-vega-pink-400 dark:from-vega-pink-600 to-20% bg-highlight':
|
||||
'pink' === getTierColor(d.tier),
|
||||
'from-vega-orange-400 dark:from-vega-orange-600 to-20% bg-highlight':
|
||||
'orange' === getTierColor(d.tier),
|
||||
'from-vega-clight-200 dark:from-vega-cdark-200 to-20% bg-highlight':
|
||||
'none' === getTierColor(d.tier),
|
||||
}),
|
||||
className: classNames(getTierGradient(d.tier, data.length)),
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -19,9 +19,11 @@ export const Tile = ({
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'rounded-lg overflow-hidden relative',
|
||||
'bg-vega-clight-800 dark:bg-vega-cdark-800 text-black dark:text-white',
|
||||
'p-6',
|
||||
'text-black dark:text-white',
|
||||
'overflow-hidden relative',
|
||||
'p-3 md:p-6',
|
||||
'rounded-lg',
|
||||
'bg-vega-clight-800 dark:bg-vega-cdark-800',
|
||||
className
|
||||
)}
|
||||
>
|
||||
@@ -31,7 +33,8 @@ export const Tile = ({
|
||||
};
|
||||
|
||||
type StatTileProps = {
|
||||
title: string;
|
||||
title: ReactNode;
|
||||
testId?: string;
|
||||
description?: ReactNode;
|
||||
children?: ReactNode;
|
||||
overrideWithNoProgram?: boolean;
|
||||
@@ -40,6 +43,7 @@ export const StatTile = ({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
testId,
|
||||
overrideWithNoProgram = false,
|
||||
}: StatTileProps) => {
|
||||
if (overrideWithNoProgram) {
|
||||
@@ -47,10 +51,18 @@ export const StatTile = ({
|
||||
}
|
||||
return (
|
||||
<Tile>
|
||||
<h3 className="mb-1 text-sm text-vega-clight-100 dark:text-vega-cdark-100 calt">
|
||||
<h3
|
||||
data-testid={testId}
|
||||
className="mb-1 text-sm text-vega-clight-100 dark:text-vega-cdark-100 calt"
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
<div className="text-5xl text-left">{children}</div>
|
||||
<div
|
||||
data-testid={`${testId}-value`}
|
||||
className="text-2xl lg:text-5xl text-left"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{description && (
|
||||
<div className="text-sm text-left text-vega-clight-100 dark:text-vega-cdark-100">
|
||||
{description}
|
||||
@@ -63,7 +75,7 @@ export const StatTile = ({
|
||||
export const NoProgramTile = ({ title }: Pick<StatTileProps, 'title'>) => {
|
||||
const t = useT();
|
||||
return (
|
||||
<Tile title={title}>
|
||||
<Tile>
|
||||
<h3 className="mb-1 text-sm text-vega-clight-100 dark:text-vega-cdark-100 calt">
|
||||
{title}
|
||||
</h3>
|
||||
@@ -112,7 +124,7 @@ export const CodeTile = ({
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'relative bg-rainbow bg-clip-text text-transparent text-5xl overflow-hidden',
|
||||
'relative bg-rainbow bg-clip-text text-transparent text-2xl lg:text-5xl overflow-hidden',
|
||||
FADE_OUT_STYLE
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useT } from '../../lib/use-t';
|
||||
import { RewardsContainer } from '../../components/rewards-container';
|
||||
import { usePageTitleStore } from '../../stores';
|
||||
import { ErrorBoundary } from '../../components/error-boundary';
|
||||
import { TinyScroll } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const Rewards = () => {
|
||||
const t = useT();
|
||||
@@ -16,10 +17,10 @@ export const Rewards = () => {
|
||||
}, [updateTitle, title]);
|
||||
return (
|
||||
<ErrorBoundary feature="rewards">
|
||||
<div className="container mx-auto p-4">
|
||||
<TinyScroll className="p-4 max-h-full overflow-auto">
|
||||
<h1 className="px-4 pb-4 text-2xl">{title}</h1>
|
||||
<RewardsContainer />
|
||||
</div>
|
||||
</TinyScroll>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
fragment TeamFields on Team {
|
||||
teamId
|
||||
referrer
|
||||
name
|
||||
teamUrl
|
||||
avatarUrl
|
||||
createdAt
|
||||
createdAtEpoch
|
||||
closed
|
||||
}
|
||||
|
||||
fragment TeamStatsFields on TeamStatistics {
|
||||
teamId
|
||||
totalQuantumVolume
|
||||
totalQuantumRewards
|
||||
totalGamesPlayed
|
||||
quantumRewards {
|
||||
epoch
|
||||
total_quantum_rewards
|
||||
}
|
||||
gamesPlayed
|
||||
}
|
||||
|
||||
fragment TeamRefereeFields on TeamReferee {
|
||||
teamId
|
||||
referee
|
||||
joinedAt
|
||||
joinedAtEpoch
|
||||
}
|
||||
|
||||
fragment TeamEntity on TeamGameEntity {
|
||||
rank
|
||||
volume
|
||||
rewardMetric
|
||||
rewardEarned
|
||||
totalRewardsEarned
|
||||
team {
|
||||
teamId
|
||||
}
|
||||
}
|
||||
|
||||
fragment TeamGameFields on Game {
|
||||
id
|
||||
epoch
|
||||
numberOfParticipants
|
||||
entities {
|
||||
... on TeamGameEntity {
|
||||
...TeamEntity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query Team($teamId: ID!, $partyId: ID) {
|
||||
teams(teamId: $teamId) {
|
||||
edges {
|
||||
node {
|
||||
...TeamFields
|
||||
}
|
||||
}
|
||||
}
|
||||
partyTeams: teams(partyId: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
...TeamFields
|
||||
}
|
||||
}
|
||||
}
|
||||
teamsStatistics(teamId: $teamId) {
|
||||
edges {
|
||||
node {
|
||||
...TeamStatsFields
|
||||
}
|
||||
}
|
||||
}
|
||||
teamReferees(teamId: $teamId) {
|
||||
edges {
|
||||
node {
|
||||
...TeamRefereeFields
|
||||
}
|
||||
}
|
||||
}
|
||||
games(entityScope: ENTITY_SCOPE_TEAMS) {
|
||||
edges {
|
||||
node {
|
||||
...TeamGameFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type TeamFieldsFragment = { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean };
|
||||
|
||||
export type TeamStatsFieldsFragment = { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string>, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, total_quantum_rewards: string }> };
|
||||
|
||||
export type TeamRefereeFieldsFragment = { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number };
|
||||
|
||||
export type TeamEntityFragment = { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: string, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } };
|
||||
|
||||
export type TeamGameFieldsFragment = { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: string, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } }> };
|
||||
|
||||
export type TeamQueryVariables = Types.Exact<{
|
||||
teamId: Types.Scalars['ID'];
|
||||
partyId?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type TeamQuery = { __typename?: 'Query', teams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean } }> } | null, partyTeams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean } }> } | null, teamsStatistics?: { __typename?: 'TeamsStatisticsConnection', edges: Array<{ __typename?: 'TeamStatisticsEdge', node: { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string>, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, total_quantum_rewards: string }> } }> } | null, teamReferees?: { __typename?: 'TeamRefereeConnection', edges: Array<{ __typename?: 'TeamRefereeEdge', node: { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number } }> } | null, games: { __typename?: 'GamesConnection', edges?: Array<{ __typename?: 'GameEdge', node: { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: string, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } }> } } | null> | null } };
|
||||
|
||||
export const TeamFieldsFragmentDoc = gql`
|
||||
fragment TeamFields on Team {
|
||||
teamId
|
||||
referrer
|
||||
name
|
||||
teamUrl
|
||||
avatarUrl
|
||||
createdAt
|
||||
createdAtEpoch
|
||||
closed
|
||||
}
|
||||
`;
|
||||
export const TeamStatsFieldsFragmentDoc = gql`
|
||||
fragment TeamStatsFields on TeamStatistics {
|
||||
teamId
|
||||
totalQuantumVolume
|
||||
totalQuantumRewards
|
||||
totalGamesPlayed
|
||||
quantumRewards {
|
||||
epoch
|
||||
total_quantum_rewards
|
||||
}
|
||||
gamesPlayed
|
||||
}
|
||||
`;
|
||||
export const TeamRefereeFieldsFragmentDoc = gql`
|
||||
fragment TeamRefereeFields on TeamReferee {
|
||||
teamId
|
||||
referee
|
||||
joinedAt
|
||||
joinedAtEpoch
|
||||
}
|
||||
`;
|
||||
export const TeamEntityFragmentDoc = gql`
|
||||
fragment TeamEntity on TeamGameEntity {
|
||||
rank
|
||||
volume
|
||||
rewardMetric
|
||||
rewardEarned
|
||||
totalRewardsEarned
|
||||
team {
|
||||
teamId
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const TeamGameFieldsFragmentDoc = gql`
|
||||
fragment TeamGameFields on Game {
|
||||
id
|
||||
epoch
|
||||
numberOfParticipants
|
||||
entities {
|
||||
... on TeamGameEntity {
|
||||
...TeamEntity
|
||||
}
|
||||
}
|
||||
}
|
||||
${TeamEntityFragmentDoc}`;
|
||||
export const TeamDocument = gql`
|
||||
query Team($teamId: ID!, $partyId: ID) {
|
||||
teams(teamId: $teamId) {
|
||||
edges {
|
||||
node {
|
||||
...TeamFields
|
||||
}
|
||||
}
|
||||
}
|
||||
partyTeams: teams(partyId: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
...TeamFields
|
||||
}
|
||||
}
|
||||
}
|
||||
teamsStatistics(teamId: $teamId) {
|
||||
edges {
|
||||
node {
|
||||
...TeamStatsFields
|
||||
}
|
||||
}
|
||||
}
|
||||
teamReferees(teamId: $teamId) {
|
||||
edges {
|
||||
node {
|
||||
...TeamRefereeFields
|
||||
}
|
||||
}
|
||||
}
|
||||
games(entityScope: ENTITY_SCOPE_TEAMS) {
|
||||
edges {
|
||||
node {
|
||||
...TeamGameFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${TeamFieldsFragmentDoc}
|
||||
${TeamStatsFieldsFragmentDoc}
|
||||
${TeamRefereeFieldsFragmentDoc}
|
||||
${TeamGameFieldsFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useTeamQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useTeamQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useTeamQuery` 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 } = useTeamQuery({
|
||||
* variables: {
|
||||
* teamId: // value for 'teamId'
|
||||
* partyId: // value for 'partyId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useTeamQuery(baseOptions: Apollo.QueryHookOptions<TeamQuery, TeamQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<TeamQuery, TeamQueryVariables>(TeamDocument, options);
|
||||
}
|
||||
export function useTeamLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<TeamQuery, TeamQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<TeamQuery, TeamQueryVariables>(TeamDocument, options);
|
||||
}
|
||||
export type TeamQueryHookResult = ReturnType<typeof useTeamQuery>;
|
||||
export type TeamLazyQueryHookResult = ReturnType<typeof useTeamLazyQuery>;
|
||||
export type TeamQueryResult = Apollo.QueryResult<TeamQuery, TeamQueryVariables>;
|
||||
@@ -0,0 +1 @@
|
||||
export { Team } from './team';
|
||||
@@ -0,0 +1,395 @@
|
||||
import { useState, type ReactNode, type ButtonHTMLAttributes } from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import countBy from 'lodash/countBy';
|
||||
import {
|
||||
TradingButton as Button,
|
||||
Intent,
|
||||
Pill,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
Tooltip,
|
||||
Splash,
|
||||
truncateMiddle,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { Table } from '../../components/table';
|
||||
import { formatNumberRounded, getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
import {
|
||||
useTeam,
|
||||
type Team as TeamType,
|
||||
type TeamStats,
|
||||
type Member,
|
||||
type TeamGame,
|
||||
} from './use-team';
|
||||
import { DApp, EXPLORER_PARTIES, useLinks } from '@vegaprotocol/environment';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
export const Team = () => {
|
||||
const t = useT();
|
||||
const { teamId } = useParams<{ teamId: string }>();
|
||||
const { team, stats, partyInTeam, members, games } = useTeam(teamId);
|
||||
|
||||
if (!team) {
|
||||
return (
|
||||
<Splash>
|
||||
<p>{t('Page not found')}</p>
|
||||
</Splash>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TeamPage
|
||||
team={team}
|
||||
stats={stats}
|
||||
partyInTeam={partyInTeam}
|
||||
members={members}
|
||||
games={games}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const TeamPage = ({
|
||||
team,
|
||||
stats,
|
||||
partyInTeam,
|
||||
members,
|
||||
games,
|
||||
}: {
|
||||
team: TeamType;
|
||||
stats?: TeamStats;
|
||||
partyInTeam: boolean;
|
||||
members?: Member[];
|
||||
games?: TeamGame[];
|
||||
}) => {
|
||||
const t = useT();
|
||||
const [showGames, setShowGames] = useState(true);
|
||||
|
||||
return (
|
||||
<div className="relative h-full overflow-y-auto">
|
||||
<div className="absolute top-0 left-0 w-full h-[40%] -z-10 bg-[40%_0px] bg-cover bg-no-repeat bg-local bg-[url(/cover.png)]">
|
||||
<div className="absolute top-o left-0 w-full h-full bg-gradient-to-t from-white dark:from-vega-cdark-900 to-transparent from-20% to-60%" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 lg:gap-6 container p-4 mx-auto">
|
||||
<header className="flex gap-3 lg:gap-4 pt-5 lg:pt-10">
|
||||
<TeamAvatar imgUrl={team.avatarUrl} />
|
||||
<div className="flex flex-col items-start gap-1 lg:gap-3">
|
||||
<h1 className="calt text-2xl lg:text-3xl xl:text-5xl">
|
||||
{team.name}
|
||||
</h1>
|
||||
<JoinButton joined={partyInTeam} />
|
||||
</div>
|
||||
</header>
|
||||
<StatSection>
|
||||
<StatList>
|
||||
<Stat value={members ? members.length : 0} label={t('Members')} />
|
||||
<Stat
|
||||
value={stats ? stats.totalGamesPlayed : 0}
|
||||
label={t('Total games')}
|
||||
tooltip={t('Total number of games this team has participated in')}
|
||||
/>
|
||||
<StatSectionSeparator />
|
||||
<Stat
|
||||
value={
|
||||
stats
|
||||
? formatNumberRounded(
|
||||
new BigNumber(stats.totalQuantumVolume),
|
||||
'1e3'
|
||||
)
|
||||
: 0
|
||||
}
|
||||
label={t('Total volume')}
|
||||
/>
|
||||
<Stat
|
||||
value={
|
||||
stats
|
||||
? formatNumberRounded(
|
||||
new BigNumber(stats.totalQuantumRewards),
|
||||
'1e3'
|
||||
)
|
||||
: 0
|
||||
}
|
||||
label={t('Rewards paid')}
|
||||
tooltip={'Total amount of rewards paid out to this team in qUSD'}
|
||||
/>
|
||||
</StatList>
|
||||
</StatSection>
|
||||
{games && games.length ? (
|
||||
<StatSection>
|
||||
<FavoriteGame games={games} />
|
||||
<StatSectionSeparator />
|
||||
<LatestResults games={games} />
|
||||
</StatSection>
|
||||
) : null}
|
||||
<section>
|
||||
<div className="flex gap-4 lg:gap-8 mb-4 border-b border-default">
|
||||
<ToggleButton active={showGames} onClick={() => setShowGames(true)}>
|
||||
{t('Games ({{count}})', { count: games ? games.length : 0 })}
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
active={!showGames}
|
||||
onClick={() => setShowGames(false)}
|
||||
>
|
||||
{t('Members ({{count}})', {
|
||||
count: members ? members.length : 0,
|
||||
})}
|
||||
</ToggleButton>
|
||||
</div>
|
||||
{showGames ? <Games games={games} /> : <Members members={members} />}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Games = ({ games }: { games?: TeamGame[] }) => {
|
||||
const t = useT();
|
||||
|
||||
if (!games?.length) {
|
||||
return <p>{t('No games')}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={[
|
||||
{ name: 'rank', displayName: t('Rank') },
|
||||
{
|
||||
name: 'epoch',
|
||||
displayName: t('Epoch'),
|
||||
headerClassName: 'hidden md:block',
|
||||
className: 'hidden md:block',
|
||||
},
|
||||
{ name: 'type', displayName: t('Type') },
|
||||
{ name: 'amount', displayName: t('Amount earned') },
|
||||
{
|
||||
name: 'teams',
|
||||
displayName: t('No. of participating teams'),
|
||||
headerClassName: 'hidden md:block',
|
||||
className: 'hidden md:block',
|
||||
},
|
||||
{ name: 'status', displayName: t('Status') },
|
||||
]}
|
||||
data={games.map((game) => ({
|
||||
rank: game.team.rank,
|
||||
epoch: game.epoch,
|
||||
type: game.team.rewardMetric,
|
||||
amount: game.team.totalRewardsEarned,
|
||||
teams: game.numberOfParticipants,
|
||||
}))}
|
||||
noCollapse={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const TeamAvatar = ({ imgUrl }: { imgUrl: string }) => {
|
||||
// TODO: add fallback avatars
|
||||
return (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={imgUrl}
|
||||
alt="Team avatar"
|
||||
className="rounded-full w-20 h-20 lg:w-[112px] lg:h-[112px] bg-vega-clight-700 dark:bg-vega-cdark-700 shrink-0"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Members = ({ members }: { members?: Member[] }) => {
|
||||
const t = useT();
|
||||
|
||||
if (!members?.length) {
|
||||
return <p>{t('No members')}</p>;
|
||||
}
|
||||
|
||||
const data = orderBy(
|
||||
members.map((m) => ({
|
||||
referee: <RefereeCell pubkey={m.referee} />,
|
||||
joinedAt: getDateTimeFormat().format(new Date(m.joinedAt)),
|
||||
joinedAtEpoch: Number(m.joinedAtEpoch),
|
||||
explorerLink: <RefereeLink pubkey={m.referee} />,
|
||||
})),
|
||||
'joinedAtEpoch',
|
||||
'desc'
|
||||
);
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={[
|
||||
{ name: 'referee', displayName: t('Referee') },
|
||||
{
|
||||
name: 'joinedAt',
|
||||
displayName: t('Joined at'),
|
||||
},
|
||||
{
|
||||
name: 'joinedAtEpoch',
|
||||
displayName: t('Joined epoch'),
|
||||
headerClassName: 'text-right',
|
||||
className: 'text-right',
|
||||
},
|
||||
{
|
||||
name: 'explorerLink',
|
||||
displayName: '',
|
||||
headerClassName: 'hidden md:block',
|
||||
className: 'hidden md:block text-right',
|
||||
},
|
||||
]}
|
||||
data={data}
|
||||
noCollapse={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const RefereeCell = ({ pubkey }: { pubkey: string }) => {
|
||||
return <span title={pubkey}>{truncateMiddle(pubkey)}</span>;
|
||||
};
|
||||
|
||||
const RefereeLink = ({ pubkey }: { pubkey: string }) => {
|
||||
const t = useT();
|
||||
|
||||
const linkCreator = useLinks(DApp.Explorer);
|
||||
const link = linkCreator(EXPLORER_PARTIES.replace(':id', pubkey));
|
||||
|
||||
return (
|
||||
<Link to={link} className="underline underline-offset-4">
|
||||
{t('View on explorer')}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const JoinButton = ({ joined }: { joined: boolean }) => {
|
||||
const t = useT();
|
||||
|
||||
if (joined) {
|
||||
return (
|
||||
<Button intent={Intent.None} disabled={true}>
|
||||
<span className="flex items-center gap-2">
|
||||
{t('Joined')}{' '}
|
||||
<span className="text-vega-green-600 dark:text-vega-green">
|
||||
<VegaIcon name={VegaIconNames.TICK} />
|
||||
</span>
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return <Button intent={Intent.Primary}>{t('Join this team')}</Button>;
|
||||
};
|
||||
|
||||
const LatestResults = ({ games }: { games: TeamGame[] }) => {
|
||||
const t = useT();
|
||||
const latestGames = games.slice(0, 5);
|
||||
|
||||
return (
|
||||
<dl>
|
||||
<dt className="text-muted text-sm">
|
||||
{t('Last {{count}} game results', { count: latestGames.length })}
|
||||
</dt>
|
||||
<dd className="flex gap-1">
|
||||
{latestGames.map((game) => {
|
||||
return (
|
||||
<Pill key={game.id} className="text-sm">
|
||||
{t('place', { count: game.team.rank, ordinal: true })}
|
||||
</Pill>
|
||||
);
|
||||
})}
|
||||
</dd>
|
||||
</dl>
|
||||
);
|
||||
};
|
||||
|
||||
const FavoriteGame = ({ games }: { games: TeamGame[] }) => {
|
||||
const t = useT();
|
||||
|
||||
const rewardMetrics = games.map((game) => game.team.rewardMetric);
|
||||
const count = countBy(rewardMetrics);
|
||||
|
||||
let favoriteMetric = '';
|
||||
let mostOccurances = 0;
|
||||
|
||||
for (const key in count) {
|
||||
if (count[key] > mostOccurances) {
|
||||
favoriteMetric = key;
|
||||
mostOccurances = count[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (!favoriteMetric) return null;
|
||||
|
||||
return (
|
||||
<dl>
|
||||
<dt className="text-muted text-sm">{t('Favorite game')}</dt>
|
||||
<dd>
|
||||
<Pill className="flex-inline items-center gap-2 bg-transparent text-sm">
|
||||
<VegaIcon
|
||||
name={VegaIconNames.STAR}
|
||||
className="text-vega-yellow-400"
|
||||
/>{' '}
|
||||
{favoriteMetric}
|
||||
</Pill>
|
||||
</dd>
|
||||
</dl>
|
||||
);
|
||||
};
|
||||
|
||||
const ToggleButton = ({
|
||||
active,
|
||||
...props
|
||||
}: ButtonHTMLAttributes<HTMLButtonElement> & { active: boolean }) => {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className={classNames('relative top-px uppercase border-b-2 py-4', {
|
||||
'text-muted border-transparent': !active,
|
||||
'border-vega-yellow': active,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const StatSection = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<section className="flex flex-col lg:flex-row gap-2 lg:gap-8">
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const StatSectionSeparator = () => {
|
||||
return <div className="hidden md:block border-r border-default" />;
|
||||
};
|
||||
|
||||
const StatList = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<dl className="grid grid-cols-[min-content_min-content] md:flex gap-4 md:gap-6 lg:gap-8 whitespace-nowrap">
|
||||
{children}
|
||||
</dl>
|
||||
);
|
||||
};
|
||||
|
||||
const Stat = ({
|
||||
value,
|
||||
label,
|
||||
tooltip,
|
||||
}: {
|
||||
value: ReactNode;
|
||||
label: ReactNode;
|
||||
tooltip?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<dd className="text-3xl lg:text-4xl">{value}</dd>
|
||||
<dt className="text-sm text-muted">
|
||||
{tooltip ? (
|
||||
<Tooltip description={tooltip} underline={false}>
|
||||
<span className="flex items-center gap-2">
|
||||
{label}
|
||||
<VegaIcon name={VegaIconNames.INFO} size={12} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
label
|
||||
)}
|
||||
</dt>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import compact from 'lodash/compact';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import {
|
||||
useTeamQuery,
|
||||
type TeamFieldsFragment,
|
||||
type TeamStatsFieldsFragment,
|
||||
type TeamRefereeFieldsFragment,
|
||||
type TeamEntityFragment,
|
||||
} from './__generated__/Team';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
|
||||
export type Team = TeamFieldsFragment;
|
||||
export type TeamStats = TeamStatsFieldsFragment;
|
||||
export type Member = TeamRefereeFieldsFragment;
|
||||
export type TeamEntity = TeamEntityFragment;
|
||||
export type TeamGame = ReturnType<typeof useTeam>['games'][number];
|
||||
|
||||
export const useTeam = (teamId?: string) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const { data, loading, error } = useTeamQuery({
|
||||
variables: { teamId: teamId || '', partyId: pubKey },
|
||||
skip: !teamId,
|
||||
});
|
||||
|
||||
const teamEdge = data?.teams?.edges.find((e) => e.node.teamId === teamId);
|
||||
const partyTeamEdge = data?.partyTeams?.edges[0];
|
||||
const teamStatsEdge = data?.teamsStatistics?.edges.find(
|
||||
(e) => e.node.teamId === teamId
|
||||
);
|
||||
const members = data?.teamReferees?.edges
|
||||
.filter((e) => e.node.teamId === teamId)
|
||||
.map((e) => e.node);
|
||||
|
||||
// Find games where the current team participated in
|
||||
const gamesWithTeam = compact(data?.games.edges).map((edge) => {
|
||||
const team = edge.node.entities.find((e) => {
|
||||
if (e.__typename !== 'TeamGameEntity') return false;
|
||||
if (e.team.teamId !== teamId) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!team) return null;
|
||||
|
||||
return {
|
||||
id: edge.node.id,
|
||||
epoch: edge.node.epoch,
|
||||
numberOfParticipants: edge.node.numberOfParticipants,
|
||||
team: team as TeamEntity, // TS can't infer that all the game entities are teams
|
||||
};
|
||||
});
|
||||
const games = orderBy(compact(gamesWithTeam), 'epoch', 'desc');
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
stats: teamStatsEdge?.node,
|
||||
team: teamEdge?.node,
|
||||
members,
|
||||
games,
|
||||
partyInTeam: Boolean(partyTeamEdge),
|
||||
};
|
||||
};
|
||||
@@ -13,6 +13,7 @@ import { ViewType, useSidebar } from '../sidebar';
|
||||
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
|
||||
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { usePersistentDepositStore } from '@vegaprotocol/deposits';
|
||||
|
||||
export const AccountsContainer = ({
|
||||
pinnedAsset,
|
||||
@@ -25,6 +26,7 @@ export const AccountsContainer = ({
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
const currentRouteId = useGetCurrentRouteId();
|
||||
const setViews = useSidebar((store) => store.setViews);
|
||||
const setDepositAsset = usePersistentDepositStore((store) => store.saveValue);
|
||||
|
||||
const gridStore = useAccountStore((store) => store.gridStore);
|
||||
const updateGridStore = useAccountStore((store) => store.updateGridStore);
|
||||
@@ -55,7 +57,10 @@ export const AccountsContainer = ({
|
||||
setViews({ type: ViewType.Withdraw, assetId }, currentRouteId);
|
||||
}}
|
||||
onClickDeposit={(assetId) => {
|
||||
setViews({ type: ViewType.Deposit, assetId }, currentRouteId);
|
||||
setViews({ type: ViewType.Deposit }, currentRouteId);
|
||||
if (assetId) {
|
||||
setDepositAsset({ assetId });
|
||||
}
|
||||
}}
|
||||
onClickTransfer={(assetId) => {
|
||||
setViews({ type: ViewType.Transfer, assetId }, currentRouteId);
|
||||
@@ -68,7 +73,7 @@ export const AccountsContainer = ({
|
||||
);
|
||||
};
|
||||
|
||||
const useAccountStore = create<DataGridSlice>()(
|
||||
export const useAccountStore = create<DataGridSlice>()(
|
||||
persist(createDataGridSlice, {
|
||||
name: 'vega_accounts_store',
|
||||
})
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { GridSettings } from '../grid-settings/grid-settings';
|
||||
import { useAccountStore } from './accounts-container';
|
||||
|
||||
export const AccountsSettings = () => (
|
||||
<GridSettings
|
||||
updateGridStore={useAccountStore((store) => store.updateGridStore)}
|
||||
/>
|
||||
);
|
||||
@@ -1 +1,2 @@
|
||||
export * from './accounts-container';
|
||||
export * from './accounts-settings';
|
||||
|
||||
@@ -12,6 +12,7 @@ import { VegaWalletProvider } from '@vegaprotocol/wallet';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Web3Provider } from './web3-provider';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { DataLoader } from './data-loader';
|
||||
|
||||
export const Bootstrapper = ({ children }: { children: ReactNode }) => {
|
||||
const t = useT();
|
||||
@@ -52,28 +53,38 @@ export const Bootstrapper = ({ children }: { children: ReactNode }) => {
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Web3Provider
|
||||
<DataLoader
|
||||
skeleton={<AppLoader />}
|
||||
failure={
|
||||
<AppFailure title={t('Could not configure web3 provider')} />
|
||||
<AppFailure
|
||||
title={t('Could not load market data or asset data')}
|
||||
error={error}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<VegaWalletProvider
|
||||
config={{
|
||||
network: VEGA_ENV,
|
||||
vegaUrl: VEGA_URL,
|
||||
vegaWalletServiceUrl: VEGA_WALLET_URL,
|
||||
links: {
|
||||
explorer: VEGA_EXPLORER_URL,
|
||||
concepts: DocsLinks.VEGA_WALLET_CONCEPTS_URL,
|
||||
chromeExtensionUrl: CHROME_EXTENSION_URL,
|
||||
mozillaExtensionUrl: MOZILLA_EXTENSION_URL,
|
||||
},
|
||||
}}
|
||||
<Web3Provider
|
||||
skeleton={<AppLoader />}
|
||||
failure={
|
||||
<AppFailure title={t('Could not configure web3 provider')} />
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</VegaWalletProvider>
|
||||
</Web3Provider>
|
||||
<VegaWalletProvider
|
||||
config={{
|
||||
network: VEGA_ENV,
|
||||
vegaUrl: VEGA_URL,
|
||||
vegaWalletServiceUrl: VEGA_WALLET_URL,
|
||||
links: {
|
||||
explorer: VEGA_EXPLORER_URL,
|
||||
concepts: DocsLinks.VEGA_WALLET_CONCEPTS_URL,
|
||||
chromeExtensionUrl: CHROME_EXTENSION_URL,
|
||||
mozillaExtensionUrl: MOZILLA_EXTENSION_URL,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</VegaWalletProvider>
|
||||
</Web3Provider>
|
||||
</DataLoader>
|
||||
</NodeGuard>
|
||||
</NetworkLoader>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useAssetsMapProvider } from '@vegaprotocol/assets';
|
||||
import { useMarketsMapProvider } from '@vegaprotocol/markets';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export const DataLoader = ({
|
||||
children,
|
||||
failure,
|
||||
skeleton,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
failure: ReactNode;
|
||||
skeleton: ReactNode;
|
||||
}) => {
|
||||
// Query all markets and assets to ensure they are cached
|
||||
const { data: markets, error, loading } = useMarketsMapProvider();
|
||||
const {
|
||||
data: assets,
|
||||
error: errorAssets,
|
||||
loading: loadingAssets,
|
||||
} = useAssetsMapProvider();
|
||||
|
||||
if (loading || loadingAssets) {
|
||||
// eslint-disable-next-line
|
||||
return <>{skeleton}</>;
|
||||
}
|
||||
|
||||
if (error || errorAssets || !markets || !assets) {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{failure}</>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{children}</>;
|
||||
};
|
||||
@@ -8,25 +8,42 @@ export const Card = ({
|
||||
className,
|
||||
loading = false,
|
||||
highlight = false,
|
||||
testId,
|
||||
noBackgroundOnMobile = false,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
title: string;
|
||||
className?: string;
|
||||
loading?: boolean;
|
||||
highlight?: boolean;
|
||||
testId?: string;
|
||||
noBackgroundOnMobile?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
data-testid={testId}
|
||||
className={classNames(
|
||||
'bg-vega-clight-800 dark:bg-vega-cdark-800 col-span-full p-0.5 lg:col-auto',
|
||||
'rounded-lg',
|
||||
'col-span-full lg:col-auto',
|
||||
{
|
||||
'rounded-lg bg-vega-clight-800 dark:bg-vega-cdark-800 p-0.5':
|
||||
!noBackgroundOnMobile,
|
||||
'mt-3 md:mt-0 md:rounded-lg md:bg-vega-clight-800 md:dark:bg-vega-cdark-800 md:p-0.5':
|
||||
noBackgroundOnMobile,
|
||||
},
|
||||
{
|
||||
'bg-rainbow': highlight,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="bg-vega-clight-800 dark:bg-vega-cdark-800 h-full w-full rounded p-4">
|
||||
<div
|
||||
className={classNames('h-full w-full', {
|
||||
'bg-vega-clight-800 dark:bg-vega-cdark-800 rounded p-4':
|
||||
!noBackgroundOnMobile,
|
||||
'md:bg-vega-clight-800 md:dark:bg-vega-cdark-800 md:rounded md:p-4':
|
||||
noBackgroundOnMobile,
|
||||
})}
|
||||
>
|
||||
<h2 className="mb-3">{title}</h2>
|
||||
{loading ? <CardLoader /> : children}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ChartContainer } from './chart-container';
|
||||
import { useChartSettingsStore } from './use-chart-settings';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
|
||||
jest.mock('@vegaprotocol/candles-chart', () => ({
|
||||
...jest.requireActual('@vegaprotocol/candles-chart'),
|
||||
CandlesChartContainer: ({ marketId }: { marketId: string }) => (
|
||||
<div data-testid="pennant">{marketId}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('@vegaprotocol/trading-view', () => ({
|
||||
...jest.requireActual('@vegaprotocol/trading-view'),
|
||||
TradingViewContainer: ({ marketId }: { marketId: string }) => (
|
||||
<div data-testid="tradingview">{marketId}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe('ChartContainer', () => {
|
||||
it('renders pennant if no library path is set', () => {
|
||||
useChartSettingsStore.setState({
|
||||
chartlib: 'tradingview',
|
||||
});
|
||||
|
||||
useEnvironment.setState({
|
||||
CHARTING_LIBRARY_PATH: undefined,
|
||||
CHARTING_LIBRARY_HASH: undefined,
|
||||
});
|
||||
|
||||
const marketId = 'market-id';
|
||||
|
||||
render(<ChartContainer marketId={marketId} />);
|
||||
|
||||
expect(screen.getByTestId('pennant')).toHaveTextContent(marketId);
|
||||
});
|
||||
|
||||
it('renders trading view if library path is set', () => {
|
||||
useChartSettingsStore.setState({
|
||||
chartlib: 'tradingview',
|
||||
});
|
||||
|
||||
useEnvironment.setState({
|
||||
CHARTING_LIBRARY_PATH: 'dummy-path',
|
||||
CHARTING_LIBRARY_HASH: 'hash',
|
||||
});
|
||||
|
||||
const marketId = 'market-id';
|
||||
|
||||
render(<ChartContainer marketId={marketId} />);
|
||||
|
||||
expect(screen.getByTestId('tradingview')).toHaveTextContent(marketId);
|
||||
});
|
||||
|
||||
it('renders pennant chart if stored in settings', () => {
|
||||
useChartSettingsStore.setState({
|
||||
chartlib: 'pennant',
|
||||
});
|
||||
|
||||
const marketId = 'market-id';
|
||||
|
||||
render(<ChartContainer marketId={marketId} />);
|
||||
|
||||
expect(screen.getByTestId('pennant')).toHaveTextContent(marketId);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
import invert from 'lodash/invert';
|
||||
import { type Interval } from '@vegaprotocol/types';
|
||||
import {
|
||||
TradingViewContainer,
|
||||
ALLOWED_TRADINGVIEW_HOSTNAMES,
|
||||
TRADINGVIEW_INTERVAL_MAP,
|
||||
} from '@vegaprotocol/trading-view';
|
||||
import {
|
||||
CandlesChartContainer,
|
||||
PENNANT_INTERVAL_MAP,
|
||||
} from '@vegaprotocol/candles-chart';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { useChartSettings, STUDY_SIZE } from './use-chart-settings';
|
||||
|
||||
/**
|
||||
* Renders either the pennant chart or the tradingview chart
|
||||
*/
|
||||
export const ChartContainer = ({ marketId }: { marketId: string }) => {
|
||||
const { CHARTING_LIBRARY_PATH, CHARTING_LIBRARY_HASH } = useEnvironment();
|
||||
|
||||
const {
|
||||
chartlib,
|
||||
interval,
|
||||
chartType,
|
||||
overlays,
|
||||
studies,
|
||||
studySizes,
|
||||
tradingViewStudies,
|
||||
setInterval,
|
||||
setStudies,
|
||||
setStudySizes,
|
||||
setOverlays,
|
||||
setTradingViewStudies,
|
||||
} = useChartSettings();
|
||||
|
||||
const pennantChart = (
|
||||
<CandlesChartContainer
|
||||
marketId={marketId}
|
||||
interval={toPennantInterval(interval)}
|
||||
chartType={chartType}
|
||||
overlays={overlays}
|
||||
studies={studies}
|
||||
studySizes={studySizes}
|
||||
setStudySizes={setStudySizes}
|
||||
setStudies={setStudies}
|
||||
setOverlays={setOverlays}
|
||||
defaultStudySize={STUDY_SIZE}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!ALLOWED_TRADINGVIEW_HOSTNAMES.includes(window.location.hostname)) {
|
||||
return pennantChart;
|
||||
}
|
||||
|
||||
if (!CHARTING_LIBRARY_PATH || !CHARTING_LIBRARY_HASH) {
|
||||
return pennantChart;
|
||||
}
|
||||
|
||||
switch (chartlib) {
|
||||
case 'tradingview': {
|
||||
return (
|
||||
<TradingViewContainer
|
||||
libraryPath={CHARTING_LIBRARY_PATH}
|
||||
libraryHash={CHARTING_LIBRARY_HASH}
|
||||
marketId={marketId}
|
||||
interval={toTradingViewResolution(interval)}
|
||||
studies={tradingViewStudies}
|
||||
onIntervalChange={(newInterval) => {
|
||||
setInterval(fromTradingViewResolution(newInterval));
|
||||
}}
|
||||
onAutoSaveNeeded={(data: { studies: string[] }) => {
|
||||
setTradingViewStudies(data.studies);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case 'pennant': {
|
||||
return pennantChart;
|
||||
}
|
||||
default: {
|
||||
throw new Error('invalid chart lib');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toTradingViewResolution = (interval: Interval) => {
|
||||
const resolution = TRADINGVIEW_INTERVAL_MAP[interval];
|
||||
|
||||
if (!resolution) {
|
||||
throw new Error(
|
||||
`failed to convert interval: ${interval} to valid resolution`
|
||||
);
|
||||
}
|
||||
|
||||
return resolution;
|
||||
};
|
||||
|
||||
const fromTradingViewResolution = (resolution: string) => {
|
||||
const interval = invert(TRADINGVIEW_INTERVAL_MAP)[resolution];
|
||||
|
||||
if (!interval) {
|
||||
throw new Error(
|
||||
`failed to convert resolution: ${resolution} to valid interval`
|
||||
);
|
||||
}
|
||||
|
||||
return interval as Interval;
|
||||
};
|
||||
|
||||
const toPennantInterval = (interval: Interval) => {
|
||||
const pennantInterval = PENNANT_INTERVAL_MAP[interval];
|
||||
|
||||
if (!pennantInterval) {
|
||||
throw new Error(
|
||||
`failed to convert interval: ${interval} to valid pennant interval`
|
||||
);
|
||||
}
|
||||
|
||||
return pennantInterval;
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { ChartMenu } from './chart-menu';
|
||||
import {
|
||||
useChartSettingsStore,
|
||||
DEFAULT_CHART_SETTINGS,
|
||||
} from './use-chart-settings';
|
||||
import { Overlay, Study, overlayLabels, studyLabels } from 'pennant';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
|
||||
describe('ChartMenu', () => {
|
||||
it('doesnt show trading view option if library path undefined', () => {
|
||||
useEnvironment.setState({ CHARTING_LIBRARY_PATH: undefined });
|
||||
|
||||
render(<ChartMenu />);
|
||||
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'TradingView' })
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'Vega chart' })
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('can switch between charts if library path', async () => {
|
||||
useEnvironment.setState({ CHARTING_LIBRARY_PATH: 'dummy' });
|
||||
|
||||
render(<ChartMenu />);
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'TradingView' }));
|
||||
expect(useChartSettingsStore.getState().chartlib).toEqual('tradingview');
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Vega chart' }));
|
||||
expect(useChartSettingsStore.getState().chartlib).toEqual('pennant');
|
||||
});
|
||||
|
||||
describe('tradingview', () => {
|
||||
beforeEach(() => {
|
||||
useEnvironment.setState({ CHARTING_LIBRARY_PATH: 'dummy-path' });
|
||||
|
||||
// clear store each time to avoid conditional testing of defaults
|
||||
useChartSettingsStore.setState({
|
||||
chartlib: 'tradingview',
|
||||
});
|
||||
});
|
||||
|
||||
it('only shows chartlib switch and attribution', () => {
|
||||
render(<ChartMenu />);
|
||||
|
||||
const buttons = screen.getAllByRole('button');
|
||||
expect(buttons).toHaveLength(1);
|
||||
expect(buttons[0]).toHaveTextContent('Vega chart');
|
||||
|
||||
expect(screen.getByText('Chart by')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('pennant', () => {
|
||||
const openDropdown = async () => {
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Indicators',
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// clear store each time to avoid conditional testing of defaults
|
||||
useChartSettingsStore.setState({
|
||||
chartlib: 'pennant',
|
||||
overlays: [],
|
||||
studies: [],
|
||||
});
|
||||
});
|
||||
|
||||
it.each(Object.values(Overlay))('can set %s overlay', async (overlay) => {
|
||||
render(<ChartMenu />);
|
||||
|
||||
await openDropdown();
|
||||
|
||||
const menu = within(await screen.findByRole('menu'));
|
||||
|
||||
await userEvent.click(menu.getByText(overlayLabels[overlay as Overlay]));
|
||||
|
||||
// re-open the dropdown
|
||||
await openDropdown();
|
||||
|
||||
expect(
|
||||
screen.getByText(overlayLabels[overlay as Overlay])
|
||||
).toHaveAttribute('data-state', 'checked');
|
||||
});
|
||||
|
||||
it.each(Object.values(Study))('can set %s study', async (study) => {
|
||||
render(<ChartMenu />);
|
||||
|
||||
await openDropdown();
|
||||
|
||||
const menu = within(await screen.findByRole('menu'));
|
||||
|
||||
await userEvent.click(menu.getByText(studyLabels[study as Study]));
|
||||
|
||||
// re-open the dropdown
|
||||
await openDropdown();
|
||||
|
||||
expect(screen.getByText(studyLabels[study as Study])).toHaveAttribute(
|
||||
'data-state',
|
||||
'checked'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render with the correct default studies and overlays', async () => {
|
||||
useChartSettingsStore.setState({
|
||||
...DEFAULT_CHART_SETTINGS,
|
||||
chartlib: 'pennant',
|
||||
});
|
||||
|
||||
render(<ChartMenu />);
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: 'Indicators',
|
||||
})
|
||||
);
|
||||
const menu = within(await screen.findByRole('menu'));
|
||||
|
||||
expect(menu.getByText(studyLabels.volume)).toHaveAttribute(
|
||||
'data-state',
|
||||
'checked'
|
||||
);
|
||||
expect(menu.getByText(studyLabels.macd)).toHaveAttribute(
|
||||
'data-state',
|
||||
'checked'
|
||||
);
|
||||
expect(menu.getByText(overlayLabels.movingAverage)).toHaveAttribute(
|
||||
'data-state',
|
||||
'checked'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
+85
-14
@@ -1,14 +1,12 @@
|
||||
import 'pennant/dist/style.css';
|
||||
import {
|
||||
ChartType,
|
||||
Interval,
|
||||
Overlay,
|
||||
Study,
|
||||
chartTypeLabels,
|
||||
intervalLabels,
|
||||
overlayLabels,
|
||||
studyLabels,
|
||||
} from 'pennant';
|
||||
import { Trans } from 'react-i18next';
|
||||
import {
|
||||
TradingButton,
|
||||
TradingDropdown,
|
||||
@@ -20,10 +18,21 @@ import {
|
||||
TradingDropdownTrigger,
|
||||
Icon,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { type IconName } from '@blueprintjs/icons';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import { useCandlesChartSettings } from './use-candles-chart-settings';
|
||||
import { useT } from './use-t';
|
||||
import { Interval } from '@vegaprotocol/types';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { ALLOWED_TRADINGVIEW_HOSTNAMES } from '@vegaprotocol/trading-view';
|
||||
import { IconNames, type IconName } from '@blueprintjs/icons';
|
||||
import { useChartSettings } from './use-chart-settings';
|
||||
import { useT } from '../../lib/use-t';
|
||||
|
||||
const INTERVALS = [
|
||||
Interval.INTERVAL_I1M,
|
||||
Interval.INTERVAL_I5M,
|
||||
Interval.INTERVAL_I15M,
|
||||
Interval.INTERVAL_I1H,
|
||||
Interval.INTERVAL_I6H,
|
||||
Interval.INTERVAL_I1D,
|
||||
];
|
||||
|
||||
const chartTypeIcon = new Map<ChartType, IconName>([
|
||||
[ChartType.AREA, IconNames.TIMELINE_AREA_CHART],
|
||||
@@ -32,30 +41,46 @@ const chartTypeIcon = new Map<ChartType, IconName>([
|
||||
[ChartType.OHLC, IconNames.WATERFALL_CHART],
|
||||
]);
|
||||
|
||||
export const CandlesMenu = () => {
|
||||
export const ChartMenu = () => {
|
||||
const { CHARTING_LIBRARY_PATH } = useEnvironment();
|
||||
const {
|
||||
chartlib,
|
||||
interval,
|
||||
chartType,
|
||||
studies,
|
||||
overlays,
|
||||
setChartlib,
|
||||
setInterval,
|
||||
setType,
|
||||
setStudies,
|
||||
setOverlays,
|
||||
} = useCandlesChartSettings();
|
||||
} = useChartSettings();
|
||||
const t = useT();
|
||||
const triggerClasses = 'text-xs';
|
||||
|
||||
const contentAlign = 'end';
|
||||
const triggerClasses = 'text-xs';
|
||||
const triggerButtonProps = { size: 'extra-small' } as const;
|
||||
|
||||
return (
|
||||
const isPennant = chartlib === 'pennant';
|
||||
const commonMenuItems = (
|
||||
<TradingButton
|
||||
onClick={() => {
|
||||
setChartlib(isPennant ? 'tradingview' : 'pennant');
|
||||
}}
|
||||
size="extra-small"
|
||||
>
|
||||
{isPennant ? 'TradingView' : t('Vega chart')}
|
||||
</TradingButton>
|
||||
);
|
||||
|
||||
const pennantMenuItems = (
|
||||
<>
|
||||
<TradingDropdown
|
||||
trigger={
|
||||
<TradingDropdownTrigger className={triggerClasses}>
|
||||
<TradingButton {...triggerButtonProps}>
|
||||
{t('Interval: {{interval}}', {
|
||||
interval: intervalLabels[interval],
|
||||
interval: t(interval),
|
||||
})}
|
||||
</TradingButton>
|
||||
</TradingDropdownTrigger>
|
||||
@@ -68,13 +93,13 @@ export const CandlesMenu = () => {
|
||||
setInterval(value as Interval);
|
||||
}}
|
||||
>
|
||||
{Object.values(Interval).map((timeInterval) => (
|
||||
{INTERVALS.map((timeInterval) => (
|
||||
<TradingDropdownRadioItem
|
||||
key={timeInterval}
|
||||
inset
|
||||
value={timeInterval}
|
||||
>
|
||||
{intervalLabels[timeInterval]}
|
||||
{t(timeInterval)}
|
||||
<TradingDropdownItemIndicator />
|
||||
</TradingDropdownRadioItem>
|
||||
))}
|
||||
@@ -158,4 +183,50 @@ export const CandlesMenu = () => {
|
||||
</TradingDropdown>
|
||||
</>
|
||||
);
|
||||
|
||||
const tradingViewMenuItems = (
|
||||
<p className="text-xs mr-2 whitespace-nowrap">
|
||||
<Trans
|
||||
i18nKey="Chart by <0>TradingView</0>"
|
||||
components={[
|
||||
// eslint-disable-next-line
|
||||
<a
|
||||
className="underline"
|
||||
target="_blank"
|
||||
href="https://www.tradingview.com"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
|
||||
if (!ALLOWED_TRADINGVIEW_HOSTNAMES.includes(window.location.hostname)) {
|
||||
return pennantMenuItems;
|
||||
}
|
||||
|
||||
if (!CHARTING_LIBRARY_PATH) {
|
||||
return pennantMenuItems;
|
||||
}
|
||||
|
||||
switch (chartlib) {
|
||||
case 'tradingview': {
|
||||
return (
|
||||
<>
|
||||
{tradingViewMenuItems}
|
||||
{commonMenuItems}
|
||||
</>
|
||||
);
|
||||
}
|
||||
case 'pennant': {
|
||||
return (
|
||||
<>
|
||||
{pennantMenuItems}
|
||||
{commonMenuItems}
|
||||
</>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
throw new Error('invalid chart lib');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { ChartContainer } from './chart-container';
|
||||
export { ChartMenu } from './chart-menu';
|
||||
+31
-8
@@ -1,18 +1,23 @@
|
||||
import { getValidItem, getValidSubset } from '@vegaprotocol/react-helpers';
|
||||
import { ChartType, Interval, Study } from 'pennant';
|
||||
import { Overlay } from 'pennant';
|
||||
import { ChartType, Overlay, Study } from 'pennant';
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { Interval } from '@vegaprotocol/types';
|
||||
import { getValidItem, getValidSubset } from '@vegaprotocol/react-helpers';
|
||||
|
||||
type StudySizes = { [S in Study]?: number };
|
||||
export type Chartlib = 'pennant' | 'tradingview';
|
||||
|
||||
interface StoredSettings {
|
||||
chartlib: Chartlib;
|
||||
// For interval we use the enum from @vegaprotocol/types, this is to make mapping between different
|
||||
// chart types easier and more consistent
|
||||
interval: Interval;
|
||||
type: ChartType;
|
||||
overlays: Overlay[];
|
||||
studies: Study[];
|
||||
studySizes: StudySizes;
|
||||
tradingViewStudies: string[];
|
||||
}
|
||||
|
||||
export const STUDY_SIZE = 90;
|
||||
@@ -25,20 +30,24 @@ const STUDY_ORDER: Study[] = [
|
||||
];
|
||||
|
||||
export const DEFAULT_CHART_SETTINGS = {
|
||||
interval: Interval.I15M,
|
||||
chartlib: 'pennant' as const,
|
||||
interval: Interval.INTERVAL_I15M,
|
||||
type: ChartType.CANDLE,
|
||||
overlays: [Overlay.MOVING_AVERAGE],
|
||||
studies: [Study.MACD, Study.VOLUME],
|
||||
studySizes: {},
|
||||
tradingViewStudies: ['Volume'],
|
||||
};
|
||||
|
||||
export const useCandlesChartSettingsStore = create<
|
||||
export const useChartSettingsStore = create<
|
||||
StoredSettings & {
|
||||
setType: (type: ChartType) => void;
|
||||
setInterval: (interval: Interval) => void;
|
||||
setOverlays: (overlays?: Overlay[]) => void;
|
||||
setStudies: (studies?: Study[]) => void;
|
||||
setStudySizes: (sizes: number[]) => void;
|
||||
setChartlib: (lib: Chartlib) => void;
|
||||
setTradingViewStudies: (studies: string[]) => void;
|
||||
}
|
||||
>()(
|
||||
persist(
|
||||
@@ -81,6 +90,16 @@ export const useCandlesChartSettingsStore = create<
|
||||
});
|
||||
});
|
||||
},
|
||||
setChartlib: (lib) => {
|
||||
set((state) => {
|
||||
state.chartlib = lib;
|
||||
});
|
||||
},
|
||||
setTradingViewStudies: (studies: string[]) => {
|
||||
set((state) => {
|
||||
state.tradingViewStudies = studies;
|
||||
});
|
||||
},
|
||||
})),
|
||||
{
|
||||
name: 'vega_candles_chart_store',
|
||||
@@ -88,13 +107,13 @@ export const useCandlesChartSettingsStore = create<
|
||||
)
|
||||
);
|
||||
|
||||
export const useCandlesChartSettings = () => {
|
||||
const settings = useCandlesChartSettingsStore();
|
||||
export const useChartSettings = () => {
|
||||
const settings = useChartSettingsStore();
|
||||
|
||||
const interval: Interval = getValidItem(
|
||||
settings.interval,
|
||||
Object.values(Interval),
|
||||
Interval.I15M
|
||||
Interval.INTERVAL_I15M
|
||||
);
|
||||
|
||||
const chartType: ChartType = getValidItem(
|
||||
@@ -122,15 +141,19 @@ export const useCandlesChartSettings = () => {
|
||||
});
|
||||
|
||||
return {
|
||||
chartlib: settings.chartlib,
|
||||
interval,
|
||||
chartType,
|
||||
overlays,
|
||||
studies,
|
||||
studySizes,
|
||||
tradingViewStudies: settings.tradingViewStudies,
|
||||
setInterval: settings.setInterval,
|
||||
setType: settings.setType,
|
||||
setStudies: settings.setStudies,
|
||||
setOverlays: settings.setOverlays,
|
||||
setStudySizes: settings.setStudySizes,
|
||||
setChartlib: settings.setChartlib,
|
||||
setTradingViewStudies: settings.setTradingViewStudies,
|
||||
};
|
||||
};
|
||||
@@ -13,7 +13,7 @@ import { MarketFees } from './market-fees';
|
||||
import { useVolumeStats } from './use-volume-stats';
|
||||
import { useReferralStats } from './use-referral-stats';
|
||||
import { formatPercentage, getAdjustedFee } from './utils';
|
||||
import { Table, Td, Th, THead, Tr } from './table';
|
||||
import { Table as SimpleTable } from '../../components/table';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { Links } from '../../lib/links';
|
||||
import { Link } from 'react-router-dom';
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
truncateMiddle,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import classNames from 'classnames';
|
||||
import { getTierGradient } from '../helpers/tiers';
|
||||
|
||||
export const FeesContainer = () => {
|
||||
const t = useT();
|
||||
@@ -36,7 +38,11 @@ export const FeesContainer = () => {
|
||||
const { data: markets, loading: marketsLoading } = useMarketList();
|
||||
|
||||
const { data: programData, loading: programLoading } =
|
||||
useDiscountProgramsQuery({ errorPolicy: 'ignore' });
|
||||
useDiscountProgramsQuery({
|
||||
errorPolicy: 'ignore',
|
||||
fetchPolicy: 'cache-and-network',
|
||||
pollInterval: 15000,
|
||||
});
|
||||
|
||||
const volumeDiscountWindowLength =
|
||||
programData?.currentVolumeDiscountProgram?.windowLength || 1;
|
||||
@@ -47,6 +53,8 @@ export const FeesContainer = () => {
|
||||
partyId: pubKey || '',
|
||||
},
|
||||
skip: !pubKey,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
pollInterval: 15000,
|
||||
});
|
||||
|
||||
const previousEpoch = (Number(feesData?.epoch.id) || 0) - 1;
|
||||
@@ -84,7 +92,10 @@ export const FeesContainer = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid auto-rows-min grid-cols-4 gap-3">
|
||||
<div
|
||||
className="grid auto-rows-min grid-cols-4 gap-3"
|
||||
data-testid="fees-container"
|
||||
>
|
||||
{isConnected && (
|
||||
<>
|
||||
<Card
|
||||
@@ -124,7 +135,10 @@ export const FeesContainer = () => {
|
||||
windowLength={volumeDiscountWindowLength}
|
||||
/>
|
||||
) : (
|
||||
<p className="text-muted pt-3 text-sm">
|
||||
<p
|
||||
className="text-muted pt-3 text-sm"
|
||||
data-testid="no-volume-discount"
|
||||
>
|
||||
{t('No volume discount program active')}
|
||||
</p>
|
||||
)}
|
||||
@@ -133,17 +147,22 @@ export const FeesContainer = () => {
|
||||
title={t('Referral benefits')}
|
||||
className="sm:col-span-2"
|
||||
loading={loading}
|
||||
data-testid="referral-benefits-card"
|
||||
>
|
||||
{isReferrer ? (
|
||||
<ReferrerInfo code={code} />
|
||||
<ReferrerInfo code={code} data-testid="referrer-info" />
|
||||
) : isReferralProgramRunning ? (
|
||||
<ReferralBenefits
|
||||
setRunningNotionalTakerVolume={referralVolumeInWindow}
|
||||
epochsInSet={epochsInSet}
|
||||
epochs={referralDiscountWindowLength}
|
||||
data-testid="referral-benefits"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-muted pt-3 text-sm">
|
||||
<p
|
||||
className="text-muted pt-3 text-sm"
|
||||
data-testid="no-referral-program"
|
||||
>
|
||||
{t('No referral program active')}
|
||||
</p>
|
||||
)}
|
||||
@@ -154,6 +173,8 @@ export const FeesContainer = () => {
|
||||
title={t('Volume discount')}
|
||||
className="lg:col-span-full xl:col-span-2"
|
||||
loading={loading}
|
||||
data-testid="volume-discount-card"
|
||||
noBackgroundOnMobile={true}
|
||||
>
|
||||
<VolumeTiers
|
||||
tiers={volumeTiers}
|
||||
@@ -166,18 +187,23 @@ export const FeesContainer = () => {
|
||||
title={t('Referral discount')}
|
||||
className="lg:col-span-full xl:col-span-2"
|
||||
loading={loading}
|
||||
data-testid="referral-discount-card"
|
||||
noBackgroundOnMobile={true}
|
||||
>
|
||||
<ReferralTiers
|
||||
tiers={referralTiers}
|
||||
tierIndex={referralTierIndex}
|
||||
epochsInSet={epochsInSet}
|
||||
referralVolumeInWindow={referralVolumeInWindow}
|
||||
referralDiscountWindowLength={referralDiscountWindowLength}
|
||||
/>
|
||||
</Card>
|
||||
<Card
|
||||
title={t('Fees by market')}
|
||||
className="lg:col-span-full"
|
||||
loading={marketsLoading}
|
||||
data-testid="fees-by-market-card"
|
||||
noBackgroundOnMobile={true}
|
||||
>
|
||||
<MarketFees
|
||||
markets={markets}
|
||||
@@ -245,7 +271,7 @@ export const TradingFees = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pt-4">
|
||||
<div className="pt-4" data-testid="trading-fees">
|
||||
<div className="leading-none">
|
||||
<p className="block text-3xl leading-none" data-testid="adjusted-fees">
|
||||
{minAdjustedTotal !== undefined && maxAdjustedTotal !== undefined
|
||||
@@ -255,7 +281,7 @@ export const TradingFees = ({
|
||||
: `${formatPercentage(adjustedTotal)}%`}
|
||||
</p>
|
||||
<CardTable>
|
||||
<tr className="text-default">
|
||||
<tr className="text-default" data-testid="total-fee-before-discount">
|
||||
<CardTableTH>{t('Total fee before discount')}</CardTableTH>
|
||||
<CardTableTD>
|
||||
{minTotal !== undefined && maxTotal !== undefined
|
||||
@@ -265,7 +291,7 @@ export const TradingFees = ({
|
||||
: `${formatPercentage(total.toNumber())}%`}
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr data-testid="infrastructure-fees">
|
||||
<CardTableTH>{t('Infrastructure')}</CardTableTH>
|
||||
<CardTableTD>
|
||||
{formatPercentage(
|
||||
@@ -274,14 +300,14 @@ export const TradingFees = ({
|
||||
%
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr data-testid="maker-fees">
|
||||
<CardTableTH>{t('Maker')}</CardTableTH>
|
||||
<CardTableTD>
|
||||
{formatPercentage(Number(params.market_fee_factors_makerFee))}%
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
{minLiq && maxLiq && (
|
||||
<tr>
|
||||
<tr data-testid="liquidity-fees">
|
||||
<CardTableTH>{t('Liquidity')}</CardTableTH>
|
||||
<CardTableTD>
|
||||
{formatPercentage(Number(minLiq.fees.factors.liquidityFee))}%
|
||||
@@ -317,21 +343,19 @@ export const CurrentVolume = ({
|
||||
const currentVolume = new BigNumber(windowLengthVolume);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 pt-4">
|
||||
<div className="flex flex-col gap-3 pt-4" data-testid="current-volume">
|
||||
<CardStat
|
||||
value={
|
||||
currentVolume.isZero()
|
||||
? `<${formatNumberRounded(requiredForNextTier)}`
|
||||
: formatNumberRounded(currentVolume)
|
||||
}
|
||||
value={formatNumberRounded(currentVolume)}
|
||||
text={t('pastEpochs', 'Past {{count}} epochs', {
|
||||
count: windowLength,
|
||||
})}
|
||||
testId="past-epochs-volume"
|
||||
/>
|
||||
{requiredForNextTier.isGreaterThan(0) && (
|
||||
<CardStat
|
||||
value={formatNumber(requiredForNextTier)}
|
||||
text={t('Required for next tier')}
|
||||
testId="required-for-next-tier"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -349,7 +373,7 @@ const ReferralBenefits = ({
|
||||
}) => {
|
||||
const t = useT();
|
||||
return (
|
||||
<div className="flex flex-col gap-3 pt-4">
|
||||
<div className="flex flex-col gap-3 pt-4" data-testid="referral-benefits">
|
||||
<CardStat
|
||||
// all sets volume (not just current party)
|
||||
value={formatNumber(setRunningNotionalTakerVolume)}
|
||||
@@ -360,8 +384,13 @@ const ReferralBenefits = ({
|
||||
count: epochs,
|
||||
}
|
||||
)}
|
||||
testId="running-notional-taker-volume"
|
||||
/>
|
||||
<CardStat
|
||||
value={epochsInSet}
|
||||
text={t('epochs in referral set')}
|
||||
testId="epochs-in-referral-set"
|
||||
/>
|
||||
<CardStat value={epochsInSet} text={t('epochs in referral set')} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -389,7 +418,7 @@ const TotalDiscount = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="pt-4">
|
||||
<div className="pt-4" data-testid="total-discount-card-stats">
|
||||
<CardStat
|
||||
description={
|
||||
<>
|
||||
@@ -399,9 +428,10 @@ const TotalDiscount = ({
|
||||
}
|
||||
value={formatPercentage(totalDiscount) + '%'}
|
||||
highlight={true}
|
||||
testId="total-discount"
|
||||
/>
|
||||
<CardTable>
|
||||
<tr>
|
||||
<tr data-testid="volume-discount-row">
|
||||
<CardTableTH>{t('Volume discount')}</CardTableTH>
|
||||
<CardTableTD>
|
||||
{formatPercentage(volumeDiscount)}%
|
||||
@@ -415,7 +445,7 @@ const TotalDiscount = ({
|
||||
)}
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr data-testid="referral-discount-row">
|
||||
<CardTableTH>{t('Referral discount')}</CardTableTH>
|
||||
<CardTableTD>
|
||||
{formatPercentage(referralDiscount)}%
|
||||
@@ -459,36 +489,65 @@ const VolumeTiers = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table>
|
||||
<THead>
|
||||
<tr>
|
||||
<Th>{t('Tier')}</Th>
|
||||
<Th>{t('Discount')}</Th>
|
||||
<Th>{t('Min. trading volume')}</Th>
|
||||
<Th>
|
||||
{t('myVolume', 'My volume (last {{count}} epochs)', {
|
||||
count: windowLength,
|
||||
})}
|
||||
</Th>
|
||||
<Th />
|
||||
</tr>
|
||||
</THead>
|
||||
<tbody>
|
||||
{Array.from(tiers).map((tier, i) => {
|
||||
const isUserTier = tierIndex === i;
|
||||
|
||||
return (
|
||||
<Tr key={i}>
|
||||
<Td>{i + 1}</Td>
|
||||
<Td>{formatPercentage(Number(tier.volumeDiscountFactor))}%</Td>
|
||||
<Td>{formatNumber(tier.minimumRunningNotionalTakerVolume)}</Td>
|
||||
<Td>{isUserTier ? formatNumber(lastEpochVolume) : ''}</Td>
|
||||
<Td>{isUserTier ? <YourTier /> : null}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
<SimpleTable
|
||||
className="bg-white dark:bg-vega-cdark-900"
|
||||
columns={[
|
||||
{ name: 'tier', displayName: t('Tier'), testId: 'col-tier-value' },
|
||||
{
|
||||
name: 'discount',
|
||||
displayName: t('Discount'),
|
||||
testId: 'discount-value',
|
||||
},
|
||||
{
|
||||
name: 'minTradingVolume',
|
||||
displayName: t('Min. trading volume'),
|
||||
testId: 'min-volume-value',
|
||||
},
|
||||
{
|
||||
name: 'myVolume',
|
||||
displayName: t('myVolume', 'My volume (last {{count}} epochs)', {
|
||||
count: windowLength,
|
||||
}),
|
||||
testId: 'my-volume-value',
|
||||
},
|
||||
{
|
||||
name: 'indicator',
|
||||
className: 'max-md:hidden',
|
||||
testId: 'your-tier',
|
||||
},
|
||||
]}
|
||||
data={Array.from(tiers).map((tier, i) => {
|
||||
const isUserTier = tierIndex === i;
|
||||
const indicator = isUserTier ? (
|
||||
<YourTier testId={`your-volume-tier-${i}`} />
|
||||
) : null;
|
||||
const tierIndicator = (
|
||||
<div className="flex justify-between">
|
||||
<span data-testid={`tier-value-${i}`}>{i + 1}</span>
|
||||
<span className="md:hidden">{indicator}</span>
|
||||
</div>
|
||||
);
|
||||
return {
|
||||
tier: tierIndicator,
|
||||
discount: (
|
||||
<>{formatPercentage(Number(tier.volumeDiscountFactor))}%</>
|
||||
),
|
||||
minTradingVolume: (
|
||||
<>{formatNumber(tier.minimumRunningNotionalTakerVolume)}</>
|
||||
),
|
||||
myVolume: isUserTier ? (
|
||||
formatNumber(lastEpochVolume)
|
||||
) : (
|
||||
<span className="md:hidden">-</span>
|
||||
),
|
||||
indicator: indicator,
|
||||
className: classNames(
|
||||
getTierGradient(i + 1, tiers.length),
|
||||
'text-xs'
|
||||
),
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -498,6 +557,7 @@ const ReferralTiers = ({
|
||||
tierIndex,
|
||||
epochsInSet,
|
||||
referralVolumeInWindow,
|
||||
referralDiscountWindowLength,
|
||||
}: {
|
||||
tiers: Array<{
|
||||
referralDiscountFactor: string;
|
||||
@@ -507,6 +567,7 @@ const ReferralTiers = ({
|
||||
tierIndex: number;
|
||||
epochsInSet: number;
|
||||
referralVolumeInWindow: number;
|
||||
referralDiscountWindowLength: number;
|
||||
}) => {
|
||||
const t = useT();
|
||||
|
||||
@@ -518,55 +579,97 @@ const ReferralTiers = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table>
|
||||
<THead>
|
||||
<tr>
|
||||
<Th>{t('Tier')}</Th>
|
||||
<Th>{t('Discount')}</Th>
|
||||
<Th>{t('Min. trading volume')}</Th>
|
||||
<Th>{t('Required epochs')}</Th>
|
||||
<Th />
|
||||
</tr>
|
||||
</THead>
|
||||
<tbody>
|
||||
{Array.from(tiers).map((t, i) => {
|
||||
const isUserTier = tierIndex === i;
|
||||
<SimpleTable
|
||||
className="bg-white dark:bg-vega-cdark-900"
|
||||
columns={[
|
||||
{ name: 'tier', displayName: t('Tier'), testId: 'col-tier-value' },
|
||||
{
|
||||
name: 'discount',
|
||||
displayName: t('Discount'),
|
||||
tooltip: t(
|
||||
"The proportion of the referee's taker fees to be discounted"
|
||||
),
|
||||
testId: 'discount-value',
|
||||
},
|
||||
{
|
||||
name: 'volume',
|
||||
displayName: t(
|
||||
'minTradingVolume',
|
||||
'Min. trading volume (last {{count}} epochs)',
|
||||
{
|
||||
count: referralDiscountWindowLength,
|
||||
}
|
||||
),
|
||||
tooltip: t(
|
||||
'The minimum running notional for the given benefit tier'
|
||||
),
|
||||
testId: 'min-volume-value',
|
||||
},
|
||||
{
|
||||
name: 'epochs',
|
||||
displayName: t('Min. epochs'),
|
||||
tooltip: t(
|
||||
'The minimum number of epochs the party needs to be in the referral set to be eligible for the benefit'
|
||||
),
|
||||
testId: 'required-epochs-value',
|
||||
},
|
||||
{
|
||||
name: 'indicator',
|
||||
className: 'max-md:hidden',
|
||||
testId: 'user-tier-or-unlocks',
|
||||
},
|
||||
]}
|
||||
data={Array.from(tiers).map((tier, i) => {
|
||||
const isUserTier = tierIndex === i;
|
||||
const requiredVolume = Number(tier.minimumRunningNotionalTakerVolume);
|
||||
|
||||
const requiredVolume = Number(t.minimumRunningNotionalTakerVolume);
|
||||
let unlocksIn = null;
|
||||
const indicator = isUserTier ? (
|
||||
<YourTier testId={`your-referral-tier-${i}`} />
|
||||
) : referralVolumeInWindow >= requiredVolume &&
|
||||
epochsInSet < tier.minimumEpochs ? (
|
||||
<span className="text-muted text-xs">
|
||||
Unlocks in {tier.minimumEpochs - epochsInSet} epochs
|
||||
</span>
|
||||
) : null;
|
||||
|
||||
if (
|
||||
referralVolumeInWindow >= requiredVolume &&
|
||||
epochsInSet < t.minimumEpochs
|
||||
) {
|
||||
unlocksIn = (
|
||||
<span className="text-muted">
|
||||
Unlocks in {t.minimumEpochs - epochsInSet} epochs
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const tierIndicator = (
|
||||
<div className="flex justify-between">
|
||||
<span data-testid={`tier-value-${i}`}>{i + 1}</span>
|
||||
<span className="md:hidden">{indicator}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tr key={i}>
|
||||
<Td>{i + 1}</Td>
|
||||
<Td>{formatPercentage(Number(t.referralDiscountFactor))}%</Td>
|
||||
<Td>{formatNumber(t.minimumRunningNotionalTakerVolume)}</Td>
|
||||
<Td>{t.minimumEpochs}</Td>
|
||||
<Td>{isUserTier ? <YourTier /> : unlocksIn}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
return {
|
||||
tier: tierIndicator,
|
||||
discount: (
|
||||
<>{formatPercentage(Number(tier.referralDiscountFactor))}%</>
|
||||
),
|
||||
volume: formatNumber(tier.minimumRunningNotionalTakerVolume),
|
||||
epochs: tier.minimumEpochs,
|
||||
indicator,
|
||||
className: classNames(
|
||||
getTierGradient(i + 1, tiers.length),
|
||||
'text-xs'
|
||||
),
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const YourTier = () => {
|
||||
interface YourTierProps {
|
||||
testId?: string;
|
||||
}
|
||||
|
||||
const YourTier = ({ testId }: YourTierProps) => {
|
||||
const t = useT();
|
||||
|
||||
return (
|
||||
<span className="bg-rainbow whitespace-nowrap rounded-xl px-4 py-1.5 text-white">
|
||||
<span
|
||||
className="bg-rainbow whitespace-nowrap rounded-xl px-4 py-1.5 text-white text-xs"
|
||||
data-testid={testId}
|
||||
>
|
||||
{t('Your tier')}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -8,43 +8,53 @@ import { useNavigateWithMeta } from '../../lib/hooks/use-market-click-handler';
|
||||
import { Links } from '../../lib/links';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { useMemo } from 'react';
|
||||
import { type ColDef } from 'ag-grid-community/dist/lib/entities/colDef';
|
||||
|
||||
const useFeesTableColumnDefs = () => {
|
||||
const useFeesTableColumnDefs = (): ColDef[] => {
|
||||
const t = useT();
|
||||
return useMemo(
|
||||
() => [
|
||||
{ field: 'code', cellRenderer: 'MarketCodeCell' },
|
||||
{
|
||||
field: 'feeAfterDiscount',
|
||||
headerName: t('Total fee after discount'),
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
{
|
||||
field: 'infraFee',
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
{
|
||||
field: 'makerFee',
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
{
|
||||
field: 'liquidityFee',
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
{
|
||||
field: 'totalFee',
|
||||
headerName: t('Total fee before discount'),
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
],
|
||||
() =>
|
||||
[
|
||||
{
|
||||
field: 'code',
|
||||
cellRenderer: 'MarketCodeCell',
|
||||
pinned: 'left',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
field: 'liquidityFee',
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
{
|
||||
field: 'feeAfterDiscount',
|
||||
headerName: t('Total fee after discount'),
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
{
|
||||
field: 'totalFee',
|
||||
headerName: t('Total fee before discount'),
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
{
|
||||
field: 'infraFee',
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
{
|
||||
field: 'makerFee',
|
||||
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||
},
|
||||
] as ColDef[],
|
||||
[t]
|
||||
);
|
||||
};
|
||||
|
||||
const feesTableDefaultColDef = {
|
||||
flex: 1,
|
||||
minWidth: 62,
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
suppressMovable: true,
|
||||
pinned: false,
|
||||
};
|
||||
|
||||
const components = {
|
||||
@@ -62,6 +72,8 @@ export const MarketFees = ({
|
||||
}) => {
|
||||
const navigateWithMeta = useNavigateWithMeta();
|
||||
|
||||
const colDef = useFeesTableColumnDefs();
|
||||
|
||||
const rows = compact(markets || []).map((m) => {
|
||||
const infraFee = new BigNumber(m.fees.factors.infrastructureFee);
|
||||
const makerFee = new BigNumber(m.fees.factors.makerFee);
|
||||
@@ -88,9 +100,9 @@ export const MarketFees = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="border rounded-sm border-default">
|
||||
<div className="border rounded-lg md:rounded-sm overflow-hidden border-default">
|
||||
<AgGrid
|
||||
columnDefs={useFeesTableColumnDefs()}
|
||||
columnDefs={colDef}
|
||||
rowData={rows}
|
||||
getRowId={({ data }) => data.id}
|
||||
defaultColDef={feesTableDefaultColDef}
|
||||
|
||||
@@ -3,21 +3,31 @@ import type { ReactNode } from 'react';
|
||||
|
||||
const cellClass = 'px-4 py-2 text-xs font-normal text-left last:text-right';
|
||||
|
||||
export const Th = ({ children }: { children?: ReactNode }) => {
|
||||
export const Th = ({ children, ...props }: { children?: ReactNode }) => {
|
||||
return (
|
||||
<th className={classNames(cellClass, 'text-secondary leading-none py-3')}>
|
||||
<th
|
||||
className={classNames(cellClass, 'text-secondary leading-none py-3')}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</th>
|
||||
);
|
||||
};
|
||||
|
||||
export const Td = ({ children }: { children?: ReactNode }) => {
|
||||
return <th className={cellClass}>{children}</th>;
|
||||
export const Td = ({ children, ...props }: { children?: ReactNode }) => {
|
||||
return (
|
||||
<th className={cellClass} {...props}>
|
||||
{children}
|
||||
</th>
|
||||
);
|
||||
};
|
||||
|
||||
export const Tr = ({ children }: { children?: ReactNode }) => {
|
||||
export const Tr = ({ children, ...props }: { children?: ReactNode }) => {
|
||||
return (
|
||||
<tr className="hover:bg-vega-clight-600 dark:hover:bg-vega-cdark-700">
|
||||
<tr
|
||||
className="hover:bg-vega-clight-600 dark:hover:bg-vega-cdark-700"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</tr>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
|
||||
|
||||
export const useReferralStats = (
|
||||
previousEpoch?: number,
|
||||
referralStats?: NonNullable<
|
||||
|
||||
@@ -38,7 +38,7 @@ export const FillsContainer = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const useFillsStore = create<DataGridSlice>()(
|
||||
export const useFillsStore = create<DataGridSlice>()(
|
||||
persist(createDataGridSlice, {
|
||||
name: 'vega_fills_store',
|
||||
})
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { GridSettings } from '../grid-settings/grid-settings';
|
||||
import { useFillsStore } from './fills-container';
|
||||
|
||||
export const FillsSettings = () => (
|
||||
<GridSettings
|
||||
updateGridStore={useFillsStore((store) => store.updateGridStore)}
|
||||
/>
|
||||
);
|
||||
@@ -1 +1,2 @@
|
||||
export * from './fills-container';
|
||||
export * from './fills-settings';
|
||||
|
||||
@@ -45,7 +45,7 @@ export const FundingPaymentsContainer = ({
|
||||
);
|
||||
};
|
||||
|
||||
const useFundingPaymentsStore = create<DataGridSlice>()(
|
||||
export const useFundingPaymentsStore = create<DataGridSlice>()(
|
||||
persist(createDataGridSlice, {
|
||||
name: 'vega_funding_payments_store',
|
||||
})
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { GridSettings } from '../grid-settings/grid-settings';
|
||||
import { useFundingPaymentsStore } from './funding-payments-container';
|
||||
|
||||
export const FundingPaymentsSettings = () => (
|
||||
<GridSettings
|
||||
updateGridStore={useFundingPaymentsStore((store) => store.updateGridStore)}
|
||||
/>
|
||||
);
|
||||
@@ -1 +1,2 @@
|
||||
export * from './funding-payments-container';
|
||||
export * from './funding-payments-settings';
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useT } from '../../lib/use-t';
|
||||
import type { DataGridStore } from '../../stores/datagrid-store-slice';
|
||||
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const GridSettings = ({
|
||||
updateGridStore,
|
||||
}: {
|
||||
updateGridStore: (gridStore: DataGridStore) => void;
|
||||
}) => {
|
||||
const t = useT();
|
||||
return (
|
||||
<Button
|
||||
onClick={() =>
|
||||
updateGridStore({
|
||||
columnState: undefined,
|
||||
filterModel: undefined,
|
||||
})
|
||||
}
|
||||
size="extra-small"
|
||||
>
|
||||
{t('Reset Columns')}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
+9
-1
@@ -2,7 +2,15 @@ import type { HTMLAttributes } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
type TagProps = {
|
||||
color?: 'yellow' | 'green' | 'blue' | 'purple' | 'pink' | 'orange' | 'none';
|
||||
color?:
|
||||
| 'yellow'
|
||||
| 'green'
|
||||
| 'blue'
|
||||
| 'purple'
|
||||
| 'pink'
|
||||
| 'orange'
|
||||
| 'red'
|
||||
| 'none';
|
||||
};
|
||||
export const Tag = ({
|
||||
color = 'none',
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { ComponentProps } from 'react';
|
||||
import type { Tag } from './tag';
|
||||
import classNames from 'classnames';
|
||||
|
||||
// rainbow-ish order
|
||||
export const TIER_COLORS: Array<ComponentProps<typeof Tag>['color']> = [
|
||||
'none', // worst tier if 8 tiers, otherwise relative to the best tier
|
||||
'red',
|
||||
'pink',
|
||||
'orange',
|
||||
'yellow',
|
||||
'green',
|
||||
'blue',
|
||||
'purple', // best tier
|
||||
];
|
||||
|
||||
export const getTierColor = (tier: number, max = TIER_COLORS.length) => {
|
||||
const available =
|
||||
max < TIER_COLORS.length
|
||||
? TIER_COLORS.slice(TIER_COLORS.length - max)
|
||||
: TIER_COLORS;
|
||||
const tiers = Object.keys(available).length;
|
||||
let index = Math.abs(tier - 1);
|
||||
if (tier >= tiers) {
|
||||
index = index % tiers;
|
||||
}
|
||||
return available[index];
|
||||
};
|
||||
|
||||
export const getTierGradient = (tier: number, max = TIER_COLORS.length) =>
|
||||
classNames({
|
||||
'from-vega-yellow-400 dark:from-vega-yellow-600 to-20% bg-highlight':
|
||||
'yellow' === getTierColor(tier, max),
|
||||
'from-vega-green-400 dark:from-vega-green-600 to-20% bg-highlight':
|
||||
'green' === getTierColor(tier, max),
|
||||
'from-vega-blue-400 dark:from-vega-blue-600 to-20% bg-highlight':
|
||||
'blue' === getTierColor(tier, max),
|
||||
'from-vega-purple-400 dark:from-vega-purple-600 to-20% bg-highlight':
|
||||
'purple' === getTierColor(tier, max),
|
||||
'from-vega-pink-400 dark:from-vega-pink-600 to-20% bg-highlight':
|
||||
'pink' === getTierColor(tier, max),
|
||||
'from-vega-orange-400 dark:from-vega-orange-600 to-20% bg-highlight':
|
||||
'orange' === getTierColor(tier, max),
|
||||
'from-vega-red-400 dark:from-vega-red-600 to-20% bg-highlight':
|
||||
'red' === getTierColor(tier, max),
|
||||
'from-vega-clight-600 dark:from-vega-cdark-600 to-20% bg-highlight':
|
||||
'none' === getTierColor(tier, max),
|
||||
});
|
||||
@@ -1,3 +1 @@
|
||||
export * from './market-successor-banner';
|
||||
export * from './market-successor-proposal-banner';
|
||||
export * from './market-termination-banner';
|
||||
export { MarketBanner } from './market-banner';
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
import compact from 'lodash/compact';
|
||||
import { MarketState } from '@vegaprotocol/types';
|
||||
import { Intent, NotificationBanner } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
useMarketState,
|
||||
type Market,
|
||||
useMaliciousOracle,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { useState } from 'react';
|
||||
import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { MarketSuspendedBanner } from './market-suspended-banner';
|
||||
import { MarketUpdateBanner } from './market-update-banner';
|
||||
import { MarketUpdateStateBanner } from './market-update-state-banner';
|
||||
import { MarketSettledBanner } from './market-settled-banner';
|
||||
import { MarketSuccessorProposalBanner } from './market-successor-proposal-banner';
|
||||
import { MarketOracleBanner, type Oracle } from './market-oracle-banner';
|
||||
import {
|
||||
useSuccessorMarketProposals,
|
||||
useUpdateMarketProposals,
|
||||
useUpdateMarketStateProposals,
|
||||
} from './use-market-proposals';
|
||||
|
||||
type UpdateMarketBanner = {
|
||||
kind: 'UpdateMarket';
|
||||
proposals: MarketViewProposalFieldsFragment[];
|
||||
};
|
||||
|
||||
type UpdateMarketStateBanner = {
|
||||
kind: 'UpdateMarketState';
|
||||
proposals: MarketViewProposalFieldsFragment[];
|
||||
};
|
||||
|
||||
type NewMarketBanner = {
|
||||
kind: 'NewMarket'; // aka a proposal of NewMarket which succeeds the current market
|
||||
proposals: MarketViewProposalFieldsFragment[];
|
||||
};
|
||||
|
||||
type SettledBanner = {
|
||||
kind: 'Settled';
|
||||
market: Market;
|
||||
};
|
||||
|
||||
type SuspendedBanner = {
|
||||
kind: 'Suspended';
|
||||
market: Market;
|
||||
};
|
||||
|
||||
type OracleBanner = {
|
||||
kind: 'Oracle';
|
||||
oracle: Oracle;
|
||||
};
|
||||
|
||||
type Banner =
|
||||
| UpdateMarketBanner
|
||||
| UpdateMarketStateBanner
|
||||
| NewMarketBanner
|
||||
| SettledBanner
|
||||
| SuspendedBanner
|
||||
| OracleBanner;
|
||||
|
||||
export const MarketBanner = ({ market }: { market: Market }) => {
|
||||
const { data: marketState } = useMarketState(market.id);
|
||||
|
||||
const { proposals: successorProposals, loading: successorLoading } =
|
||||
useSuccessorMarketProposals(market.id);
|
||||
|
||||
const { proposals: updateMarketProposals, loading: updateMarketLoading } =
|
||||
useUpdateMarketProposals(market.id);
|
||||
|
||||
const {
|
||||
proposals: updateMarketStateProposals,
|
||||
loading: updateMarketStateLoading,
|
||||
} = useUpdateMarketStateProposals(market.id);
|
||||
|
||||
const { data: maliciousOracle, loading: oracleLoading } = useMaliciousOracle(
|
||||
market.id
|
||||
);
|
||||
|
||||
const loading =
|
||||
successorLoading ||
|
||||
updateMarketLoading ||
|
||||
updateMarketStateLoading ||
|
||||
oracleLoading;
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const banners = compact([
|
||||
updateMarketStateProposals.length
|
||||
? {
|
||||
kind: 'UpdateMarketState' as const,
|
||||
proposals: updateMarketStateProposals,
|
||||
}
|
||||
: undefined,
|
||||
updateMarketProposals.length
|
||||
? {
|
||||
kind: 'UpdateMarket' as const,
|
||||
proposals: updateMarketProposals,
|
||||
}
|
||||
: undefined,
|
||||
successorProposals.length
|
||||
? {
|
||||
kind: 'NewMarket' as const,
|
||||
proposals: successorProposals,
|
||||
}
|
||||
: undefined,
|
||||
marketState === MarketState.STATE_SETTLED
|
||||
? {
|
||||
kind: 'Settled' as const,
|
||||
market,
|
||||
}
|
||||
: undefined,
|
||||
marketState === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
|
||||
? {
|
||||
kind: 'Suspended' as const,
|
||||
market,
|
||||
}
|
||||
: undefined,
|
||||
maliciousOracle !== undefined
|
||||
? {
|
||||
kind: 'Oracle' as const,
|
||||
oracle: maliciousOracle,
|
||||
}
|
||||
: undefined,
|
||||
]);
|
||||
|
||||
return <BannerQueue banners={banners} market={market} />;
|
||||
};
|
||||
|
||||
const BannerQueue = ({
|
||||
banners,
|
||||
market,
|
||||
}: {
|
||||
banners: Banner[];
|
||||
market: Market;
|
||||
}) => {
|
||||
// for showing banner by index
|
||||
const [index, setIndex] = useState(0);
|
||||
|
||||
const banner = banners[index];
|
||||
if (!banner) return null;
|
||||
|
||||
let content = null;
|
||||
let intent = Intent.Primary;
|
||||
|
||||
switch (banner.kind) {
|
||||
case 'UpdateMarket': {
|
||||
content = <MarketUpdateBanner proposals={banner.proposals} />;
|
||||
break;
|
||||
}
|
||||
case 'UpdateMarketState': {
|
||||
content = (
|
||||
<MarketUpdateStateBanner proposals={banner.proposals} market={market} />
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'NewMarket': {
|
||||
content = <MarketSuccessorProposalBanner proposals={banner.proposals} />;
|
||||
intent = Intent.Warning;
|
||||
break;
|
||||
}
|
||||
case 'Settled': {
|
||||
content = <MarketSettledBanner market={market} />;
|
||||
break;
|
||||
}
|
||||
case 'Suspended': {
|
||||
content = <MarketSuspendedBanner />;
|
||||
intent = Intent.Warning;
|
||||
break;
|
||||
}
|
||||
case 'Oracle': {
|
||||
// @ts-ignore oracle cannot be undefined
|
||||
content = <MarketOracleBanner oracle={banner.oracle} />;
|
||||
intent = Intent.Danger;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const showCount = banners.length > 1;
|
||||
|
||||
const onClose = () => {
|
||||
setIndex((x) => x + 1);
|
||||
};
|
||||
|
||||
return (
|
||||
<NotificationBanner
|
||||
intent={intent}
|
||||
onClose={onClose}
|
||||
data-testid="market-banner"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
{content}
|
||||
{showCount ? (
|
||||
<p>
|
||||
{index + 1}/{banners.length}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</NotificationBanner>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { MarketOracleBanner } from './market-oracle-banner';
|
||||
|
||||
jest.mock('@vegaprotocol/markets', () => ({
|
||||
...jest.requireActual('@vegaprotocol/markets'),
|
||||
OracleDialog: ({ open }: { open: boolean }) => {
|
||||
return <div data-testid="oracle-dialog">{open ? 'open' : 'closed'}</div>;
|
||||
},
|
||||
}));
|
||||
|
||||
const oracle = {
|
||||
dataSourceSpecId: 'someId',
|
||||
provider: {
|
||||
name: 'Test oracle',
|
||||
url: 'https://zombo.com',
|
||||
description_markdown:
|
||||
'Some markdown describing the oracle provider.\n\nTwitter: @FacesPics2\n',
|
||||
oracle: {
|
||||
status: 'COMPROMISED' as const,
|
||||
status_reason: 'Reason here',
|
||||
first_verified: '2021-01-01T00:00:00Z',
|
||||
last_verified: '2022-01-01T00:00:00Z',
|
||||
type: 'public_key' as const,
|
||||
public_key: '0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC',
|
||||
},
|
||||
proofs: [
|
||||
{
|
||||
format: 'url' as const,
|
||||
available: true,
|
||||
type: 'web' as const,
|
||||
url: 'https://proofweb.com',
|
||||
},
|
||||
{
|
||||
format: 'signed_message' as const,
|
||||
available: true,
|
||||
type: 'public_key' as const,
|
||||
public_key: '0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC',
|
||||
message: 'SOMEHEX',
|
||||
},
|
||||
],
|
||||
github_link:
|
||||
'https://github.com/vegaprotocol/well-known/blob/main/oracle-providers/eth_address-0xfCEAdAFab14d46e20144F48824d0C09B1a03F2BC.toml',
|
||||
},
|
||||
};
|
||||
|
||||
describe('MarketOracleBanner', () => {
|
||||
it('should render successfully', async () => {
|
||||
render(<MarketOracleBanner oracle={oracle} />);
|
||||
|
||||
expect(screen.getByTestId('oracle-dialog')).toHaveTextContent('closed');
|
||||
expect(screen.getByTestId('oracle-banner-status')).toHaveTextContent(
|
||||
'COMPROMISED'
|
||||
);
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Show more' }));
|
||||
expect(screen.getByTestId('oracle-dialog')).toHaveTextContent('open');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useState } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
import {
|
||||
OracleDialog,
|
||||
useOracleStatuses,
|
||||
type useMaliciousOracle,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export type Oracle = ReturnType<typeof useMaliciousOracle>['data'];
|
||||
|
||||
export const MarketOracleBanner = ({
|
||||
oracle,
|
||||
}: {
|
||||
oracle: NonNullable<Oracle>;
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const oracleStatuses = useOracleStatuses();
|
||||
|
||||
return (
|
||||
<>
|
||||
<OracleDialog open={open} onChange={setOpen} {...oracle} />
|
||||
<p>
|
||||
<Trans
|
||||
defaults="Oracle status for this market is <0>{{status}}</0>. {{description}} <1>Show more</1>"
|
||||
components={[
|
||||
<span key="oracle-status" data-testid="oracle-banner-status">
|
||||
status
|
||||
</span>,
|
||||
<ButtonLink
|
||||
key="oracle-button"
|
||||
onClick={() => setOpen((x) => !x)}
|
||||
data-testid="oracle-banner-dialog-trigger"
|
||||
>
|
||||
Show more
|
||||
</ButtonLink>,
|
||||
]}
|
||||
values={{
|
||||
status: oracle.provider.oracle.status,
|
||||
description: oracleStatuses[oracle.provider.oracle.status],
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,219 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { addDays, subDays, subHours } from 'date-fns';
|
||||
import { MockedProvider, type MockedResponse } from '@apollo/react-testing';
|
||||
import {
|
||||
SuccessorMarketDocument,
|
||||
MarketCandlesDocument,
|
||||
MarketCandlesUpdateDocument,
|
||||
SuccessorMarketIdDocument,
|
||||
type Market,
|
||||
type SuccessorMarketQuery,
|
||||
type SuccessorMarketQueryVariables,
|
||||
type MarketCandlesQuery,
|
||||
type MarketCandlesQueryVariables,
|
||||
type MarketCandlesUpdateSubscription,
|
||||
type MarketCandlesUpdateSubscriptionVariables,
|
||||
type SuccessorMarketIdQuery,
|
||||
type SuccessorMarketIdQueryVariables,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { createMarketFragment } from '@vegaprotocol/mock';
|
||||
import { Interval, MarketState, MarketTradingMode } from '@vegaprotocol/types';
|
||||
import { MarketSettledBanner } from './market-settled-banner';
|
||||
|
||||
describe('MarketSettledBanner', () => {
|
||||
const now = 1701388800000;
|
||||
const origDateNow = Date.now;
|
||||
|
||||
beforeAll(() => {
|
||||
Date.now = () => now;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
Date.now = origDateNow;
|
||||
});
|
||||
|
||||
const marketId = 'market-0';
|
||||
const successorId = 'successor-id';
|
||||
const successorName = 'successor-name';
|
||||
|
||||
const successorMarketIdMock: MockedResponse<
|
||||
SuccessorMarketIdQuery,
|
||||
SuccessorMarketIdQueryVariables
|
||||
> = {
|
||||
request: {
|
||||
query: SuccessorMarketIdDocument,
|
||||
variables: {
|
||||
marketId: marketId,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
market: {
|
||||
successorMarketID: successorId,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const successorMock: MockedResponse<
|
||||
SuccessorMarketQuery,
|
||||
SuccessorMarketQueryVariables
|
||||
> = {
|
||||
request: {
|
||||
query: SuccessorMarketDocument,
|
||||
variables: {
|
||||
marketId: successorId,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
market: {
|
||||
id: successorId,
|
||||
state: MarketState.STATE_ACTIVE,
|
||||
tradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||
positionDecimalPlaces: 0,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: successorName,
|
||||
code: 'code',
|
||||
},
|
||||
},
|
||||
proposal: {
|
||||
id: 'proposal-id',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const since = subDays(new Date(now), 5).toISOString();
|
||||
const successorCandlesMock: MockedResponse<
|
||||
MarketCandlesQuery,
|
||||
MarketCandlesQueryVariables
|
||||
> = {
|
||||
request: {
|
||||
query: MarketCandlesDocument,
|
||||
variables: {
|
||||
marketId: successorId,
|
||||
interval: Interval.INTERVAL_I1H,
|
||||
since,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
marketsConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
candlesConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
high: '100',
|
||||
low: '100',
|
||||
open: '100',
|
||||
close: '100',
|
||||
volume: '100',
|
||||
periodStart: subHours(new Date(now), 1).toISOString(),
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
high: '100',
|
||||
low: '100',
|
||||
open: '100',
|
||||
close: '200',
|
||||
volume: '100',
|
||||
periodStart: subHours(new Date(now), 2).toISOString(),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const successorCandlesUpdateMock: MockedResponse<
|
||||
MarketCandlesUpdateSubscription,
|
||||
MarketCandlesUpdateSubscriptionVariables
|
||||
> = {
|
||||
request: {
|
||||
query: MarketCandlesUpdateDocument,
|
||||
variables: {
|
||||
marketId: successorId,
|
||||
interval: Interval.INTERVAL_I1H,
|
||||
// @ts-ignore this query doesn't need the 'since' variable, but the data-provider
|
||||
// uses all variables for both the query and the subscription so its needed here
|
||||
// to match the mock
|
||||
since,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
candles: {
|
||||
high: '100',
|
||||
low: '100',
|
||||
open: '100',
|
||||
close: '100',
|
||||
volume: '100',
|
||||
periodStart: '2020-01-01T00:00:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const renderComponent = (market: Market, mocks?: MockedResponse[]) => {
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider mocks={mocks}>
|
||||
<MarketSettledBanner market={market} />
|
||||
</MockedProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
it('shows that the market is settled if there is no successor', () => {
|
||||
const market = createMarketFragment({
|
||||
id: marketId,
|
||||
successorMarketID: undefined,
|
||||
});
|
||||
renderComponent(market, [successorMarketIdMock, successorMock]);
|
||||
expect(
|
||||
screen.getByText('This market has been settled')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows that the market has been succeeded with an active market', async () => {
|
||||
const market = createMarketFragment({
|
||||
id: marketId,
|
||||
successorMarketID: successorId,
|
||||
marketTimestamps: {
|
||||
close: addDays(new Date(now), 1).toISOString(),
|
||||
},
|
||||
});
|
||||
renderComponent(market, [
|
||||
successorMarketIdMock,
|
||||
successorMock,
|
||||
successorCandlesMock,
|
||||
successorCandlesUpdateMock,
|
||||
]);
|
||||
expect(
|
||||
await screen.findByText('This market has been succeeded')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByText(/has a 24h trading volume of 200$/i)
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('This market expires in 1 day.')
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: successorName })).toHaveAttribute(
|
||||
'href',
|
||||
`/markets/${successorId}`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { isBefore, formatDuration, intervalToDuration } from 'date-fns';
|
||||
import {
|
||||
calcCandleVolume,
|
||||
useCandles,
|
||||
useSuccessorMarket,
|
||||
type Market,
|
||||
} from '@vegaprotocol/markets';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
getMarketExpiryDate,
|
||||
isNumeric,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { useT, ns } from '../../lib/use-t';
|
||||
import { Links } from '../../lib/links';
|
||||
|
||||
const getExpiryDate = (tags: string[], close?: string): Date | null => {
|
||||
const expiryDate = getMarketExpiryDate(tags);
|
||||
return expiryDate || (close && new Date(close)) || null;
|
||||
};
|
||||
|
||||
export const MarketSettledBanner = ({ market }: { market: Market }) => {
|
||||
const t = useT();
|
||||
const { data: successorMarket } = useSuccessorMarket(market.id);
|
||||
|
||||
const expiry = market
|
||||
? getExpiryDate(
|
||||
market.tradableInstrument.instrument.metadata.tags || [],
|
||||
market.marketTimestamps.close
|
||||
)
|
||||
: null;
|
||||
|
||||
const duration =
|
||||
expiry && isBefore(new Date(Date.now()), expiry)
|
||||
? intervalToDuration({ start: new Date(Date.now()), end: expiry })
|
||||
: null;
|
||||
|
||||
const { oneDayCandles } = useCandles({
|
||||
marketId: successorMarket?.id,
|
||||
});
|
||||
|
||||
const candleVolume = oneDayCandles?.length
|
||||
? calcCandleVolume(oneDayCandles)
|
||||
: null;
|
||||
|
||||
const successorVolume =
|
||||
candleVolume && isNumeric(successorMarket?.positionDecimalPlaces)
|
||||
? addDecimalsFormatNumber(
|
||||
candleVolume,
|
||||
successorMarket?.positionDecimalPlaces as number
|
||||
)
|
||||
: null;
|
||||
|
||||
if (successorMarket) {
|
||||
return (
|
||||
<div>
|
||||
<p>{t('This market has been succeeded')}</p>
|
||||
{duration && (
|
||||
<div className="mt-1">
|
||||
<span>
|
||||
{t('This market expires in {{duration}}.', {
|
||||
duration: formatDuration(duration, {
|
||||
format: [
|
||||
'years',
|
||||
'months',
|
||||
'weeks',
|
||||
'days',
|
||||
'hours',
|
||||
'minutes',
|
||||
],
|
||||
}),
|
||||
})}
|
||||
</span>
|
||||
<>
|
||||
{' '}
|
||||
{successorVolume ? (
|
||||
<Trans
|
||||
defaults="The successor market <0>{{instrumentName}}</0> has a 24h trading volume of {{successorVolume}}"
|
||||
values={{
|
||||
successorVolume,
|
||||
instrumentName:
|
||||
successorMarket?.tradableInstrument.instrument.name,
|
||||
}}
|
||||
components={[
|
||||
<Link
|
||||
to={Links.MARKET(successorMarket.id)}
|
||||
key="link"
|
||||
target="_blank"
|
||||
>
|
||||
successor market name
|
||||
</Link>,
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<Trans
|
||||
defaults="The successor market is <0>{{instrumentName}}</0>"
|
||||
values={{
|
||||
instrumentName:
|
||||
successorMarket?.tradableInstrument.instrument.name,
|
||||
}}
|
||||
components={[
|
||||
<Link
|
||||
to={Links.MARKET(successorMarket.id)}
|
||||
key="link"
|
||||
target="_blank"
|
||||
>
|
||||
successor market name
|
||||
</Link>,
|
||||
]}
|
||||
ns={ns}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <p>{t('This market has been settled')}</p>;
|
||||
};
|
||||
@@ -1,155 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MockedProvider } from '@apollo/react-testing';
|
||||
import { MarketSuccessorBanner } from './market-successor-banner';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
import * as allUtils from '@vegaprotocol/utils';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
|
||||
const market = {
|
||||
id: 'marketId',
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
metadata: {
|
||||
tags: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
marketTimestamps: {
|
||||
close: null,
|
||||
},
|
||||
} as unknown as Market;
|
||||
|
||||
let mockDataSuccessorMarket: PartialDeep<Market> | null = null;
|
||||
let mockDataMarketState: Market['state'] | null = null;
|
||||
jest.mock('@vegaprotocol/data-provider', () => ({
|
||||
...jest.requireActual('@vegaprotocol/data-provider'),
|
||||
useDataProvider: jest.fn().mockImplementation((args) => {
|
||||
if (args.skip) {
|
||||
return {
|
||||
data: null,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
data: mockDataSuccessorMarket,
|
||||
error: null,
|
||||
};
|
||||
}),
|
||||
}));
|
||||
jest.mock('@vegaprotocol/utils', () => ({
|
||||
...jest.requireActual('@vegaprotocol/utils'),
|
||||
getMarketExpiryDate: jest.fn(),
|
||||
}));
|
||||
let mockCandles = {};
|
||||
jest.mock('@vegaprotocol/markets', () => ({
|
||||
...jest.requireActual('@vegaprotocol/markets'),
|
||||
useMarketState: (marketId: string) =>
|
||||
marketId
|
||||
? {
|
||||
data: mockDataMarketState,
|
||||
}
|
||||
: { data: undefined },
|
||||
useSuccessorMarket: (marketId: string) =>
|
||||
marketId
|
||||
? {
|
||||
data: mockDataSuccessorMarket,
|
||||
}
|
||||
: { data: undefined },
|
||||
useCandles: () => mockCandles,
|
||||
}));
|
||||
|
||||
describe('MarketSuccessorBanner', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockDataSuccessorMarket = {
|
||||
id: 'successorMarketID',
|
||||
state: Types.MarketState.STATE_ACTIVE,
|
||||
tradingMode: Types.MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
name: 'Successor Market Name',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
describe('should be hidden', () => {
|
||||
it('when no market', () => {
|
||||
const { container } = render(<MarketSuccessorBanner market={null} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('no successor market data', () => {
|
||||
mockDataSuccessorMarket = null;
|
||||
const { container } = render(<MarketSuccessorBanner market={market} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should be displayed', () => {
|
||||
it('should be rendered', () => {
|
||||
render(<MarketSuccessorBanner market={market} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
expect(
|
||||
screen.getByText('This market has been succeeded')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('link', { name: 'Successor Market Name' })
|
||||
).toHaveAttribute('href', '/#/markets/successorMarketID');
|
||||
});
|
||||
|
||||
it('no successor market data, market settled', () => {
|
||||
mockDataSuccessorMarket = null;
|
||||
mockDataMarketState = Types.MarketState.STATE_SETTLED;
|
||||
render(<MarketSuccessorBanner market={market} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
expect(
|
||||
screen.getByText('This market has been settled')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display optionally successor volume', () => {
|
||||
mockDataSuccessorMarket = {
|
||||
...mockDataSuccessorMarket,
|
||||
positionDecimalPlaces: 3,
|
||||
};
|
||||
mockCandles = {
|
||||
oneDayCandles: [
|
||||
{ volume: 123 },
|
||||
{ volume: 456 },
|
||||
{ volume: 789 },
|
||||
{ volume: 99999 },
|
||||
],
|
||||
};
|
||||
|
||||
render(<MarketSuccessorBanner market={market} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
expect(
|
||||
screen.getByText('has a 24h trading volume of 101.367', {
|
||||
exact: false,
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display optionally duration', () => {
|
||||
jest
|
||||
.spyOn(allUtils, 'getMarketExpiryDate')
|
||||
.mockReturnValue(
|
||||
new Date(Date.now() + 24 * 60 * 60 * 1000 + 60 * 1000)
|
||||
);
|
||||
render(<MarketSuccessorBanner market={market} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
expect(
|
||||
screen.getByText(/^This market expires in 1 day/)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,146 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { isBefore, formatDuration, intervalToDuration } from 'date-fns';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import {
|
||||
calcCandleVolume,
|
||||
useCandles,
|
||||
useMarketState,
|
||||
useSuccessorMarket,
|
||||
} from '@vegaprotocol/markets';
|
||||
import {
|
||||
ExternalLink,
|
||||
Intent,
|
||||
NotificationBanner,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
getMarketExpiryDate,
|
||||
isNumeric,
|
||||
} from '@vegaprotocol/utils';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
import { useT, ns } from '../../lib/use-t';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
const getExpiryDate = (tags: string[], close?: string): Date | null => {
|
||||
const expiryDate = getMarketExpiryDate(tags);
|
||||
return expiryDate || (close && new Date(close)) || null;
|
||||
};
|
||||
|
||||
export const MarketSuccessorBanner = ({
|
||||
market,
|
||||
}: {
|
||||
market: Market | null;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const { data: marketState } = useMarketState(market?.id);
|
||||
const isSettled = marketState === Types.MarketState.STATE_SETTLED;
|
||||
const { data: successorData, loading } = useSuccessorMarket(market?.id);
|
||||
|
||||
const [visible, setVisible] = useState(true);
|
||||
|
||||
const expiry = market
|
||||
? getExpiryDate(
|
||||
market.tradableInstrument.instrument.metadata.tags || [],
|
||||
market.marketTimestamps.close
|
||||
)
|
||||
: null;
|
||||
|
||||
const duration =
|
||||
expiry && isBefore(new Date(), expiry)
|
||||
? intervalToDuration({ start: new Date(), end: expiry })
|
||||
: null;
|
||||
|
||||
const { oneDayCandles } = useCandles({
|
||||
marketId: successorData?.id,
|
||||
});
|
||||
|
||||
const candleVolume = oneDayCandles?.length
|
||||
? calcCandleVolume(oneDayCandles)
|
||||
: null;
|
||||
|
||||
const successorVolume =
|
||||
candleVolume && isNumeric(successorData?.positionDecimalPlaces)
|
||||
? addDecimalsFormatNumber(
|
||||
candleVolume,
|
||||
successorData?.positionDecimalPlaces as number
|
||||
)
|
||||
: null;
|
||||
|
||||
if (!loading && (isSettled || successorData) && visible) {
|
||||
return (
|
||||
<NotificationBanner
|
||||
intent={Intent.Primary}
|
||||
onClose={() => {
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<div className="uppercase">
|
||||
{successorData
|
||||
? t('This market has been succeeded')
|
||||
: t('This market has been settled')}
|
||||
</div>
|
||||
{(duration || successorData) && (
|
||||
<div className="mt-1">
|
||||
{duration && (
|
||||
<span>
|
||||
{t('This market expires in {{duration}}.', {
|
||||
duration: formatDuration(duration, {
|
||||
format: [
|
||||
'years',
|
||||
'months',
|
||||
'weeks',
|
||||
'days',
|
||||
'hours',
|
||||
'minutes',
|
||||
],
|
||||
}),
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
{successorData && (
|
||||
<>
|
||||
{' '}
|
||||
{successorVolume ? (
|
||||
<Trans
|
||||
defaults="The successor market <0>{{instrumentName}}</0> has a 24h trading volume of {{successorVolume}}"
|
||||
values={{
|
||||
successorVolume,
|
||||
instrumentName:
|
||||
successorData?.tradableInstrument.instrument.name,
|
||||
}}
|
||||
components={[
|
||||
<ExternalLink
|
||||
href={`/#/markets/${successorData?.id}`}
|
||||
key="link"
|
||||
>
|
||||
successor market name
|
||||
</ExternalLink>,
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<Trans
|
||||
defaults="The successor market is <0>{{instrumentName}}</0>"
|
||||
values={{
|
||||
instrumentName:
|
||||
successorData?.tradableInstrument.instrument.name,
|
||||
}}
|
||||
components={[
|
||||
<ExternalLink
|
||||
href={`/#/markets/${successorData?.id}`}
|
||||
key="link"
|
||||
>
|
||||
successor market name
|
||||
</ExternalLink>,
|
||||
]}
|
||||
ns={ns}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</NotificationBanner>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -1,164 +1,77 @@
|
||||
import { act, render, screen, waitFor } from '@testing-library/react';
|
||||
import type { SingleExecutionResult } from '@apollo/client';
|
||||
import type { MockedResponse } from '@apollo/react-testing';
|
||||
import { MockedProvider } from '@apollo/react-testing';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MarketSuccessorProposalBanner } from './market-successor-proposal-banner';
|
||||
import type { MarketViewProposalsQuery } from '@vegaprotocol/proposals';
|
||||
import { MarketViewProposalsDocument } from '@vegaprotocol/proposals';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
|
||||
const marketProposalMock: MockedResponse<MarketViewProposalsQuery> = {
|
||||
request: {
|
||||
query: MarketViewProposalsDocument,
|
||||
variables: {
|
||||
inState: Types.ProposalState.STATE_OPEN,
|
||||
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
|
||||
const proposal = {
|
||||
__typename: 'Proposal' as const,
|
||||
id: 'proposal-1',
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
__typename: 'ProposalTerms' as const,
|
||||
closingDatetime: '2023-09-27',
|
||||
enactmentDatetime: '2023-09-28',
|
||||
change: {
|
||||
__typename: 'NewMarket' as const,
|
||||
instrument: {
|
||||
name: 'New proposal of the market successor',
|
||||
},
|
||||
successorConfiguration: {
|
||||
parentMarketId: 'marketId',
|
||||
},
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
proposalsConnection: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
__typename: 'Proposal',
|
||||
id: 'proposal-1',
|
||||
state: Types.ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
__typename: 'ProposalTerms',
|
||||
closingDatetime: '2023-09-27',
|
||||
enactmentDatetime: '2023-09-28',
|
||||
change: {
|
||||
__typename: 'NewMarket',
|
||||
instrument: {
|
||||
name: 'New proposal of the market successor',
|
||||
},
|
||||
successorConfiguration: {
|
||||
parentMarketId: 'marketId',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const proposal2 = {
|
||||
__typename: 'Proposal' as const,
|
||||
id: 'proposal-2',
|
||||
state: ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
__typename: 'ProposalTerms' as const,
|
||||
closingDatetime: '2023-09-27',
|
||||
enactmentDatetime: '2023-09-28',
|
||||
change: {
|
||||
__typename: 'NewMarket' as const,
|
||||
instrument: {
|
||||
name: 'New second proposal of the market successor',
|
||||
},
|
||||
successorConfiguration: {
|
||||
parentMarketId: 'marketId',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('MarketSuccessorProposalBanner', () => {
|
||||
it('should display single proposal', async () => {
|
||||
render(
|
||||
<MockedProvider mocks={[marketProposalMock]}>
|
||||
<MarketSuccessorProposalBanner marketId="marketId" />
|
||||
</MockedProvider>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('A successors to this market has been proposed')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
it('should display single proposal', () => {
|
||||
render(<MarketSuccessorProposalBanner proposals={[proposal]} />);
|
||||
|
||||
expect(
|
||||
screen
|
||||
.getByRole('link')
|
||||
.getAttribute('href')
|
||||
?.endsWith('/proposals/proposal-1') ?? false
|
||||
).toBe(true);
|
||||
});
|
||||
it('should display plural proposals', async () => {
|
||||
const dualProposalMock = {
|
||||
...marketProposalMock,
|
||||
result: {
|
||||
...marketProposalMock.result,
|
||||
data: {
|
||||
proposalsConnection: {
|
||||
edges: [
|
||||
...((
|
||||
marketProposalMock?.result as SingleExecutionResult<MarketViewProposalsQuery>
|
||||
)?.data?.proposalsConnection?.edges ?? []),
|
||||
{
|
||||
node: {
|
||||
__typename: 'Proposal',
|
||||
id: 'proposal-2',
|
||||
state: Types.ProposalState.STATE_OPEN,
|
||||
terms: {
|
||||
__typename: 'ProposalTerms',
|
||||
change: {
|
||||
__typename: 'NewMarket',
|
||||
instrument: {
|
||||
name: 'New second proposal of the market successor',
|
||||
},
|
||||
successorConfiguration: {
|
||||
parentMarketId: 'marketId',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
screen.getByText('A successor to this market has been proposed')
|
||||
).toBeInTheDocument();
|
||||
|
||||
render(
|
||||
<MockedProvider mocks={[dualProposalMock]}>
|
||||
<MarketSuccessorProposalBanner marketId="marketId" />
|
||||
</MockedProvider>
|
||||
expect(screen.getByRole('link')).toHaveAttribute(
|
||||
'href',
|
||||
expect.stringContaining(proposal.id)
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('Successors to this market have been proposed')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display plural proposals', () => {
|
||||
const proposals = [proposal, proposal2];
|
||||
render(<MarketSuccessorProposalBanner proposals={proposals} />);
|
||||
|
||||
expect(
|
||||
screen
|
||||
.getAllByRole('link')[0]
|
||||
.getAttribute('href')
|
||||
?.endsWith('/proposals/proposal-1') ?? false
|
||||
).toBe(true);
|
||||
expect(
|
||||
screen
|
||||
.getAllByRole('link')[1]
|
||||
.getAttribute('href')
|
||||
?.endsWith('/proposals/proposal-2') ?? false
|
||||
).toBe(true);
|
||||
});
|
||||
screen.getByText('Successors to this market have been proposed')
|
||||
).toBeInTheDocument();
|
||||
|
||||
it('banner should be hidden because no proposals', () => {
|
||||
const { container } = render(
|
||||
<MockedProvider>
|
||||
<MarketSuccessorProposalBanner marketId="marketId" />
|
||||
</MockedProvider>
|
||||
const links = screen.getAllByRole('link');
|
||||
expect(links).toHaveLength(proposals.length);
|
||||
expect(links[0]).toHaveAttribute(
|
||||
'href',
|
||||
expect.stringContaining(proposal.id)
|
||||
);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('banner should be hidden because no proposals for the market', () => {
|
||||
const { container } = render(
|
||||
<MockedProvider mocks={[marketProposalMock]}>
|
||||
<MarketSuccessorProposalBanner marketId="otherMarketId" />
|
||||
</MockedProvider>
|
||||
expect(links[1]).toHaveAttribute(
|
||||
'href',
|
||||
expect.stringContaining(proposal2.id)
|
||||
);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('banner should be hidden after user close click', async () => {
|
||||
const { container } = render(
|
||||
<MockedProvider mocks={[marketProposalMock]}>
|
||||
<MarketSuccessorProposalBanner marketId="marketId" />
|
||||
</MockedProvider>
|
||||
);
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText('A successors to this market has been proposed')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
await act(() => {
|
||||
screen.getByTestId('notification-banner-close').click();
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,89 +1,51 @@
|
||||
import { Fragment, useState } from 'react';
|
||||
import {
|
||||
marketViewProposalsDataProvider,
|
||||
type NewMarketSuccessorFieldsFragment,
|
||||
} from '@vegaprotocol/proposals';
|
||||
|
||||
import {
|
||||
ExternalLink,
|
||||
Intent,
|
||||
NotificationBanner,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { Fragment } from 'react';
|
||||
import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { useT } from '../../lib/use-t';
|
||||
|
||||
export const MarketSuccessorProposalBanner = ({
|
||||
marketId,
|
||||
proposals,
|
||||
}: {
|
||||
marketId?: string;
|
||||
proposals: MarketViewProposalFieldsFragment[];
|
||||
}) => {
|
||||
const t = useT();
|
||||
const { data: proposals } = useDataProvider({
|
||||
dataProvider: marketViewProposalsDataProvider,
|
||||
skip: !marketId,
|
||||
variables: {
|
||||
inState: Types.ProposalState.STATE_OPEN,
|
||||
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
|
||||
},
|
||||
});
|
||||
|
||||
const successors =
|
||||
proposals?.filter((item) => {
|
||||
if (item.terms.change.__typename === 'NewMarket') {
|
||||
const newMarket = item.terms.change;
|
||||
if (
|
||||
newMarket.successorConfiguration?.parentMarketId === marketId &&
|
||||
item.state === Types.ProposalState.STATE_OPEN
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}) ?? [];
|
||||
const [visible, setVisible] = useState(true);
|
||||
const tokenLink = useLinks(DApp.Governance);
|
||||
if (visible && successors.length) {
|
||||
return (
|
||||
<NotificationBanner
|
||||
intent={Intent.Primary}
|
||||
onClose={() => {
|
||||
setVisible(false);
|
||||
}}
|
||||
>
|
||||
<div className="uppercase mb-1">
|
||||
{successors.length === 1
|
||||
? t('A successors to this market has been proposed')
|
||||
: t('Successors to this market have been proposed')}
|
||||
</div>
|
||||
<div>
|
||||
{t(
|
||||
'checkOutProposalsAndVote',
|
||||
'Check out the terms of the proposals and vote:',
|
||||
{
|
||||
count: successors.length,
|
||||
}
|
||||
)}{' '}
|
||||
{successors.map((item, i) => {
|
||||
const externalLink = tokenLink(
|
||||
TOKEN_PROPOSAL.replace(':id', item.id || '')
|
||||
);
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
<ExternalLink href={externalLink} key={i}>
|
||||
{
|
||||
(item.terms?.change as NewMarketSuccessorFieldsFragment)
|
||||
?.instrument.name
|
||||
}
|
||||
</ExternalLink>
|
||||
{i < successors.length - 1 && ', '}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</NotificationBanner>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="uppercase mb-1">
|
||||
{proposals.length === 1
|
||||
? t('A successor to this market has been proposed')
|
||||
: t('Successors to this market have been proposed')}
|
||||
</div>
|
||||
<div>
|
||||
{t(
|
||||
'checkOutProposalsAndVote',
|
||||
'Check out the terms of the proposals and vote:',
|
||||
{
|
||||
count: proposals.length,
|
||||
}
|
||||
)}{' '}
|
||||
{proposals.map((item, i) => {
|
||||
if (item.terms.change.__typename !== 'NewMarket') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const externalLink = tokenLink(
|
||||
TOKEN_PROPOSAL.replace(':id', item.id || '')
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
<ExternalLink href={externalLink} key={i}>
|
||||
{item.terms.change.instrument.name}
|
||||
</ExternalLink>
|
||||
{i < proposals.length - 1 && ', '}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user