Compare commits

..
276 changed files with 3731 additions and 6575 deletions
+3 -46
View File
@@ -19,7 +19,7 @@ jobs:
create-docker-image:
name: Create docker image for console-test
runs-on: ubuntu-22.04
timeout-minutes: 90
timeout-minutes: 20
steps:
#----------------------------------------------
# check-out frontend-monorepo
@@ -138,7 +138,7 @@ jobs:
name: run-tests
runs-on: 8-cores
needs: [create-docker-image, console-test-branch]
timeout-minutes: 90
timeout-minutes: 20
steps:
#----------------------------------------------
# load docker image
@@ -205,7 +205,7 @@ jobs:
# run tests
#----------------------------------------------
- name: Run tests
run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v -s --numprocesses 2 --dist loadfile --durations=90
run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v -s --numprocesses 4 --dist loadfile --durations=15
working-directory: apps/trading/e2e
#----------------------------------------------
# upload traces
@@ -227,46 +227,3 @@ jobs:
name: worker-logs
path: ./logs/
retention-days: 15
#----------------------------------------------
# ----- upload market-sim logs -----
#----------------------------------------------
- name: Prepare and Zip market-sim-logs
if: always()
run: |
parent_dir="/tmp/market-sim-logs"
echo "Creating parent directory at $parent_dir"
mkdir -p "$parent_dir"
echo "Waiting for vega-sim-* folders to be created..."
sleep 10 # Waits 10 seconds to ensure all folders are created
echo "Before searching for vega-sim-* folders in /tmp..."
folders=$(find /tmp -mindepth 1 -type d -name 'vega-sim-*' -print) || echo "Find command failed with exit code $?"
echo "After searching for vega-sim-* folders in /tmp..."
if [ -z "$folders" ]; then
echo "No vega-sim-* folders found."
exit 0
fi
echo "Moving vega-sim-* folders to $parent_dir"
echo "$folders" | xargs -I {} mv {} "$parent_dir/"
echo "Checking if $parent_dir is not empty..."
if [ "$(ls -A $parent_dir)" ]; then
echo "Zipping the parent directory..."
zip -r market-sim-logs.zip "$parent_dir" && echo "Zip file created successfully."
else
echo "$parent_dir is empty. No zip file created."
exit 0
fi
shell: /usr/bin/bash -e {0}
- name: Upload market-sim-logs
uses: actions/upload-artifact@v3
if: always()
with:
name: market-sim-logs
path: market-sim-logs.zip
retention-days: 15
@@ -44,7 +44,7 @@ context('Proposal page', { tags: '@smoke' }, function () {
cy.getByTestId('icon-cross').click();
});
it('Proposal page displayed on mobile', function () {
it.skip('Proposal page displayed on mobile', function () {
const proposalTitle = 'Add Lorem Ipsum market';
cy.common_switch_to_mobile_and_click_toggle();
@@ -55,7 +55,7 @@ context('Proposal page', { tags: '@smoke' }, function () {
});
});
it.skip('Able to view new asset proposal', function () {
it('Able to view new asset proposal', function () {
const proposalTitle = 'Test new asset proposal';
const newAssetProposalBody = getNewAssetTxBody();
cy.VegaWalletSubmitProposal(newAssetProposalBody);
@@ -24,7 +24,7 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
const navigate = useNavigate();
const ref = useRef<AgGridReact>(null);
const showColumnsOnDesktop = () => {
ref.current?.api.setColumnsVisible(
ref.current?.columnApi.setColumnsVisible(
['id', 'type', 'status'],
window.innerWidth > BREAKPOINT_MD
);
@@ -60,7 +60,7 @@ const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
}
return (
<span>
<span className="whitespace-nowrap">
{useName && <Icon size={4} name="cube" className="mr-2" />}
<Link
className="underline font-mono"
@@ -29,7 +29,7 @@ export const MarketsTable = ({ data }: MarketsTableProps) => {
const gridRef = useRef<AgGridReact>(null);
useLayoutEffect(() => {
const showColumnsOnDesktop = () => {
gridRef.current?.api.setColumnsVisible(
gridRef.current?.columnApi.setColumnsVisible(
['id', 'state', 'asset'],
window.innerWidth > BREAKPOINT_MD
);
@@ -60,7 +60,7 @@ const DeterministicOrderDetails = ({
const o = data.orderByID;
return (
<div className={wrapperClasses}>
<div className="mb-0">
<div className="mb-12 lg: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 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">
<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">
{t('Status')}
</h2>
<h5 className="text-lg font-medium text-gray-500 mb-0 capitalize">
@@ -99,17 +99,15 @@ const DeterministicOrderDetails = ({
</h5>
</div>
<div className="mb-6 md:mb-0">
<h2 className="text-2xl font-bold text-dark mb-0 md:mb-4">
{t('Size')}
</h2>
<div className="mb-12 md:mb-0">
<h2 className="text-2xl font-bold text-dark 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="mb-6 md:mb-0">
<h2 className="text-2xl font-bold text-dark mb-0 md:mb-4">
<div className="">
<h2 className="text-2xl font-bold text-dark mb-4">
{t('Version')}
</h2>
<h5 className="text-lg font-medium text-gray-500 mb-0">
@@ -117,8 +115,8 @@ const DeterministicOrderDetails = ({
</h5>
</div>
{o.type ? (
<div className="mb-6 md:mb-0">
<h2 className="text-2xl font-bold text-dark mb-0 md:mb-4">
<div className="">
<h2 className="text-2xl font-bold text-dark mb-4">
{t('Type')}
</h2>
<h5 className="text-lg font-medium text-gray-500 mb-0">
@@ -44,11 +44,11 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
const gridRef = useRef<AgGridReact>(null);
useLayoutEffect(() => {
const showColumnsOnDesktop = () => {
gridRef.current?.api.setColumnsVisible(
gridRef.current?.columnApi.setColumnsVisible(
['voting', 'cDate', 'eDate', 'type'],
window.innerWidth > BREAKPOINT_MD
);
gridRef.current?.api.setColumnWidth(
gridRef.current?.columnApi.setColumnWidth(
'actions',
window.innerWidth > BREAKPOINT_MD ? 221 : 80
);
@@ -30,12 +30,12 @@ export const Signature = ({ signature }: SignatureProps) => {
return (
<div className="inline-flex border rounded signature-component relative pr-[20px]">
<div
<span
className="bg-gray-100 px-2.5 py-0.5 text-xs text-gray-500 select-none cursor-default"
title={`${signature.algo}`}
title={`Version ${signature.version}`}
>
<span>v{signature.version}</span>
</div>
{signature.algo}
</span>
<div
className={
isOpen
@@ -54,7 +54,7 @@ const Block = () => {
</Button>
</Link>
</div>
{blockData && 'result' in blockData && (
{blockData && (
<>
<TableWithTbody className="mb-8">
<TableRow modifier="bordered">
+1 -1
View File
@@ -73,7 +73,7 @@ export const Layout = () => {
<ProtocolUpgradeInProgressNotification />
</div>
<div className={fixedWidthClasses}>
<main className="md:p-4">
<main className="p-4">
{!isHome && <BreadcrumbsContainer className="mb-4" />}
<Outlet />
</main>
@@ -55,10 +55,7 @@ context('View functionality with public key', { tags: '@smoke' }, function () {
cy.getByTestId('dialog-content')
.first()
.within(() => {
cy.getByTestId('dialog-title').should(
'have.text',
'Transaction failed'
);
cy.get('h1').should('have.text', 'Transaction failed');
cy.getByTestId('Error').should('have.text', expectedErrorTxt);
});
});
@@ -196,7 +196,6 @@ export function createNewMarketProposalTxBody(): ProposalSubmissionBody {
timeWindow: '3600',
scalingFactor: 10,
},
// FIXME: workaround because of https://github.com/vegaprotocol/vega/issues/10343
triggeringRatio: '0.7',
auctionExtension: '1',
},
@@ -335,7 +334,6 @@ export function createSuccessorMarketProposalTxBody(
timeWindow: '3600',
scalingFactor: 10,
},
// FIXME: workaround because of https://github.com/vegaprotocol/vega/issues/10343
triggeringRatio: '0.7',
auctionExtension: '1',
},
+1 -1
View File
@@ -32,7 +32,7 @@ CYPRESS_FAIRGROUND=false
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
NX_METAMASK_SNAPS=false
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
+1 -1
View File
@@ -23,7 +23,7 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
NX_METAMASK_SNAPS=false
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
+1 -1
View File
@@ -22,7 +22,7 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
NX_METAMASK_SNAPS=false
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
+1 -1
View File
@@ -21,7 +21,7 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
NX_METAMASK_SNAPS=false
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
@@ -281,8 +281,8 @@ describe('VoteBreakdown', () => {
});
it('Progress bar displays status - LP majority', () => {
const yesVotesLP = 0.8;
const noVotesLP = 0.2;
const yesVotesLP = 800;
const noVotesLP = 200;
const expectedProgress = (yesVotesLP / (yesVotesLP + noVotesLP)) * 100; // 80%
renderComponent(
@@ -105,6 +105,8 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
yesLPPercentage,
yesTokens,
noTokens,
yesEquityLikeShareWeight,
noEquityLikeShareWeight,
totalEquityLikeShareWeight,
requiredMajorityPercentage,
requiredMajorityLPPercentage,
@@ -133,7 +135,6 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
.multipliedBy(100),
new BigNumber(100)
);
const willPass = willPassByTokenVote || willPassByLPVote;
const updateMarketVotePassMethod = willPassByTokenVote
? t('byTokenVote')
@@ -201,24 +202,50 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
<div className="flex items-center gap-1">
<span>{t('liquidityProviderVotesFor')}:</span>
<Tooltip
description={
<span>{yesLPPercentage.toFixed(defaultDP)}%</span>
}
description={formatNumber(
yesEquityLikeShareWeight,
defaultDP
)}
>
<button>{yesLPPercentage.toFixed(1)}%</button>
<button>
<CompactVotes number={yesEquityLikeShareWeight} />
</button>
</Tooltip>
<span>
(
<Tooltip
description={
<span>{yesLPPercentage.toFixed(defaultDP)}%</span>
}
>
<button>{yesLPPercentage.toFixed(0)}%</button>
</Tooltip>
)
</span>
</div>
<div className="flex items-center gap-1">
<span>{t('liquidityProviderVotesAgainst')}:</span>
<Tooltip
description={formatNumber(
noEquityLikeShareWeight,
defaultDP
)}
>
<button>
<CompactVotes number={noEquityLikeShareWeight} />
</button>
</Tooltip>
<span>
(
<Tooltip
description={
<span>{noLPPercentage.toFixed(defaultDP)}%</span>
}
>
<button>{noLPPercentage.toFixed(1)}%</button>
<button>{noLPPercentage.toFixed(0)}%</button>
</Tooltip>
)
</span>
</div>
</div>
@@ -255,8 +282,13 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
defaultDP
)}
>
<span>{totalEquityLikeShareWeight.toFixed(1)}%</span>
<button>
<CompactVotes number={totalEquityLikeShareWeight} />
</button>
</Tooltip>
<span>
({totalEquityLikeShareWeight.toFixed(defaultDP)}%)
</span>
</div>
</div>
</section>
@@ -54,8 +54,8 @@ describe('use-vote-information', () => {
it('returns all required vote information', () => {
const yesVotes = 40;
const noVotes = 60;
const yesEquityLikeShareWeight = '0.30';
const noEquityLikeShareWeight = '0.70';
const yesEquityLikeShareWeight = '30';
const noEquityLikeShareWeight = '70';
// Note - giving a fixedTokenValue of 1 means a ratio of 1:1 votes to tokens, making sums easier :)
const fixedTokenValue = 1000000000000000000;
@@ -195,10 +195,10 @@ describe('use-vote-information', () => {
});
it('correctly shows whether an update market proposal will pass by token or LP vote - both failing', () => {
const yesVotes = 0.2;
const noVotes = 0.7;
const yesEquityLikeShareWeight = '0.30';
const noEquityLikeShareWeight = '0.60';
const yesVotes = 20;
const noVotes = 70;
const yesEquityLikeShareWeight = '30';
const noEquityLikeShareWeight = '60';
const fixedTokenValue = 1000000000000000000;
const proposal = generateProposal({
@@ -61,7 +61,7 @@ export const useVoteInformation = ({
const noEquityLikeShareWeight = !proposal?.votes.no
.totalEquityLikeShareWeight
? new BigNumber(0)
: new BigNumber(proposal.votes.no.totalEquityLikeShareWeight).times(100);
: new BigNumber(proposal.votes.no.totalEquityLikeShareWeight);
const yesTokens = new BigNumber(
addDecimal(proposal?.votes.yes.totalTokens ?? 0, decimals)
@@ -70,7 +70,7 @@ export const useVoteInformation = ({
const yesEquityLikeShareWeight = !proposal?.votes.yes
.totalEquityLikeShareWeight
? new BigNumber(0)
: new BigNumber(proposal.votes.yes.totalEquityLikeShareWeight).times(100);
: new BigNumber(proposal.votes.yes.totalEquityLikeShareWeight);
const totalTokensVoted = yesTokens.plus(noTokens);
@@ -81,7 +81,12 @@ export const useVoteInformation = ({
const yesPercentage = totalTokensVoted.isZero()
? new BigNumber(0)
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
const yesLPPercentage = yesEquityLikeShareWeight;
const yesLPPercentage = totalEquityLikeShareWeight.isZero()
? new BigNumber(0)
: yesEquityLikeShareWeight
.multipliedBy(100)
.dividedBy(totalEquityLikeShareWeight);
const noPercentage = totalTokensVoted.isZero()
? new BigNumber(0)
@@ -98,7 +103,9 @@ export const useVoteInformation = ({
);
const participationLPMet = requiredParticipationLP
? totalEquityLikeShareWeight.isGreaterThan(requiredParticipationLP)
? totalEquityLikeShareWeight.isGreaterThan(
totalSupply.multipliedBy(requiredParticipationLP)
)
: false;
const majorityMet = yesPercentage.isGreaterThanOrEqualTo(
@@ -113,7 +120,9 @@ export const useVoteInformation = ({
.multipliedBy(100)
.dividedBy(totalSupply);
const totalLPTokensPercentage = totalEquityLikeShareWeight;
const totalLPTokensPercentage = totalEquityLikeShareWeight
.multipliedBy(100)
.dividedBy(totalSupply);
const willPassByTokenVote =
participationMet &&
@@ -205,6 +205,7 @@ query Proposal(
}
}
liquidityMonitoringParameters {
triggeringRatio
targetStakeParameters {
timeWindow
scalingFactor
@@ -365,6 +366,7 @@ query Proposal(
}
}
liquidityMonitoringParameters {
triggeringRatio
targetStakeParameters {
timeWindow
scalingFactor
File diff suppressed because one or more lines are too long
@@ -95,7 +95,7 @@ export const ProtocolUpgradeProposalContainer = () => {
time={
pending && time ? (
convertToCountdownString(time, '0:00:00:00')
) : blockInfo && 'result' in blockInfo && blockInfo?.result ? (
) : blockInfo?.result ? (
<span title={blockInfo.result.block.header.time}>
{formatDateWithLocalTimezone(
new Date(blockInfo.result.block.header.time)
@@ -116,8 +116,7 @@ export const generateYesVotes = (
fixedTokenValue?: number,
totalEquityLikeShareWeight?: string
): Votes => {
const votes = [];
for (let i = 0; i < numberOfVotes; i++) {
const votes = Array.from(Array(numberOfVotes)).map(() => {
const vote: Vote = {
__typename: 'Vote',
value: Schema.VoteValue.VALUE_YES,
@@ -153,9 +152,8 @@ export const generateYesVotes = (
datetime: faker.date.past().toISOString(),
};
votes.push(vote);
}
return vote;
});
return {
__typename: 'ProposalVoteSide',
totalNumber: votes.length.toString(),
@@ -174,8 +172,7 @@ export const generateNoVotes = (
fixedTokenValue?: number,
totalEquityLikeShareWeight?: string
): Votes => {
const votes = [];
for (let i = 0; i < numberOfVotes; i++) {
const votes = Array.from(Array(numberOfVotes)).map(() => {
const vote: Vote = {
__typename: 'Vote',
value: Schema.VoteValue.VALUE_NO,
@@ -210,9 +207,8 @@ export const generateNoVotes = (
},
datetime: faker.date.past().toISOString(),
};
votes.push(vote);
}
return vote;
});
return {
__typename: 'ProposalVoteSide',
totalNumber: votes.length.toString(),
-1
View File
@@ -25,4 +25,3 @@ NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_REFERRALS=true
NX_TEAM_COMPETITION=true
-3
View File
@@ -28,6 +28,3 @@ NX_REFERRALS=true
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
-2
View File
@@ -54,5 +54,3 @@ To run the UI automation tests with a mocked API, run:
```bash
yarn nx run trading-e2e:e2e
```
To run tests with market sim please read [the readme](e2e/README.md).
@@ -188,11 +188,12 @@ const useNow = () => {
return now;
};
const useEvery = (marketId: string, skip: boolean) => {
const useEvery = (marketId: string) => {
const { data: marketTradingMode } = useMarketTradingMode(marketId);
const { data: marketInfo } = useDataProvider({
dataProvider: marketInfoProvider,
variables: { marketId },
skip,
skip: !marketTradingMode || isMarketInAuction(marketTradingMode),
});
let every: number | undefined = undefined;
const sourceType =
@@ -210,10 +211,8 @@ const useEvery = (marketId: string, skip: boolean) => {
return every;
};
const useStartTime = (marketId: string, skip: boolean) => {
const useStartTime = (marketId: string) => {
const { data: fundingPeriods } = useFundingPeriodsQuery({
pollInterval: 5000,
skip,
variables: {
marketId: marketId,
pagination: { first: 1 },
@@ -247,10 +246,8 @@ const useFormatCountdown = (
export const FundingCountdown = ({ marketId }: { marketId: string }) => {
const now = useNow();
const { data: marketTradingMode } = useMarketTradingMode(marketId);
const skip = !marketTradingMode || isMarketInAuction(marketTradingMode);
const startTime = useStartTime(marketId, skip);
const every = useEvery(marketId, skip);
const startTime = useStartTime(marketId);
const every = useEvery(marketId);
return (
<div data-testid="funding-countdown">
+13 -8
View File
@@ -1,4 +1,4 @@
import { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { addDecimalsFormatNumber, titlefy } from '@vegaprotocol/utils';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import { useThrottledDataProvider } from '@vegaprotocol/data-provider';
@@ -72,6 +72,17 @@ 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>
@@ -107,8 +118,6 @@ export const MarketPage = () => {
);
}
const pinnedAsset = data && getAsset(data);
return (
<>
<TitleUpdater
@@ -116,11 +125,7 @@ export const MarketPage = () => {
marketName={data?.tradableInstrument.instrument.name}
decimalPlaces={data?.decimalPlaces}
/>
{largeScreen ? (
<TradeGrid market={data} pinnedAsset={pinnedAsset} />
) : (
<TradePanels market={data} pinnedAsset={pinnedAsset} />
)}
{tradeView}
</>
);
};
+44 -51
View File
@@ -1,10 +1,11 @@
import { memo, type ReactNode } from 'react';
import { memo } from 'react';
import type { ReactNode } from 'react';
import { LayoutPriority } from 'allotment';
import classNames from 'classnames';
import AutoSizer from 'react-virtualized-auto-sizer';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { type PinnedAsset } from '@vegaprotocol/accounts';
import { type Market } from '@vegaprotocol/markets';
import type { PinnedAsset } from '@vegaprotocol/accounts';
import { OracleBanner, useMarket } from '@vegaprotocol/markets';
import type { Market } from '@vegaprotocol/markets';
import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit';
import {
ResizableGrid,
@@ -12,19 +13,31 @@ 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;
pinnedAsset?: PinnedAsset | undefined;
market: Market | null;
pinnedAsset?: PinnedAsset;
}
const MainGrid = memo(
({ market, pinnedAsset }: { market: Market; pinnedAsset?: PinnedAsset }) => {
({
marketId,
pinnedAsset,
}: {
marketId: string;
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',
@@ -52,17 +65,17 @@ const MainGrid = memo(
menu={<TradingViews.chart.menu />}
>
<ErrorBoundary feature="chart">
<TradingViews.chart.component marketId={market.id} />
<TradingViews.chart.component marketId={marketId} />
</ErrorBoundary>
</Tab>
<Tab id="depth" name={t('Depth')}>
<ErrorBoundary feature="depth">
<TradingViews.depth.component marketId={market.id} />
<TradingViews.depth.component marketId={marketId} />
</ErrorBoundary>
</Tab>
<Tab id="liquidity" name={t('Liquidity')}>
<ErrorBoundary feature="liquidity">
<TradingViews.liquidity.component marketId={market.id} />
<TradingViews.liquidity.component marketId={marketId} />
</ErrorBoundary>
</Tab>
{market &&
@@ -70,21 +83,17 @@ const MainGrid = memo(
'Perpetual' ? (
<Tab id="funding-history" name={t('Funding history')}>
<ErrorBoundary feature="funding-history">
<TradingViews.funding.component marketId={market.id} />
<TradingViews.funding.component marketId={marketId} />
</ErrorBoundary>
</Tab>
) : null}
{market &&
market.tradableInstrument.instrument.product.__typename ===
'Perpetual' ? (
<Tab
id="funding-payments"
name={t('Funding payments')}
settings={<TradingViews.fundingPayments.settings />}
>
<Tab id="funding-payments" name={t('Funding payments')}>
<ErrorBoundary feature="funding-payments">
<TradingViews.fundingPayments.component
marketId={market.id}
marketId={marketId}
/>
</ErrorBoundary>
</Tab>
@@ -100,16 +109,12 @@ const MainGrid = memo(
<Tabs storageKey="console-trade-grid-main-right">
<Tab id="orderbook" name={t('Orderbook')}>
<ErrorBoundary feature="orderbook">
<TradingViews.orderbook.component marketId={market.id} />
<TradingViews.orderbook.component marketId={marketId} />
</ErrorBoundary>
</Tab>
<Tab
id="trades"
name={t('Trades')}
settings={<TradingViews.trades.settings />}
>
<Tab id="trades" name={t('Trades')}>
<ErrorBoundary feature="trades">
<TradingViews.trades.component marketId={market.id} />
<TradingViews.trades.component marketId={marketId} />
</ErrorBoundary>
</Tab>
</Tabs>
@@ -128,7 +133,6 @@ const MainGrid = memo(
id="positions"
name={t('Positions')}
menu={<TradingViews.positions.menu />}
settings={<TradingViews.positions.settings />}
>
<ErrorBoundary feature="positions">
<TradingViews.positions.component />
@@ -138,26 +142,17 @@ const MainGrid = memo(
id="open-orders"
name={t('Open')}
menu={<TradingViews.activeOrders.menu />}
settings={<TradingViews.activeOrders.settings />}
>
<ErrorBoundary feature="activeOrders">
<TradingViews.activeOrders.component />
</ErrorBoundary>
</Tab>
<Tab
id="closed-orders"
name={t('Closed')}
settings={<TradingViews.closedOrders.settings />}
>
<Tab id="closed-orders" name={t('Closed')}>
<ErrorBoundary feature="closedOrders">
<TradingViews.closedOrders.component />
</ErrorBoundary>
</Tab>
<Tab
id="rejected-orders"
name={t('Rejected')}
settings={<TradingViews.rejectedOrders.settings />}
>
<Tab id="rejected-orders" name={t('Rejected')}>
<ErrorBoundary feature="rejectedOrders">
<TradingViews.rejectedOrders.component />
</ErrorBoundary>
@@ -166,35 +161,25 @@ const MainGrid = memo(
id="orders"
name={t('All')}
menu={<TradingViews.orders.menu />}
settings={<TradingViews.orders.settings />}
>
<ErrorBoundary feature="orders">
<TradingViews.orders.component />
</ErrorBoundary>
</Tab>
{featureFlags.STOP_ORDERS ? (
<Tab
id="stop-orders"
name={t('Stop orders')}
settings={<TradingViews.stopOrders.settings />}
>
<Tab id="stop-orders" name={t('Stop orders')}>
<ErrorBoundary feature="stop-orders">
<TradingViews.stopOrders.component />
</ErrorBoundary>
</Tab>
) : null}
<Tab
id="fills"
name={t('Fills')}
settings={<TradingViews.fills.settings />}
>
<Tab id="fills" name={t('Fills')}>
<TradingViews.fills.component />
</Tab>
<Tab
id="accounts"
name={t('Collateral')}
menu={<TradingViews.collateral.menu />}
settings={<TradingViews.collateral.settings />}
>
<ErrorBoundary feature="collateral">
<TradingViews.collateral.component
@@ -212,6 +197,7 @@ 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]'
@@ -220,10 +206,17 @@ export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
return (
<div className={wrapperClasses}>
<div>
<MarketBanner market={market} />
{featureFlags.SUCCESSOR_MARKETS && (
<>
<MarketSuccessorBanner market={market} />
<MarketSuccessorProposalBanner marketId={market?.id} />
</>
)}
<MarketTerminationBanner market={market} />
<OracleBanner marketId={market?.id || ''} />
</div>
<div className="min-h-0 p-0.5">
<MainGrid market={market} pinnedAsset={pinnedAsset} />
<MainGrid marketId={market?.id || ''} pinnedAsset={pinnedAsset} />
</div>
</div>
);
@@ -1,30 +1,29 @@
import { type PinnedAsset } from '@vegaprotocol/accounts';
import { type Market } from '@vegaprotocol/markets';
// TODO: handle oracle banner
// import { OracleBanner } from '@vegaprotocol/markets';
import { OracleBanner } from '@vegaprotocol/markets';
import { useState } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import classNames from 'classnames';
import {
Popover,
Splash,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { useT } from '../../lib/use-t';
import { MarketBanner } from '../../components/market-banner';
import {
MarketSuccessorBanner,
MarketSuccessorProposalBanner,
MarketTerminationBanner,
} 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;
market: Market | null;
pinnedAsset?: PinnedAsset;
}
export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
const featureFlags = useFeatureFlags((state) => state.flags);
const [view, setView] = useState<TradingView>('chart');
const viewCfg = TradingViews[view];
const renderView = () => {
const Component = TradingViews[view].component;
@@ -45,36 +44,35 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
};
const renderMenu = () => {
if ('menu' in viewCfg || 'settings' in viewCfg) {
const viewCfg = TradingViews[view];
if ('menu' in viewCfg) {
const Menu = viewCfg.menu;
return (
<div className="flex items-center justify-end gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default">
{'menu' in viewCfg ? <viewCfg.menu /> : null}
{'settings' in viewCfg ? (
<Popover
align="end"
trigger={
<span className="ml-1 flex items-center justify-center h-6 w-6">
<VegaIcon name={VegaIconNames.COG} size={16} />
</span>
}
>
<div className="p-4 flex justify-end">
<viewCfg.settings />
</div>
</Popover>
) : null}
<Menu />
</div>
);
}
return null;
};
return (
<div className="h-full grid grid-rows-[min-content_min-content_1fr_min-content]">
<div>
<MarketBanner market={market} />
{featureFlags.SUCCESSOR_MARKETS && (
<>
<MarketSuccessorBanner market={market} />
<MarketSuccessorProposalBanner marketId={market?.id} />
</>
)}
<MarketTerminationBanner market={market} />
<OracleBanner marketId={market?.id || ''} />
</div>
<div>{renderMenu()}</div>
<div className="h-full relative">
<div className="h-full">
<AutoSizer>
{({ width, height }) => (
<div style={{ width, height }} className="overflow-auto">
@@ -112,9 +110,7 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
key={key}
view={key}
isActive={isActive}
onClick={() => {
setView(key);
}}
onClick={() => setView(key)}
/>
);
})}
@@ -1,36 +1,15 @@
import { DepthChartContainer } from '@vegaprotocol/market-depth';
import { Filter, OpenOrdersMenu } from '@vegaprotocol/orders';
import {
TradesContainer,
TradesSettings,
} from '../../components/trades-container';
import { TradesContainer } from '../../components/trades-container';
import { OrderbookContainer } from '../../components/orderbook-container';
import {
FillsContainer,
FillsSettings,
} from '../../components/fills-container';
import {
PositionsContainer,
PositionsSettings,
} from '../../components/positions-container';
import {
AccountsContainer,
AccountsSettings,
} from '../../components/accounts-container';
import { FillsContainer } from '../../components/fills-container';
import { PositionsContainer } from '../../components/positions-container';
import { AccountsContainer } from '../../components/accounts-container';
import { LiquidityContainer } from '../../components/liquidity-container';
import { FundingContainer } from '../../components/funding-container';
import {
FundingPaymentsContainer,
FundingPaymentsSettings,
} from '../../components/funding-payments-container';
import {
OrdersContainer,
OrdersSettings,
} from '../../components/orders-container';
import {
StopOrdersContainer,
StopOrdersSettings,
} from '../../components/stop-orders-container';
import { FundingPaymentsContainer } from '../../components/funding-payments-container';
import { OrdersContainer } from '../../components/orders-container';
import { StopOrdersContainer } from '../../components/stop-orders-container';
import { AccountsMenu } from '../../components/accounts-menu';
import { PositionsMenu } from '../../components/positions-menu';
import { ChartContainer, ChartMenu } from '../../components/chart-container';
@@ -53,46 +32,37 @@ export const TradingViews = {
},
fundingPayments: {
component: FundingPaymentsContainer,
settings: FundingPaymentsSettings,
},
orderbook: {
component: OrderbookContainer,
},
trades: {
component: TradesContainer,
settings: TradesSettings,
},
positions: {
component: PositionsContainer,
menu: PositionsMenu,
settings: PositionsSettings,
},
activeOrders: {
component: () => <OrdersContainer filter={Filter.Open} />,
menu: OpenOrdersMenu,
settings: () => <OrdersSettings filter={Filter.Open} />,
},
closedOrders: {
component: () => <OrdersContainer filter={Filter.Closed} />,
settings: () => <OrdersSettings filter={Filter.Closed} />,
},
rejectedOrders: {
component: () => <OrdersContainer filter={Filter.Rejected} />,
settings: () => <OrdersSettings filter={Filter.Rejected} />,
},
orders: {
component: OrdersContainer,
menu: OpenOrdersMenu,
settings: OrdersSettings,
},
stopOrders: {
component: StopOrdersContainer,
settings: StopOrdersSettings,
},
collateral: {
component: AccountsContainer,
menu: AccountsMenu,
settings: AccountsSettings,
},
fills: { component: FillsContainer, settings: FillsSettings },
fills: { component: FillsContainer },
} as const;
@@ -314,7 +314,7 @@ describe('Closed', () => {
});
it('display market actions', async () => {
// Use market with a successor Id as the actions dropdown will optionally
// Use market with a succcessor Id as the actions dropdown will optionally
// show a link to the successor market
const marketsWithSuccessorAndParent = [
{
@@ -137,8 +137,6 @@ const ClosedMarketsDataGrid = ({
headerName: t('Market'),
field: 'code',
cellRenderer: 'MarketCodeCell',
width: 150,
resizable: true,
},
{
headerName: t('Status'),
@@ -282,7 +280,6 @@ const ClosedMarketsDataGrid = ({
return (
<AgGrid
rowData={rowData}
defaultColDef={COL_DEFS.default}
columnDefs={colDefs}
getRowId={({ data }) => data.id}
overlayNoRowsTemplate={error ? error.message : t('No markets')}
@@ -17,7 +17,6 @@ const defaultColDef = {
filter: true,
resizable: true,
filterParams: { buttons: ['reset'] },
minWidth: 120,
};
const components = {
@@ -43,7 +42,7 @@ export const createDataGridSlice: StateCreator<DataGridSlice> = (set) => ({
},
});
export const useMarketsStore = create<DataGridSlice>()(
const useMarketsStore = create<DataGridSlice>()(
persist(createDataGridSlice, {
name: 'vega_market_list_store',
})
@@ -16,7 +16,6 @@ import {
} from '@vegaprotocol/environment';
import { useT } from '../../lib/use-t';
import { ErrorBoundary } from '../../components/error-boundary';
import { MarketsSettings } from './markets-settings';
export const MarketsPage = () => {
const t = useT();
@@ -35,11 +34,7 @@ export const MarketsPage = () => {
<div className="h-full pt-0.5 pb-3 px-1.5">
<div className="h-full my-1 border rounded-sm border-default">
<Tabs storageKey="console-markets">
<Tab
id="open-markets"
name={t('Open markets')}
settings={<MarketsSettings />}
>
<Tab id="open-markets" name={t('Open markets')}>
<ErrorBoundary feature="markets-open">
<OpenMarkets />
</ErrorBoundary>
@@ -47,13 +42,11 @@ export const MarketsPage = () => {
<Tab
id="proposed-markets"
name={t('Proposed markets')}
settings={<MarketsSettings />}
menu={
<TradingAnchorButton
size="extra-small"
data-testid="propose-new-market"
href={externalLink}
target="_blank"
>
{t('Propose a new market')}
</TradingAnchorButton>
@@ -63,11 +56,7 @@ export const MarketsPage = () => {
<Proposed />
</ErrorBoundary>
</Tab>
<Tab
id="closed-markets"
name={t('Closed markets')}
settings={<MarketsSettings />}
>
<Tab id="closed-markets" name={t('Closed markets')}>
<ErrorBoundary feature="markets-closed">
<Closed />
</ErrorBoundary>
@@ -1,8 +0,0 @@
import { GridSettings } from '../../components/grid-settings/grid-settings';
import { useMarketsStore } from './market-list-table';
export const MarketsSettings = () => (
<GridSettings
updateGridStore={useMarketsStore((store) => store.updateGridStore)}
/>
);
@@ -29,7 +29,6 @@ export const useColumnDefs = () => {
{
headerName: t('Market'),
field: 'tradableInstrument.instrument.code',
pinned: true,
cellRenderer: ({
value,
data,
@@ -5,29 +5,14 @@ import { titlefy } from '@vegaprotocol/utils';
import { useIncompleteWithdrawals } from '@vegaprotocol/withdraws';
import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit';
import { usePageTitleStore } from '../../stores';
import {
AccountsContainer,
AccountsSettings,
} from '../../components/accounts-container';
import { AccountsContainer } from '../../components/accounts-container';
import { DepositsContainer } from '../../components/deposits-container';
import {
FillsContainer,
FillsSettings,
} from '../../components/fills-container';
import {
FundingPaymentsContainer,
FundingPaymentsSettings,
} from '../../components/funding-payments-container';
import {
PositionsContainer,
PositionsSettings,
} from '../../components/positions-container';
import { FillsContainer } from '../../components/fills-container';
import { FundingPaymentsContainer } from '../../components/funding-payments-container';
import { PositionsContainer } from '../../components/positions-container';
import { PositionsMenu } from '../../components/positions-menu';
import { WithdrawalsContainer } from '../../components/withdrawals-container';
import {
OrdersContainer,
OrdersSettings,
} from '../../components/orders-container';
import { OrdersContainer } from '../../components/orders-container';
import { LedgerContainer } from '../../components/ledger-container';
import {
ResizableGrid,
@@ -91,27 +76,22 @@ export const Portfolio = () => {
id="positions"
name={t('Positions')}
menu={<PositionsMenu />}
settings={<PositionsSettings />}
>
<ErrorBoundary feature="portfolio-positions">
<PositionsContainer allKeys />
</ErrorBoundary>
</Tab>
<Tab id="orders" name={t('Orders')} settings={<OrdersSettings />}>
<Tab id="orders" name={t('Orders')}>
<ErrorBoundary feature="portfolio-orders">
<OrdersContainer />
</ErrorBoundary>
</Tab>
<Tab id="fills" name={t('Fills')} settings={<FillsSettings />}>
<Tab id="fills" name={t('Fills')}>
<ErrorBoundary feature="portfolio-fills">
<FillsContainer />
</ErrorBoundary>
</Tab>
<Tab
id="funding-payments"
name={t('Funding payments')}
settings={<FundingPaymentsSettings />}
>
<Tab id="funding-payments" name={t('Funding payments')}>
<ErrorBoundary feature="portfolio-funding-payments">
<FundingPaymentsContainer />
</ErrorBoundary>
@@ -134,7 +114,6 @@ export const Portfolio = () => {
<Tab
id="collateral"
name={t('Collateral')}
settings={<AccountsSettings />}
menu={<AccountsMenu />}
>
<ErrorBoundary feature="portfolio-accounts">
@@ -296,7 +296,7 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
<>
<div
data-testid="referral-apply-code-form"
className="bg-vega-clight-800 dark:bg-vega-cdark-800 mx-auto md:w-2/3 max-w-md rounded-lg p-8"
className="bg-vega-clight-800 dark:bg-vega-cdark-800 mx-auto 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-[37%_0px] bg-[length:1440px] bg-no-repeat bg-local';
'bg-[url(/sky-light.png)] dark:bg-[url(/sky-dark.png)] bg-[40%_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="md:w-2/3 max-w-md mx-auto bg-vega-clight-800 dark:bg-vega-cdark-800 p-8 rounded-lg"
className="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 '../../components/table';
import { Table } from './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-10 lg:mb-20')}>
<div className={classNames('relative mb-20')}>
<div className="">
<div
aria-hidden
className="absolute top-20 right-[220px] md:right-[240px] max-sm:hidden"
className="absolute top-20 right-[120px] md:right-[240px] max-sm:hidden"
>
<AnimatedDudeWithWire />
</div>
<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">
<div className="pt-20 sm:w-[50%]">
<h1 className="text-6xl 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-1">
<p className="text-lg mb-10">
{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-4 lg:px-32 pb-32',
'mx-auto px-16 md: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 '../../components/table';
import { Table } from './table';
import {
addDecimalsFormatNumber,
getDateFormat,
@@ -72,7 +72,7 @@ export const Referrals = () => {
{showNav && <Nav />}
<div
className={classNames({
'py-8 lg:py-16': showNav,
'py-16': showNav,
'h-[300px] relative': loading || error,
})}
>
@@ -1,17 +1,13 @@
import { Tooltip, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import { forwardRef, type ReactNode, type HTMLAttributes } from 'react';
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';
import { BORDER_COLOR, GRADIENT } from './constants';
type TableColumnDefinition = {
displayName?: ReactNode;
name: string;
tooltip?: string;
className?: string;
testId?: string;
};
type TableProps = {
@@ -46,7 +42,7 @@ export const Table = forwardRef<
key={name}
col-id={name}
className={classNames(
'px-5 py-3 text-xs text-vega-clight-100 dark:text-vega-cdark-100 font-normal',
'px-5 py-3 text-sm text-vega-clight-100 dark:text-vega-cdark-100 font-normal',
INNER_BORDER_STYLE
)}
>
@@ -70,7 +66,7 @@ export const Table = forwardRef<
ref={ref}
className={classNames(
'w-full',
'border-separate border rounded-md border-spacing-0 overflow-hidden',
'border-separate border rounded-md border-spacing-0',
BORDER_COLOR,
GRADIENT,
className
@@ -86,10 +82,10 @@ export const Table = forwardRef<
'max-md:flex flex-col w-full': !noCollapse,
})}
>
{columns.map(({ name, displayName, className, testId }, j) => (
{columns.map(({ name, displayName, className }, j) => (
<td
className={classNames(
'px-5 py-3',
'px-5 py-3 text-base',
{
'max-md:flex max-md:flex-col max-md:justify-between':
!noCollapse,
@@ -114,7 +110,7 @@ export const Table = forwardRef<
{displayName}
</span>
)}
<span data-testid={`${testId || name}-${i}`}>{d[name]}</span>
<span>{d[name]}</span>
</td>
))}
</tr>
@@ -2,15 +2,7 @@ import type { HTMLAttributes } from 'react';
import classNames from 'classnames';
type TagProps = {
color?:
| 'yellow'
| 'green'
| 'blue'
| 'purple'
| 'pink'
| 'orange'
| 'red'
| 'none';
color?: 'yellow' | 'green' | 'blue' | 'purple' | 'pink' | 'orange' | 'none';
};
export const Tag = ({
color = 'none',
+41 -24
View File
@@ -3,12 +3,11 @@ import {
getDateTimeFormat,
} from '@vegaprotocol/utils';
import { useReferralProgram } from './hooks/use-referral-program';
import { Table } from '../../components/table';
import { Table } from './table';
import classNames from 'classnames';
import { BORDER_COLOR, GRADIENT } from './constants';
import { Tag } from '../../components/helpers/tag';
import { getTierColor, getTierGradient } from '../../components/helpers/tiers';
import type { ReactNode } from 'react';
import { Tag } from './tag';
import type { ComponentProps, ReactNode } from 'react';
import { ExternalLink, truncateMiddle } from '@vegaprotocol/ui-toolkit';
import {
DApp,
@@ -20,6 +19,25 @@ 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(
@@ -35,12 +53,10 @@ const StakingTier = ({
tier,
referralRewardMultiplier,
minimumStakedTokens,
max,
}: {
tier: number;
referralRewardMultiplier: string;
minimumStakedTokens: string;
max?: number;
}) => {
const t = useT();
const minimum = addDecimalsFormatNumber(minimumStakedTokens, 18);
@@ -81,7 +97,7 @@ const StakingTier = ({
)}
>
<div>
<Tag color={getTierColor(tier, max)}>
<Tag color={getTierColor(tier)}>
{t('Multiplier')} {referralRewardMultiplier}x
</Tag>
<p className="mt-1 text-sm text-vega-clight-100 dark:text-vega-cdark-100">
@@ -143,9 +159,7 @@ export const TiersContainer = () => {
return (
<>
<h2 className="text-3xl mt-10 font-alpha calt">
{t('Current program details')}
</h2>
<h2 className="text-3xl mt-10">{t('Current program details')}</h2>
{details?.id && (
<p>
<Trans
@@ -184,18 +198,7 @@ export const TiersContainer = () => {
</div>
{/* Container */}
<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'
)}
>
<div className="bg-vega-clight-800 dark:bg-vega-cdark-800 text-black dark:text-white rounded-lg p-6 mt-1 mb-20">
{/* Benefit tiers */}
<div className="flex flex-col mb-5">
<h3 className="text-2xl calt">{t('Benefit tiers')}</h3>
@@ -258,7 +261,6 @@ const StakingTiers = ({
<StakingTier
key={i}
tier={tier}
max={data.length}
referralRewardMultiplier={referralRewardMultiplier}
minimumStakedTokens={minimumStakedTokens}
/>
@@ -320,7 +322,22 @@ const TiersTable = ({
className="bg-white dark:bg-vega-cdark-900"
data={data.map((d) => ({
...d,
className: classNames(getTierGradient(d.tier, data.length)),
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),
}),
}))}
/>
);
+5 -10
View File
@@ -19,11 +19,9 @@ export const Tile = ({
return (
<div
className={classNames(
'text-black dark:text-white',
'overflow-hidden relative',
'p-3 md:p-6',
'rounded-lg',
'bg-vega-clight-800 dark:bg-vega-cdark-800',
'rounded-lg overflow-hidden relative',
'bg-vega-clight-800 dark:bg-vega-cdark-800 text-black dark:text-white',
'p-6',
className
)}
>
@@ -57,10 +55,7 @@ export const StatTile = ({
>
{title}
</h3>
<div
data-testid={`${testId}-value`}
className="text-2xl lg:text-5xl text-left"
>
<div data-testid={`${testId}-value`} className="text-5xl text-left">
{children}
</div>
{description && (
@@ -124,7 +119,7 @@ export const CodeTile = ({
>
<div
className={classNames(
'relative bg-rainbow bg-clip-text text-transparent text-2xl lg:text-5xl overflow-hidden',
'relative bg-rainbow bg-clip-text text-transparent text-5xl overflow-hidden',
FADE_OUT_STYLE
)}
>
@@ -4,7 +4,6 @@ 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();
@@ -17,10 +16,10 @@ export const Rewards = () => {
}, [updateTitle, title]);
return (
<ErrorBoundary feature="rewards">
<TinyScroll className="p-4 max-h-full overflow-auto">
<div className="container mx-auto p-4">
<h1 className="px-4 pb-4 text-2xl">{title}</h1>
<RewardsContainer />
</TinyScroll>
</div>
</ErrorBoundary>
);
};
-1
View File
@@ -1 +0,0 @@
export { Teams } from './teams';
@@ -1,7 +0,0 @@
export const Teams = () => {
return (
<div>
<h1>Teams</h1>
</div>
);
};
@@ -73,7 +73,7 @@ export const AccountsContainer = ({
);
};
export const useAccountStore = create<DataGridSlice>()(
const useAccountStore = create<DataGridSlice>()(
persist(createDataGridSlice, {
name: 'vega_accounts_store',
})
@@ -1,8 +0,0 @@
import { GridSettings } from '../grid-settings/grid-settings';
import { useAccountStore } from './accounts-container';
export const AccountsSettings = () => (
<GridSettings
updateGridStore={useAccountStore((store) => store.updateGridStore)}
/>
);
@@ -1,2 +1 @@
export * from './accounts-container';
export * from './accounts-settings';
@@ -12,7 +12,6 @@ import { VegaWalletProvider } from '@vegaprotocol/wallet';
import type { ReactNode } from 'react';
import { Web3Provider } from './web3-provider';
import { useT } from '../../lib/use-t';
import { DataLoader } from './data-loader';
export const Bootstrapper = ({ children }: { children: ReactNode }) => {
const t = useT();
@@ -53,38 +52,28 @@ export const Bootstrapper = ({ children }: { children: ReactNode }) => {
/>
}
>
<DataLoader
<Web3Provider
skeleton={<AppLoader />}
failure={
<AppFailure
title={t('Could not load market data or asset data')}
error={error}
/>
<AppFailure title={t('Could not configure web3 provider')} />
}
>
<Web3Provider
skeleton={<AppLoader />}
failure={
<AppFailure title={t('Could not configure web3 provider')} />
}
<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,
},
}}
>
<VegaWalletProvider
config={{
network: VEGA_ENV,
vegaUrl: VEGA_URL,
vegaWalletServiceUrl: VEGA_WALLET_URL,
links: {
explorer: VEGA_EXPLORER_URL,
concepts: DocsLinks.VEGA_WALLET_CONCEPTS_URL,
chromeExtensionUrl: CHROME_EXTENSION_URL,
mozillaExtensionUrl: MOZILLA_EXTENSION_URL,
},
}}
>
{children}
</VegaWalletProvider>
</Web3Provider>
</DataLoader>
{children}
</VegaWalletProvider>
</Web3Provider>
</NodeGuard>
</NetworkLoader>
);
@@ -1,34 +0,0 @@
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}</>;
};
+3 -17
View File
@@ -9,7 +9,6 @@ export const Card = ({
loading = false,
highlight = false,
testId,
noBackgroundOnMobile = false,
}: {
children: ReactNode;
title: string;
@@ -17,33 +16,20 @@ export const Card = ({
loading?: boolean;
highlight?: boolean;
testId?: string;
noBackgroundOnMobile?: boolean;
}) => {
return (
<div
data-testid={testId}
className={classNames(
'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-vega-clight-800 dark:bg-vega-cdark-800 col-span-full p-0.5 lg:col-auto',
'rounded-lg',
{
'bg-rainbow': highlight,
},
className
)}
>
<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,
})}
>
<div className="bg-vega-clight-800 dark:bg-vega-cdark-800 h-full w-full rounded p-4">
<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 as SimpleTable } from '../../components/table';
import { Table, Td, Th, THead, Tr } from './table';
import BigNumber from 'bignumber.js';
import { Links } from '../../lib/links';
import { Link } from 'react-router-dom';
@@ -24,8 +24,6 @@ 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();
@@ -38,11 +36,7 @@ export const FeesContainer = () => {
const { data: markets, loading: marketsLoading } = useMarketList();
const { data: programData, loading: programLoading } =
useDiscountProgramsQuery({
errorPolicy: 'ignore',
fetchPolicy: 'cache-and-network',
pollInterval: 15000,
});
useDiscountProgramsQuery({ errorPolicy: 'ignore' });
const volumeDiscountWindowLength =
programData?.currentVolumeDiscountProgram?.windowLength || 1;
@@ -53,8 +47,6 @@ export const FeesContainer = () => {
partyId: pubKey || '',
},
skip: !pubKey,
fetchPolicy: 'cache-and-network',
pollInterval: 15000,
});
const previousEpoch = (Number(feesData?.epoch.id) || 0) - 1;
@@ -174,7 +166,6 @@ export const FeesContainer = () => {
className="lg:col-span-full xl:col-span-2"
loading={loading}
data-testid="volume-discount-card"
noBackgroundOnMobile={true}
>
<VolumeTiers
tiers={volumeTiers}
@@ -188,14 +179,12 @@ 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
@@ -203,7 +192,6 @@ export const FeesContainer = () => {
className="lg:col-span-full"
loading={marketsLoading}
data-testid="fees-by-market-card"
noBackgroundOnMobile={true}
>
<MarketFees
markets={markets}
@@ -345,7 +333,11 @@ export const CurrentVolume = ({
return (
<div className="flex flex-col gap-3 pt-4" data-testid="current-volume">
<CardStat
value={formatNumberRounded(currentVolume)}
value={
currentVolume.isZero()
? `<${formatNumberRounded(requiredForNextTier)}`
: formatNumberRounded(currentVolume)
}
text={t('pastEpochs', 'Past {{count}} epochs', {
count: windowLength,
})}
@@ -489,65 +481,44 @@ const VolumeTiers = ({
return (
<div>
<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'
),
};
})}
/>
<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>
</div>
);
};
@@ -557,7 +528,6 @@ const ReferralTiers = ({
tierIndex,
epochsInSet,
referralVolumeInWindow,
referralDiscountWindowLength,
}: {
tiers: Array<{
referralDiscountFactor: string;
@@ -567,7 +537,6 @@ const ReferralTiers = ({
tierIndex: number;
epochsInSet: number;
referralVolumeInWindow: number;
referralDiscountWindowLength: number;
}) => {
const t = useT();
@@ -579,81 +548,60 @@ const ReferralTiers = ({
return (
<div>
<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);
<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;
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;
const requiredVolume = Number(
tier.minimumRunningNotionalTakerVolume
);
let unlocksIn = null;
const tierIndicator = (
<div className="flex justify-between">
<span data-testid={`tier-value-${i}`}>{i + 1}</span>
<span className="md:hidden">{indicator}</span>
</div>
);
if (
referralVolumeInWindow >= requiredVolume &&
epochsInSet < tier.minimumEpochs
) {
unlocksIn = (
<span className="text-muted">
Unlocks in {tier.minimumEpochs - epochsInSet} epochs
</span>
);
}
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'
),
};
})}
/>
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>
</div>
);
};
@@ -667,7 +615,7 @@ const YourTier = ({ testId }: YourTierProps) => {
return (
<span
className="bg-rainbow whitespace-nowrap rounded-xl px-4 py-1.5 text-white text-xs"
className="bg-rainbow whitespace-nowrap rounded-xl px-4 py-1.5 text-white"
data-testid={testId}
>
{t('Your tier')}
@@ -8,53 +8,43 @@ 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 = (): ColDef[] => {
const useFeesTableColumnDefs = () => {
const t = useT();
return useMemo(
() =>
[
{
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[],
() => [
{ 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 + '%',
},
],
[t]
);
};
const feesTableDefaultColDef = {
flex: 1,
minWidth: 62,
resizable: true,
sortable: true,
suppressMovable: true,
pinned: false,
};
const components = {
@@ -72,8 +62,6 @@ 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);
@@ -100,9 +88,9 @@ export const MarketFees = ({
});
return (
<div className="border rounded-lg md:rounded-sm overflow-hidden border-default">
<div className="border rounded-sm border-default">
<AgGrid
columnDefs={colDef}
columnDefs={useFeesTableColumnDefs()}
rowData={rows}
getRowId={({ data }) => data.id}
defaultColDef={feesTableDefaultColDef}
@@ -1,5 +1,4 @@
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
export const useReferralStats = (
previousEpoch?: number,
referralStats?: NonNullable<
@@ -38,7 +38,7 @@ export const FillsContainer = () => {
);
};
export const useFillsStore = create<DataGridSlice>()(
const useFillsStore = create<DataGridSlice>()(
persist(createDataGridSlice, {
name: 'vega_fills_store',
})
@@ -1,8 +0,0 @@
import { GridSettings } from '../grid-settings/grid-settings';
import { useFillsStore } from './fills-container';
export const FillsSettings = () => (
<GridSettings
updateGridStore={useFillsStore((store) => store.updateGridStore)}
/>
);
@@ -1,2 +1 @@
export * from './fills-container';
export * from './fills-settings';
@@ -45,7 +45,7 @@ export const FundingPaymentsContainer = ({
);
};
export const useFundingPaymentsStore = create<DataGridSlice>()(
const useFundingPaymentsStore = create<DataGridSlice>()(
persist(createDataGridSlice, {
name: 'vega_funding_payments_store',
})
@@ -1,8 +0,0 @@
import { GridSettings } from '../grid-settings/grid-settings';
import { useFundingPaymentsStore } from './funding-payments-container';
export const FundingPaymentsSettings = () => (
<GridSettings
updateGridStore={useFundingPaymentsStore((store) => store.updateGridStore)}
/>
);
@@ -1,2 +1 @@
export * from './funding-payments-container';
export * from './funding-payments-settings';
@@ -1,24 +0,0 @@
import { useT } from '../../lib/use-t';
import type { DataGridStore } from '../../stores/datagrid-store-slice';
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
export const GridSettings = ({
updateGridStore,
}: {
updateGridStore: (gridStore: DataGridStore) => void;
}) => {
const t = useT();
return (
<Button
onClick={() =>
updateGridStore({
columnState: undefined,
filterModel: undefined,
})
}
size="extra-small"
>
{t('Reset Columns')}
</Button>
);
};
-48
View File
@@ -1,48 +0,0 @@
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),
});
@@ -42,9 +42,13 @@ export const LiquidityHeader = () => {
const assetDecimalPlaces = asset?.decimals || 0;
const symbol = asset?.symbol;
const triggeringRatio =
market?.liquidityMonitoringParameters.triggeringRatio || '1';
const { percentage, status } = useCheckLiquidityStatus({
suppliedStake: suppliedStake || 0,
targetStake: targetStake || 0,
triggeringRatio,
});
const feesObject = feesPaidRes?.paidLiquidityFees?.edges?.find(
@@ -47,6 +47,9 @@ export const MarketLiquiditySupplied = ({
]);
const stakeToCcyVolume = params.market_liquidity_stakeToCcyVolume;
const triggeringRatio = Number(
params.market_liquidity_targetstake_triggering_ratio
);
const variables = useMemo(
() => ({
@@ -91,6 +94,7 @@ export const MarketLiquiditySupplied = ({
const { percentage, status } = useCheckLiquidityStatus({
suppliedStake: market?.suppliedStake || 0,
targetStake: market?.targetStake || 0,
triggeringRatio,
});
const showMessage =
@@ -1 +1,3 @@
export { MarketBanner } from './market-banner';
export * from './market-successor-banner';
export * from './market-successor-proposal-banner';
export * from './market-termination-banner';
@@ -1,205 +0,0 @@
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>
);
};
@@ -1,58 +0,0 @@
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');
});
});
@@ -1,46 +0,0 @@
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>
</>
);
};
@@ -1,219 +0,0 @@
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}`
);
});
});
@@ -1,122 +0,0 @@
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>;
};
@@ -0,0 +1,155 @@
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();
});
});
});
@@ -0,0 +1,146 @@
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,77 +1,164 @@
import { render, screen } from '@testing-library/react';
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 { MarketSuccessorProposalBanner } from './market-successor-proposal-banner';
import { ProposalState } from '@vegaprotocol/types';
import type { MarketViewProposalsQuery } from '@vegaprotocol/proposals';
import { MarketViewProposalsDocument } from '@vegaprotocol/proposals';
import * as Types from '@vegaprotocol/types';
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',
},
const marketProposalMock: MockedResponse<MarketViewProposalsQuery> = {
request: {
query: MarketViewProposalsDocument,
variables: {
inState: Types.ProposalState.STATE_OPEN,
proposalType: Types.ProposalType.TYPE_NEW_MARKET,
},
},
};
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',
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',
},
},
},
},
},
],
},
},
},
};
describe('MarketSuccessorProposalBanner', () => {
it('should display single proposal', () => {
render(<MarketSuccessorProposalBanner proposals={[proposal]} />);
expect(
screen.getByText('A successor to this market has been proposed')
).toBeInTheDocument();
expect(screen.getByRole('link')).toHaveAttribute(
'href',
expect.stringContaining(proposal.id)
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();
});
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',
},
},
},
},
},
],
},
},
},
};
render(
<MockedProvider mocks={[dualProposalMock]}>
<MarketSuccessorProposalBanner marketId="marketId" />
</MockedProvider>
);
await waitFor(() => {
expect(
screen.getByText('Successors to this market have been proposed')
).toBeInTheDocument();
});
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);
});
it('should display plural proposals', () => {
const proposals = [proposal, proposal2];
render(<MarketSuccessorProposalBanner proposals={proposals} />);
expect(
screen.getByText('Successors to this market have been proposed')
).toBeInTheDocument();
const links = screen.getAllByRole('link');
expect(links).toHaveLength(proposals.length);
expect(links[0]).toHaveAttribute(
'href',
expect.stringContaining(proposal.id)
it('banner should be hidden because no proposals', () => {
const { container } = render(
<MockedProvider>
<MarketSuccessorProposalBanner marketId="marketId" />
</MockedProvider>
);
expect(links[1]).toHaveAttribute(
'href',
expect.stringContaining(proposal2.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(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,51 +1,89 @@
import { Fragment } from 'react';
import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { Fragment, useState } from 'react';
import {
marketViewProposalsDataProvider,
type NewMarketSuccessorFieldsFragment,
} from '@vegaprotocol/proposals';
import {
ExternalLink,
Intent,
NotificationBanner,
} 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 = ({
proposals,
marketId,
}: {
proposals: MarketViewProposalFieldsFragment[];
marketId?: string;
}) => {
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);
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>
);
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;
};
@@ -1,6 +0,0 @@
import { useT } from '../../lib/use-t';
export const MarketSuspendedBanner = () => {
const t = useT();
return <p>{t('Market was suspended by governance')}</p>;
};
@@ -0,0 +1,230 @@
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();
});
});
@@ -1,120 +1,44 @@
import { type ReactNode } from 'react';
import sortBy from 'lodash/sortBy';
import type { ReactNode } from 'react';
import { useState } from 'react';
import { format, formatDuration, intervalToDuration } from 'date-fns';
import { type MarketViewProposalFieldsFragment } from '@vegaprotocol/proposals';
import { ProposalState } from '@vegaprotocol/types';
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 sortBy from 'lodash/sortBy';
import {
DApp,
TOKEN_PROPOSAL,
TOKEN_PROPOSALS,
useLinks,
} from '@vegaprotocol/environment';
import { getQuoteName, type Market } from '@vegaprotocol/markets';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { useT } from '../../lib/use-t';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
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 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
);
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);
@@ -138,3 +62,126 @@ 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>
);
};
@@ -1,84 +0,0 @@
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'));
});
});
@@ -1,71 +0,0 @@
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,88 +0,0 @@
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 };
};
@@ -27,7 +27,7 @@ export const MarketHeader = () => {
title={
<Popover
open={open}
onOpenChange={setOpen}
onChange={setOpen}
trigger={
<HeaderTitle>
<span>
@@ -97,9 +97,9 @@ const MarketData = ({
return (
<>
<div className="w-2/6" role="gridcell">
<div className="w-2/5" role="gridcell">
<h3 className="flex items-baseline">
<span className="overflow-hidden text-xs md:text-sm lg:text-base text-ellipsis whitespace-nowrap">
<span className="overflow-hidden 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-2/6 overflow-hidden text-xs lg:text-sm whitespace-nowrap text-ellipsis text-right"
className="w-1/5 overflow-hidden text-xs lg:text-sm whitespace-nowrap text-ellipsis"
title={symbol}
data-testid="market-selector-price"
role="gridcell"
@@ -121,14 +121,14 @@ const MarketData = ({
{price} {symbol}
</div>
<div
className="w-2/6 sm:w-1/6 overflow-hidden text-xs lg:text-sm whitespace-nowrap text-ellipsis text-right"
className="w-1/5 overflow-hidden text-xs text-right lg:text-sm whitespace-nowrap text-ellipsis"
title={t('24h vol')}
data-testid="market-selector-volume"
role="gridcell"
>
{volume}
</div>
<div className="hidden sm:w-1/6 sm:flex justify-end" role="gridcell">
<div className="flex justify-end w-1/5" role="gridcell">
{oneDayCandles && (
<Sparkline
width={64}
@@ -64,7 +64,7 @@ export const MarketSelector = ({
setFilter((curr) => ({ ...curr, product }));
}}
/>
<div className="text-sm flex sm:grid grid-cols-[2fr_1fr_1fr] gap-1 ">
<div className="text-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/6" role="columnheader">
<div className="w-2/5" role="columnheader">
{t('Name')}
</div>
<div className="w-2/6 text-right pr-4" role="columnheader">
<div className="w-1/5" role="columnheader">
{t('Price')}
</div>
<div className="w-2/6 sm:w-1/6 text-right" role="columnheader">
<div className="w-1/5 text-right" role="columnheader">
{t('24h volume')}
</div>
<div className="hidden sm:w-1/6" role="columnheader" />
<div className="w-1/5" role="columnheader" />
</div>
<div ref={listRef}>
<List
+1 -11
View File
@@ -5,7 +5,6 @@ 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
@@ -31,16 +30,7 @@ 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')}
<span
className={classNames(
'transition-transform ease-in-out duration-300',
{
'rotate-180': open,
}
)}
>
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={20} />
</span>
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={20} />
</h1>
}
>
@@ -1,5 +1,9 @@
import { act, renderHook } from '@testing-library/react';
import { STORAGE_KEY, useOrderListGridState } from './orders-container';
import {
FilterStatusValue,
STORAGE_KEY,
useOrderListGridState,
} from './orders-container';
import { Filter } from '@vegaprotocol/orders';
import { OrderType } from '@vegaprotocol/types';
@@ -12,6 +16,31 @@ describe('useOrderListGridState', () => {
return renderHook(() => useOrderListGridState(filter));
};
it.each(Object.values(Filter))(
'providers correct AgGrid filter for %s',
(filter) => {
const { result } = setup(filter);
expect(typeof result.current.updateGridState).toBe('function');
expect(result.current.gridState).toEqual({
columnState: undefined,
filterModel: {
status: {
value: FilterStatusValue[filter],
},
},
});
}
);
it('provides correct AgGrid filter for all', () => {
const { result } = setup(undefined);
expect(typeof result.current.updateGridState).toBe('function');
expect(result.current.gridState).toEqual({
columnState: undefined,
filterModel: undefined,
});
});
it.each(Object.values(Filter))(
'sets and stores column state and filters for %s',
(filter) => {
@@ -30,7 +59,12 @@ describe('useOrderListGridState', () => {
expect(result.current.gridState).toEqual({
columnState: undefined,
filterModel,
filterModel: {
...filterModel,
status: {
value: FilterStatusValue[filter],
},
},
});
const columnState = [{ colId: 'status', width: 200 }];
@@ -43,7 +77,12 @@ describe('useOrderListGridState', () => {
expect(result.current.gridState).toEqual({
columnState,
filterModel,
filterModel: {
...filterModel,
status: {
value: FilterStatusValue[filter],
},
},
});
const storeKeyMap = {
@@ -9,7 +9,6 @@ import type { DataGridStore } from '../../stores/datagrid-store-slice';
import { OrderStatus } from '@vegaprotocol/types';
import { Links } from '../../lib/links';
import { useT } from '../../lib/use-t';
import { GridSettings } from '../grid-settings/grid-settings';
const resolveNoRowsMessage = (
filter: Filter | undefined,
@@ -39,24 +38,6 @@ export const FilterStatusValue = {
[Filter.Rejected]: [OrderStatus.STATUS_REJECTED],
};
export const DefaultFilterModel = {
[Filter.Open]: {
status: {
value: FilterStatusValue[Filter.Open],
},
},
[Filter.Closed]: {
status: {
value: FilterStatusValue[Filter.Closed],
},
},
[Filter.Rejected]: {
status: {
value: FilterStatusValue[Filter.Rejected],
},
},
};
export interface OrderContainerProps {
filter?: Filter;
}
@@ -73,8 +54,7 @@ export const OrdersContainer = ({ filter }: OrderContainerProps) => {
(newState) => {
updateGridState(filter, newState);
},
AUTO_SIZE_COLUMNS,
filter && DefaultFilterModel[filter]
AUTO_SIZE_COLUMNS
);
if (!pubKey) {
@@ -100,7 +80,7 @@ export const OrdersContainer = ({ filter }: OrderContainerProps) => {
};
export const STORAGE_KEY = 'vega_order_list_store';
export const useOrderListStore = create<{
const useOrderListStore = create<{
open: DataGridStore;
closed: DataGridStore;
rejected: DataGridStore;
@@ -169,19 +149,34 @@ export const useOrderListGridState = (filter: Filter | undefined) => {
case Filter.Open: {
return {
columnState: store.open.columnState,
filterModel: store.open.filterModel,
filterModel: {
...store.open.filterModel,
status: {
value: FilterStatusValue[Filter.Open],
},
},
};
}
case Filter.Closed: {
return {
columnState: store.closed.columnState,
filterModel: store.closed.filterModel,
filterModel: {
...store.closed.filterModel,
status: {
value: FilterStatusValue[Filter.Closed],
},
},
};
}
case Filter.Rejected: {
return {
columnState: store.rejected.columnState,
filterModel: store.rejected.filterModel,
filterModel: {
...store.rejected.filterModel,
status: {
value: FilterStatusValue[Filter.Rejected],
},
},
};
}
default: {
@@ -192,14 +187,3 @@ export const useOrderListGridState = (filter: Filter | undefined) => {
return { gridState, updateGridState };
};
export const OrdersSettings = ({ filter }: { filter?: Filter }) => {
const updateGridState = useOrderListStore((state) => state.update);
return (
<GridSettings
updateGridStore={(gridStore: DataGridStore) =>
updateGridState(filter, gridStore)
}
/>
);
};
@@ -1,2 +1 @@
export * from './positions-container';
export * from './positions-settings';
@@ -1,8 +0,0 @@
import { GridSettings } from '../grid-settings/grid-settings';
import { usePositionsStore } from './positions-container';
export const PositionsSettings = () => (
<GridSettings
updateGridStore={usePositionsStore((store) => store.updateGridStore)}
/>
);
@@ -2,19 +2,14 @@ query RewardsPage($partyId: ID!) {
party(id: $partyId) {
id
vestingStats {
# AKA hoarder reward multiplier
rewardBonusMultiplier
quantumBalance
epochSeq
}
activityStreak {
activeFor
isActive
inactiveFor
rewardDistributionMultiplier
# vesting multiplier
rewardVestingMultiplier
epoch
tradedVolume
openVolume
# AKA streak multiplier
rewardDistributionMultiplier
}
vestingBalancesSummary {
epoch
@@ -41,74 +36,6 @@ 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
@@ -165,18 +92,3 @@ query RewardsEpoch {
id
}
}
query MarketForRewards($marketId: ID!) {
market(id: $marketId) {
tradableInstrument {
instrument {
id
name
code
metadata {
tags
}
}
}
}
}
@@ -8,17 +8,7 @@ export type RewardsPageQueryVariables = Types.Exact<{
}>;
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 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 RewardsHistoryQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
@@ -36,13 +26,6 @@ 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!) {
@@ -50,18 +33,10 @@ export const RewardsPageDocument = gql`
id
vestingStats {
rewardBonusMultiplier
quantumBalance
epochSeq
}
activityStreak {
activeFor
isActive
inactiveFor
rewardDistributionMultiplier
rewardVestingMultiplier
epoch
tradedVolume
openVolume
rewardDistributionMultiplier
}
vestingBalancesSummary {
epoch
@@ -116,101 +91,6 @@ 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(
@@ -322,48 +202,4 @@ 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 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>;
export type RewardsEpochQueryResult = Apollo.QueryResult<RewardsEpochQuery, RewardsEpochQueryVariables>;
@@ -1,174 +0,0 @@
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();
});
});
});

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