Compare commits

..
Author SHA1 Message Date
asiaznik b48306c366 chore: disabled timeouts as it's broken with browser wallet 2024-01-18 18:07:20 +01:00
asiaznik 3cc4ec9ef5 chore: make connect button red 2024-01-18 16:03:20 +01:00
asiaznik 8ba323072b chore: increase timeout to 1s 2024-01-18 14:53:10 +01:00
asiaznik fd22c00269 chore: extract disconnect toast hook, enable in governance 2024-01-18 14:52:26 +01:00
asiaznik 85b8380827 chore: backward compatibility 2024-01-16 13:36:23 +01:00
asiaznik 28ac02ce1e fix: incomplete objects 2024-01-15 15:46:04 +01:00
asiaznik 756781097c chore: vega chain id refactor 2024-01-15 15:35:47 +01:00
asiaznik 808232f1c5 chore: handle disconnection per extension crash 2024-01-15 15:34:18 +01:00
asiaznik f743cffcd4 fix: diconnect event 2024-01-15 15:34:18 +01:00
ArtandDexter Edwards ec1032cbd2 Update libs/wallet/src/connectors/injected-connector.ts
Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com>
2024-01-15 15:34:18 +01:00
asiaznik 0bd40744c6 chore: new browser wallet connection model 2024-01-15 15:34:18 +01:00
m.ray d32f27fcb1 feat(trading): mobile responsiveness - market selector (#5582) 2024-01-11 11:23:48 +00:00
m.ray d05cd6a2ed fix(trading): tiny scroll for rewards (#5586) 2024-01-10 15:49:58 +00:00
m.ray c003e5fa30 fix(trading): liquidity table improve readability and remove grouping (#5598) 2024-01-10 15:49:35 +00:00
Ben f62d3289ab chore(trading): retry on http error (#5601) 2024-01-10 15:44:01 +00:00
Ben a3d3d18c5c chore(trading): fix price monitoring test (#5600) 2024-01-10 14:56:17 +00:00
77ca101781 chore(trading): merge main back in develop (#5597)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
Co-authored-by: Edd <edd@vega.xyz>
2024-01-10 10:31:15 +00:00
Ben d238c37d0c chore(trading): vesting test (#5595) 2024-01-10 10:07:59 +00:00
Edd 79feb485f6 fix(governance): improve error message for configuration error (#5589) 2024-01-09 17:21:56 +00:00
Ben 6aa5c3b6e3 chore(trading): rewards page e2e (#5578) 2024-01-09 15:54:40 +00:00
Ben 82abc13fda chore(trading): skip test due to issue (#5583) 2024-01-08 16:16:03 +00:00
Edd 933f07cddf fix(explorer): minor tidyup of deterministic order view (#5573) 2024-01-08 13:25:03 +00:00
Matthew Russell c8067669c2 fix(trading): ensure fees queries always fetch on mount and poll (#5566) 2024-01-05 12:43:30 +00:00
78f5a9c520 feat(trading): activity streaks, reward hoarder bonus and active rewards (#5491)
Co-authored-by: candida-d <62548908+candida-d@users.noreply.github.com>
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
Co-authored-by: bwallacee <ben@vega.xyz>
2024-01-05 11:16:59 +00:00
Ben 462066959d chore(trading): update vega (#5563) 2024-01-03 12:00:00 +00:00
daro-maj f12f5ab961 chore(trading): fix volume issue for market selector tests (#5564) 2024-01-03 12:04:49 +01:00
98b2460d8b fix(trading): mobile friendly referral and fees views (#5530)
Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2024-01-02 15:50:04 +00:00
Matthew Russell 67c182f3de chore(trading-view): minor tv chart tweaks (#5545) 2024-01-02 13:20:01 +00:00
68e0009ab0 feat(trading,markets,proposals): market banners (#5494)
Co-authored-by: bwallacee <ben@vega.xyz>
Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
2024-01-02 11:04:43 +00:00
Bartłomiej Głownia ad74b12908 feat(trading): do not show error message if there is no faucet tx error (#5539) 2024-01-02 10:19:10 +01:00
Matthew Russell 7fff33e493 feat(trading): competition feature flag and team page (#5549) 2023-12-29 15:50:51 +00:00
daro-maj e7cb2c8276 chore(trading): fix fees tests (#5548) 2023-12-29 15:25:33 +00:00
4adaeea40a feat(trading): pane settings view (#5509)
Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-12-28 12:33:18 +00:00
Matthew Russell d47d894147 fix(deal-ticket): stray curly in stop order deal ticket (#5547) 2023-12-28 12:27:56 +00:00
154 changed files with 5161 additions and 2245 deletions
@@ -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"
@@ -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">
@@ -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
+1 -1
View File
@@ -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>
+9 -2
View File
@@ -26,7 +26,7 @@ import {
} from '@vegaprotocol/web3';
import { Web3Provider } from '@vegaprotocol/web3';
import { VegaWalletDialogs } from './components/vega-wallet-dialogs';
import { VegaWalletProvider } from '@vegaprotocol/wallet';
import { VegaWalletProvider, useChainId } from '@vegaprotocol/wallet';
import {
useVegaTransactionManager,
useVegaTransactionUpdater,
@@ -96,7 +96,9 @@ const cache: InMemoryCacheConfig = {
const Web3Container = ({
chainId,
}: {
/** Ethereum chain id */
chainId: number;
/** Ethereum provider url */
providerUrl: string;
}) => {
const InitializeHandlers = () => {
@@ -123,6 +125,9 @@ const Web3Container = ({
MOZILLA_EXTENSION_URL,
VEGA_WALLET_URL,
} = useEnvironment();
const vegaChainId = useChainId(VEGA_URL);
useEffect(() => {
if (chainId) {
return initializeConnectors(
@@ -157,7 +162,8 @@ const Web3Container = ({
!VEGA_EXPLORER_URL ||
!DocsLinks ||
!CHROME_EXTENSION_URL ||
!MOZILLA_EXTENSION_URL
!MOZILLA_EXTENSION_URL ||
!vegaChainId
) {
return null;
}
@@ -169,6 +175,7 @@ const Web3Container = ({
config={{
network: VEGA_ENV,
vegaUrl: VEGA_URL,
chainId: vegaChainId,
vegaWalletServiceUrl: VEGA_WALLET_URL,
links: {
explorer: VEGA_EXPLORER_URL,
@@ -48,6 +48,7 @@ const vegaWalletConfig: VegaWalletConfig = {
chromeExtensionUrl: 'chrome',
mozillaExtensionUrl: 'mozilla',
},
chainId: 'VEGA_CHAIN_ID',
};
const renderComponent = (proposal: ProposalQuery['proposal']) => {
+37 -1
View File
@@ -1,14 +1,50 @@
import { ToastsContainer, useToasts } from '@vegaprotocol/ui-toolkit';
import {
Intent,
ToastsContainer,
TradingButton,
VegaIcon,
VegaIconNames,
useToasts,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import {
useEthereumTransactionToasts,
useEthereumWithdrawApprovalsToasts,
useVegaTransactionToasts,
useWalletDisconnectToastActions,
useWalletDisconnectedToasts,
} from '@vegaprotocol/web3';
import { useTranslation } from 'react-i18next';
const WalletDisconnectAdditionalContent = () => {
const { t } = useTranslation();
const { hideToast } = useWalletDisconnectToastActions();
const openVegaWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
return (
<p className="mt-2">
<TradingButton
data-testid="connect-vega-wallet"
onClick={() => {
hideToast();
openVegaWalletDialog();
}}
size="small"
intent={Intent.Danger}
icon={<VegaIcon name={VegaIconNames.ARROW_RIGHT} size={14} />}
>
<span className="whitespace-nowrap uppercase">{t('Connect')}</span>
</TradingButton>
</p>
);
};
export const ToastsManager = () => {
useVegaTransactionToasts();
useEthereumTransactionToasts();
useEthereumWithdrawApprovalsToasts();
useWalletDisconnectedToasts(<WalletDisconnectAdditionalContent />);
const toasts = useToasts((store) => store.toasts);
return <ToastsContainer order="desc" toasts={toasts} />;
+1
View File
@@ -25,3 +25,4 @@ NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_REFERRALS=true
NX_TEAM_COMPETITION=true
+2
View File
@@ -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).
+8 -13
View File
@@ -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';
@@ -72,17 +72,6 @@ export const MarketPage = () => {
}
}, [update, lastMarketId, data?.id]);
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>
@@ -118,6 +107,8 @@ export const MarketPage = () => {
);
}
const pinnedAsset = data && getAsset(data);
return (
<>
<TitleUpdater
@@ -125,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} />
)}
</>
);
};
+17 -38
View File
@@ -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,31 +12,19 @@ import {
usePaneLayout,
} from '../../components/resizable-grid';
import { TradingViews } from './trade-views';
import {
MarketSuccessorBanner,
MarketSuccessorProposalBanner,
MarketTerminationBanner,
} from '../../components/market-banner';
import { useFeatureFlags } 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',
@@ -65,17 +52,17 @@ const MainGrid = memo(
menu={<TradingViews.chart.menu />}
>
<ErrorBoundary feature="chart">
<TradingViews.chart.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 &&
@@ -83,7 +70,7 @@ 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}
@@ -97,7 +84,7 @@ const MainGrid = memo(
>
<ErrorBoundary feature="funding-payments">
<TradingViews.fundingPayments.component
marketId={marketId}
marketId={market.id}
/>
</ErrorBoundary>
</Tab>
@@ -113,7 +100,7 @@ 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
@@ -122,7 +109,7 @@ const MainGrid = memo(
settings={<TradingViews.trades.settings />}
>
<ErrorBoundary feature="trades">
<TradingViews.trades.component marketId={marketId} />
<TradingViews.trades.component marketId={market.id} />
</ErrorBoundary>
</Tab>
</Tabs>
@@ -225,7 +212,6 @@ const MainGrid = memo(
MainGrid.displayName = 'MainGrid';
export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
const featureFlags = useFeatureFlags((state) => state.flags);
const wrapperClasses = classNames(
'h-full grid',
'grid-rows-[min-content_1fr]'
@@ -234,17 +220,10 @@ export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
return (
<div className={wrapperClasses}>
<div>
{featureFlags.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,6 +1,7 @@
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';
@@ -11,23 +12,17 @@ import {
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { useT } from '../../lib/use-t';
import {
MarketSuccessorBanner,
MarketSuccessorProposalBanner,
MarketTerminationBanner,
} from '../../components/market-banner';
import { MarketBanner } from '../../components/market-banner';
import { ErrorBoundary } from '../../components/error-boundary';
import { type TradingView } from './trade-views';
import { TradingViews } from './trade-views';
import { useFeatureFlags } from '@vegaprotocol/environment';
interface TradePanelsProps {
market: Market | null;
market: Market;
pinnedAsset?: PinnedAsset;
}
export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
const featureFlags = useFeatureFlags((state) => state.flags);
const [view, setView] = useState<TradingView>('chart');
const viewCfg = TradingViews[view];
@@ -76,14 +71,7 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
return (
<div className="h-full grid grid-rows-[min-content_min-content_1fr_min-content]">
<div>
{featureFlags.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 relative">
@@ -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')}
@@ -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
)}
@@ -14,7 +14,7 @@ import {
useUpdateReferees,
} from './hooks/use-referral';
import classNames from 'classnames';
import { Table } from './table';
import { Table } from '../../components/table';
import {
addDecimalsFormatNumber,
getDateFormat,
@@ -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,
})}
>
+24 -41
View File
@@ -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)),
}))}
/>
);
+10 -5
View File
@@ -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
)}
>
@@ -55,7 +57,10 @@ export const StatTile = ({
>
{title}
</h3>
<div data-testid={`${testId}-value`} className="text-5xl text-left">
<div
data-testid={`${testId}-value`}
className="text-2xl lg:text-5xl text-left"
>
{children}
</div>
{description && (
@@ -119,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>
);
};
+1
View File
@@ -0,0 +1 @@
export { Teams } from './teams';
@@ -0,0 +1,7 @@
export const Teams = () => {
return (
<div>
<h1>Teams</h1>
</div>
);
};
@@ -12,6 +12,8 @@ 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';
import { useChainId } from '@vegaprotocol/wallet';
export const Bootstrapper = ({ children }: { children: ReactNode }) => {
const t = useT();
@@ -25,13 +27,16 @@ export const Bootstrapper = ({ children }: { children: ReactNode }) => {
CHROME_EXTENSION_URL,
} = useEnvironment();
const chainId = useChainId(VEGA_URL);
if (
!VEGA_URL ||
!VEGA_WALLET_URL ||
!VEGA_EXPLORER_URL ||
!CHROME_EXTENSION_URL ||
!MOZILLA_EXTENSION_URL ||
!DocsLinks
!DocsLinks ||
!chainId
) {
return <AppLoader />;
}
@@ -52,28 +57,39 @@ 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,
chainId,
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}</>;
};
+17 -3
View File
@@ -9,6 +9,7 @@ export const Card = ({
loading = false,
highlight = false,
testId,
noBackgroundOnMobile = false,
}: {
children: ReactNode;
title: string;
@@ -16,20 +17,33 @@ export const Card = ({
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>
@@ -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;
@@ -166,6 +174,7 @@ export const FeesContainer = () => {
className="lg:col-span-full xl:col-span-2"
loading={loading}
data-testid="volume-discount-card"
noBackgroundOnMobile={true}
>
<VolumeTiers
tiers={volumeTiers}
@@ -179,12 +188,14 @@ export const FeesContainer = () => {
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
@@ -192,6 +203,7 @@ export const FeesContainer = () => {
className="lg:col-span-full"
loading={marketsLoading}
data-testid="fees-by-market-card"
noBackgroundOnMobile={true}
>
<MarketFees
markets={markets}
@@ -477,44 +489,65 @@ const VolumeTiers = ({
return (
<div>
<Table>
<THead>
<Tr>
<Th data-testid="tier-header">{t('Tier')}</Th>
<Th data-testid="discount-header">{t('Discount')}</Th>
<Th data-testid="min-volume-header">{t('Min. trading volume')}</Th>
<Th data-testid="my-volume-header">
{t('myVolume', 'My volume (last {{count}} epochs)', {
count: windowLength,
})}
</Th>
<Th data-testid="actions-header" />
</Tr>
</THead>
<tbody>
{Array.from(tiers).map((tier, i) => {
const isUserTier = tierIndex === i;
return (
<Tr key={i} data-testid={`tier-row-${i}`}>
<Td data-testid={`tier-value-${i}`}>{i + 1}</Td>
<Td data-testid={`discount-value-${i}`}>
{formatPercentage(Number(tier.volumeDiscountFactor))}%
</Td>
<Td data-testid={`min-volume-value-${i}`}>
{formatNumber(tier.minimumRunningNotionalTakerVolume)}
</Td>
<Td data-testid={`my-volume-value-${i}`}>
{isUserTier ? formatNumber(lastEpochVolume) : ''}
</Td>
<Td data-testid={`your-tier-${i}`}>
{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>
);
};
@@ -524,6 +557,7 @@ const ReferralTiers = ({
tierIndex,
epochsInSet,
referralVolumeInWindow,
referralDiscountWindowLength,
}: {
tiers: Array<{
referralDiscountFactor: string;
@@ -533,6 +567,7 @@ const ReferralTiers = ({
tierIndex: number;
epochsInSet: number;
referralVolumeInWindow: number;
referralDiscountWindowLength: number;
}) => {
const t = useT();
@@ -544,60 +579,81 @@ const ReferralTiers = ({
return (
<div>
<Table>
<THead>
<Tr>
<Th data-testid="tier-header">{t('Tier')}</Th>
<Th data-testid="discount-header">{t('Discount')}</Th>
<Th data-testid="min-volume-header">{t('Min. trading volume')}</Th>
<Th data-testid="required-epochs-header">{t('Required epochs')}</Th>
<Th data-testid="extra-header" />
</Tr>
</THead>
<tbody>
{Array.from(tiers).map((tier, 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(
tier.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 < tier.minimumEpochs
) {
unlocksIn = (
<span className="text-muted">
Unlocks in {tier.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} data-testid={`tier-row-${i}`}>
<Td data-testid={`tier-value-${i}`}>{i + 1}</Td>
<Td data-testid={`discount-value-${i}`}>
{formatPercentage(Number(tier.referralDiscountFactor))}%
</Td>
<Td data-testid={`min-volume-value-${i}`}>
{formatNumber(tier.minimumRunningNotionalTakerVolume)}
</Td>
<Td data-testid={`required-epochs-value-${i}`}>
{tier.minimumEpochs}
</Td>
<Td data-testid={`user-tier-or-unlocks-${i}`}>
{isUserTier ? (
<YourTier testId={`your-tier-${i}`} />
) : (
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>
);
};
@@ -611,7 +667,7 @@ const YourTier = ({ testId }: YourTierProps) => {
return (
<span
className="bg-rainbow whitespace-nowrap rounded-xl px-4 py-1.5 text-white"
className="bg-rainbow whitespace-nowrap rounded-xl px-4 py-1.5 text-white text-xs"
data-testid={testId}
>
{t('Your tier')}
@@ -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}
@@ -1,4 +1,5 @@
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
export const useReferralStats = (
previousEpoch?: number,
referralStats?: NonNullable<
@@ -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',
+48
View File
@@ -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>
);
};
@@ -0,0 +1,6 @@
import { useT } from '../../lib/use-t';
export const MarketSuspendedBanner = () => {
const t = useT();
return <p>{t('Market was suspended by governance')}</p>;
};
@@ -1,230 +0,0 @@
import { render, screen, waitFor } from '@testing-library/react';
import * as Types from '@vegaprotocol/types';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import type { MarketViewProposalsQuery } from '@vegaprotocol/proposals';
import { MarketViewProposalsDocument } from '@vegaprotocol/proposals';
import type { Market } from '@vegaprotocol/markets';
import { MarketTerminationBanner } from './market-termination-banner';
const marketMock = {
id: 'market-1',
decimalPlaces: 3,
tradableInstrument: {
instrument: {
product: {
__typename: 'Future',
quoteName: 'tDAI',
},
},
},
} as Market;
const passedProposalMock: MockedResponse<MarketViewProposalsQuery> = {
request: {
query: MarketViewProposalsDocument,
variables: {
inState: Types.ProposalState.STATE_PASSED,
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
},
},
result: {
data: {
proposalsConnection: {
edges: [
{
node: {
id: '1',
state: Types.ProposalState.STATE_PASSED,
terms: {
closingDatetime: '2023-09-27T11:48:18Z',
enactmentDatetime: '2023-09-30T11:48:18',
change: {
__typename: 'UpdateMarketState',
updateType:
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
price: '',
market: {
id: 'market-1',
tradableInstrument: {
instrument: {
name: 'Market one name',
code: 'Market one',
},
},
},
},
},
},
},
{
node: {
id: '2',
state: Types.ProposalState.STATE_PASSED,
terms: {
closingDatetime: '2023-09-27T11:48:18Z',
enactmentDatetime: '2023-10-01T11:48:18',
change: {
__typename: 'UpdateMarketState',
updateType:
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
price: '',
market: {
id: 'market-2',
tradableInstrument: {
instrument: {
name: 'Market two name',
code: 'Market two',
},
},
},
},
},
},
},
],
},
},
},
};
const openProposalMock: MockedResponse<MarketViewProposalsQuery> = {
request: {
query: MarketViewProposalsDocument,
variables: {
inState: Types.ProposalState.STATE_OPEN,
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
},
},
result: {
data: {
proposalsConnection: {
edges: [
{
node: {
id: '3',
state: Types.ProposalState.STATE_OPEN,
terms: {
closingDatetime: '2023-09-27T11:48:18Z',
enactmentDatetime: '2023-10-01T11:48:18',
change: {
__typename: 'UpdateMarketState',
updateType:
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
price: '',
market: {
id: 'market-3',
tradableInstrument: {
instrument: {
name: 'Market three name',
code: 'Market three',
},
},
},
},
},
},
},
{
node: {
id: '4',
state: Types.ProposalState.STATE_OPEN,
terms: {
closingDatetime: '2023-09-27T11:48:18Z',
enactmentDatetime: '2023-10-11T11:48:18',
change: {
__typename: 'UpdateMarketState',
updateType:
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
price: '',
market: {
id: 'market-3',
tradableInstrument: {
instrument: {
name: 'Market three name',
code: 'Market three',
},
},
},
},
},
},
},
{
node: {
id: '5',
state: Types.ProposalState.STATE_OPEN,
terms: {
closingDatetime: '2023-09-27T11:48:18Z',
enactmentDatetime: '2023-10-01T11:48:18',
change: {
__typename: 'UpdateMarketState',
updateType:
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE,
price: '',
market: {
id: 'market-4',
tradableInstrument: {
instrument: {
name: 'Market four name',
code: 'Market four',
},
},
},
},
},
},
},
],
},
},
},
};
const mocks: MockedResponse[] = [passedProposalMock, openProposalMock];
describe('MarketTerminationBanner', () => {
beforeAll(() => {
jest.useFakeTimers().setSystemTime(new Date('2023-09-28T10:10:10.000Z'));
});
afterAll(() => {
jest.useRealTimers();
});
it('should be properly rendered', async () => {
const { container } = render(
<MockedProvider mocks={mocks}>
<MarketTerminationBanner market={marketMock} />
</MockedProvider>
);
await waitFor(() => {
expect(container).not.toBeEmptyDOMElement();
});
expect(
screen.getByTestId('termination-warning-banner-market-1')
).toBeInTheDocument();
});
it('should render link to proposals', async () => {
const { container } = render(
<MockedProvider mocks={mocks}>
<MarketTerminationBanner market={{ ...marketMock, id: 'market-3' }} />
</MockedProvider>
);
await waitFor(() => {
expect(container).not.toBeEmptyDOMElement();
});
expect(screen.getByText('View proposals')).toBeInTheDocument();
});
it('should render link to proposal', async () => {
const { container } = render(
<MockedProvider mocks={mocks}>
<MarketTerminationBanner market={{ ...marketMock, id: 'market-4' }} />
</MockedProvider>
);
await waitFor(() => {
expect(container).not.toBeEmptyDOMElement();
});
expect(screen.getByText('View proposal')).toBeInTheDocument();
});
});
@@ -0,0 +1,84 @@
import { render, screen } from '@testing-library/react';
import { ProposalState } from '@vegaprotocol/types';
import { MarketUpdateBanner } from './market-update-banner';
import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
describe('MarketUpdateBanner', () => {
const change = {
__typename: 'UpdateMarket' as const,
marketId: 'update-market-id',
};
const openProposal = {
__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,
},
};
const passedProposal = {
__typename: 'Proposal' as const,
id: 'proposal-2',
state: ProposalState.STATE_PASSED,
terms: {
__typename: 'ProposalTerms' as const,
closingDatetime: '2023-09-27',
enactmentDatetime: '2023-09-28',
change,
},
};
it('renders content for a single open proposal', () => {
render(
<MarketUpdateBanner
proposals={[openProposal as MarketViewProposalFieldsFragment]}
/>
);
expect(
screen.getByText(/^changes have been proposed/i)
).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'View proposal' })).toHaveAttribute(
'href',
expect.stringContaining(openProposal.id as string)
);
});
it('renders content for a single passed proposal', () => {
render(
<MarketUpdateBanner
proposals={[passedProposal as MarketViewProposalFieldsFragment]}
/>
);
expect(
screen.getByText(/^proposal set to change market/i)
).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'View proposal' })).toHaveAttribute(
'href',
expect.stringContaining(passedProposal.id)
);
});
it('renders content for multiple passed proposals', () => {
const proposals = [
openProposal,
openProposal,
] as MarketViewProposalFieldsFragment[];
render(<MarketUpdateBanner proposals={proposals} />);
expect(
screen.getByText(
new RegExp(`^There are ${proposals.length} open proposals`)
)
).toBeInTheDocument();
expect(
screen.getByRole('link', { name: 'View proposals' })
).toHaveAttribute('href', expect.stringContaining('proposals'));
});
});
@@ -0,0 +1,71 @@
import sortBy from 'lodash/sortBy';
import { format } from 'date-fns';
import {
DApp,
TOKEN_PROPOSAL,
TOKEN_PROPOSALS,
useLinks,
} from '@vegaprotocol/environment';
import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { ProposalState } from '@vegaprotocol/types';
import { useT } from '../../lib/use-t';
export const MarketUpdateBanner = ({
proposals,
}: {
proposals: MarketViewProposalFieldsFragment[];
}) => {
const governanceLink = useLinks(DApp.Governance);
const t = useT();
const openProposals = sortBy(
proposals.filter((p) => p.state === ProposalState.STATE_OPEN),
(p) => p.terms.enactmentDatetime
);
const passedProposals = sortBy(
proposals.filter((p) => p.state === ProposalState.STATE_PASSED),
(p) => p.terms.enactmentDatetime
);
let content = null;
if (openProposals.length > 1) {
content = (
<p>
{t('There are {{count}} open proposals to change this market', {
count: openProposals.length,
})}
<ExternalLink href={governanceLink(TOKEN_PROPOSALS)}>
{t('View proposals')}
</ExternalLink>
</p>
);
} else if (passedProposals.length) {
const proposal = passedProposals[0];
const proposalLink = governanceLink(
TOKEN_PROPOSAL.replace(':id', proposal?.id || '')
);
content = (
<p>
{t('Proposal set to change market on {{date}}.', {
date: format(new Date(proposal.terms.enactmentDatetime), 'dd MMMM'),
})}
<ExternalLink href={proposalLink}>{t('View proposal')}</ExternalLink>,
</p>
);
} else {
const proposal = openProposals[0];
const proposalLink = governanceLink(
TOKEN_PROPOSAL.replace(':id', proposal?.id || '')
);
content = (
<p>
{t('Changes have been proposed for this market.')}{' '}
<ExternalLink href={proposalLink}>{t('View proposal')}</ExternalLink>
</p>
);
}
return <div data-testid="market-proposal-notification">{content}</div>;
};
@@ -1,44 +1,120 @@
import type { ReactNode } from 'react';
import { useState } from 'react';
import { format, formatDuration, intervalToDuration } from 'date-fns';
import {
ExternalLink,
Intent,
NotificationBanner,
} from '@vegaprotocol/ui-toolkit';
import type { MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
import { marketViewProposalsDataProvider } from '@vegaprotocol/proposals';
import * as Types from '@vegaprotocol/types';
import type { Market } from '@vegaprotocol/markets';
import { getQuoteName } from '@vegaprotocol/markets';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { type ReactNode } from 'react';
import sortBy from 'lodash/sortBy';
import { format, formatDuration, intervalToDuration } from 'date-fns';
import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
import { ProposalState } from '@vegaprotocol/types';
import {
DApp,
TOKEN_PROPOSAL,
TOKEN_PROPOSALS,
useLinks,
} from '@vegaprotocol/environment';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { getQuoteName, type Market } from '@vegaprotocol/markets';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { useT } from '../../lib/use-t';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
const filterProposals = (
data: MarketViewProposalFieldsFragment[] | null,
marketId: string,
now: number
) =>
sortBy(
(data || []).filter(
(item) =>
item.terms.change.__typename === 'UpdateMarketState' &&
item.terms.change.market.id === marketId &&
item.terms.change.updateType ===
Types.MarketUpdateType.MARKET_STATE_UPDATE_TYPE_TERMINATE &&
item.terms.enactmentDatetime &&
new Date(item.terms.enactmentDatetime).getTime() > now
),
(item) => item.terms.enactmentDatetime
export const MarketUpdateStateBanner = ({
market,
proposals,
}: {
market: Market;
proposals: MarketViewProposalFieldsFragment[];
}) => {
const t = useT();
const governanceLink = useLinks(DApp.Governance);
const openProposals = sortBy(
proposals.filter((p) => p.state === ProposalState.STATE_OPEN),
(p) => p.terms.enactmentDatetime
);
const passedProposals = sortBy(
proposals.filter((p) => p.state === ProposalState.STATE_PASSED),
(p) => p.terms.enactmentDatetime
);
if (!passedProposals.length && !openProposals.length) {
return null;
}
const name = market.tradableInstrument.instrument.code;
const assetSymbol = getQuoteName(market);
const proposalLink =
!passedProposals.length && openProposals[0]?.id
? governanceLink(TOKEN_PROPOSAL.replace(':id', openProposals[0]?.id))
: undefined;
const proposalsLink =
openProposals.length > 1 ? governanceLink(TOKEN_PROPOSALS) : undefined;
let content: ReactNode;
if (passedProposals.length) {
const { date, duration, price } = getMessageVariables(passedProposals[0]);
content = (
<>
<p className="uppercase mb-1">
{t('Trading on market {{name}} will stop on {{date}}', {
name,
date,
})}
</p>
<p>
{t(
'You will no longer be able to hold a position on this market when it closes in {{duration}}.',
{ duration }
)}{' '}
{price &&
assetSymbol &&
t('The final price will be {{price}} {{assetSymbol}}.', {
price: addDecimalsFormatNumber(price, market.decimalPlaces),
assetSymbol,
})}
</p>
</>
);
} else if (openProposals.length > 1) {
content = (
<>
<p className="uppercase mb-1">
{t(
'Trading on market {{name}} may stop. There are open proposals to close this market',
{ name }
)}
</p>
<p>
<ExternalLink href={proposalsLink}>
{t('View proposals')}
</ExternalLink>
</p>
</>
);
} else {
const { date, price } = getMessageVariables(openProposals[0]);
content = (
<>
<p className="mb-1">
{t(
'Trading on market {{name}} may stop on {{date}}. There is an open proposal to close this market.',
{ name, date }
)}
</p>
<p>
{price &&
assetSymbol &&
t('Proposed final price is {{price}} {{assetSymbol}}.', {
price: addDecimalsFormatNumber(price, market.decimalPlaces),
assetSymbol,
})}{' '}
<ExternalLink href={proposalLink}>{t('View proposal')}</ExternalLink>
</p>
</>
);
}
return <div data-testid={`update-state-banner-${market.id}`}>{content}</div>;
};
const getMessageVariables = (proposal: MarketViewProposalFieldsFragment) => {
const enactmentDatetime = new Date(proposal.terms.enactmentDatetime);
@@ -62,126 +138,3 @@ const getMessageVariables = (proposal: MarketViewProposalFieldsFragment) => {
price,
};
};
export const MarketTerminationBanner = ({
market,
}: {
market: Market | null;
}) => {
const t = useT();
const [visible, setVisible] = useState(true);
const skip = !market || !visible;
const { data: passedProposalsData } = useDataProvider({
dataProvider: marketViewProposalsDataProvider,
skip,
variables: {
inState: Types.ProposalState.STATE_PASSED,
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
},
});
const { data: openProposalsData } = useDataProvider({
dataProvider: marketViewProposalsDataProvider,
skip,
variables: {
inState: Types.ProposalState.STATE_OPEN,
proposalType: Types.ProposalType.TYPE_UPDATE_MARKET_STATE,
},
});
const governanceLink = useLinks(DApp.Governance);
if (!market) return null;
const now = Date.now();
const passedProposals = filterProposals(passedProposalsData, market.id, now);
const openProposals = filterProposals(openProposalsData, market.id, now);
const name = market.tradableInstrument.instrument.code;
if (!passedProposals.length && !openProposals.length) {
return null;
}
const assetSymbol = getQuoteName(market);
const proposalLink =
!passedProposals.length && openProposals[0]?.id
? governanceLink(TOKEN_PROPOSAL.replace(':id', openProposals[0]?.id))
: undefined;
const proposalsLink =
openProposals.length > 1 ? governanceLink(TOKEN_PROPOSALS) : undefined;
let content: ReactNode;
if (passedProposals.length) {
const { date, duration, price } = getMessageVariables(passedProposals[0]);
content = (
<>
<div className="uppercase mb-1">
{t('Trading on Market {{name}} will stop on {{date}}', {
name,
date,
})}
</div>
<div>
{t(
'You will no longer be able to hold a position on this market when it closes in {{duration}}.',
{ duration }
)}{' '}
{price &&
assetSymbol &&
t('The final price will be {{price}} {{assetSymbol}}.', {
price: addDecimalsFormatNumber(price, market.decimalPlaces),
assetSymbol,
})}
</div>
</>
);
} else if (openProposals.length > 1) {
content = (
<>
<div className="uppercase mb-1">
{t(
'Trading on Market {{name}} may stop. There are open proposals to close this market',
{ name }
)}
</div>
<div>
<ExternalLink href={proposalsLink}>
{t('View proposals')}
</ExternalLink>
</div>
</>
);
} else {
const { date, price } = getMessageVariables(openProposals[0]);
content = (
<>
<div className="uppercase mb-1">
{t(
'Trading on Market {{name}} may stop on {{date}}. There is open proposal to close this market.',
{ name, date }
)}
</div>
<div>
{price &&
assetSymbol &&
t('Proposed final price is {{price}} {{assetSymbol}}.', {
price: addDecimalsFormatNumber(price, market.decimalPlaces),
assetSymbol,
})}
</div>
<div>
<ExternalLink href={proposalLink}>{t('View proposal')}</ExternalLink>
</div>
</>
);
}
return (
<NotificationBanner
intent={openProposals.length ? Intent.Warning : Intent.Info}
onClose={() => {
setVisible(false);
}}
data-testid={`termination-warning-banner-${market.id}`}
>
{content}
</NotificationBanner>
);
};
@@ -0,0 +1,88 @@
import {
useMarketProposals,
type MarketViewProposalFieldsFragment,
} from '@vegaprotocol/proposals';
import { ProposalState, ProposalType } from '@vegaprotocol/types';
const isPending = (p: MarketViewProposalFieldsFragment) => {
return [ProposalState.STATE_OPEN, ProposalState.STATE_PASSED].includes(
p.state
);
};
export const useUpdateMarketStateProposals = (
marketId: string,
inState?: ProposalState
) => {
const { data, error, loading } = useMarketProposals({
proposalType: ProposalType.TYPE_UPDATE_MARKET_STATE,
inState,
});
const proposals = data
? data.filter(isPending).filter((p) => {
const change = p.terms.change;
if (
change.__typename === 'UpdateMarketState' &&
change.market.id === marketId
) {
return true;
}
return false;
})
: [];
return { data, error, loading, proposals };
};
export const useUpdateMarketProposals = (
marketId: string,
inState?: ProposalState
) => {
const { data, error, loading } = useMarketProposals({
proposalType: ProposalType.TYPE_UPDATE_MARKET,
inState,
});
const proposals = data
? data.filter(isPending).filter((p) => {
const change = p.terms.change;
if (
change.__typename === 'UpdateMarket' &&
change.marketId === marketId
) {
return true;
}
return false;
})
: [];
return { data, error, loading, proposals };
};
export const useSuccessorMarketProposals = (
marketId: string,
inState?: ProposalState
) => {
const { data, loading, error } = useMarketProposals({
proposalType: ProposalType.TYPE_NEW_MARKET,
inState,
});
const proposals = data
? data.filter(isPending).filter((p) => {
const change = p.terms.change;
if (
change.__typename === 'NewMarket' &&
change.successorConfiguration?.parentMarketId === marketId
) {
return true;
}
return false;
})
: [];
return { data, error, loading, proposals };
};
@@ -97,9 +97,9 @@ const MarketData = ({
return (
<>
<div className="w-2/5" role="gridcell">
<div className="w-2/6" role="gridcell">
<h3 className="flex items-baseline">
<span className="overflow-hidden text-sm lg:text-base text-ellipsis whitespace-nowrap">
<span className="overflow-hidden text-xs md:text-sm lg:text-base text-ellipsis whitespace-nowrap">
{market.tradableInstrument.instrument.code}
</span>
{allProducts && productType && (
@@ -113,7 +113,7 @@ const MarketData = ({
)}
</div>
<div
className="w-1/5 overflow-hidden text-xs lg:text-sm whitespace-nowrap text-ellipsis"
className="w-2/6 overflow-hidden text-xs lg:text-sm whitespace-nowrap text-ellipsis text-right"
title={symbol}
data-testid="market-selector-price"
role="gridcell"
@@ -121,14 +121,14 @@ const MarketData = ({
{price} {symbol}
</div>
<div
className="w-1/5 overflow-hidden text-xs text-right lg:text-sm whitespace-nowrap text-ellipsis"
className="w-2/6 sm:w-1/6 overflow-hidden text-xs lg:text-sm whitespace-nowrap text-ellipsis text-right"
title={t('24h vol')}
data-testid="market-selector-volume"
role="gridcell"
>
{volume}
</div>
<div className="flex justify-end w-1/5" role="gridcell">
<div className="hidden sm:w-1/6 sm:flex justify-end" role="gridcell">
{oneDayCandles && (
<Sparkline
width={64}
@@ -64,7 +64,7 @@ export const MarketSelector = ({
setFilter((curr) => ({ ...curr, product }));
}}
/>
<div className="text-sm grid grid-cols-[2fr_1fr_1fr] gap-1 ">
<div className="text-sm flex sm:grid grid-cols-[2fr_1fr_1fr] gap-1 ">
<div className="flex-1">
<TradingInput
onChange={(e) =>
@@ -182,16 +182,16 @@ const MarketList = ({
'p-2 mx-2 border-b border-default text-xs text-secondary'
)}
>
<div className="w-2/5" role="columnheader">
<div className="w-2/6" role="columnheader">
{t('Name')}
</div>
<div className="w-1/5" role="columnheader">
<div className="w-2/6 text-right pr-4" role="columnheader">
{t('Price')}
</div>
<div className="w-1/5 text-right" role="columnheader">
<div className="w-2/6 sm:w-1/6 text-right" role="columnheader">
{t('24h volume')}
</div>
<div className="w-1/5" role="columnheader" />
<div className="hidden sm:w-1/6" role="columnheader" />
</div>
<div ref={listRef}>
<List
+11 -1
View File
@@ -5,6 +5,7 @@ import { useParams } from 'react-router-dom';
import * as PopoverPrimitive from '@radix-ui/react-popover';
import { useState } from 'react';
import { useT } from '../../lib/use-t';
import classNames from 'classnames';
/**
* This is only rendered for the mobile navigation
@@ -30,7 +31,16 @@ export const NavHeader = () => {
trigger={
<h1 className="flex gap-1 sm:gap-2 md:gap-4 items-center text-default text-lg whitespace-nowrap xl:pr-4 xl:border-r border-default">
{data ? data.tradableInstrument.instrument.code : t('Select market')}
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={20} />
<span
className={classNames(
'transition-transform ease-in-out duration-300',
{
'rotate-180': open,
}
)}
>
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={20} />
</span>
</h1>
}
>
@@ -2,14 +2,19 @@ query RewardsPage($partyId: ID!) {
party(id: $partyId) {
id
vestingStats {
# AKA hoarder reward multiplier
rewardBonusMultiplier
quantumBalance
epochSeq
}
activityStreak {
# vesting multiplier
rewardVestingMultiplier
# AKA streak multiplier
activeFor
isActive
inactiveFor
rewardDistributionMultiplier
rewardVestingMultiplier
epoch
tradedVolume
openVolume
}
vestingBalancesSummary {
epoch
@@ -36,6 +41,74 @@ query RewardsPage($partyId: ID!) {
}
}
query ActiveRewards(
$isReward: Boolean
$partyId: ID
$direction: TransferDirection
$pagination: Pagination
) {
transfersConnection(
partyId: $partyId
isReward: $isReward
direction: $direction
pagination: $pagination
) {
edges {
node {
transfer {
amount
id
from
fromAccountType
to
toAccountType
asset {
id
symbol
decimals
name
quantum
status
}
reference
status
timestamp
kind {
... on RecurringTransfer {
startEpoch
endEpoch
dispatchStrategy {
dispatchMetric
dispatchMetricAssetId
marketIdsInScope
entityScope
individualScope
teamScope
nTopPerformers
stakingRequirement
notionalTimeWeightedAveragePositionRequirement
windowLength
lockPeriod
distributionStrategy
rankTable {
startRank
shareRatio
}
}
}
}
reason
}
fees {
transferId
amount
epoch
}
}
}
}
}
query RewardsHistory(
$partyId: ID!
$epochRewardSummariesPagination: Pagination
@@ -92,3 +165,18 @@ query RewardsEpoch {
id
}
}
query MarketForRewards($marketId: ID!) {
market(id: $marketId) {
tradableInstrument {
instrument {
id
name
code
metadata {
tags
}
}
}
}
}
@@ -8,7 +8,17 @@ export type RewardsPageQueryVariables = Types.Exact<{
}>;
export type RewardsPageQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, vestingStats?: { __typename?: 'PartyVestingStats', rewardBonusMultiplier: string } | null, activityStreak?: { __typename?: 'PartyActivityStreak', rewardVestingMultiplier: string, rewardDistributionMultiplier: string } | null, vestingBalancesSummary: { __typename?: 'PartyVestingBalancesSummary', epoch?: number | null, vestingBalances?: Array<{ __typename?: 'PartyVestingBalance', balance: string, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, quantum: string } }> | null, lockedBalances?: Array<{ __typename?: 'PartyLockedBalance', balance: string, untilEpoch: number, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, quantum: string } }> | null } } | null };
export type RewardsPageQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, vestingStats?: { __typename?: 'PartyVestingStats', rewardBonusMultiplier: string, quantumBalance: string, epochSeq: number } | null, activityStreak?: { __typename?: 'PartyActivityStreak', activeFor: number, isActive: boolean, inactiveFor: number, rewardDistributionMultiplier: string, rewardVestingMultiplier: string, epoch: number, tradedVolume: string, openVolume: string } | null, vestingBalancesSummary: { __typename?: 'PartyVestingBalancesSummary', epoch?: number | null, vestingBalances?: Array<{ __typename?: 'PartyVestingBalance', balance: string, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, quantum: string } }> | null, lockedBalances?: Array<{ __typename?: 'PartyLockedBalance', balance: string, untilEpoch: number, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, quantum: string } }> | null } } | null };
export type ActiveRewardsQueryVariables = Types.Exact<{
isReward?: Types.InputMaybe<Types.Scalars['Boolean']>;
partyId?: Types.InputMaybe<Types.Scalars['ID']>;
direction?: Types.InputMaybe<Types.TransferDirection>;
pagination?: Types.InputMaybe<Types.Pagination>;
}>;
export type ActiveRewardsQuery = { __typename?: 'Query', transfersConnection?: { __typename?: 'TransferConnection', edges?: Array<{ __typename?: 'TransferEdge', node: { __typename?: 'TransferNode', transfer: { __typename?: 'Transfer', amount: string, id: string, from: string, fromAccountType: Types.AccountType, to: string, toAccountType: Types.AccountType, reference?: string | null, status: Types.TransferStatus, timestamp: any, reason?: string | null, asset?: { __typename?: 'Asset', id: string, symbol: string, decimals: number, name: string, quantum: string, status: Types.AssetStatus } | null, kind: { __typename?: 'OneOffGovernanceTransfer' } | { __typename?: 'OneOffTransfer' } | { __typename?: 'RecurringGovernanceTransfer' } | { __typename?: 'RecurringTransfer', startEpoch: number, endEpoch?: number | null, dispatchStrategy?: { __typename?: 'DispatchStrategy', dispatchMetric: Types.DispatchMetric, dispatchMetricAssetId: string, marketIdsInScope?: Array<string> | null, entityScope: Types.EntityScope, individualScope?: Types.IndividualScope | null, teamScope?: Array<string | null> | null, nTopPerformers?: string | null, stakingRequirement: string, notionalTimeWeightedAveragePositionRequirement: string, windowLength: number, lockPeriod: number, distributionStrategy: Types.DistributionStrategy, rankTable?: Array<{ __typename?: 'RankTable', startRank: number, shareRatio: number } | null> | null } | null } }, fees?: Array<{ __typename?: 'TransferFee', transferId: string, amount: string, epoch: number } | null> | null } } | null> | null } | null };
export type RewardsHistoryQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
@@ -26,6 +36,13 @@ export type RewardsEpochQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type RewardsEpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string } };
export type MarketForRewardsQueryVariables = Types.Exact<{
marketId: Types.Scalars['ID'];
}>;
export type MarketForRewardsQuery = { __typename?: 'Query', market?: { __typename?: 'Market', tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null } } } } | null };
export const RewardsPageDocument = gql`
query RewardsPage($partyId: ID!) {
@@ -33,10 +50,18 @@ export const RewardsPageDocument = gql`
id
vestingStats {
rewardBonusMultiplier
quantumBalance
epochSeq
}
activityStreak {
rewardVestingMultiplier
activeFor
isActive
inactiveFor
rewardDistributionMultiplier
rewardVestingMultiplier
epoch
tradedVolume
openVolume
}
vestingBalancesSummary {
epoch
@@ -91,6 +116,101 @@ export function useRewardsPageLazyQuery(baseOptions?: Apollo.LazyQueryHookOption
export type RewardsPageQueryHookResult = ReturnType<typeof useRewardsPageQuery>;
export type RewardsPageLazyQueryHookResult = ReturnType<typeof useRewardsPageLazyQuery>;
export type RewardsPageQueryResult = Apollo.QueryResult<RewardsPageQuery, RewardsPageQueryVariables>;
export const ActiveRewardsDocument = gql`
query ActiveRewards($isReward: Boolean, $partyId: ID, $direction: TransferDirection, $pagination: Pagination) {
transfersConnection(
partyId: $partyId
isReward: $isReward
direction: $direction
pagination: $pagination
) {
edges {
node {
transfer {
amount
id
from
fromAccountType
to
toAccountType
asset {
id
symbol
decimals
name
quantum
status
}
reference
status
timestamp
kind {
... on RecurringTransfer {
startEpoch
endEpoch
dispatchStrategy {
dispatchMetric
dispatchMetricAssetId
marketIdsInScope
entityScope
individualScope
teamScope
nTopPerformers
stakingRequirement
notionalTimeWeightedAveragePositionRequirement
windowLength
lockPeriod
distributionStrategy
rankTable {
startRank
shareRatio
}
}
}
}
reason
}
fees {
transferId
amount
epoch
}
}
}
}
}
`;
/**
* __useActiveRewardsQuery__
*
* To run a query within a React component, call `useActiveRewardsQuery` and pass it any options that fit your needs.
* When your component renders, `useActiveRewardsQuery` 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 } = useActiveRewardsQuery({
* variables: {
* isReward: // value for 'isReward'
* partyId: // value for 'partyId'
* direction: // value for 'direction'
* pagination: // value for 'pagination'
* },
* });
*/
export function useActiveRewardsQuery(baseOptions?: Apollo.QueryHookOptions<ActiveRewardsQuery, ActiveRewardsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ActiveRewardsQuery, ActiveRewardsQueryVariables>(ActiveRewardsDocument, options);
}
export function useActiveRewardsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ActiveRewardsQuery, ActiveRewardsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ActiveRewardsQuery, ActiveRewardsQueryVariables>(ActiveRewardsDocument, options);
}
export type ActiveRewardsQueryHookResult = ReturnType<typeof useActiveRewardsQuery>;
export type ActiveRewardsLazyQueryHookResult = ReturnType<typeof useActiveRewardsLazyQuery>;
export type ActiveRewardsQueryResult = Apollo.QueryResult<ActiveRewardsQuery, ActiveRewardsQueryVariables>;
export const RewardsHistoryDocument = gql`
query RewardsHistory($partyId: ID!, $epochRewardSummariesPagination: Pagination, $partyRewardsPagination: Pagination, $fromEpoch: Int, $toEpoch: Int) {
epochRewardSummaries(
@@ -202,4 +322,48 @@ export function useRewardsEpochLazyQuery(baseOptions?: Apollo.LazyQueryHookOptio
}
export type RewardsEpochQueryHookResult = ReturnType<typeof useRewardsEpochQuery>;
export type RewardsEpochLazyQueryHookResult = ReturnType<typeof useRewardsEpochLazyQuery>;
export type RewardsEpochQueryResult = Apollo.QueryResult<RewardsEpochQuery, RewardsEpochQueryVariables>;
export type RewardsEpochQueryResult = Apollo.QueryResult<RewardsEpochQuery, RewardsEpochQueryVariables>;
export const MarketForRewardsDocument = gql`
query MarketForRewards($marketId: ID!) {
market(id: $marketId) {
tradableInstrument {
instrument {
id
name
code
metadata {
tags
}
}
}
}
}
`;
/**
* __useMarketForRewardsQuery__
*
* To run a query within a React component, call `useMarketForRewardsQuery` and pass it any options that fit your needs.
* When your component renders, `useMarketForRewardsQuery` 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 } = useMarketForRewardsQuery({
* variables: {
* marketId: // value for 'marketId'
* },
* });
*/
export function useMarketForRewardsQuery(baseOptions: Apollo.QueryHookOptions<MarketForRewardsQuery, MarketForRewardsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<MarketForRewardsQuery, MarketForRewardsQueryVariables>(MarketForRewardsDocument, options);
}
export function useMarketForRewardsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MarketForRewardsQuery, MarketForRewardsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<MarketForRewardsQuery, MarketForRewardsQueryVariables>(MarketForRewardsDocument, options);
}
export type MarketForRewardsQueryHookResult = ReturnType<typeof useMarketForRewardsQuery>;
export type MarketForRewardsLazyQueryHookResult = ReturnType<typeof useMarketForRewardsLazyQuery>;
export type MarketForRewardsQueryResult = Apollo.QueryResult<MarketForRewardsQuery, MarketForRewardsQueryVariables>;
@@ -0,0 +1,174 @@
import { render, screen } from '@testing-library/react';
import {
ActiveRewardCard,
applyFilter,
isActiveReward,
} from './active-rewards';
import {
AccountType,
AssetStatus,
DispatchMetric,
DistributionStrategy,
EntityScope,
IndividualScope,
type RecurringTransfer,
type TransferNode,
TransferStatus,
type Transfer,
} from '@vegaprotocol/types';
jest.mock('./__generated__/Rewards', () => ({
useMarketForRewardsQuery: () => ({
data: undefined,
}),
}));
jest.mock('@vegaprotocol/assets', () => ({
useAssetDataProvider: () => {
return {
data: {
assetId: 'asset-1',
},
};
},
}));
describe('ActiveRewards', () => {
const mockRecurringTransfer: RecurringTransfer = {
__typename: 'RecurringTransfer',
startEpoch: 115332,
endEpoch: 115432,
factor: '1',
dispatchStrategy: {
__typename: 'DispatchStrategy',
dispatchMetric: DispatchMetric.DISPATCH_METRIC_LP_FEES_RECEIVED,
dispatchMetricAssetId:
'c9fe6fc24fce121b2cc72680543a886055abb560043fda394ba5376203b7527d',
marketIdsInScope: null,
entityScope: EntityScope.ENTITY_SCOPE_INDIVIDUALS,
individualScope: IndividualScope.INDIVIDUAL_SCOPE_ALL,
teamScope: null,
nTopPerformers: '',
stakingRequirement: '',
notionalTimeWeightedAveragePositionRequirement: '',
windowLength: 1,
lockPeriod: 0,
distributionStrategy: DistributionStrategy.DISTRIBUTION_STRATEGY_PRO_RATA,
rankTable: null,
},
};
const mockTransferNode: TransferNode = {
__typename: 'TransferNode',
transfer: {
__typename: 'Transfer',
amount: '1613000000',
id: 'c4e59bd389c8098e6c7669f2d3e1613b9f42d30b1c4c3793ac44380f4c522835',
from: '69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f',
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
to: 'network',
toAccountType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
asset: {
__typename: 'Asset',
id: 'c9fe6fc24fce121b2cc72680543a886055abb560043fda394ba5376203b7527d',
symbol: 'tUSDC',
decimals: 5,
name: 'tUSDC TEST',
quantum: '1',
status: AssetStatus.STATUS_ENABLED,
source: {
__typename: 'ERC20' as const,
contractAddress: '0x123',
lifetimeLimit: '100',
withdrawThreshold: '100',
},
},
reference: 'reward',
status: TransferStatus.STATUS_PENDING,
timestamp: '2023-12-18T13:05:35.948706Z',
kind: mockRecurringTransfer,
reason: null,
},
fees: [],
};
it('renders with valid props', () => {
render(
<ActiveRewardCard
transferNode={mockTransferNode}
currentEpoch={1}
kind={mockRecurringTransfer}
/>
);
expect(
screen.getByText(/Liquidity provision fees received/i)
).toBeInTheDocument();
expect(screen.getByText('Entity scope')).toBeInTheDocument();
expect(screen.getByText('Average position')).toBeInTheDocument();
expect(screen.getByText('Ends in')).toBeInTheDocument();
expect(screen.getByText('115431 epochs')).toBeInTheDocument();
expect(screen.getByText('Assessed over')).toBeInTheDocument();
expect(screen.getByText('1 epoch')).toBeInTheDocument();
});
describe('isActiveReward', () => {
it('returns true for valid active reward', () => {
const node = {
transfer: {
kind: {
__typename: 'RecurringTransfer',
dispatchStrategy: {},
endEpoch: 10,
},
status: TransferStatus.STATUS_PENDING,
},
} as TransferNode;
expect(isActiveReward(node, 5)).toBeTruthy();
});
it('returns false for invalid active reward', () => {
const node = {
transfer: {
kind: {
__typename: 'RecurringTransfer',
dispatchStrategy: {},
endEpoch: 10,
},
status: TransferStatus.STATUS_PENDING,
},
} as TransferNode;
expect(isActiveReward(node, 15)).toBeFalsy();
});
});
describe('applyFilter', () => {
it('returns true when filter matches dispatch metric label', () => {
const transfer = {
kind: {
__typename: 'RecurringTransfer',
dispatchStrategy: {
dispatchMetric: DispatchMetric.DISPATCH_METRIC_AVERAGE_POSITION,
},
},
asset: { symbol: 'XYZ' },
} as Transfer;
const filter = { searchTerm: 'average position' };
expect(applyFilter({ transfer }, filter)).toBeTruthy();
});
it('returns true when filter matches asset symbol', () => {
const transfer = {
kind: {
__typename: 'RecurringTransfer',
dispatchStrategy: {
dispatchMetric: DispatchMetric.DISPATCH_METRIC_AVERAGE_POSITION,
},
},
asset: { symbol: 'XYZ' },
} as Transfer;
const filter = { searchTerm: 'average position' };
expect(applyFilter({ transfer }, filter)).toBeTruthy();
});
});
});
@@ -0,0 +1,568 @@
import {
useActiveRewardsQuery,
useMarketForRewardsQuery,
} from './__generated__/Rewards';
import { useT } from '../../lib/use-t';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import classNames from 'classnames';
import {
Icon,
type IconName,
Intent,
Tooltip,
VegaIcon,
VegaIconNames,
type VegaIconSize,
TradingInput,
TinyScroll,
} from '@vegaprotocol/ui-toolkit';
import { IconNames } from '@blueprintjs/icons';
import {
DistributionStrategyDescriptionMapping,
DistributionStrategyMapping,
EntityScope,
EntityScopeMapping,
type Maybe,
type Transfer,
type TransferNode,
TransferStatus,
TransferStatusMapping,
DispatchMetric,
DispatchMetricDescription,
DispatchMetricLabels,
type RecurringTransfer,
EntityScopeLabelMapping,
} from '@vegaprotocol/types';
import { Card } from '../card/card';
import { useMemo, useState } from 'react';
import {
type AssetFieldsFragment,
useAssetDataProvider,
useAssetsMapProvider,
} from '@vegaprotocol/assets';
import {
type MarketFieldsFragment,
useMarketsMapProvider,
} from '@vegaprotocol/markets';
export type Filter = {
searchTerm: string;
};
export const isActiveReward = (node: TransferNode, currentEpoch: number) => {
const { transfer } = node;
if (transfer.kind.__typename !== 'RecurringTransfer') {
return false;
}
const { dispatchStrategy } = transfer.kind;
if (!dispatchStrategy) {
return false;
}
if (transfer.kind.endEpoch && transfer.kind.endEpoch < currentEpoch) {
return false;
}
if (transfer.status !== TransferStatus.STATUS_PENDING) {
return false;
}
return true;
};
export const applyFilter = (
node: TransferNode & {
asset?: AssetFieldsFragment | null;
marketIds?: (MarketFieldsFragment | null)[];
},
filter: Filter
) => {
const { transfer } = node;
if (
transfer.kind.__typename !== 'RecurringTransfer' ||
!transfer.kind.dispatchStrategy?.dispatchMetric
) {
return false;
}
if (
DispatchMetricLabels[transfer.kind.dispatchStrategy.dispatchMetric]
.toLowerCase()
.includes(filter.searchTerm.toLowerCase()) ||
transfer.asset?.symbol
.toLowerCase()
.includes(filter.searchTerm.toLowerCase()) ||
EntityScopeLabelMapping[transfer.kind.dispatchStrategy.entityScope]
.toLowerCase()
.includes(filter.searchTerm.toLowerCase()) ||
node.asset?.name
.toLocaleLowerCase()
.includes(filter.searchTerm.toLowerCase()) ||
node.marketIds?.some((m) =>
m?.tradableInstrument?.instrument?.name
.toLocaleLowerCase()
.includes(filter.searchTerm.toLowerCase())
)
) {
return true;
}
return false;
};
export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
const t = useT();
const { data: activeRewardsData } = useActiveRewardsQuery({
variables: {
isReward: true,
},
});
const [filter, setFilter] = useState<Filter>({
searchTerm: '',
});
const { data: assets } = useAssetsMapProvider();
const { data: markets } = useMarketsMapProvider();
const transfers = activeRewardsData?.transfersConnection?.edges
?.map((e) => e?.node as TransferNode)
.filter((node) => isActiveReward(node, currentEpoch))
.map((node) => {
if (node.transfer.kind.__typename !== 'RecurringTransfer') {
return node;
}
const asset =
assets &&
assets[
node.transfer.kind.dispatchStrategy?.dispatchMetricAssetId || ''
];
const marketIds =
node.transfer.kind.dispatchStrategy?.marketIdsInScope?.map(
(id) => markets && markets[id]
);
return { ...node, asset, marketIds };
});
if (!transfers || !transfers.length) return null;
return (
<Card title={t('Active rewards')} className="lg:col-span-full">
{transfers.length > 1 && (
<TradingInput
onChange={(e) =>
setFilter((curr) => ({ ...curr, searchTerm: e.target.value }))
}
value={filter.searchTerm}
type="text"
placeholder={t(
'Search by reward dispatch metric, entity scope or asset name'
)}
data-testid="search-term"
className="mb-4 w-20 mr-2"
prependElement={<VegaIcon name={VegaIconNames.SEARCH} />}
/>
)}
<TinyScroll className="grid gap-x-8 gap-y-10 h-fit grid-cols-[repeat(auto-fill,_minmax(230px,_1fr))] md:grid-cols-[repeat(auto-fill,_minmax(230px,_1fr))] lg:grid-cols-[repeat(auto-fill,_minmax(320px,_1fr))] xl:grid-cols-[repeat(auto-fill,_minmax(335px,_1fr))] max-h-[40rem] overflow-auto pr-2">
{transfers
.filter((n) => applyFilter(n, filter))
.map((node, i) => {
const { transfer } = node;
if (
transfer.kind.__typename !== 'RecurringTransfer' ||
!transfer.kind.dispatchStrategy?.dispatchMetric
) {
return null;
}
return (
node && (
<ActiveRewardCard
key={i}
transferNode={node}
kind={transfer.kind}
currentEpoch={currentEpoch}
/>
)
);
})}
</TinyScroll>
</Card>
);
};
// This was built to be a status indicator for the rewards based on the transfer status
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const StatusIndicator = ({
status,
reason,
}: {
status: TransferStatus;
reason?: Maybe<string> | undefined;
}) => {
const t = useT();
const getIconIntent = (status: string) => {
switch (status) {
case TransferStatus.STATUS_DONE:
return { icon: IconNames.TICK_CIRCLE, intent: Intent.Success };
case TransferStatus.STATUS_CANCELLED:
return { icon: IconNames.MOON, intent: Intent.None };
case TransferStatus.STATUS_PENDING:
return { icon: IconNames.HELP, intent: Intent.Primary };
case TransferStatus.STATUS_REJECTED:
return { icon: IconNames.ERROR, intent: Intent.Danger };
case TransferStatus.STATUS_STOPPED:
return { icon: IconNames.ERROR, intent: Intent.Danger };
default:
return { icon: IconNames.HELP, intent: Intent.Primary };
}
};
const { icon, intent } = getIconIntent(status);
return (
<Tooltip
description={
<span>
{t('Transfer status: {{status}} {{reason}}', {
status: TransferStatusMapping[status],
reason: reason ? `(${reason})` : '',
})}
</span>
}
>
<span
className={classNames(
{
'text-gray-700 dark:text-gray-300': intent === Intent.None,
'text-vega-blue': intent === Intent.Primary,
'text-vega-green dark:text-vega-green': intent === Intent.Success,
'dark:text-yellow text-yellow-600': intent === Intent.Warning,
'text-vega-red': intent === Intent.Danger,
},
'flex items-start p-1 align-text-bottom'
)}
>
<Icon size={3} name={icon as IconName} />
</span>
</Tooltip>
);
};
export const ActiveRewardCard = ({
transferNode,
currentEpoch,
kind,
}: {
transferNode: TransferNode;
currentEpoch: number;
kind: RecurringTransfer;
}) => {
const t = useT();
const { transfer } = transferNode;
const { dispatchStrategy } = kind;
const marketIds = dispatchStrategy?.marketIdsInScope;
const { data: marketNameData } = useMarketForRewardsQuery({
variables: {
marketId: marketIds ? marketIds[0] : '',
},
});
const marketName = useMemo(() => {
if (marketNameData && marketIds && marketIds.length > 1) {
return 'Specific markets';
} else if (
marketNameData &&
marketIds &&
marketNameData &&
marketIds.length === 1
) {
return marketNameData?.market?.tradableInstrument?.instrument?.name || '';
}
return '';
}, [marketIds, marketNameData]);
const { data: dispatchAsset } = useAssetDataProvider(
dispatchStrategy?.dispatchMetricAssetId || ''
);
if (!dispatchStrategy) {
return null;
}
const { gradientClassName, mainClassName } = getGradientClasses(
dispatchStrategy.dispatchMetric
);
const entityScope = dispatchStrategy.entityScope;
return (
<div>
<div
className={classNames(
'bg-gradient-to-r col-span-full p-0.5 lg:col-auto h-full',
'rounded-lg',
gradientClassName
)}
>
<div
className={classNames(
mainClassName,
'bg-gradient-to-b bg-vega-clight-800 dark:bg-vega-cdark-800 h-full w-full rounded p-4 flex flex-col gap-4'
)}
>
<div className="flex justify-between gap-4">
<div className="flex flex-col gap-2 items-center text-center">
<EntityIcon transfer={transfer} />
{entityScope && (
<span className="text-muted text-xs">
{EntityScopeLabelMapping[entityScope] || t('Unspecified')}
</span>
)}
</div>
<div className="flex flex-col gap-2 items-center text-center">
<h3 className="flex flex-col gap-1 text-2xl shrink-1 text-center">
<span className="font-glitch">
{addDecimalsFormatNumber(
transferNode.transfer.amount,
transferNode.transfer.asset?.decimals || 0,
6
)}
</span>
<span className="font-alpha">
{transferNode.transfer.asset?.symbol}
</span>
</h3>
{
<Tooltip
description={t(
DistributionStrategyDescriptionMapping[
dispatchStrategy.distributionStrategy
]
)}
underline={true}
>
<span className="text-xs">
{
DistributionStrategyMapping[
dispatchStrategy.distributionStrategy
]
}
</span>
</Tooltip>
}
</div>
<div className="flex flex-col gap-2 items-center text-center">
<CardIcon
iconName={VegaIconNames.LOCK}
tooltip={t(
'Number of epochs after distribution to delay vesting of rewards by'
)}
/>
<span className="text-muted text-xs whitespace-nowrap">
{t('numberEpochs', '{{count}} epochs', {
count: kind.dispatchStrategy?.lockPeriod,
})}
</span>
</div>
</div>
<span className="border-[0.5px] border-gray-700" />
<span>
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]}
{marketName ? `${marketName}` : `${dispatchAsset?.name}`}
</span>
<div className="flex items-center gap-8 flex-wrap">
{kind.endEpoch && (
<span className="flex flex-col">
<span className="text-muted text-xs">{t('Ends in')}</span>
<span>
{t('numberEpochs', '{{count}} epochs', {
count: kind.endEpoch - currentEpoch,
})}
</span>
</span>
)}
{
<span className="flex flex-col">
<span className="text-muted text-xs">{t('Assessed over')}</span>
<span>
{t('numberEpochs', '{{count}} epochs', {
count: dispatchStrategy.windowLength,
})}
</span>
</span>
}
</div>
{dispatchStrategy?.dispatchMetric && (
<span className="text-muted text-sm h-[2rem]">
{t(DispatchMetricDescription[dispatchStrategy?.dispatchMetric])}
</span>
)}
<span className="border-[0.5px] border-gray-700" />
<div className="flex justify-between flex-wrap items-center gap-3 text-xs">
<span className="flex flex-col gap-1">
<span className="flex items-center gap-1 text-muted">
{t('Entity scope')}{' '}
</span>
<span className="flex items-center gap-1">
{kind.dispatchStrategy?.teamScope && (
<Tooltip
description={
<span>{kind.dispatchStrategy?.teamScope}</span>
}
>
<span className="flex items-center p-1 rounded-full border border-gray-600">
{<VegaIcon name={VegaIconNames.TEAM} size={16} />}
</span>
</Tooltip>
)}
{kind.dispatchStrategy?.individualScope && (
<Tooltip
description={
<span>{kind.dispatchStrategy?.individualScope}</span>
}
>
<span className="flex items-center p-1 rounded-full border border-gray-600">
{<VegaIcon name={VegaIconNames.MAN} size={16} />}
</span>
</Tooltip>
)}
{/* Shows transfer status */}
{/* <StatusIndicator
status={transfer.status}
reason={transfer.reason}
/> */}
</span>
</span>
<span className="flex flex-col gap-1">
<span className="flex items-center gap-1 text-muted">
{t('Staked VEGA')}{' '}
</span>
<span className="flex items-center gap-1">
{addDecimalsFormatNumber(
kind.dispatchStrategy?.stakingRequirement || 0,
transfer.asset?.decimals || 0
)}
</span>
</span>
<span className="flex flex-col gap-1">
<span className="flex items-center gap-1 text-muted">
{t('Average position')}{' '}
</span>
<span className="flex items-center gap-1">
{addDecimalsFormatNumber(
kind.dispatchStrategy
?.notionalTimeWeightedAveragePositionRequirement || 0,
transfer.asset?.decimals || 0
)}
</span>
</span>
</div>
</div>
</div>
</div>
);
};
const getGradientClasses = (d: DispatchMetric | undefined) => {
switch (d) {
case DispatchMetric.DISPATCH_METRIC_AVERAGE_POSITION:
return {
gradientClassName: 'from-vega-pink-500 to-vega-purple-400',
mainClassName: 'from-vega-pink-400 dark:from-vega-pink-600 to-20%',
};
case DispatchMetric.DISPATCH_METRIC_LP_FEES_RECEIVED:
return {
gradientClassName: 'from-vega-green-500 to-vega-yellow-500',
mainClassName: 'from-vega-green-400 dark:from-vega-green-600 to-20%',
};
case DispatchMetric.DISPATCH_METRIC_MAKER_FEES_PAID:
return {
gradientClassName: 'from-vega-orange-500 to-vega-pink-400',
mainClassName: 'from-vega-orange-400 dark:from-vega-orange-600 to-20%',
};
case DispatchMetric.DISPATCH_METRIC_MARKET_VALUE:
case DispatchMetric.DISPATCH_METRIC_RELATIVE_RETURN:
return {
gradientClassName: 'from-vega-purple-500 to-vega-blue-400',
mainClassName: 'from-vega-purple-400 dark:from-vega-purple-600 to-20%',
};
case DispatchMetric.DISPATCH_METRIC_RETURN_VOLATILITY:
return {
gradientClassName: 'from-vega-blue-500 to-vega-green-400',
mainClassName: 'from-vega-blue-400 dark:from-vega-blue-600 to-20%',
};
case DispatchMetric.DISPATCH_METRIC_VALIDATOR_RANKING:
default:
return {
gradientClassName: 'from-vega-pink-500 to-vega-purple-400',
mainClassName: 'from-vega-pink-400 dark:from-vega-pink-600 to-20%',
};
}
};
const CardIcon = ({
size = 18,
iconName,
tooltip,
}: {
size?: VegaIconSize;
iconName: VegaIconNames;
tooltip: string;
}) => {
return (
<Tooltip description={<span>{tooltip}</span>}>
<span className="flex items-center p-2 rounded-full border border-gray-600">
<VegaIcon name={iconName} size={size} />
</span>
</Tooltip>
);
};
const EntityIcon = ({
transfer,
size = 18,
}: {
transfer: Transfer;
size?: VegaIconSize;
}) => {
if (transfer.kind.__typename !== 'RecurringTransfer') {
return null;
}
const entityScope = transfer.kind.dispatchStrategy?.entityScope;
const getIconName = () => {
switch (entityScope) {
case EntityScope.ENTITY_SCOPE_TEAMS:
return VegaIconNames.TEAM;
case EntityScope.ENTITY_SCOPE_INDIVIDUALS:
return VegaIconNames.MAN;
default:
return VegaIconNames.QUESTION_MARK;
}
};
const iconName = getIconName();
return (
<Tooltip
description={
<span>{entityScope ? EntityScopeMapping[entityScope] : ''}</span>
}
>
<span className="flex items-center p-2 rounded-full border border-gray-600">
{iconName && <VegaIcon name={iconName} size={size} />}
</span>
</Tooltip>
);
};
@@ -33,6 +33,10 @@ import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { RewardsHistoryContainer } from './rewards-history';
import { useT } from '../../lib/use-t';
import { useAssetsMapProvider } from '@vegaprotocol/assets';
import { ActiveRewards } from './active-rewards';
import { ActivityStreak } from './streaks/activity-streaks';
import { RewardHoarderBonus } from './streaks/reward-hoarder-bonus';
import classNames from 'classnames';
const ASSETS_WITH_INCORRECT_VESTING_REWARD_DATA = [
'bf1e88d19db4b3ca0d1d5bdb73718a01686b18cf731ca26adedf3c8b83802bba', // USDT mainnet
@@ -45,6 +49,7 @@ export const RewardsContainer = () => {
const { params, loading: paramsLoading } = useNetworkParams([
NetworkParams.reward_asset,
NetworkParams.rewards_activityStreak_benefitTiers,
NetworkParams.rewards_vesting_benefitTiers,
NetworkParams.rewards_vesting_baseRate,
]);
@@ -54,6 +59,14 @@ export const RewardsContainer = () => {
const { data: epochData } = useRewardsEpochQuery();
const { rewards_activityStreak_benefitTiers, rewards_vesting_benefitTiers } =
params || {};
const activityStreakBenefitTiers = JSON.parse(
rewards_activityStreak_benefitTiers
);
const vestingBenefitTiers = JSON.parse(rewards_vesting_benefitTiers);
// No need to specify the fromEpoch as it will by default give you the last
// Note activityStreak in query will fail
const { data: rewardsData, loading: rewardsLoading } = useRewardsPageQuery({
@@ -68,6 +81,9 @@ export const RewardsContainer = () => {
pollInterval: 10000,
});
const partyActivityStreak = rewardsData?.party?.activityStreak;
const vestingDetails = rewardsData?.party?.vestingStats;
if (!epochData?.epoch || !assetMap) return null;
const loading = paramsLoading || accountsLoading || rewardsLoading;
@@ -114,74 +130,98 @@ export const RewardsContainer = () => {
]);
return (
<div className="grid auto-rows-min grid-cols-6 gap-3">
{/* Always show reward information for vega */}
<Card
key={params.reward_asset}
title={t('Vega Reward pot')}
className="lg:col-span-3 xl:col-span-2"
loading={loading}
highlight={true}
>
<RewardPot
pubKey={pubKey}
accounts={accounts}
assetId={params.reward_asset}
vestingBalancesSummary={rewardsData?.party?.vestingBalancesSummary}
/>
</Card>
<Card
title={t('Vesting')}
className="lg:col-span-3 xl:col-span-2"
loading={loading}
>
<Vesting
pubKey={pubKey}
baseRate={params.rewards_vesting_baseRate}
multiplier={
rewardsData?.party?.activityStreak?.rewardVestingMultiplier
}
/>
</Card>
<Card
title={t('Rewards multipliers')}
className="lg:col-span-3 xl:col-span-2"
loading={loading}
highlight={true}
>
<Multipliers
pubKey={pubKey}
hoarderMultiplier={
rewardsData?.party?.vestingStats?.rewardBonusMultiplier
}
streakMultiplier={
rewardsData?.party?.activityStreak?.rewardDistributionMultiplier
}
/>
</Card>
<div className="flex flex-col w-full gap-3">
<div className="grid auto-rows-min grid-cols-6 gap-3">
{/* Always show reward information for vega */}
<Card
key={params.reward_asset}
title={t('Vega Reward pot')}
className="lg:col-span-3 xl:col-span-2"
loading={loading}
highlight={true}
>
<RewardPot
pubKey={pubKey}
accounts={accounts}
assetId={params.reward_asset}
vestingBalancesSummary={rewardsData?.party?.vestingBalancesSummary}
/>
</Card>
<Card
title={t('Vesting')}
className="lg:col-span-3 xl:col-span-2"
loading={loading}
>
<Vesting
pubKey={pubKey}
baseRate={params.rewards_vesting_baseRate}
multiplier={
rewardsData?.party?.activityStreak?.rewardVestingMultiplier
}
/>
</Card>
<Card
title={t('Rewards multipliers')}
className="lg:col-span-3 xl:col-span-2"
loading={loading}
highlight={true}
>
<Multipliers
pubKey={pubKey}
hoarderMultiplier={
rewardsData?.party?.vestingStats?.rewardBonusMultiplier
}
streakMultiplier={
rewardsData?.party?.activityStreak?.rewardDistributionMultiplier
}
/>
</Card>
{/* Show all other reward pots, most of the time users will not have other rewards */}
{assets
.filter((assetId) => assetId !== params.reward_asset)
.map((assetId) => {
const asset = assetMap ? assetMap[assetId] : null;
{/* Show all other reward pots, most of the time users will not have other rewards */}
{assets
.filter((assetId) => assetId !== params.reward_asset)
.map((assetId) => {
const asset = assetMap ? assetMap[assetId] : null;
if (!asset) return null;
if (!asset) return null;
// Following code is for mitigating an issue due to a core bug where locked and vesting
// balances were incorrectly increased for infrastructure rewards for USDT on mainnet
//
// We don't want to incorrectly show the wring locked/vesting values, but we DO want to
// show the user that they have rewards available to withdraw
if (ASSETS_WITH_INCORRECT_VESTING_REWARD_DATA.includes(asset.id)) {
const accountsForAsset = rewardAccountsAssetMap[asset.id];
const vestedAccount = accountsForAsset?.find(
(a) => a.type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS
);
// Following code is for mitigating an issue due to a core bug where locked and vesting
// balances were incorrectly increased for infrastructure rewards for USDT on mainnet
//
// We don't want to incorrectly show the wring locked/vesting values, but we DO want to
// show the user that they have rewards available to withdraw
if (ASSETS_WITH_INCORRECT_VESTING_REWARD_DATA.includes(asset.id)) {
const accountsForAsset = rewardAccountsAssetMap[asset.id];
const vestedAccount = accountsForAsset?.find(
(a) => a.type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS
);
// No vested rewards available to withdraw, so skip over USDT
if (!vestedAccount || Number(vestedAccount.balance) <= 0) {
return null;
// No vested rewards available to withdraw, so skip over USDT
if (!vestedAccount || Number(vestedAccount.balance) <= 0) {
return null;
}
return (
<Card
key={assetId}
title={t('{{assetSymbol}} Reward pot', {
assetSymbol: asset.symbol,
})}
className="lg:col-span-3 xl:col-span-2"
loading={loading}
>
<RewardPot
pubKey={pubKey}
accounts={accounts}
assetId={assetId}
// Ensure that these values are shown as 0
vestingBalancesSummary={{
lockedBalances: [],
vestingBalances: [],
}}
/>
</Card>
);
}
return (
@@ -197,47 +237,69 @@ export const RewardsContainer = () => {
pubKey={pubKey}
accounts={accounts}
assetId={assetId}
// Ensure that these values are shown as 0
vestingBalancesSummary={{
lockedBalances: [],
vestingBalances: [],
}}
vestingBalancesSummary={
rewardsData?.party?.vestingBalancesSummary
}
/>
</Card>
);
}
return (
<Card
key={assetId}
title={t('{{assetSymbol}} Reward pot', {
assetSymbol: asset.symbol,
})}
className="lg:col-span-3 xl:col-span-2"
loading={loading}
>
<RewardPot
pubKey={pubKey}
accounts={accounts}
assetId={assetId}
vestingBalancesSummary={
rewardsData?.party?.vestingBalancesSummary
}
})}
</div>
<div className="grid auto-rows-min grid-cols-6 gap-3">
{pubKey && activityStreakBenefitTiers.tiers?.length > 0 && (
<Card
title={t('Activity Streak')}
className={classNames(
{
'lg:col-span-6 xl:col-span-3':
activityStreakBenefitTiers.tiers.length <= 4,
'xl:col-span-6': activityStreakBenefitTiers.tiers.length > 4,
},
'hidden md:block'
)}
>
<span className="flex flex-col mr-8 pr-4">
<ActivityStreak
tiers={activityStreakBenefitTiers.tiers}
streak={partyActivityStreak}
/>
</Card>
);
})}
<Card
title={t('Rewards history')}
className="lg:col-span-full"
loading={rewardsLoading}
>
<RewardsHistoryContainer
epoch={Number(epochData?.epoch.id)}
pubKey={pubKey}
assets={assetMap}
/>
</Card>
</span>
</Card>
)}
{pubKey && vestingBenefitTiers.tiers?.length > 0 && (
<Card
title={t('Reward Hoarder Bonus')}
className={classNames(
{
'lg:col-span-6 xl:col-span-3':
vestingBenefitTiers.tiers.length <= 4,
'xl:col-span-6': vestingBenefitTiers.tiers.length > 4,
},
'hidden md:block'
)}
>
<span className="flex flex-col mr-8 pr-4">
<RewardHoarderBonus
tiers={vestingBenefitTiers.tiers}
vestingDetails={vestingDetails}
/>
</span>
</Card>
)}
<ActiveRewards currentEpoch={Number(epochData?.epoch.id)} />
<Card
title={t('Rewards history')}
className="lg:col-span-full hidden md:block"
loading={rewardsLoading}
noBackgroundOnMobile={true}
>
<RewardsHistoryContainer
epoch={Number(epochData?.epoch.id)}
pubKey={pubKey}
assets={assetMap}
/>
</Card>
</div>
</div>
);
};
@@ -342,7 +404,7 @@ export const RewardPot = ({
})}
<VegaIcon name={VegaIconNames.LOCK} size={12} />
</CardTableTH>
<CardTableTD>
<CardTableTD data-testid="locked-value">
{addDecimalsFormatNumberQuantum(
totalLocked.toString(),
rewardAsset.decimals,
@@ -356,7 +418,7 @@ export const RewardPot = ({
assetSymbol: rewardAsset.symbol,
})}
</CardTableTH>
<CardTableTD>
<CardTableTD data-testid="vesting-value">
{addDecimalsFormatNumberQuantum(
totalVesting.toString(),
rewardAsset.decimals,
@@ -368,7 +430,7 @@ export const RewardPot = ({
<CardTableTH>
{t('Available to withdraw this epoch')}
</CardTableTH>
<CardTableTD>
<CardTableTD data-testid="available-to-withdraw-value">
{addDecimalsFormatNumberQuantum(
totalVestedRewardsByRewardAsset.toString(),
rewardAsset.decimals,
@@ -387,6 +449,7 @@ export const RewardPot = ({
)
}
size="small"
data-testid="redeem-rewards-button"
>
{t('Redeem rewards')}
</TradingButton>
@@ -421,12 +484,16 @@ export const Vesting = ({
<CardTable>
<tr>
<CardTableTH>{t('Base rate')}</CardTableTH>
<CardTableTD>{baseRateFormatted}%</CardTableTD>
<CardTableTD data-testid="base-rate-value">
{baseRateFormatted}%
</CardTableTD>
</tr>
{pubKey && (
<tr>
<CardTableTH>{t('Vesting multiplier')}</CardTableTH>
<CardTableTD>{multiplier ? `${multiplier}x` : '-'}</CardTableTD>
<CardTableTD data-testid="vesting multiplier-value">
{multiplier ? `${multiplier}x` : '-'}
</CardTableTD>
</tr>
)}
</CardTable>
@@ -466,13 +533,13 @@ export const Multipliers = ({
<CardTable>
<tr>
<CardTableTH>{t('Streak reward multiplier')}</CardTableTH>
<CardTableTD>
<CardTableTD data-testid="streak-reward-multiplier-value">
{streakMultiplier ? `${streakMultiplier}x` : '-'}
</CardTableTD>
</tr>
<tr>
<CardTableTH>{t('Hoarder reward multiplier')}</CardTableTH>
<CardTableTD>
<CardTableTD data-testid="hoarder-reward-multiplier-value">
{hoarderMultiplier ? `${hoarderMultiplier}x` : '-'}
</CardTableTD>
</tr>
@@ -99,16 +99,25 @@ describe('RewardsHistoryTable', () => {
it('renders table with accounts summed up by asset', () => {
render(<RewardHistoryTable {...props} />);
const container = within(
const containerLeft = within(
document.querySelector('.ag-pinned-left-cols-container') as HTMLElement
);
const pinnedRows = containerLeft.getAllByRole('row');
const containerCenter = within(
document.querySelector('.ag-center-cols-container') as HTMLElement
);
const rows = container.getAllByRole('row');
const rows = containerCenter.getAllByRole('row');
expect(rows).toHaveLength(
Object.keys(groupBy(rewardSummaries, 'node.assetId')).length
);
let row = within(rows[0]);
let cells = row.getAllByRole('gridcell');
let pinnedRow = within(pinnedRows[0]);
let cells = [
...pinnedRow.getAllByRole('gridcell'),
...row.getAllByRole('gridcell'),
];
let assetCell = getCell(cells, 'asset.symbol');
expect(assetCell.getByTestId('stack-cell-primary')).toHaveTextContent(
@@ -140,7 +149,11 @@ describe('RewardsHistoryTable', () => {
// Second row
row = within(rows[1]);
cells = row.getAllByRole('gridcell');
pinnedRow = within(pinnedRows[1]);
cells = [
...pinnedRow.getAllByRole('gridcell'),
...row.getAllByRole('gridcell'),
];
assetCell = getCell(cells, 'asset.symbol');
expect(assetCell.getByTestId('stack-cell-primary')).toHaveTextContent(
@@ -93,6 +93,7 @@ export const RewardsHistoryContainer = ({
const defaultColDef = {
flex: 1,
minWidth: 62,
resizable: true,
sortable: true,
};
@@ -195,6 +196,8 @@ export const RewardHistoryTable = ({
return <StackedCell primary={value} secondary={data.asset.name} />;
},
sort: 'desc',
pinned: 'left',
width: 150,
},
{
field: 'infrastructureFees',
@@ -257,7 +260,7 @@ export const RewardHistoryTable = ({
return (
<div>
<div className="mb-2 flex items-center justify-between gap-2">
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
<h4 className="text-muted flex items-center gap-2 text-sm">
<label htmlFor="fromEpoch">{t('From epoch')}</label>
<EpochInput
@@ -316,6 +319,7 @@ export const RewardHistoryTable = ({
onClick={() => setIsParty(false)}
size="extra-small"
minimal={isParty}
data-testid="total-distributed-button"
>
{t('Total distributed')}
</TradingButton>
@@ -324,20 +328,23 @@ export const RewardHistoryTable = ({
size="extra-small"
disabled={!pubKey}
minimal={!isParty}
data-testid="earned-by-me-button"
>
{t('Earned by me')}
</TradingButton>
</div>
</div>
<AgGrid
columnDefs={columnDefs}
defaultColDef={defaultColDef}
rowData={rowData}
rowHeight={45}
domLayout="autoHeight"
// Show loading message without wiping out the current rows
overlayNoRowsTemplate={loading ? t('Loading...') : t('No rows')}
/>
<div className="border rounded-lg md:rounded-sm overflow-hidden border-default">
<AgGrid
columnDefs={columnDefs}
defaultColDef={defaultColDef}
rowData={rowData}
rowHeight={45}
domLayout="autoHeight"
// Show loading message without wiping out the current rows
overlayNoRowsTemplate={loading ? t('Loading...') : t('No rows')}
/>
</div>
</div>
);
};
@@ -0,0 +1,66 @@
import { render, screen } from '@testing-library/react';
import { ActivityStreak } from './activity-streaks';
describe('ActivityStreak', () => {
it('renders null when streak is not active', () => {
const tiers: {
minimum_activity_streak: number;
reward_multiplier: string;
vesting_multiplier: string;
}[] = [];
const streak = null;
render(<ActivityStreak tiers={tiers} streak={streak} />);
const component = screen.queryByText(/epochs streak/i);
expect(component).toBeNull();
});
it('renders null when tiers are empty', () => {
const tiers: {
minimum_activity_streak: number;
reward_multiplier: string;
vesting_multiplier: string;
}[] = [];
const streak = {
activeFor: 10,
isActive: true,
inactiveFor: 10,
rewardDistributionMultiplier: '45678',
rewardVestingMultiplier: '45678',
epoch: 10,
tradedVolume: '45678',
openVolume: '45678',
};
render(<ActivityStreak tiers={tiers} streak={streak} />);
const component = screen.queryByText(/epochs streak/i);
expect(component).toBeNull();
});
it('renders the component with tiers and active streak', () => {
const tiers = [
{
minimum_activity_streak: 5,
reward_multiplier: '1.5x',
vesting_multiplier: '2x',
},
{
minimum_activity_streak: 10,
reward_multiplier: '2x',
vesting_multiplier: '3x',
},
];
const streak = {
activeFor: 7,
isActive: true,
inactiveFor: 10,
rewardDistributionMultiplier: '45678',
rewardVestingMultiplier: '45678',
epoch: 10,
tradedVolume: '45678',
openVolume: '45678',
};
render(<ActivityStreak tiers={tiers} streak={streak} />);
const tierLabels = screen.getAllByText(/Tier/i);
expect(tierLabels.length).toBe(3); // 2 tiers + 1 label
});
});
@@ -0,0 +1,226 @@
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { useT } from '../../../lib/use-t';
import classNames from 'classnames';
import BigNumber from 'bignumber.js';
import type { PartyActivityStreak } from '@vegaprotocol/types';
export const safeProgress = (
i: number,
userTierIndex: number,
total: number | string,
progress?: number | string
) => {
if (i < userTierIndex) return 100;
if (i > userTierIndex) return 0;
if (!progress || !total) return 0;
if (new BigNumber(progress).isGreaterThan(total)) return 100;
return new BigNumber(progress)
.multipliedBy(100)
.dividedBy(total || 1)
.toNumber();
};
export const useGetUserTier = (
tiers: {
minimum_activity_streak: number;
reward_multiplier: string;
vesting_multiplier: string;
}[],
progress?: number
) => {
if (!progress) return 0;
if (!tiers || tiers.length === 0) return 0;
let userTier = 0;
let i = 0;
while (
i < tiers.length &&
tiers[userTier].minimum_activity_streak < progress
) {
userTier = i;
i++;
}
if (
i === tiers.length &&
tiers[userTier].minimum_activity_streak <= progress
) {
userTier = i;
}
if (userTier > tiers.length) {
userTier--;
}
return userTier;
};
export const ActivityStreak = ({
tiers,
streak,
}: {
tiers: {
minimum_activity_streak: number;
reward_multiplier: string;
vesting_multiplier: string;
}[];
streak?: PartyActivityStreak | null;
}) => {
const t = useT();
const userTierIndex = useGetUserTier(tiers, streak?.activeFor);
if (!tiers || tiers.length === 0) return null;
const progressBarHeight = 'h-10';
return (
<>
<div className="flex flex-col gap-1 w-full">
<div className="flex flex-col gap-1">
<div
className="grid"
style={{
gridTemplateColumns:
'repeat(' + tiers.length + ', minmax(0, 1fr))',
}}
>
{tiers.map((tier, index) => {
return (
<div key={index} className="flex justify-end -mr-[2.85rem]">
<span className="flex flex-col items-center gap-4 justify-between">
<span className="flex flex-col items-center gap-1">
<span className="flex flex-col items-center font-medium">
<span className="text-sm">
{t('Tier {{tier}}', {
tier: index + 1,
})}
</span>
<span className="text-muted text-xs">
{t('numberEpochs', '{{count}} epochs', {
count: tier.minimum_activity_streak,
})}
</span>
</span>
<span
className={classNames(
'text-xs flex flex-col items-center justify-center px-2 py-1 rounded-lg text-white border',
{
'border-pink-600 bg-pink-900': index % 6 === 0,
'border-purple-600 bg-purple-900': index % 6 === 1,
'border-blue-600 bg-blue-900': index % 6 === 2,
'border-orange-600 bg-orange-900': index % 6 === 3,
'border-green-600 bg-green-900': index % 6 === 4,
'border-yellow-600 bg-yellow-900': index % 6 === 5,
}
)}
>
<span>
{t('Reward {{reward}}x', {
reward: tier.reward_multiplier,
})}
</span>
<span>
{t('Vesting {{vesting}}x', {
vesting: tier.vesting_multiplier,
})}
</span>
</span>
</span>
<span
className={classNames(
{
'text-pink-500': index % 6 === 0,
'text-purple-500': index % 6 === 1,
'text-blue-500': index % 6 === 2,
'text-orange-500': index % 6 === 3,
'text-green-500': index % 6 === 4,
'text-yellow-500': index % 6 === 5,
},
'leading-[0] font-sans text-[48px]'
)}
>
</span>
</span>
</div>
);
})}
</div>
</div>
<div className="flex items-center gap-1">
{tiers.map((_tier, index) => {
return (
<div
key={index}
className="bg-white dark:bg-gray-800 shadow-card rounded-[100px] grow"
>
<div
className={classNames(
'relative w-full rounded-[100px] bg-gray-200 dark:bg-gray-800',
progressBarHeight
)}
>
<div
className={classNames(
'absolute left-0 top-0 h-full rounded-[100px] bg-gradient-to-r',
{
'from-vega-dark-400 to-vega-dark-200':
userTierIndex === 0 || streak?.isActive === false,
'from-vega-pink-600 to-vega-pink-500':
userTierIndex % 6 === 1,
'from-vega-purple-600 to-vega-purple-500':
userTierIndex % 6 === 2,
'from-vega-blue-600 to-vega-blue-500':
userTierIndex % 6 === 3,
'from-vega-orange-600 to-vega-orange-500':
userTierIndex % 6 === 4,
'from-vega-green-600 to-vega-green-500':
userTierIndex % 6 === 5,
'from-vega-yellow-600 to-vega-yellow-500':
userTierIndex % 6 === 0,
}
)}
style={{
width:
safeProgress(
index,
userTierIndex,
tiers[index].minimum_activity_streak,
streak?.activeFor
) + '%',
}}
></div>
</div>
</div>
);
})}
</div>
<div className="flex items-center gap-1">
<VegaIcon name={VegaIconNames.STREAK} />
<span className="flex flex-col">
{streak?.isActive && (
<span data-testid="epoch-streak">
{t('userActive', '{{active}} trader: {{count}} epochs so far', {
active: streak?.isActive ? 'Active' : 'Inactive',
count: streak?.activeFor || 0,
})}{' '}
{userTierIndex > 0 &&
new BigNumber(
tiers[0].minimum_activity_streak
).isLessThanOrEqualTo(streak?.activeFor || 0) &&
t('(Tier {{tier}} as of last epoch)', {
tier: userTierIndex,
})}
</span>
)}
</span>
</div>
</div>
</>
);
};
@@ -0,0 +1,58 @@
import { render, screen } from '@testing-library/react';
import { RewardHoarderBonus } from './reward-hoarder-bonus';
import type { PartyVestingStats } from '@vegaprotocol/types';
describe('RewardHoarderBonus', () => {
it('renders null when vestingDetails is not provided', () => {
const tiers: {
minimum_quantum_balance: string;
reward_multiplier: string;
}[] = [];
const vestingDetails = null;
render(
<RewardHoarderBonus tiers={tiers} vestingDetails={vestingDetails} />
);
const component = screen.queryByText(/Reward bonus/i);
expect(component).toBeNull();
});
it('renders null when tiers are empty', () => {
const tiers: {
minimum_quantum_balance: string;
reward_multiplier: string;
}[] = [];
const vestingDetails: PartyVestingStats = {
epochSeq: 0,
rewardBonusMultiplier: '1.5',
quantumBalance: '100',
};
render(
<RewardHoarderBonus tiers={tiers} vestingDetails={vestingDetails} />
);
const component = screen.queryByText(/Reward bonus/i);
expect(component).toBeNull();
});
it('renders the component with tiers and vestingDetails', () => {
const tiers = [
{
minimum_quantum_balance: '50',
reward_multiplier: '1.5x',
},
{
minimum_quantum_balance: '100',
reward_multiplier: '2x',
},
];
const vestingDetails = {
epochSeq: 0,
rewardBonusMultiplier: '1.5',
quantumBalance: '75',
};
render(
<RewardHoarderBonus tiers={tiers} vestingDetails={vestingDetails} />
);
const tierLabels = screen.getAllByText(/Tier/i);
expect(tierLabels.length).toBe(3); // 2 tiers + 1 label
});
});
@@ -0,0 +1,197 @@
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { useT } from '../../../lib/use-t';
import classNames from 'classnames';
import type { PartyVestingStats } from '@vegaprotocol/types';
import BigNumber from 'bignumber.js';
import { formatNumber } from '@vegaprotocol/utils';
import { safeProgress } from './activity-streaks';
export const useGetUserTier = (
tiers: {
minimum_quantum_balance: string;
reward_multiplier: string;
}[],
progress?: number | string
) => {
if (!progress) return 0;
if (!tiers || tiers.length === 0) return 0;
let userTier = 0;
let i = 0;
let minProgress = '0';
while (i < tiers.length && new BigNumber(minProgress).isLessThan(progress)) {
userTier = i;
i++;
minProgress = tiers[userTier].minimum_quantum_balance;
}
if (
i === tiers.length &&
new BigNumber(minProgress).isLessThanOrEqualTo(progress)
) {
userTier = i;
}
if (userTier > tiers.length) {
userTier--;
}
return userTier;
};
export const RewardHoarderBonus = ({
tiers,
vestingDetails,
}: {
tiers: {
minimum_quantum_balance: string;
reward_multiplier: string;
}[];
vestingDetails?: PartyVestingStats | null;
}) => {
const t = useT();
const userTierIndex = useGetUserTier(tiers, vestingDetails?.quantumBalance);
if (!tiers || tiers.length === 0) return null;
// There is only value to compare to the tiers that covers all the user' rewards across all assets
const qUSD = 'qUSD';
const progressBarHeight = 'h-10';
return (
<>
<div className="flex flex-col gap-1 w-full">
<div className="flex flex-col gap-1">
<div
className="grid"
style={{
gridTemplateColumns:
'repeat(' + tiers.length + ', minmax(0, 1fr))',
}}
>
{tiers.map((tier, index) => {
return (
<div key={index} className="flex justify-end -mr-[2.95rem]">
<span className="flex flex-col items-center gap-4 justify-between">
<span className="flex flex-col items-center gap-1">
<span className="flex flex-col items-center font-medium">
<span className="text-sm">
{t('Tier {{tier}}', {
tier: index + 1,
})}
</span>
<span className="text-muted text-xs">
{formatNumber(tier.minimum_quantum_balance)} {qUSD}
</span>
</span>
<span
className={classNames(
'text-xs flex flex-col items-center justify-center px-2 py-1 rounded-lg text-white border',
{
'border-pink-600 bg-pink-900': index % 6 === 0,
'border-purple-600 bg-purple-900': index % 6 === 1,
'border-blue-600 bg-blue-900': index % 6 === 2,
'border-orange-600 bg-orange-900': index % 6 === 3,
'border-green-600 bg-green-900': index % 6 === 4,
'border-yellow-600 bg-yellow-900': index % 6 === 5,
}
)}
>
<span>{t('Reward bonus')}</span>
<span>
{t('{{reward}}x', {
reward: tier.reward_multiplier,
})}
</span>
</span>
</span>
<span
className={classNames(
{
'text-pink-500': index % 6 === 0,
'text-purple-500': index % 6 === 1,
'text-blue-500': index % 6 === 2,
'text-orange-500': index % 6 === 3,
'text-green-500': index % 6 === 4,
'text-yellow-500': index % 6 === 5,
},
'leading-[0] font-sans text-[48px]'
)}
>
</span>
</span>
</div>
);
})}
</div>
</div>
<div className="flex items-center gap-1">
{tiers.map((_tier, index) => {
return (
<div
key={index}
className="bg-white dark:bg-gray-800 shadow-card rounded-[100px] grow"
>
<div
className={classNames(
'relative w-full rounded-[100px] bg-gray-200 dark:bg-gray-800',
progressBarHeight
)}
>
<div
className={classNames(
'absolute left-0 top-0 h-full rounded-[100px] bg-gradient-to-r',
{
'from-vega-dark-400 to-vega-dark-200':
userTierIndex === 0,
'from-vega-pink-600 to-vega-pink-500':
userTierIndex % 6 === 1,
'from-vega-purple-600 to-vega-purple-500':
userTierIndex % 6 === 2,
'from-vega-blue-600 to-vega-blue-500':
userTierIndex % 6 === 3,
'from-vega-orange-600 to-vega-orange-500':
userTierIndex % 6 === 4,
'from-vega-green-600 to-vega-green-500':
userTierIndex % 6 === 5,
'from-vega-yellow-600 to-vega-yellow-500':
userTierIndex % 6 === 0,
}
)}
style={{
width:
safeProgress(
index,
userTierIndex,
tiers[index].minimum_quantum_balance,
vestingDetails?.quantumBalance
) + '%',
}}
></div>
</div>
</div>
);
})}
</div>
<div className="flex items-center gap-1">
<VegaIcon name={VegaIconNames.STREAK} />
<span data-testid="hoarder-bonus-total-hoarded">
{formatNumber(vestingDetails?.quantumBalance || 0)} {qUSD}{' '}
{userTierIndex > 0 &&
new BigNumber(
tiers[0].minimum_quantum_balance
).isLessThanOrEqualTo(vestingDetails?.quantumBalance || 0) &&
t('(Tier {{tier}} as of last epoch)', { tier: userTierIndex })}
</span>
</div>
</div>
</>
);
};
+1
View File
@@ -0,0 +1 @@
export * from './table';
@@ -1,13 +1,17 @@
import { Tooltip, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import { forwardRef, type ReactNode, type HTMLAttributes } from 'react';
import { BORDER_COLOR, GRADIENT } from './constants';
export const BORDER_COLOR = 'border-vega-clight-500 dark:border-vega-cdark-500';
export const GRADIENT =
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
type TableColumnDefinition = {
displayName?: ReactNode;
name: string;
tooltip?: string;
className?: string;
testId?: string;
};
type TableProps = {
@@ -42,7 +46,7 @@ export const Table = forwardRef<
key={name}
col-id={name}
className={classNames(
'px-5 py-3 text-sm text-vega-clight-100 dark:text-vega-cdark-100 font-normal',
'px-5 py-3 text-xs text-vega-clight-100 dark:text-vega-cdark-100 font-normal',
INNER_BORDER_STYLE
)}
>
@@ -66,7 +70,7 @@ export const Table = forwardRef<
ref={ref}
className={classNames(
'w-full',
'border-separate border rounded-md border-spacing-0',
'border-separate border rounded-md border-spacing-0 overflow-hidden',
BORDER_COLOR,
GRADIENT,
className
@@ -82,10 +86,10 @@ export const Table = forwardRef<
'max-md:flex flex-col w-full': !noCollapse,
})}
>
{columns.map(({ name, displayName, className }, j) => (
{columns.map(({ name, displayName, className, testId }, j) => (
<td
className={classNames(
'px-5 py-3 text-base',
'px-5 py-3',
{
'max-md:flex max-md:flex-col max-md:justify-between':
!noCollapse,
@@ -110,7 +114,7 @@ export const Table = forwardRef<
{displayName}
</span>
)}
<span>{d[name]}</span>
<span data-testid={`${testId || name}-${i}`}>{d[name]}</span>
</td>
))}
</tr>
+1 -1
View File
@@ -1,3 +1,3 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
VEGA_VERSION=v0.73.9
VEGA_VERSION=v0.73.10
LOCAL_SERVER=false
+2 -1
View File
@@ -1,2 +1,3 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:develop
VEGA_VERSION=v0.73.9
VEGA_VERSION=v0.73.10
LOCAL_SERVER=false
+1 -1
View File
@@ -1,3 +1,3 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:main
VEGA_VERSION=v0.73.8
VEGA_VERSION=v0.73.10
LOCAL_SERVER=false
+23 -1
View File
@@ -8,6 +8,9 @@ import docker
import http.server
import sys
from dotenv import load_dotenv
from playwright.sync_api import Error as PlaywrightError
from docker.models.containers import Container
from docker.errors import APIError
from contextlib import contextmanager
from vega_sim.null_service import VegaServiceNull, Ports
from playwright.sync_api import Browser, Page
@@ -100,12 +103,16 @@ def init_vega(request=None):
container = docker_client.containers.run(
console_image_name, detach=True, ports={"80/tcp": vega.console_port}
)
if not isinstance(container, Container):
raise Exception("container instance invalid")
logger.info(
f"Container {container.id} started",
extra={"worker_id": os.environ.get("PYTEST_XDIST_WORKER")},
)
yield vega
except docker.errors.APIError as e:
except APIError as e:
logger.info(f"Container creation failed.")
logger.info(e)
raise e
@@ -267,3 +274,18 @@ def perps_market(vega, request):
if hasattr(request, "param"):
kwargs.update(request.param)
return setup_perps_market(vega, **kwargs)
@pytest.fixture(autouse=True)
def retry_on_http_error(request):
retry_count = 3
for i in range(retry_count):
try:
yield
return
except requests.exceptions.HTTPError:
if i < retry_count - 1:
print(f"Retrying due to HTTPError (attempt {i+1}/{retry_count})")
else:
raise
+54 -16
View File
@@ -18,7 +18,7 @@ def setup_simple_market(
custom_market_name=market_name,
custom_asset_name="tDAI",
custom_asset_symbol="tDAI",
custom_quantum=1
custom_quantum=1,
):
for wallet in wallets:
vega.create_key(wallet.name)
@@ -117,18 +117,30 @@ def setup_simple_successor_market(
return market_id
def setup_opening_auction_market(vega: VegaService, market_id: str = None, buy_orders=default_buy_orders, sell_orders=default_sell_orders, add_liquidity=True, **kwargs):
def setup_opening_auction_market(
vega: VegaService,
market_id: str = None,
buy_orders=default_buy_orders,
sell_orders=default_sell_orders,
add_liquidity=True,
custom_market_name="BTC:DAI_2023",
custom_asset_name="tDAI",
custom_asset_symbol="tDAI",
**kwargs,
):
if not market_exists(vega, market_id):
market_id = setup_simple_market(vega, **kwargs)
market_id = setup_simple_market(
vega,
custom_market_name=custom_market_name,
custom_asset_name=custom_asset_name,
custom_asset_symbol=custom_asset_symbol,
**kwargs,
)
if add_liquidity:
submit_liquidity(vega, MM_WALLET.name, market_id)
submit_multiple_orders(
vega, MM_WALLET.name, market_id, "SIDE_SELL", sell_orders
)
submit_multiple_orders(
vega, MM_WALLET2.name, market_id, "SIDE_BUY", buy_orders
)
submit_multiple_orders(vega, MM_WALLET.name, market_id, "SIDE_SELL", sell_orders)
submit_multiple_orders(vega, MM_WALLET2.name, market_id, "SIDE_BUY", buy_orders)
vega.forward("10s")
vega.wait_fn(1)
@@ -146,13 +158,37 @@ def market_exists(vega: VegaService, market_id: str):
# Add sell orders and buy orders to put on the book
def setup_continuous_market(vega: VegaService, market_id: str = None, buy_orders=default_buy_orders, sell_orders=default_sell_orders, add_liquidity=True, **kwargs):
if not market_exists(vega, market_id) or buy_orders != default_buy_orders or sell_orders != default_sell_orders:
def setup_continuous_market(
vega: VegaService,
market_id: str = None,
buy_orders=default_buy_orders,
sell_orders=default_sell_orders,
add_liquidity=True,
custom_market_name="BTC:DAI_2023",
custom_asset_name="tDAI",
custom_asset_symbol="tDAI",
**kwargs,
):
if (
not market_exists(vega, market_id)
or buy_orders != default_buy_orders
or sell_orders != default_sell_orders
):
market_id = setup_opening_auction_market(
vega, market_id, buy_orders, sell_orders, add_liquidity, **kwargs)
vega,
market_id,
buy_orders,
sell_orders,
add_liquidity,
custom_market_name=custom_market_name,
custom_asset_name=custom_asset_name,
custom_asset_symbol=custom_asset_symbol,
**kwargs,
)
submit_order(vega, "Key 1", market_id, "SIDE_BUY",
sell_orders[0][0], sell_orders[0][1])
submit_order(
vega, "Key 1", market_id, "SIDE_BUY", sell_orders[0][0], sell_orders[0][1]
)
vega.forward("10s")
vega.wait_fn(1)
@@ -250,6 +286,8 @@ def setup_perps_market(
def market_exists(vega: VegaService, market_id: str):
if market_id is None:
return False
all_markets = vega.all_markets()
all_markets = vega.all_markets()
market_ids = [market.id for market in all_markets]
return market_id in market_ids
print("Checking for market ID:", market_id)
print("Available market IDs:", market_ids)
return market_id in market_ids
+13 -9
View File
@@ -27,8 +27,6 @@ MIN_VOLUME_VALUE_0 = "min-volume-value-0"
MIN_VOLUME_VALUE_1 = "min-volume-value-1"
MY_VOLUME_VALUE_0 = "my-volume-value-0"
MY_VOLUME_VALUE_1 = "my-volume-value-1"
YOUR_TIER_0 = "your-tier-0"
YOUR_TIER_1 = "your-tier-1"
ORDER_SIZE = "order-size"
ORDER_PRICE = "order-price"
DISCOUNT_PILL = "discount-pill"
@@ -52,6 +50,7 @@ REQUIRED_EPOCHS_VALUE_1 = "required-epochs-value-1"
FILLS = "Fills"
TAB_FILLS = "tab-fills"
FEE_BREAKDOWN_TOOLTIP = "fee-breakdown-tooltip"
PINNED_ROW_LOCATOR = ".ag-pinned-left-cols-container .ag-row"
ROW_LOCATOR = ".ag-center-cols-container .ag-row"
# Col-Ids:
COL_INSTRUMENT_CODE = '[col-id="market.tradableInstrument.instrument.code"]'
@@ -405,10 +404,10 @@ def test_fees_page_referral_discount_program_referral_benefits(
@pytest.mark.parametrize(
"tier, discount_program, my_volume_test_id, my_volume_value, your_tier",
[
(1, "volume", "my-volume-value-0", "103", "your-tier-0"),
(2, "volume", "my-volume-value-1", "206", "your-tier-1"),
(1, "referral", "my-volume-value-0", "103", "your-tier-0"),
(2, "referral", "my-volume-value-1", "206", "your-tier-1"),
(1, "volume", "my-volume-value-0", "103", "your-volume-tier-0"),
(2, "volume", "my-volume-value-1", "206", "your-volume-tier-1"),
(1, "referral", "my-volume-value-0", "103", "your-referral-tier-0"),
(2, "referral", "my-volume-value-1", "206", "your-referral-tier-1"),
],
)
@pytest.mark.usefixtures("risk_accepted", "auth", "market_ids")
@@ -439,8 +438,8 @@ def test_fees_page_discount_program_discount(
expect(page.get_by_test_id(REQUIRED_EPOCHS_VALUE_0)).to_have_text("1")
expect(page.get_by_test_id(REQUIRED_EPOCHS_VALUE_1)).to_have_text("2")
expect(page.get_by_test_id(your_tier)).to_be_visible()
expect(page.get_by_test_id(your_tier)).to_have_text("Your tier")
expect(page.get_by_test_id(your_tier).nth(1)).to_be_visible()
expect(page.get_by_test_id(your_tier).nth(1)).to_have_text("Your tier")
@pytest.mark.parametrize(
@@ -461,8 +460,9 @@ def test_fees_page_discount_program_fees_by_market(
vega_instance, tier, discount_program, market_ids
)
page.goto("/#/fees")
pinned = page.locator(PINNED_ROW_LOCATOR)
row = page.locator(ROW_LOCATOR)
expect(row.locator(COL_CODE)).to_have_text("BTC:DAI_2023Futr")
expect(pinned.locator(COL_CODE)).to_have_text("BTC:DAI_2023Futr")
expect(row.locator(COL_FEE_AFTER_DISCOUNT)).to_have_text(fees_after_discount)
expect(row.locator(COL_INFRA_FEE)).to_have_text("0.05%")
expect(row.locator(COL_MAKER_FEE)).to_have_text("10%")
@@ -639,6 +639,8 @@ def test_fills_maker_fee_tooltip_discount_program(
change_keys(page, vega_instance, MM_WALLET.name)
page.get_by_test_id(FILLS).click()
row = page.get_by_test_id(TAB_FILLS).locator(ROW_LOCATOR).first
# tbd - tooltip is not visible without this wait
page.wait_for_timeout(1000)
row.locator(COL_FEE).hover()
expect(page.get_by_test_id(FEE_BREAKDOWN_TOOLTIP)).to_have_text(
f"If the market was activeFee revenue to be received by the maker, takers' fee discounts already applied.During continuous trading the maker pays no infrastructure and liquidity fees.Infrastructure fee0.00 tDAILiquidity fee0.00 tDAIMaker fee-{fee} tDAITotal fees-{fee} tDAI"
@@ -676,6 +678,8 @@ def test_fills_taker_fee_tooltip_discount_program(
page.goto(f"/#/markets/{market_id}")
page.get_by_test_id(FILLS).click()
row = page.get_by_test_id(TAB_FILLS).locator(ROW_LOCATOR).first
# tbd - tooltip is not visible without this wait
page.wait_for_timeout(1000)
row.locator(COL_FEE).hover()
expect(page.get_by_test_id(FEE_BREAKDOWN_TOOLTIP)).to_have_text(
f"If the market was activeFees to be paid by the taker; discounts are already applied.Infrastructure fee{infra_fee} tDAILiquidity fee0.00 tDAIMaker fee{maker_fee} tDAITotal fees{total_fee} tDAI"
@@ -129,7 +129,8 @@ class TestGetStarted:
page.get_by_test_id("get-started-button").click()
# Assert dialog isn't visible
expect(page.get_by_test_id("welcome-dialog")).not_to_be_visible()
@pytest.mark.skip("TODO: this test is flakey, needs fixing")
@pytest.mark.usefixtures("risk_accepted")
def test_get_started_seen_already(self, simple_market, page: Page):
page.goto(f"/#/markets/{simple_market}")
@@ -177,10 +178,10 @@ class TestGetStarted:
# 0007-FUGS-018
expect(page.get_by_test_id("welcome-dialog")).not_to_be_visible()
class TestBrowseAll:
class TestBrowseAll:
def test_get_started_browse_all(self, simple_market, vega: VegaServiceNull, page: Page):
page.goto("/")
print(simple_market)
page.get_by_test_id("browse-markets-button").click()
# 0007-FUGS-005
expect(page).to_have_url(f"http://localhost:{vega.console_port}/#/markets/{simple_market}")
expect(page).to_have_url(f"http://localhost:{vega.console_port}/#/markets/{simple_market}")
@@ -70,7 +70,6 @@ def test_iceberg_open_order(continuous_market, vega: VegaServiceNull, page: Page
expect(
page.locator(".ag-center-cols-container .ag-row [col-id='size']").first
).to_have_text("-102")
page.pause()
expect(
page.locator(".ag-center-cols-container .ag-row [col-id='type'] ").first
).to_have_text("Limit (Iceberg)")
@@ -16,7 +16,6 @@ def vega(request):
def continuous_market(vega):
return setup_continuous_market(vega)
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_liquidity_provision_amendment(continuous_market, vega: VegaServiceNull, page: Page):
# TODO Refactor asserting the grid
+6 -2
View File
@@ -206,13 +206,17 @@ def test_auction_uncross_fees(continuous_market, vega: VegaServiceNull, page: Pa
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id("Fills").click()
expect(page.locator(COL_ID_FEE)).to_have_text("0.00 tDAI")
page.locator(COL_ID_FEE).hover()
# tbd - tooltip is not visible without this wait
page.wait_for_timeout(1000)
page.get_by_role("gridcell", name="0.00 tDAI").nth(0).hover()
expect(page.get_by_test_id("fee-breakdown-tooltip")).to_have_text(
"If the market was suspendedDuring auction, half the infrastructure and liquidity fees will be paid.Infrastructure fee0.00 tDAILiquidity fee0.00 tDAIMaker fee0.00 tDAITotal fees0.00 tDAI"
)
change_keys(page, vega, "market_maker")
expect(page.locator(COL_ID_FEE)).to_have_text("0.00 tDAI")
page.locator(COL_ID_FEE).hover()
# tbd - tooltip is not visible without this wait
page.wait_for_timeout(1000)
page.get_by_role("gridcell", name="0.00 tDAI").nth(0).hover()
expect(page.get_by_test_id("fee-breakdown-tooltip")).to_have_text(
"If the market was suspendedDuring auction, half the infrastructure and liquidity fees will be paid.Infrastructure fee0.00 tDAILiquidity fee0.00 tDAIMaker fee0.00 tDAITotal fees0.00 tDAI"
)
@@ -9,16 +9,16 @@ def test_market_selector(continuous_market, page: Page):
page.get_by_test_id("header-title").click()
# 6001-MARK-066
expect(page.get_by_test_id("market-selector")).to_be_visible()
# 6001-MARK-021
# 6001-MARK-022
# 6001-MARK-024
# 6001-MARK-025
btc_market = page.locator('[data-testid="market-selector-list"] a')
expect(btc_market.locator("h3")).to_have_text("BTC:DAI_2023Futr")
expect(btc_market.locator('[data-testid="market-selector-volume"]')).to_have_text(
"0.00"
)
# tbd - 5465
# expect(btc_market.locator('[data-testid="market-selector-volume"]')).to_have_text(
# "1"
# )
expect(btc_market.locator('[data-testid="market-selector-price"]')).to_have_text(
"107.50 tDAI"
)
@@ -43,7 +43,6 @@ def test_market_selector_filter(continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id("header-title").click()
# 6001-MARK-027
page.get_by_test_id("product-Spot").click()
expect(page.get_by_test_id("market-selector-list")).to_contain_text(
"Spot markets coming soon."
@@ -58,8 +57,9 @@ def test_market_selector_filter(continuous_market, page: Page):
# 6001-MARK-029
page.get_by_test_id("search-term").fill("btc")
expect(page.locator('[data-testid="market-selector-list"] a')).to_have_count(1)
expect(page.locator('[data-testid="market-selector-list"] a').nth(0)).to_have_text(
"BTC:DAI_2023107.50 tDAI0.00"
# tbd - 5465
expect(page.locator('[data-testid="market-selector-list"] a').nth(0)).to_contain_text(
"BTC:DAI_2023107.50 tDAI"
)
page.get_by_test_id("search-term").clear()
@@ -83,6 +83,7 @@ def test_market_selector_filter(continuous_market, page: Page):
page.get_by_test_id("asset-trigger").click()
page.get_by_role("menuitemcheckbox").nth(0).get_by_text("tDAI").click()
expect(page.locator('[data-testid="market-selector-list"] a')).to_have_count(1)
expect(page.locator('[data-testid="market-selector-list"] a').nth(0)).to_have_text(
"BTC:DAI_2023107.50 tDAI0.00"
# tbd - 5465
expect(page.locator('[data-testid="market-selector-list"] a').nth(0)).to_contain_text(
"BTC:DAI_2023107.50 tDAI"
)
@@ -64,7 +64,6 @@ def setup_market_monitoring_auction(vega: VegaServiceNull, simple_market):
submit_order(vega, MM_WALLET.name, simple_market, "SIDE_SELL", 1, 1 + 0.1 / 2)
submit_order(vega, MM_WALLET2.name, simple_market, "SIDE_SELL", 1, 1)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
@@ -75,7 +74,6 @@ def setup_market_monitoring_auction(vega: VegaServiceNull, simple_market):
submit_order(vega, MM_WALLET2.name, simple_market, "SIDE_BUY", 100, 95)
submit_order(vega, MM_WALLET2.name, simple_market, "SIDE_BUY", 1, 105)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
@@ -109,7 +107,6 @@ def test_market_monitoring_auction_price_volatility_limit_order(
page.get_by_test_id("place-order").click()
wait_for_toast_confirmation(page)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
page.get_by_test_id("All").click()
@@ -1,14 +1,13 @@
import pytest
import re
import vega_sim.api.governance as governance
from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull
from vega_sim.service import PeggedOrder
from vega_sim.service import PeggedOrder, MarketStateUpdateType
import vega_sim.api.governance as governance
from actions.vega import submit_order
from actions.utils import next_epoch
from wallet_config import MM_WALLET, MM_WALLET2, GOVERNANCE_WALLET
from vega_sim.api import governance
@pytest.mark.usefixtures("risk_accepted")
def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page):
@@ -127,6 +126,44 @@ def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page):
expect(trading_mode).to_have_text("Continuous")
expect(market_state).to_have_text("Active")
vega.update_market_state(
market_id = market_id,
proposal_key = MM_WALLET.name,
market_state = MarketStateUpdateType.Suspend,
forward_time_to_enactment = False
)
expect(
page
.get_by_test_id("market-banner")
.get_by_test_id(f"update-state-banner-{market_id}")
).to_be_visible()
vega.forward("60s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
expect(
page.get_by_test_id("market-banner")
).to_have_text("Market was suspended by governance")
# banner should not show after resume
vega.update_market_state(
market_id = market_id,
proposal_key = MM_WALLET.name,
market_state = MarketStateUpdateType.Resume,
forward_time_to_enactment = False
)
vega.forward("60s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
expect(page.get_by_test_id("market-banner")).not_to_be_visible()
# TODO test update market, sim will currently auto approve and enacted
# a market update proposals
# put invalid oracle to trigger market termination
governance.submit_oracle_data(
wallet=vega.wallet,
@@ -154,6 +191,9 @@ def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page):
# check market state is now settled
expect(trading_mode).to_have_text("No trading")
expect(market_state).to_have_text("Settled")
expect(
page.get_by_test_id("market-banner")
).to_have_text("This market has been settled")
""" @pytest.mark.usefixtures("page", "risk_accepted", "continuous_market")
@@ -182,19 +222,19 @@ def test_market_closing_banners(page: Page, continuous_market, vega: VegaService
forward_time_to_enactment = False,
price=110,
)
expect(page.locator(".grow")).to_have_text("Trading on Market BTC:DAI_2023 may stop. There are open proposals to close this marketView proposals")
governance.approve_proposal(
governance.approve_proposal(
proposal_id=proposalID,
wallet=vega.wallet,
key_name="market_maker"
)
vega.forward("60s")
vega.wait_fn(10)
vega.wait_for_total_catchup()
will_close_pattern = r"TRADING ON MARKET BTC:DAI_2023 WILL STOP ON \d+ \w+\nYou will no longer be able to hold a position on this market when it closes in \d+ days \d+ hours\. The final price will be 107\.00 BTC\."
match_result = re.fullmatch(will_close_pattern, page.locator(".grow").inner_text())
assert match_result is not None
@@ -42,6 +42,10 @@ def verify_order_value(
expect(element).to_be_visible()
if is_regex:
actual_text = element.text_content()
if actual_text is None:
raise Exception(f"no text found for test_id {test_id}")
assert re.match(
expected_text, actual_text
), f"Expected {expected_text}, but got {actual_text}"
@@ -109,14 +109,15 @@ def test_perps_market_termination_proposed(page: Page, vega: VegaServiceNull):
approve_proposal=True,
forward_time_to_enactment=False,
)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
banner_text = page.get_by_test_id(
f"termination-warning-banner-{perpetual_market}"
f"update-state-banner-{perpetual_market}"
).text_content()
pattern = re.compile(
r"Trading on Market BTC:DAI_Perpetual may stop on \d{2} [A-Za-z]+\. There is open proposal to close this market\.Proposed final price is 100\.00 BTC\.View proposal"
r"Trading on market BTC:DAI_Perpetual may stop on \d{2} [A-Za-z]+\. There is an open proposal to close this market\.Proposed final price is 100\.00 BTC\. View proposal"
)
assert pattern.search(
banner_text
@@ -0,0 +1,469 @@
import pytest
import vega_sim.proto.vega as vega_protos
from playwright.sync_api import Page, expect
from conftest import init_vega, init_page, auth_setup
from fixtures.market import setup_continuous_market, market_exists
from actions.utils import next_epoch, change_keys
from wallet_config import MM_WALLET, PARTY_A, PARTY_B, PARTY_C, PARTY_D
from vega_sim.null_service import VegaServiceNull
# region Constants
ACTIVITY = "activity"
HOARDER = "hoarder"
COMBO = "combo"
REWARDS_URL = "/#/rewards"
# test IDs
COMBINED_MULTIPLIERS = "combined-multipliers"
TOTAL_REWARDS = "total-rewards"
PRICE_TAKING_COL_ID = '[col-id="priceTaking"]'
TOTAL_COL_ID = '[col-id="total"]'
ROW = "row"
STREAK_REWARD_MULTIPLIER_VALUE = "streak-reward-multiplier-value"
HOARDER_REWARD_MULTIPLIER_VALUE = "hoarder-reward-multiplier-value"
HOARDER_BONUS_TOTAL_HOARDED = "hoarder-bonus-total-hoarded"
EARNED_BY_ME_BUTTON = "earned-by-me-button"
TRANSFER_AMOUNT = "transfer-amount"
EPOCH_STREAK = "epoch-streak"
# endregion
@pytest.fixture(scope="module")
def market_ids():
return {
"vega_activity_tier_0": "default_id",
"vega_hoarder_tier_0": "default_id",
"vega_combo_tier_0": "default_id",
"vega_activity_tier_1": "default_id",
"vega_hoarder_tier_1": "default_id",
"vega_combo_tier_1": "default_id",
}
@pytest.fixture(scope="module")
def vega_activity_tier_0(request):
with init_vega(request) as vega_activity_tier_0:
yield vega_activity_tier_0
@pytest.fixture(scope="module")
def vega_hoarder_tier_0(request):
with init_vega(request) as vega_hoarder_tier_0:
yield vega_hoarder_tier_0
@pytest.fixture(scope="module")
def vega_combo_tier_0(request):
with init_vega(request) as vega_combo_tier_0:
yield vega_combo_tier_0
@pytest.fixture(scope="module")
def vega_activity_tier_1(request):
with init_vega(request) as vega_activity_tier_1:
yield vega_activity_tier_1
@pytest.fixture(scope="module")
def vega_hoarder_tier_1(request):
with init_vega(request) as vega_hoarder_tier_1:
yield vega_hoarder_tier_1
@pytest.fixture(scope="module")
def vega_combo_tier_1(request):
with init_vega(request) as vega_combo_tier_1:
yield vega_combo_tier_1
@pytest.fixture
def auth(vega_instance, page):
return auth_setup(vega_instance, page)
@pytest.fixture
def page(vega_instance, browser, request):
with init_page(vega_instance, browser, request) as page_instance:
yield page_instance
@pytest.fixture
def vega_instance(
reward_program,
vega_activity_tier_0,
vega_hoarder_tier_0,
vega_combo_tier_0,
vega_activity_tier_1,
vega_hoarder_tier_1,
vega_combo_tier_1,
tier,
):
if reward_program == "activity":
return vega_activity_tier_0 if tier == 1 else vega_activity_tier_1
elif reward_program == "hoarder":
return vega_hoarder_tier_0 if tier == 1 else vega_hoarder_tier_1
elif reward_program == "combo":
return vega_combo_tier_0 if tier == 1 else vega_combo_tier_1
def setup_market_with_reward_program(vega: VegaServiceNull, reward_programs, tier):
print(f"Started setup_market_with_{reward_programs}_{tier}")
tDAI_market = setup_continuous_market(vega)
tDAI_asset_id = vega.find_asset_id(symbol="tDAI")
vega.mint(key_name=PARTY_B.name, asset=tDAI_asset_id, amount=100000)
vega.mint(key_name=PARTY_C.name, asset=tDAI_asset_id, amount=100000)
vega.mint(key_name=PARTY_A.name, asset=tDAI_asset_id, amount=100000)
vega.mint(key_name=PARTY_D.name, asset=tDAI_asset_id, amount=100000)
next_epoch(vega=vega)
if ACTIVITY in reward_programs:
vega.update_network_parameter(
proposal_key=MM_WALLET.name,
parameter="rewards.activityStreak.benefitTiers",
new_value=ACTIVITY_STREAKS,
)
print("update_network_parameter activity done")
next_epoch(vega=vega)
if HOARDER in reward_programs:
vega.update_network_parameter(
proposal_key=MM_WALLET.name,
parameter="rewards.vesting.benefitTiers",
new_value=VESTING,
)
next_epoch(vega=vega)
tDAI_asset_id = vega.find_asset_id(symbol="tDAI")
vega.update_network_parameter(
MM_WALLET.name, parameter="reward.asset", new_value=tDAI_asset_id
)
next_epoch(vega=vega)
vega.recurring_transfer(
from_key_name=PARTY_A.name,
from_account_type=vega_protos.vega.ACCOUNT_TYPE_GENERAL,
to_account_type=vega_protos.vega.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
asset=tDAI_asset_id,
reference="reward",
asset_for_metric=tDAI_asset_id,
metric=vega_protos.vega.DISPATCH_METRIC_MAKER_FEES_PAID,
# lock_period= 5,
# TODO test lock period
amount=100,
factor=1.0,
)
vega.submit_order(
trading_key=PARTY_B.name,
market_id=tDAI_market,
order_type="TYPE_MARKET",
time_in_force="TIME_IN_FORCE_IOC",
side="SIDE_BUY",
volume=1,
)
vega.submit_order(
trading_key=PARTY_A.name,
market_id=tDAI_market,
order_type="TYPE_MARKET",
time_in_force="TIME_IN_FORCE_IOC",
side="SIDE_BUY",
volume=1,
)
vega.wait_for_total_catchup()
if tier == 1:
next_epoch(vega=vega)
vega.submit_order(
trading_key=PARTY_B.name,
market_id=tDAI_market,
order_type="TYPE_LIMIT",
time_in_force="TIME_IN_FORCE_GTC",
side="SIDE_BUY",
price=1,
volume=1,
)
next_epoch(vega=vega)
vega.submit_order(
trading_key=PARTY_B.name,
market_id=tDAI_market,
order_type="TYPE_MARKET",
time_in_force="TIME_IN_FORCE_IOC",
side="SIDE_BUY",
volume=1,
)
vega.submit_order(
trading_key=PARTY_D.name,
market_id=tDAI_market,
order_type="TYPE_MARKET",
time_in_force="TIME_IN_FORCE_IOC",
side="SIDE_BUY",
volume=1,
)
vega.wait_for_total_catchup()
next_epoch(vega=vega)
next_epoch(vega=vega)
if HOARDER in reward_programs:
vega.submit_order(
trading_key=PARTY_B.name,
market_id=tDAI_market,
order_type="TYPE_MARKET",
time_in_force="TIME_IN_FORCE_IOC",
side="SIDE_BUY",
volume=1,
)
vega.submit_order(
trading_key=PARTY_D.name,
market_id=tDAI_market,
order_type="TYPE_MARKET",
time_in_force="TIME_IN_FORCE_IOC",
side="SIDE_BUY",
volume=1,
)
vega.wait_for_total_catchup()
next_epoch(vega=vega)
next_epoch(vega=vega)
next_epoch(vega=vega)
return tDAI_market, tDAI_asset_id
def set_market_reward_program(vega, reward_program, market_ids, tier):
market_id_key = f"vega_{reward_program}_tier_{tier}"
if reward_program == COMBO:
market_id_key = COMBO
market_id = market_ids.get(market_id_key, "default_id")
print(f"Checking if market exists: {market_id}")
if not market_exists(vega, market_id):
print(
f"Market doesn't exist for {reward_program} {tier}. Setting up new market."
)
reward_programs = [reward_program]
if reward_program == COMBO:
reward_programs = [ACTIVITY, HOARDER]
market_id, _ = setup_market_with_reward_program(vega, reward_programs, tier)
market_ids[market_id_key] = market_id
return market_id, market_ids
ACTIVITY_STREAKS = """
{
"tiers": [
{
"minimum_activity_streak": 2,
"reward_multiplier": "2.0",
"vesting_multiplier": "1.1"
}
]
}
"""
VESTING = """
{
"tiers": [
{
"minimum_quantum_balance": "10000000",
"reward_multiplier": "2"
}
]
}
"""
@pytest.mark.parametrize(
"reward_program, tier, total_rewards",
[
(ACTIVITY, 0, "50.00 tDAI"),
(HOARDER, 0, "50.00 tDAI"),
(COMBO, 0, "50.00 tDAI"),
(ACTIVITY, 1, "116.66666 tDAI"),
(HOARDER, 1, "166.66666 tDAI "),
(COMBO, 1, "183.33333 tDAI"),
],
)
@pytest.mark.usefixtures("auth", "risk_accepted", "market_ids")
def test_network_reward_pot(
reward_program,
vega_instance: VegaServiceNull,
page: Page,
total_rewards,
tier,
market_ids,
):
print("reward program: " + reward_program, " tier:", tier)
market_id, market_ids = set_market_reward_program(
vega_instance, reward_program, market_ids, tier
)
page.goto(REWARDS_URL)
change_keys(page, vega_instance, PARTY_B.name)
expect(page.get_by_test_id(TOTAL_REWARDS)).to_have_text(total_rewards)
@pytest.mark.parametrize(
"reward_program, tier, reward_multiplier, streak_multiplier, hoarder_multiplier",
[
(ACTIVITY, 0, "1x", "1x", "1x"),
(HOARDER, 0, "1x", "1x", "1x"),
(COMBO, 0, "1x", "1x", "1x"),
(ACTIVITY, 1, "2x", "2x", "1x"),
(HOARDER, 1, "2x", "1x", "2x"),
(COMBO, 1, "4x", "2x", "2x"),
],
)
@pytest.mark.usefixtures("auth", "risk_accepted", "market_ids")
def test_reward_multiplier(
reward_program,
vega_instance: VegaServiceNull,
page: Page,
reward_multiplier,
streak_multiplier,
hoarder_multiplier,
tier,
market_ids,
):
print("reward program: " + reward_program, " tier:", tier)
market_id, market_ids = set_market_reward_program(
vega_instance, reward_program, market_ids, tier
)
page.goto(REWARDS_URL)
change_keys(page, vega_instance, PARTY_B.name)
expect(page.get_by_test_id(COMBINED_MULTIPLIERS)).to_have_text(reward_multiplier)
expect(page.get_by_test_id(STREAK_REWARD_MULTIPLIER_VALUE)).to_have_text(
streak_multiplier
)
expect(page.get_by_test_id(HOARDER_REWARD_MULTIPLIER_VALUE)).to_have_text(
hoarder_multiplier
)
@pytest.mark.parametrize(
"reward_program, tier, epoch_streak",
[
(ACTIVITY, 0, "1"),
(ACTIVITY, 1, "7"),
],
)
@pytest.mark.usefixtures("auth", "risk_accepted", "market_ids")
def test_activity_streak(
reward_program,
vega_instance: VegaServiceNull,
page: Page,
epoch_streak,
tier,
market_ids,
):
print("reward program: " + reward_program, " tier:", tier)
market_id, market_ids = set_market_reward_program(
vega_instance, reward_program, market_ids, tier
)
page.goto(REWARDS_URL)
change_keys(page, vega_instance, PARTY_B.name)
if tier == 1:
expect(page.get_by_test_id(EPOCH_STREAK)).to_have_text(
"Active trader: " + epoch_streak + " epochs so far (Tier 1 as of last epoch)"
)
else:
expect(page.get_by_test_id(EPOCH_STREAK)).to_have_text(
"Active trader: " + epoch_streak + " epochs so far "
)
@pytest.mark.parametrize(
"reward_program, tier, rewards_hoarded",
[
(HOARDER, 0, "5,000,000"),
(HOARDER, 1, "16,666,666"),
],
)
@pytest.mark.usefixtures("auth", "risk_accepted", "market_ids")
def test_hoarder_bonus(
reward_program,
vega_instance: VegaServiceNull,
page: Page,
rewards_hoarded,
tier,
market_ids,
):
print("reward program: " + reward_program, " tier:", tier)
market_id, market_ids = set_market_reward_program(
vega_instance, reward_program, market_ids, tier
)
page.goto(REWARDS_URL)
change_keys(page, vega_instance, PARTY_B.name)
expect(page.get_by_test_id(HOARDER_BONUS_TOTAL_HOARDED)).to_contain_text(
rewards_hoarded
)
@pytest.mark.parametrize(
"reward_program, tier, price_taking, total, earned_by_me",
[
(ACTIVITY, 0, "100.00100.00%", "100.00", "50.00"),
(HOARDER, 0, "100.00100.00%", "100.00", "50.00"),
(COMBO, 0, "100.00100.00%", "100.00", "50.00"),
(ACTIVITY, 1, "300.00100.00%", "300.00", "116.66666"),
(HOARDER, 1, "299.99999100.00%", "299.99999", "166.66666"),
(COMBO, 1, "299.99999100.00%", "299.99999", "183.33333"),
],
)
@pytest.mark.usefixtures("auth", "risk_accepted", "market_ids")
def test_reward_history(
reward_program,
vega_instance: VegaServiceNull,
page: Page,
price_taking,
total,
earned_by_me,
tier,
market_ids,
):
print("reward program: " + reward_program, " tier:", tier)
market_id, market_ids = set_market_reward_program(
vega_instance, reward_program, market_ids, tier
)
page.goto(REWARDS_URL)
change_keys(page, vega_instance, PARTY_B.name)
page.locator('[name="fromEpoch"]').fill("1")
expect((page.get_by_role(ROW).locator(PRICE_TAKING_COL_ID)).nth(1)).to_have_text(
price_taking
)
expect((page.get_by_role(ROW).locator(TOTAL_COL_ID)).nth(1)).to_have_text(total)
page.get_by_test_id(EARNED_BY_ME_BUTTON).click()
expect((page.get_by_role(ROW).locator(TOTAL_COL_ID)).nth(1)).to_have_text(
earned_by_me
)
@pytest.mark.parametrize(
"reward_program, tier",
[
(ACTIVITY, 1),
],
)
@pytest.mark.usefixtures("auth", "risk_accepted", "market_ids")
def test_redeem(
reward_program, vega_instance: VegaServiceNull, page: Page, tier, market_ids
):
print("reward program: " + reward_program, " tier:", tier)
market_id, market_ids = set_market_reward_program(
vega_instance, reward_program, market_ids, tier
)
page.goto(REWARDS_URL)
change_keys(page, vega_instance, PARTY_B.name)
page.get_by_test_id("redeem-rewards-button").click()
available_to_withdraw = page.get_by_test_id(
"available-to-withdraw-value"
).text_content()
option_value = page.locator(
'[data-testid="transfer-form"] [name="fromAccount"] option[value^="ACCOUNT_TYPE_VESTED_REWARDS"]'
).first.get_attribute("value")
page.select_option(
'[data-testid="transfer-form"] [name="fromAccount"]', option_value
)
page.get_by_test_id("use-max-button").first.click()
expect(page.get_by_test_id(TRANSFER_AMOUNT)).to_have_text(available_to_withdraw)
@@ -0,0 +1,65 @@
import pytest
import vega_sim.proto.vega as vega_protos
from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull
from actions.utils import next_epoch, change_keys
from wallet_config import MM_WALLET, PARTY_A, PARTY_B
@pytest.mark.usefixtures("risk_accepted", "auth")
def test_vesting(continuous_market, vega: VegaServiceNull, page: Page):
tDAI_asset_id = vega.find_asset_id(symbol="tDAI")
vega.update_network_parameter(
MM_WALLET.name, parameter="reward.asset", new_value=tDAI_asset_id
)
vega.mint(key_name=PARTY_B.name, asset=tDAI_asset_id, amount=100000)
vega.mint(key_name=PARTY_A.name, asset=tDAI_asset_id, amount=100000)
next_epoch(vega=vega)
vega.recurring_transfer(
from_key_name=PARTY_A.name,
from_account_type=vega_protos.vega.ACCOUNT_TYPE_GENERAL,
to_account_type=vega_protos.vega.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
asset=tDAI_asset_id,
reference="reward",
asset_for_metric=tDAI_asset_id,
metric=vega_protos.vega.DISPATCH_METRIC_MAKER_FEES_PAID,
lock_period=5,
amount=100,
factor=1.0,
)
vega.submit_order(
trading_key=PARTY_B.name,
market_id=continuous_market,
order_type="TYPE_MARKET",
time_in_force="TIME_IN_FORCE_IOC",
side="SIDE_BUY",
volume=1,
)
vega.submit_order(
trading_key=PARTY_A.name,
market_id=continuous_market,
order_type="TYPE_MARKET",
time_in_force="TIME_IN_FORCE_IOC",
side="SIDE_BUY",
volume=1,
)
vega.wait_for_total_catchup()
page.goto("/#/rewards")
change_keys(page, vega, PARTY_B.name)
next_epoch(vega=vega)
page.reload()
expect(page.get_by_test_id("locked-value")).to_have_text("50.00")
# Proceed through the 5 epoch lock period
next_epoch(vega=vega)
next_epoch(vega=vega)
next_epoch(vega=vega)
next_epoch(vega=vega)
next_epoch(vega=vega)
page.reload()
expect(page.get_by_test_id("locked-value")).to_have_text("0.00")
expect(page.get_by_test_id("vesting-value")).to_have_text("37.50")
expect(page.get_by_test_id("available-to-withdraw-value")).to_have_text("12.50")
@@ -1,46 +1,155 @@
import pytest
from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull
from fixtures.market import setup_continuous_market, setup_simple_successor_market
from fixtures.market import setup_continuous_market
from wallet_config import MM_WALLET, MM_WALLET2, GOVERNANCE_WALLET
from actions.vega import submit_multiple_orders, submit_order, submit_liquidity
from actions.utils import next_epoch
@pytest.fixture
@pytest.mark.usefixtures()
def successor_market(vega: VegaServiceNull):
parent_market_id = setup_continuous_market(vega)
tdai_id = vega.find_asset_id(symbol="tDAI")
successor_market_id = setup_simple_successor_market(
vega, parent_market_id, tdai_id, "successor_market"
)
vega.submit_termination_and_settlement_data(
settlement_key="FJMKnwfZdd48C8NqvYrG",
settlement_price=110,
market_id=parent_market_id,
)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
return successor_market_id
market_banner = "market-banner"
@pytest.mark.skip("tbd")
@pytest.mark.usefixtures("risk_accepted")
def test_succession_line(page: Page, successor_market):
page.goto(f"/#/markets/{successor_market}")
page.get_by_test_id("Info").click()
page.get_by_text("Succession line").click()
def test_succession_line(vega: VegaServiceNull, page: Page):
parent_market_id = setup_continuous_market(vega)
tdai_id = vega.find_asset_id(symbol="tDAI")
expect(page.get_by_test_id("succession-line-item").first).to_contain_text(
"BTC:DAI_2023BTC:DAI_2023"
page.goto(f"/#/markets/{parent_market_id}")
# market in normal state no banner shown
expect(page.get_by_test_id(market_banner)).not_to_be_attached()
successor_name = "successor market name"
successor_id = propose_successor(vega, parent_market_id, tdai_id, successor_name)
# Check that the banner notifying about the successor proposal is shown
banner = page.get_by_test_id(market_banner)
expect(banner).to_be_attached()
expect(banner.get_by_text("A successor to this market has been proposed")).to_be_visible()
next_epoch(vega)
# Banner should not show after market has enacted
expect(page.get_by_test_id(market_banner)).not_to_be_attached()
# Check that the newly created market has the correct succession line
# shown in market info
provide_successor_liquidity(vega, successor_id)
next_epoch(vega)
page.goto(f"/#/markets/{successor_id}")
# Page reload required as the data provider doesnt receive the new market
# This also clears any toasts which can block the market info panel containing
# the succession line
page.reload()
#tbd issue - 5546
page.get_by_test_id("Info").click()
page.get_by_role("button", name="Succession line").click()
succession_item = page.get_by_test_id("succession-line-item")
expect(succession_item.nth(1)).to_contain_text(
"successor market name"
)
expect(
page.get_by_test_id("succession-line-item").first.get_by_role("link")
).to_be_attached
expect(page.get_by_test_id("succession-line-item").last).to_contain_text(
"successor_marketsuccessor_market"
)
expect(
page.get_by_test_id("succession-line-item").last.get_by_role("link")
succession_item.first.get_by_role("link")
).to_be_attached
expect(
page.get_by_test_id("succession-line-item").last.get_by_test_id("icon-bullet")
succession_item.last.get_by_role("link")
).to_be_attached
expect(
succession_item.last.get_by_test_id("icon-bullet")
).to_be_visible
page.goto(f"/#/markets/{parent_market_id}")
# Settle parent market and check that successor banner is showing
vega.submit_termination_and_settlement_data(
settlement_key=GOVERNANCE_WALLET.name,
settlement_price=100,
market_id=parent_market_id,
)
next_epoch(vega=vega)
banner = page.get_by_test_id(market_banner)
page.wait_for_selector('[data-testid="market-banner"]', state="attached")
expect(banner.get_by_text("This market has been succeeded")).to_be_visible()
@pytest.mark.usefixtures("risk_accepted")
def test_banners(vega: VegaServiceNull, page: Page):
banner_successor_text = "A successor to this market has been proposed"
parent_market_id = setup_continuous_market(vega)
tdai_id = vega.find_asset_id(symbol="tDAI")
page.goto(f"/#/markets/{parent_market_id}")
# market in normal state no banner shown
expect(page.get_by_test_id(market_banner)).not_to_be_attached()
vega.submit_termination_and_settlement_data(
settlement_key=GOVERNANCE_WALLET.name,
settlement_price=100,
market_id=parent_market_id,
)
successor_name = "successor market name"
propose_successor(vega, parent_market_id, tdai_id, successor_name)
# Check that the banners notifying about the successor proposal and market has been settled are shown
banner = page.get_by_test_id(market_banner)
expect(banner).to_be_attached()
expect(banner.get_by_text(banner_successor_text)).to_be_visible()
expect(banner.get_by_text("1/2")).to_be_visible()
# Check that the banner notifying about the successor proposal and market has been settled are shown still after reload
page.reload()
expect(banner.get_by_text(banner_successor_text)).to_be_visible()
expect(banner.get_by_text("1/2")).to_be_visible()
# Check that the banner notifying about the successor proposal is not visible after close those banners
banner.get_by_test_id("icon-cross").click()
expect(banner.get_by_text("This market has been settled")).to_be_visible()
expect(banner.get_by_text("2/2")).to_be_visible()
# Check that the banners notifying are visible after reload
banner.get_by_test_id("icon-cross").click()
expect(page.get_by_test_id(market_banner)).not_to_be_attached()
page.reload()
expect(banner).to_be_attached()
expect(banner.get_by_text(banner_successor_text)).to_be_visible()
def propose_successor(
vega: VegaServiceNull, parent_market_id, tdai_id, market_name
):
market_id = vega.create_simple_market(
market_name,
proposal_key=MM_WALLET.name,
settlement_asset_id=tdai_id,
termination_key=MM_WALLET2.name,
market_decimals=5,
approve_proposal=True,
forward_time_to_enactment=False,
parent_market_id=parent_market_id,
parent_market_insurance_pool_fraction=0.5,
)
return market_id
def provide_successor_liquidity(
vega: VegaServiceNull, market_id
):
submit_liquidity(vega, MM_WALLET.name, market_id)
submit_multiple_orders(
vega, MM_WALLET.name, market_id, "SIDE_SELL", [[1, 110], [1, 105]]
)
submit_multiple_orders(
vega, MM_WALLET2.name, market_id, "SIDE_BUY", [[1, 90], [1, 95]]
)
submit_order(vega, "Key 1", market_id, "SIDE_BUY", 1, 110)
vega.forward("10s")
vega.wait_fn(1)
vega.wait_for_total_catchup()
@@ -146,7 +146,7 @@ def test_transfer_vesting_below_minimum(
vega.wait_fn(10)
vega.wait_for_total_catchup()
page.get_by_text("Use max").first.click()
page.get_by_test_id("use-max-button").first.click()
page.locator('[data-testid=transfer-form] [type="submit"]').click()
wait_for_toast_confirmation(page)
vega.forward("10s")
+3 -1
View File
@@ -11,5 +11,7 @@ GOVERNANCE_WALLET = WalletConfig(
"FJMKnwfZdd48C8NqvYrG", "bY3DxwtsCstMIIZdNpKs")
PARTY_A = WalletConfig("party_a", "party_a")
PARTY_B = WalletConfig("party_b", "party_b")
PARTY_C = WalletConfig("party_c", "party_c")
PARTY_D = WalletConfig("party_d", "party_d")
wallets = [MM_WALLET, MM_WALLET2, TERMINATE_WALLET, GOVERNANCE_WALLET]
wallets = [MM_WALLET, MM_WALLET2, TERMINATE_WALLET, GOVERNANCE_WALLET, PARTY_A, PARTY_B, PARTY_C, PARTY_D]
@@ -1,62 +0,0 @@
import {
Intent,
useToasts,
ToastHeading,
CLOSE_AFTER,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useEffect, useMemo } from 'react';
import { useT } from '../use-t';
import { VegaWalletConnectButton } from '../../components/vega-wallet-connect-button';
const WALLET_DISCONNECTED_TOAST_ID = 'WALLET_DISCONNECTED_TOAST_ID';
export const useWalletDisconnectedToasts = () => {
const t = useT();
const [hasToast, setToast, updateToast] = useToasts((state) => [
state.hasToast,
state.setToast,
state.update,
]);
const { isAlive } = useVegaWallet();
const toast = useMemo(
() => ({
id: WALLET_DISCONNECTED_TOAST_ID,
intent: Intent.Danger,
content: (
<>
<ToastHeading>{t('Wallet connection lost')}</ToastHeading>
<p>{t('The connection to the Vega wallet has been lost.')}</p>
<p className="mt-2">
<VegaWalletConnectButton
intent={Intent.Danger}
onClick={() => {
updateToast(WALLET_DISCONNECTED_TOAST_ID, {
hidden: true,
});
}}
/>
</p>
</>
),
onClose: () => {
updateToast(WALLET_DISCONNECTED_TOAST_ID, {
hidden: true,
});
},
closeAfter: CLOSE_AFTER,
}),
[t, updateToast]
);
useEffect(() => {
if (isAlive === false) {
if (hasToast(WALLET_DISCONNECTED_TOAST_ID)) {
updateToast(WALLET_DISCONNECTED_TOAST_ID, { hidden: false });
} else {
setToast(toast);
}
}
}, [hasToast, isAlive, setToast, t, toast, updateToast]);
};
+2 -2
View File
@@ -13,9 +13,9 @@ export default function Document() {
{/* preload fonts */}
<link
rel="preload"
href="/AlphaLyrae.woff2"
href="/AlphaLyrae.woff"
as="font"
type="font/woff2"
type="font/woff"
/>
{/* icons */}
+8
View File
@@ -14,6 +14,7 @@ import { Withdraw } from '../client-pages/withdraw';
import { Transfer } from '../client-pages/transfer';
import { Fees } from '../client-pages/fees';
import { Rewards } from '../client-pages/rewards';
import { Teams } from '../client-pages/teams';
import { Routes as AppRoutes } from '../lib/links';
import { LayoutWithSky } from '../client-pages/referrals/layout';
import { Referrals } from '../client-pages/referrals/referrals';
@@ -92,6 +93,12 @@ export const useRouterConfig = (): RouteObject[] => {
],
}
: undefined,
featureFlags.TEAM_COMPETITION
? {
path: AppRoutes.TEAMS,
element: <Teams />,
}
: undefined,
{
path: 'fees/*',
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
@@ -176,6 +183,7 @@ export const useRouterConfig = (): RouteObject[] => {
},
],
},
{
path: '*',
element: <NotFound />,
+22 -3
View File
@@ -1,4 +1,4 @@
import { ToastsContainer, useToasts } from '@vegaprotocol/ui-toolkit';
import { Intent, ToastsContainer, useToasts } from '@vegaprotocol/ui-toolkit';
import { useProposalToasts } from '@vegaprotocol/proposals';
import { useVegaTransactionToasts } from '@vegaprotocol/web3';
import { useEthereumTransactionToasts } from '@vegaprotocol/web3';
@@ -6,7 +6,26 @@ import { useEthereumWithdrawApprovalsToasts } from '@vegaprotocol/web3';
import { useReadyToWithdrawalToasts } from '@vegaprotocol/withdraws';
import { Links } from '../lib/links';
import { useReferralToasts } from '../client-pages/referrals/hooks/use-referral-toasts';
import { useWalletDisconnectedToasts } from '../lib/hooks/use-wallet-disconnected-toasts';
import {
useWalletDisconnectToastActions,
useWalletDisconnectedToasts,
} from '@vegaprotocol/web3';
import { VegaWalletConnectButton } from '../components/vega-wallet-connect-button';
const WalletDisconnectAdditionalContent = () => {
const { hideToast } = useWalletDisconnectToastActions();
return (
<p className="mt-2">
<VegaWalletConnectButton
intent={Intent.Danger}
onClick={() => {
// hide toast when clicked on `Connect`
hideToast();
}}
/>
</p>
);
};
export const ToastsManager = () => {
useProposalToasts();
@@ -17,7 +36,7 @@ export const ToastsManager = () => {
withdrawalsLink: Links.PORTFOLIO(),
});
useReferralToasts();
useWalletDisconnectedToasts();
useWalletDisconnectedToasts(<WalletDisconnectAdditionalContent />);
const toasts = useToasts((store) => store.toasts);
return <ToastsContainer order="desc" toasts={toasts} />;
Binary file not shown.
+1
View File
@@ -438,6 +438,7 @@ export const TransferForm = ({
shouldValidate: true,
})
}
data-testid="use-max-button"
>
{t('Use max')}
</button>
+11 -3
View File
@@ -8,9 +8,17 @@ export const StackedCell = ({
secondary: ReactNode;
}) => {
return (
<div className="leading-4 text-ellipsis whitespace-nowrap overflow-hidden">
<div data-testid="stack-cell-primary">{primary}</div>
<div data-testid="stack-cell-secondary" className="text-muted">
<div className="leading-4">
<div
className="text-ellipsis whitespace-nowrap overflow-hidden"
data-testid="stack-cell-primary"
>
{primary}
</div>
<div
data-testid="stack-cell-secondary"
className="text-ellipsis whitespace-nowrap overflow-hidden text-muted"
>
{secondary}
</div>
</div>
@@ -649,7 +649,6 @@ const formatTrigger = (
Number(triggerTrailingPercentOffset) || 0
).toFixed(1),
})
}
}`;
const SubmitButton = ({
+4 -1
View File
@@ -17,9 +17,12 @@ export const useGetFaucetError = (error: TxError | null, symbol?: string) => {
'The faucet transaction was rejected by the connected Ethereum wallet'
),
};
if (!error) {
return error;
}
// render a customized failure message from the map above or fallback
// to a non generic error message
return error && 'reason' in error && reasonMap[error.reason]
return 'reason' in error && reasonMap[error.reason]
? reasonMap[error.reason]
: t('Faucet of {{symbol}} failed', { symbol: symbol || '' });
};
@@ -368,6 +368,12 @@ export const compileFeatureFlags = (refresh = false): FeatureFlags => {
process.env['NX_VOLUME_DISCOUNTS']
) as string
),
TEAM_COMPETITION: TRUTHY.includes(
windowOrDefault(
'NX_TEAM_COMPETITION',
process.env['NX_TEAM_COMPETITION']
) as string
),
};
const EXPLORER_FLAGS = {
+1
View File
@@ -28,6 +28,7 @@ export type CosmicElevatorFlags = Pick<
| 'GOVERNANCE_TRANSFERS'
| 'VOLUME_DISCOUNTS'
| 'DISABLE_CLOSE_POSITION'
| 'TEAM_COMPETITION'
>;
export type Configuration = z.infer<typeof tomlConfigSchema>;
export const CUSTOM_NODE_KEY = 'custom' as const;
@@ -84,6 +84,7 @@ const COSMIC_ELEVATOR_FLAGS = {
GOVERNANCE_TRANSFERS: z.optional(z.boolean()),
VOLUME_DISCOUNTS: z.optional(z.boolean()),
DISABLE_CLOSE_POSITION: z.optional(z.boolean()),
TEAM_COMPETITION: z.optional(z.boolean()),
};
const EXPLORER_FLAGS = {
+1 -1
View File
@@ -383,7 +383,7 @@
"MoreMarketsInfo": "To see Explorer data on existing markets visit",
"MoreNetParamsInfo": "To see Explorer data on network params visit",
"MoreProposalsInfo": "To see Explorer data on proposals visit",
"multisigContractIncorrect": "is incorrectly configured. Validator and delegator rewards will be penalised until this is resolved.",
"multisigContractIncorrect": "was incorrectly configured as at the end of the last epoch so rewards were penalised. Validator and delegator rewards will continue to be penalised until this is resolved.",
"multisigContractLink": "Ethereum Multisig Contract",
"multisigPenalty": "Multisig penalty",
"myPendingStake": "My pending stake",
+31 -6
View File
@@ -10,7 +10,7 @@
"24h vol": "24h vol",
"24h volume": "24h volume",
"A percentage of commission earned by the referrer": "A percentage of commission earned by the referrer",
"A successors to this market has been proposed": "A successors to this market has been proposed",
"A successor to this market has been proposed": "A successor to this market has been proposed",
"About the referral program": "About the referral program",
"Active": "Active",
"All": "All",
@@ -26,8 +26,8 @@
"Best offer": "Best offer",
"Browse": "Browse",
"By using the Vega Console, you acknowledge that you have read and understood the <0>Vega Console Disclaimer</0>": "By using the Vega Console, you acknowledge that you have read and understood the <0>Vega Console Disclaimer</0>",
"Chart": "Chart",
"Change (24h)": "Change (24h)",
"Changes have been proposed for this market. <0>View proposals</0>": "Changes have been proposed for this market. <0>View proposals</0>",
"Chart": "Chart",
"Chart by <0>TradingView</0>": "Chart by <0>TradingView</0>",
"checkOutProposalsAndVote": "Check out the terms of the proposals and vote:",
@@ -306,9 +306,9 @@
"TradingView": "TradingView",
"Trading has been terminated as a result of the product definition": "Trading has been terminated as a result of the product definition",
"Trading mode": "Trading mode",
"Trading on Market {{name}} may stop on {{date}}. There is open proposal to close this market.": "Trading on Market {{name}} may stop on {{date}}. There is open proposal to close this market.",
"Trading on Market {{name}} may stop. There are open proposals to close this market": "Trading on Market {{name}} may stop. There are open proposals to close this market",
"Trading on Market {{name}} will stop on {{date}}": "Trading on Market {{name}} will stop on {{date}}",
"Trading on market {{name}} may stop on {{date}}. There is an open proposal to close this market.": "Trading on market {{name}} may stop on {{date}}. There is an open proposal to close this market.",
"Trading on market {{name}} may stop. There are open proposals to close this market": "Trading on market {{name}} may stop. There are open proposals to close this market",
"Trading on market {{name}} will stop on {{date}}": "Trading on market {{name}} will stop on {{date}}",
"Transfer": "Transfer",
"Unknown": "Unknown",
"Unknown settlement date": "Unknown settlement date",
@@ -350,5 +350,30 @@
"Your code has been rejected": "Your code has been rejected",
"Your identity is always anonymous on Vega": "Your identity is always anonymous on Vega",
"Your referral code": "Your referral code",
"Your tier": "Your tier"
"Your tier": "Your tier",
"Number of epochs after distribution to delay vesting of rewards by": "Number of epochs after distribution to delay vesting of rewards by",
"numberEpochs": "{{count}} epochs",
"numberEpochs_other": "{{count}} epochs",
"numberEpochs_one": "{{count}} epoch",
"epochsStreak": "{{count}} epochs streak",
"epochStreak_one": "{{count}} epoch streak",
"Get rewards for providing liquidity. Get rewards for providing liquidity.": "Get rewards for providing liquidity. Get rewards for providing liquidity.",
"Entity scope": "Entity scope",
"Staked VEGA": "Staked VEGA",
"Average position": "Average position",
"Individual": "Individual",
"Rewards funded using the pro-rata strategy should be distributed pro-rata by each entity's reward metric scaled by any active multipliers that party has": " Rewards funded using the pro-rata strategy should be distributed pro-rata by each entity's reward metric scaled by any active multipliers that party has",
"Tier {{tier}}": "Tier {{tier}}",
"Reward {{reward}}x": "Reward {{reward}}x",
"Vesting {{vesting}}x": "Vesting {{vesting}}x",
"Tier {{userTier}}": "Tier {{userTier}}",
"{{reward}}x": "{{reward}}x",
"Reward bonus": "Reward bonus",
"Activity Streak": "Activity Streak",
"userActive": "{{active}} trader: {{count}} epochs so far",
"(Tier {{tier}} as of last epoch)": "(Tier {{tier}} as of last epoch)",
"Team": "Team",
"Ends in": "Ends in",
"Assessed over": "Assessed over",
"No rows": "No rows"
}
@@ -83,11 +83,6 @@ describe('LiquidityTable', () => {
h.querySelector('[ref="eText"]')?.textContent?.trim()
);
const expectedHeaders = [
undefined,
undefined,
undefined,
undefined,
undefined,
'Party',
'Status',
'Commitment ()',

Some files were not shown because too many files have changed in this diff Show More