Compare commits

..
138 changed files with 1616 additions and 2982 deletions
+1 -1
View File
@@ -205,7 +205,7 @@ jobs:
# run tests # run tests
#---------------------------------------------- #----------------------------------------------
- name: Run tests - name: Run tests
run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v --numprocesses 4 --dist loadfile --durations=45 run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v --numprocesses 1 --dist loadfile --durations=45
working-directory: apps/trading/e2e working-directory: apps/trading/e2e
#---------------------------------------------- #----------------------------------------------
# upload traces # upload traces
+5 -5
View File
@@ -4,7 +4,7 @@ The front-end monorepo provides a toolkit for building apps that interact with V
This repository is managed using [Nx](https://nx.dev). This repository is managed using [Nx](https://nx.dev).
## 🔎 Applications in this repo # 🔎 Applications in this repo
### [Block explorer](./apps/explorer) ### [Block explorer](./apps/explorer)
@@ -30,7 +30,7 @@ Hosting for static content being shared across apps, for example fonts.
The utility dApp for validators wishing to add or remove themselves as a signer of the multisig contract. The utility dApp for validators wishing to add or remove themselves as a signer of the multisig contract.
## 🧱 Libraries in this repo # 🧱 Libraries in this repo
### [UI toolkit](./libs/ui-toolkit) ### [UI toolkit](./libs/ui-toolkit)
@@ -53,7 +53,7 @@ A utility library for connecting to the Ethereum network and interacting with Ve
Generic react helpers that can be used across multiple applications, along with other utilities. Generic react helpers that can be used across multiple applications, along with other utilities.
## 💻 Develop # 💻 Develop
### Set up ### Set up
@@ -103,7 +103,7 @@ In CI linting, formatting and also run. These checks can be seen in the [CI work
Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more. Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more.
## 🐋 Hosting a console # 🐋 Hosting a console
To host a console there are two possible build scenarios for running the frontends: nx performed **outside** or **inside** docker build. For specific build instructions follow [build instructions](#build-instructions). To host a console there are two possible build scenarios for running the frontends: nx performed **outside** or **inside** docker build. For specific build instructions follow [build instructions](#build-instructions).
@@ -226,6 +226,6 @@ Note: The script is only needed if capsule was built for first time or fresh. To
vega wallet service run -n DV --load-tokens --tokens-passphrase-file passphrase --no-version-check --automatic-consent --home ~/.vegacapsule/testnet/wallet vega wallet service run -n DV --load-tokens --tokens-passphrase-file passphrase --no-version-check --automatic-consent --home ~/.vegacapsule/testnet/wallet
``` ```
## 📑 License # 📑 License
[MIT](./LICENSE) [MIT](./LICENSE)
@@ -24,6 +24,10 @@ context('Proposal page', { tags: '@smoke' }, function () {
cy.get_element_by_col_id('title').should('have.text', proposalTitle); cy.get_element_by_col_id('title').should('have.text', proposalTitle);
cy.get_element_by_col_id('type').should('have.text', 'NewMarket'); cy.get_element_by_col_id('type').should('have.text', 'NewMarket');
cy.get_element_by_col_id('state').should('have.text', 'Enacted'); cy.get_element_by_col_id('state').should('have.text', 'Enacted');
cy.getByTestId('vote-progress').should('be.visible');
cy.getByTestId('vote-progress-bar-for')
.invoke('attr', 'style')
.should('eq', 'width: 100%;');
cy.get('[col-id="cDate"]') cy.get('[col-id="cDate"]')
.invoke('text') .invoke('text')
.should('match', dateTimeRegex); .should('match', dateTimeRegex);
@@ -69,6 +73,10 @@ context('Proposal page', { tags: '@smoke' }, function () {
'have.text', 'have.text',
'Waiting for Node Vote' 'Waiting for Node Vote'
); );
cy.getByTestId('vote-progress').should('be.visible');
cy.getByTestId('vote-progress-bar-against')
.invoke('attr', 'style')
.should('eq', 'width: 100%;');
cy.get('[col-id="cDate"]') cy.get('[col-id="cDate"]')
.invoke('text') .invoke('text')
.should('match', dateTimeRegex); .should('match', dateTimeRegex);
@@ -1,4 +1,5 @@
import { type ProposalListFieldsFragment } from '@vegaprotocol/proposals'; import { type ProposalListFieldsFragment } from '@vegaprotocol/proposals';
import { VoteProgress } from '@vegaprotocol/proposals';
import { type AgGridReact } from 'ag-grid-react'; import { type AgGridReact } from 'ag-grid-react';
import { ExternalLink } from '@vegaprotocol/ui-toolkit'; import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { AgGrid } from '@vegaprotocol/datagrid'; import { AgGrid } from '@vegaprotocol/datagrid';
@@ -11,7 +12,12 @@ import { type ColDef } from 'ag-grid-community';
import type { RowClickedEvent } from 'ag-grid-community'; import type { RowClickedEvent } from 'ag-grid-community';
import { getDateTimeFormat } from '@vegaprotocol/utils'; import { getDateTimeFormat } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { ProposalStateMapping } from '@vegaprotocol/types'; import { ProposalStateMapping } from '@vegaprotocol/types';
import BigNumber from 'bignumber.js';
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment'; import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
import { BREAKPOINT_MD } from '../../config/breakpoints'; import { BREAKPOINT_MD } from '../../config/breakpoints';
import { JsonViewerDialog } from '../dialogs/json-viewer-dialog'; import { JsonViewerDialog } from '../dialogs/json-viewer-dialog';
@@ -25,7 +31,15 @@ type ProposalsTableProps = {
data: ProposalListFieldsFragment[] | null; data: ProposalListFieldsFragment[] | null;
}; };
export const ProposalsTable = ({ data }: ProposalsTableProps) => { export const ProposalsTable = ({ data }: ProposalsTableProps) => {
const { params } = useNetworkParams([
NetworkParams.governance_proposal_market_requiredMajority,
]);
const tokenLink = useLinks(DApp.Governance); const tokenLink = useLinks(DApp.Governance);
const requiredMajorityPercentage = useMemo(() => {
const requiredMajority =
params?.governance_proposal_market_requiredMajority ?? 1;
return new BigNumber(requiredMajority).times(100);
}, [params?.governance_proposal_market_requiredMajority]);
const gridRef = useRef<AgGridReact>(null); const gridRef = useRef<AgGridReact>(null);
useLayoutEffect(() => { useLayoutEffect(() => {
@@ -76,6 +90,33 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
return value ? ProposalStateMapping[value] : '-'; return value ? ProposalStateMapping[value] : '-';
}, },
}, },
{
colId: 'voting',
maxWidth: 100,
hide: window.innerWidth <= BREAKPOINT_MD,
headerName: t('Voting'),
cellRenderer: ({
data,
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
if (data) {
const yesTokens = new BigNumber(data.votes.yes.totalTokens);
const noTokens = new BigNumber(data.votes.no.totalTokens);
const totalTokensVoted = yesTokens.plus(noTokens);
const yesPercentage = totalTokensVoted.isZero()
? new BigNumber(0)
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
return (
<div className="flex h-full items-center justify-center pt-2 uppercase">
<VoteProgress
threshold={requiredMajorityPercentage}
progress={yesPercentage}
/>
</div>
);
}
return '-';
},
},
{ {
colId: 'cDate', colId: 'cDate',
maxWidth: 150, maxWidth: 150,
@@ -143,7 +184,7 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
}, },
}, },
], ],
[tokenLink] [requiredMajorityPercentage, tokenLink]
); );
return ( return (
<> <>
@@ -1,46 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import { TxDetailsShared } from '../shared/tx-details-shared';
import { TableWithTbody } from '../../../table';
import type { components } from '../../../../../types/explorer';
import type { BlockExplorerTransactionResult } from '../../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../../routes/blocks/tendermint-blocks-response';
import { TableCell, TableRow } from '../../../table';
type Update = components['schemas']['v1UpdatePartyProfile'];
interface TxDetailsUpdatePartyProfileProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* Party profiles can be an alias and arbitrary key/values pairs.
* This component displays the alias, if any, but not the metadata. When there is
* some wider usage, we can decide how to render it. For now, it's available in the
* full TX details.
*/
export const TxDetailsUpdatePartyProfile = ({
txData,
pubKey,
blockData,
}: TxDetailsUpdatePartyProfileProps) => {
if (!txData?.command.updatePartyProfile) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const update: Update = txData.command.updatePartyProfile;
return (
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
{update.alias && (
<TableRow modifier="bordered">
<TableCell>{t('New alias')}</TableCell>
<TableCell>{update.alias}</TableCell>
</TableRow>
)}
</TableWithTbody>
);
};
@@ -34,7 +34,6 @@ import { TxDetailsUpdateReferralSet } from './tx-update-referral-set';
import { TxDetailsJoinTeam } from './tx-join-team'; import { TxDetailsJoinTeam } from './tx-join-team';
import { TxDetailsUpdateMarginMode } from './tx-update-margin-mode'; import { TxDetailsUpdateMarginMode } from './tx-update-margin-mode';
import { TxBatchProposal } from './tx-batch-proposal'; import { TxBatchProposal } from './tx-batch-proposal';
import { TxDetailsUpdatePartyProfile } from './proposal/tx-update-party-profile';
interface TxDetailsWrapperProps { interface TxDetailsWrapperProps {
txData: BlockExplorerTransactionResult | undefined; txData: BlockExplorerTransactionResult | undefined;
@@ -140,8 +139,6 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
return TxDetailsUpdateMarginMode; return TxDetailsUpdateMarginMode;
case 'Batch Proposal': case 'Batch Proposal':
return TxBatchProposal; return TxBatchProposal;
case 'Update Party Profile':
return TxDetailsUpdatePartyProfile;
default: default:
return TxDetailsGeneric; return TxDetailsGeneric;
} }
@@ -44,7 +44,6 @@ export type FilterOption =
| 'Submit Order' | 'Submit Order'
| 'Transfer Funds' | 'Transfer Funds'
| 'Undelegate' | 'Undelegate'
| 'Update Party Profile'
| 'Update Referral Set' | 'Update Referral Set'
| 'Update Margin Mode' | 'Update Margin Mode'
| 'Validator Heartbeat' | 'Validator Heartbeat'
@@ -80,7 +79,6 @@ export const filterOptions: Record<string, FilterOption[]> = {
'Apply Referral Code', 'Apply Referral Code',
'Create Referral Set', 'Create Referral Set',
'Join Team', 'Join Team',
'Update Party Profile',
'Update Referral Set', 'Update Referral Set',
], ],
'External Data': ['Chain Event', 'Submit Oracle Data'], 'External Data': ['Chain Event', 'Submit Oracle Data'],
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"short_name": "Explorer VEGA", "short_name": "Mainnet Stats",
"name": "Vega Protocol - Explorer", "name": "Vega Mainnet statistics",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"short_name": "Governance VEGA", "short_name": "Mainnet Stats",
"name": "Vega Protocol - Governance", "name": "Vega Mainnet statistics",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Vega Protocol static assets</title> <title>Vega Protocol static asseets</title>
<link rel="stylesheet" href="fonts.css" /> <link rel="stylesheet" href="fonts.css" />
<link rel="icon" type="image/x-icon" href="favicon.ico" /> <link rel="icon" type="image/x-icon" href="favicon.ico" />
</head> </head>
+1 -1
View File
@@ -1,4 +1,4 @@
NX_ETHEREUM_PROVIDER_URL=https://eth-mainnet.rpc.grove.city/v1/af6a2d529a11f8158bc8ca2a NX_ETHEREUM_PROVIDER_URL=https://eth-mainnet.gateway.pokt.network/v1/lb/af6a2d529a11f8158bc8ca2a
NX_ETHERSCAN_URL=https://etherscan.io NX_ETHERSCAN_URL=https://etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.sentry.io/6300613 NX_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.sentry.io/6300613
+7 -9
View File
@@ -1,12 +1,6 @@
{ {
"name": "Vega Protocol - Trading", "short_name": "Mainnet Stats",
"short_name": "Console", "name": "Vega Mainnet statistics",
"description": "Vega Protocol - Trading dApp",
"start_url": "/",
"display": "standalone",
"orientation": "portrait",
"theme_color": "#000000",
"background_color": "#ffffff",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",
@@ -18,5 +12,9 @@
"type": "image/png", "type": "image/png",
"sizes": "192x192" "sizes": "192x192"
} }
] ],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
} }
@@ -1,10 +1,5 @@
import { Link, useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { import { Intent, TradingAnchorButton } from '@vegaprotocol/ui-toolkit';
Intent,
TradingAnchorButton,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet'; import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
@@ -35,21 +30,8 @@ export const CompetitionsCreateTeam = () => {
<LayoutWithGradient> <LayoutWithGradient>
<div className="mx-auto md:w-2/3 max-w-xl"> <div className="mx-auto md:w-2/3 max-w-xl">
<Box className="flex flex-col gap-4"> <Box className="flex flex-col gap-4">
<Link
to={Links.COMPETITIONS()}
className="text-xs inline-flex items-center gap-1 group"
>
<VegaIcon
name={VegaIconNames.CHEVRON_LEFT}
size={12}
className="text-vega-clight-100 dark:text-vega-cdark-100"
/>{' '}
<span className="group-hover:underline">
{t('Go back to the competitions')}
</span>
</Link>
<h1 className="calt text-2xl lg:text-3xl xl:text-4xl"> <h1 className="calt text-2xl lg:text-3xl xl:text-4xl">
{isSolo ? t('Create solo team') : t('Create a team')} {t('Create a team')}
</h1> </h1>
{pubKey && !isReadOnly ? ( {pubKey && !isReadOnly ? (
<CreateTeamFormContainer isSolo={isSolo} /> <CreateTeamFormContainer isSolo={isSolo} />
@@ -89,29 +71,18 @@ const CreateTeamFormContainer = ({ isSolo }: { isSolo: boolean }) => {
if (status === 'confirmed') { if (status === 'confirmed') {
return ( return (
<div <div className="flex flex-col items-start gap-2">
className="flex flex-col items-start gap-2"
data-testid="team-creation-success-message"
>
<p className="text-sm">{t('Team creation transaction successful')}</p> <p className="text-sm">{t('Team creation transaction successful')}</p>
{code && ( {code && (
<> <>
<dl> <p className="text-sm">
<dt className="text-sm">{t('Your team ID:')}</dt> Your team ID is:{' '}
<dl> <span className="font-mono break-all">{code}</span>
<span </p>
className="font-mono break-all bg-rainbow bg-clip-text text-transparent text-2xl"
data-testid="team-id-display"
>
{code}
</span>
</dl>
</dl>
<TradingAnchorButton <TradingAnchorButton
href={Links.COMPETITIONS_TEAM(code)} href={Links.COMPETITIONS_TEAM(code)}
intent={Intent.Info} intent={Intent.Info}
size="small" size="small"
data-testid="view-team-button"
> >
{t('View team')} {t('View team')}
</TradingAnchorButton> </TradingAnchorButton>
@@ -154,7 +125,7 @@ const CreateTeamFormContainer = ({ isSolo }: { isSolo: boolean }) => {
onSubmit={onSubmit} onSubmit={onSubmit}
status={status} status={status}
err={err} err={err}
isCreatingSoloTeam={isSolo} isSolo={isSolo}
/> />
); );
}; };
@@ -16,8 +16,6 @@ import { CompetitionsLeaderboard } from '../../components/competitions/competiti
import { useTeams } from '../../lib/hooks/use-teams'; import { useTeams } from '../../lib/hooks/use-teams';
import take from 'lodash/take'; import take from 'lodash/take';
import { usePageTitle } from '../../lib/hooks/use-page-title'; import { usePageTitle } from '../../lib/hooks/use-page-title';
import { TeamCard } from '../../components/competitions/team-card';
import { useMyTeam } from '../../lib/hooks/use-my-team';
export const CompetitionsHome = () => { export const CompetitionsHome = () => {
const t = useT(); const t = useT();
@@ -33,14 +31,10 @@ export const CompetitionsHome = () => {
currentEpoch, currentEpoch,
}); });
const { data: teamsData, loading: teamsLoading } = useTeams(); const { data: teamsData, loading: teamsLoading } = useTeams({
sortByField: ['totalQuantumRewards'],
const { order: 'desc',
team: myTeam, });
stats: myTeamStats,
games: myTeamGames,
rank: myTeamRank,
} = useMyTeam();
return ( return (
<ErrorBoundary> <ErrorBoundary>
@@ -52,83 +46,65 @@ export const CompetitionsHome = () => {
</p> </p>
</CompetitionsHeader> </CompetitionsHeader>
{/** Team card */} {/** Get started */}
{myTeam ? ( <h2 className="text-2xl mb-6">{t('Get started')}</h2>
<>
<h2 className="text-2xl mb-6">{t('My team')}</h2>
<div className="mb-12">
<TeamCard
team={myTeam}
rank={myTeamRank}
stats={myTeamStats}
games={myTeamGames}
/>
</div>
</>
) : (
<>
{/** Get started */}
<h2 className="text-2xl mb-6">{t('Get started')}</h2>
<CompetitionsActionsContainer> <CompetitionsActionsContainer>
<CompetitionsAction <CompetitionsAction
variant="A" variant="A"
title={t('Create a team')} title={t('Create a team')}
description={t( description={t(
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.' 'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
)} )}
actionElement={ actionElement={
<TradingButton <TradingButton
intent={Intent.Primary} intent={Intent.Primary}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
navigate(Links.COMPETITIONS_CREATE_TEAM()); navigate(Links.COMPETITIONS_CREATE_TEAM());
}} }}
data-testId="create-public-team-button" >
> {t('Create a public team')}
{t('Create a public team')} </TradingButton>
</TradingButton> }
} />
/> <CompetitionsAction
<CompetitionsAction variant="B"
variant="B" title={t('Solo team / lone wolf')}
title={t('Solo team / lone wolf')} description={t(
description={t( 'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.' )}
)} actionElement={
actionElement={ <TradingButton
<TradingButton intent={Intent.Primary}
intent={Intent.Primary} onClick={(e) => {
onClick={(e) => { e.preventDefault();
e.preventDefault(); navigate(Links.COMPETITIONS_CREATE_TEAM_SOLO());
navigate(Links.COMPETITIONS_CREATE_TEAM_SOLO()); }}
}} >
> {t('Create a private team')}
{t('Create a private team')} </TradingButton>
</TradingButton> }
} />
/> <CompetitionsAction
<CompetitionsAction variant="C"
variant="C" title={t('Join a team')}
title={t('Join a team')} description={t(
description={t( 'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.' )}
)} actionElement={
actionElement={ <TradingButton
<TradingButton intent={Intent.Primary}
intent={Intent.Primary} onClick={(e) => {
onClick={(e) => { e.preventDefault();
e.preventDefault(); navigate(Links.COMPETITIONS_TEAMS());
navigate(Links.COMPETITIONS_TEAMS()); }}
}} >
> {t('Choose a team')}
{t('Choose a team')} </TradingButton>
</TradingButton> }
} />
/> </CompetitionsActionsContainer>
</CompetitionsActionsContainer>
</>
)}
{/** List of available games */} {/** List of available games */}
<h2 className="text-2xl mb-6">{t('Games')}</h2> <h2 className="text-2xl mb-6">{t('Games')}</h2>
@@ -38,11 +38,12 @@ export const CompetitionsTeam = () => {
const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => { const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => {
const t = useT(); const t = useT();
const { pubKey } = useVegaWallet(); const { pubKey } = useVegaWallet();
const { data, team, partyTeam, stats, members, games, loading, refetch } = const { team, partyTeam, stats, members, games, loading, refetch } = useTeam(
useTeam(teamId, pubKey || undefined); teamId,
pubKey || undefined
);
// only show spinner on first load so when users join teams its smoother if (loading) {
if (!data && loading) {
return ( return (
<Splash> <Splash>
<Loader /> <Loader />
@@ -99,10 +100,8 @@ const TeamPage = ({
> >
{team.name} {team.name}
</h1> </h1>
<div className="flex gap-2"> <JoinTeam team={team} partyTeam={partyTeam} refetch={refetch} />
<JoinTeam team={team} partyTeam={partyTeam} refetch={refetch} /> <UpdateTeamButton team={team} />
<UpdateTeamButton team={team} />
</div>
</div> </div>
</header> </header>
<TeamStats stats={stats} members={members} games={games} /> <TeamStats stats={stats} members={members} games={games} />
@@ -185,10 +184,7 @@ const Members = ({ members }: { members?: Member[] }) => {
const data = orderBy( const data = orderBy(
members.map((m) => ({ members.map((m) => ({
referee: <RefereeLink pubkey={m.referee} isCreator={m.isCreator} />, referee: <RefereeLink pubkey={m.referee} />,
rewards: formatNumber(m.totalQuantumRewards),
volume: formatNumber(m.totalQuantumVolume),
gamesPlayed: formatNumber(m.totalGamesPlayed),
joinedAt: getDateTimeFormat().format(new Date(m.joinedAt)), joinedAt: getDateTimeFormat().format(new Date(m.joinedAt)),
joinedAtEpoch: Number(m.joinedAtEpoch), joinedAtEpoch: Number(m.joinedAtEpoch),
})), })),
@@ -199,10 +195,7 @@ const Members = ({ members }: { members?: Member[] }) => {
return ( return (
<Table <Table
columns={[ columns={[
{ name: 'referee', displayName: t('Member ID') }, { name: 'referee', displayName: t('Referee') },
{ name: 'rewards', displayName: t('Rewards earned') },
{ name: 'volume', displayName: t('Total volume') },
{ name: 'gamesPlayed', displayName: t('Games played') },
{ {
name: 'joinedAt', name: 'joinedAt',
displayName: t('Joined at'), displayName: t('Joined at'),
@@ -218,24 +211,14 @@ const Members = ({ members }: { members?: Member[] }) => {
); );
}; };
const RefereeLink = ({ const RefereeLink = ({ pubkey }: { pubkey: string }) => {
pubkey,
isCreator,
}: {
pubkey: string;
isCreator: boolean;
}) => {
const t = useT();
const linkCreator = useLinks(DApp.Explorer); const linkCreator = useLinks(DApp.Explorer);
const link = linkCreator(EXPLORER_PARTIES.replace(':id', pubkey)); const link = linkCreator(EXPLORER_PARTIES.replace(':id', pubkey));
return ( return (
<> <Link to={link} target="_blank" className="underline underline-offset-4">
<Link to={link} target="_blank" className="underline underline-offset-4"> {truncateMiddle(pubkey)}
{truncateMiddle(pubkey)} </Link>
</Link>{' '}
<span className="text-muted text-xs">{isCreator ? t('Owner') : ''}</span>
</>
); );
}; };
@@ -17,7 +17,10 @@ export const CompetitionsTeams = () => {
usePageTitle([t('Competitions'), t('Teams')]); usePageTitle([t('Competitions'), t('Teams')]);
const { data: teamsData, loading: teamsLoading } = useTeams(); const { data: teamsData, loading: teamsLoading } = useTeams({
sortByField: ['totalQuantumRewards'],
order: 'desc',
});
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const [filter, setFilter] = useState<string | null | undefined>(undefined); const [filter, setFilter] = useState<string | null | undefined>(undefined);
@@ -3,14 +3,7 @@ import { usePageTitle } from '../../lib/hooks/use-page-title';
import { Box } from '../../components/competitions/box'; import { Box } from '../../components/competitions/box';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet'; import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
Intent,
Loader,
Splash,
TradingAnchorButton,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { RainbowButton } from '../../components/rainbow-button'; import { RainbowButton } from '../../components/rainbow-button';
import { Link, Navigate, useParams } from 'react-router-dom'; import { Link, Navigate, useParams } from 'react-router-dom';
import { Links } from '../../lib/links'; import { Links } from '../../lib/links';
@@ -18,7 +11,6 @@ import { useReferralSetTransaction } from '../../lib/hooks/use-referral-set-tran
import { type FormFields, TeamForm, TransactionType } from './team-form'; import { type FormFields, TeamForm, TransactionType } from './team-form';
import { useTeam } from '../../lib/hooks/use-team'; import { useTeam } from '../../lib/hooks/use-team';
import { LayoutWithGradient } from '../../components/layouts-inner'; import { LayoutWithGradient } from '../../components/layouts-inner';
import { useEffect, useState } from 'react';
export const CompetitionsUpdateTeam = () => { export const CompetitionsUpdateTeam = () => {
const t = useT(); const t = useT();
@@ -37,19 +29,6 @@ export const CompetitionsUpdateTeam = () => {
<LayoutWithGradient> <LayoutWithGradient>
<div className="mx-auto md:w-2/3 max-w-xl"> <div className="mx-auto md:w-2/3 max-w-xl">
<Box className="flex flex-col gap-4"> <Box className="flex flex-col gap-4">
<Link
to={Links.COMPETITIONS_TEAM(teamId)}
className="text-xs inline-flex items-center gap-1 group"
>
<VegaIcon
name={VegaIconNames.CHEVRON_LEFT}
size={12}
className="text-vega-clight-100 dark:text-vega-cdark-100"
/>{' '}
<span className="group-hover:underline">
{t('Go back to the team profile')}
</span>
</Link>
<h1 className="calt text-2xl lg:text-3xl xl:text-5xl"> <h1 className="calt text-2xl lg:text-3xl xl:text-5xl">
{t('Update a team')} {t('Update a team')}
</h1> </h1>
@@ -78,8 +57,7 @@ const UpdateTeamFormContainer = ({
pubKey: string; pubKey: string;
}) => { }) => {
const t = useT(); const t = useT();
const [refetching, setRefetching] = useState<boolean>(false); const { team, loading, error } = useTeam(teamId, pubKey);
const { team, loading, error, refetch } = useTeam(teamId, pubKey);
const { err, status, onSubmit } = useReferralSetTransaction({ const { err, status, onSubmit } = useReferralSetTransaction({
onSuccess: () => { onSuccess: () => {
@@ -87,15 +65,7 @@ const UpdateTeamFormContainer = ({
}, },
}); });
// refetch when saved if (loading) {
useEffect(() => {
if (refetch && status === 'confirmed') {
refetch();
setRefetching(true);
}
}, [refetch, status]);
if (loading && !refetching) {
return <Loader size="small" />; return <Loader size="small" />;
} }
if (error) { if (error) {
@@ -114,33 +84,6 @@ const UpdateTeamFormContainer = ({
return <Navigate to={Links.COMPETITIONS_TEAM(teamId)} />; return <Navigate to={Links.COMPETITIONS_TEAM(teamId)} />;
} }
if (status === 'confirmed') {
return (
<div
className="flex flex-col items-start gap-2"
data-testid="team-creation-success-message"
>
<p className="text-sm">
<VegaIcon
name={VegaIconNames.TICK}
size={18}
className="text-vega-green-500"
/>{' '}
{t('Changes successfully saved to your team.')}
</p>
<TradingAnchorButton
href={Links.COMPETITIONS_TEAM(teamId)}
intent={Intent.Info}
size="small"
data-testid="view-team-button"
>
{t('View team')}
</TradingAnchorButton>
</div>
);
}
const defaultValues: FormFields = { const defaultValues: FormFields = {
id: team.teamId, id: team.teamId,
name: team.name, name: team.name,
@@ -155,7 +98,7 @@ const UpdateTeamFormContainer = ({
type={TransactionType.UpdateReferralSet} type={TransactionType.UpdateReferralSet}
status={status} status={status}
err={err} err={err}
isCreatingSoloTeam={team.closed} isSolo={team.closed}
onSubmit={onSubmit} onSubmit={onSubmit}
defaultValues={defaultValues} defaultValues={defaultValues}
/> />
@@ -6,7 +6,11 @@ import {
VegaIcon, VegaIcon,
VegaIconNames, VegaIconNames,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { useSimpleTransaction, useVegaWallet } from '@vegaprotocol/wallet'; import {
useSimpleTransaction,
useVegaWallet,
type Status,
} from '@vegaprotocol/wallet';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { type Team } from '../../lib/hooks/use-team'; import { type Team } from '../../lib/hooks/use-team';
import { useState } from 'react'; import { useState } from 'react';
@@ -23,8 +27,19 @@ export const JoinTeam = ({
refetch: () => void; refetch: () => void;
}) => { }) => {
const { pubKey, isReadOnly } = useVegaWallet(); const { pubKey, isReadOnly } = useVegaWallet();
const { send, status } = useSimpleTransaction({
onSuccess: refetch,
});
const [confirmDialog, setConfirmDialog] = useState<JoinType>(); const [confirmDialog, setConfirmDialog] = useState<JoinType>();
const joinTeam = () => {
send({
joinTeam: {
id: team.teamId,
},
});
};
return ( return (
<> <>
<JoinButton <JoinButton
@@ -41,10 +56,11 @@ export const JoinTeam = ({
{confirmDialog !== undefined && ( {confirmDialog !== undefined && (
<DialogContent <DialogContent
type={confirmDialog} type={confirmDialog}
status={status}
team={team} team={team}
partyTeam={partyTeam} partyTeam={partyTeam}
onConfirm={joinTeam}
onCancel={() => setConfirmDialog(undefined)} onCancel={() => setConfirmDialog(undefined)}
refetch={refetch}
/> />
)} )}
</Dialog> </Dialog>
@@ -94,7 +110,7 @@ export const JoinButton = ({
// Not creator of the team, but still can't switch because // Not creator of the team, but still can't switch because
// creators cannot leave their own team // creators cannot leave their own team
return ( return (
<Tooltip description={t('As a team creator, you cannot switch teams')}> <Tooltip description="As a team creator, you cannot switch teams">
<Button intent={Intent.Primary} disabled={true}> <Button intent={Intent.Primary} disabled={true}>
{t('Switch team')}{' '} {t('Switch team')}{' '}
</Button> </Button>
@@ -105,11 +121,7 @@ export const JoinButton = ({
// Party is in a team, but not this one // Party is in a team, but not this one
else if (partyTeam && partyTeam.teamId !== team.teamId) { else if (partyTeam && partyTeam.teamId !== team.teamId) {
return ( return (
<Button <Button onClick={() => onJoin('switch')} intent={Intent.Primary}>
onClick={() => onJoin('switch')}
intent={Intent.Primary}
data-testid="switch-team-button"
>
{t('Switch team')}{' '} {t('Switch team')}{' '}
</Button> </Button>
); );
@@ -137,39 +149,21 @@ export const JoinButton = ({
const DialogContent = ({ const DialogContent = ({
type, type,
status,
team, team,
partyTeam, partyTeam,
onConfirm,
onCancel, onCancel,
refetch,
}: { }: {
type: JoinType; type: JoinType;
status: Status;
team: Team; team: Team;
partyTeam?: Team; partyTeam?: Team;
onConfirm: () => void;
onCancel: () => void; onCancel: () => void;
refetch: () => void;
}) => { }) => {
const t = useT(); const t = useT();
const { send, status, error } = useSimpleTransaction({
onSuccess: refetch,
});
const joinTeam = () => {
send({
joinTeam: {
id: team.teamId,
},
});
};
if (error) {
return (
<p className="text-vega-red break-words first-letter:capitalize">
{error}
</p>
);
}
if (status === 'requested') { if (status === 'requested') {
return <p>{t('Confirm in wallet...')}</p>; return <p>{t('Confirm in wallet...')}</p>;
} }
@@ -219,11 +213,7 @@ const DialogContent = ({
</> </>
)} )}
<div className="flex justify-between gap-2"> <div className="flex justify-between gap-2">
<Button <Button onClick={onConfirm} intent={Intent.Success}>
onClick={joinTeam}
intent={Intent.Success}
data-testid="confirm-switch-button"
>
{t('Confirm')} {t('Confirm')}
</Button> </Button>
<Button onClick={onCancel} intent={Intent.Danger}> <Button onClick={onCancel} intent={Intent.Danger}>
@@ -6,8 +6,6 @@ import {
TextArea, TextArea,
TradingButton, TradingButton,
Intent, Intent,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { URL_REGEX, isValidVegaPublicKey } from '@vegaprotocol/utils'; import { URL_REGEX, isValidVegaPublicKey } from '@vegaprotocol/utils';
@@ -19,8 +17,6 @@ import type {
UpdateReferralSet, UpdateReferralSet,
Status, Status,
} from '@vegaprotocol/wallet'; } from '@vegaprotocol/wallet';
import classNames from 'classnames';
import { useLayoutEffect, useState } from 'react';
export type FormFields = { export type FormFields = {
id: string; id: string;
@@ -32,8 +28,8 @@ export type FormFields = {
}; };
export enum TransactionType { export enum TransactionType {
CreateReferralSet = 'CreateReferralSet', CreateReferralSet,
UpdateReferralSet = 'UpdateReferralSet', UpdateReferralSet,
} }
const prepareTransaction = ( const prepareTransaction = (
@@ -79,14 +75,14 @@ export const TeamForm = ({
type, type,
status, status,
err, err,
isCreatingSoloTeam, isSolo,
onSubmit, onSubmit,
defaultValues, defaultValues,
}: { }: {
type: TransactionType; type: TransactionType;
status: ReturnType<typeof useReferralSetTransaction>['status']; status: ReturnType<typeof useReferralSetTransaction>['status'];
err: ReturnType<typeof useReferralSetTransaction>['err']; err: ReturnType<typeof useReferralSetTransaction>['err'];
isCreatingSoloTeam: boolean; isSolo: boolean;
onSubmit: ReturnType<typeof useReferralSetTransaction>['onSubmit']; onSubmit: ReturnType<typeof useReferralSetTransaction>['onSubmit'];
defaultValues?: FormFields; defaultValues?: FormFields;
}) => { }) => {
@@ -100,7 +96,7 @@ export const TeamForm = ({
formState: { errors }, formState: { errors },
} = useForm<FormFields>({ } = useForm<FormFields>({
defaultValues: { defaultValues: {
private: isCreatingSoloTeam, private: isSolo,
...defaultValues, ...defaultValues,
}, },
}); });
@@ -113,14 +109,16 @@ export const TeamForm = ({
return ( return (
<form onSubmit={handleSubmit(sendTransaction)}> <form onSubmit={handleSubmit(sendTransaction)}>
<input type="hidden" {...register('id')} /> <input
type="hidden"
{...register('id', {
disabled: true,
})}
/>
<TradingFormGroup label={t('Team name')} labelFor="name"> <TradingFormGroup label={t('Team name')} labelFor="name">
<TradingInput <TradingInput {...register('name', { required: t('Required') })} />
{...register('name', { required: t('Required') })}
data-testid="team-name-input"
/>
{errors.name?.message && ( {errors.name?.message && (
<TradingInputError forInput="name" data-testid="team-name-error"> <TradingInputError forInput="name">
{errors.name.message} {errors.name.message}
</TradingInputError> </TradingInputError>
)} )}
@@ -136,10 +134,9 @@ export const TeamForm = ({
{...register('url', { {...register('url', {
pattern: { value: URL_REGEX, message: t('Invalid URL') }, pattern: { value: URL_REGEX, message: t('Invalid URL') },
})} })}
data-testid="team-url-input"
/> />
{errors.url?.message && ( {errors.url?.message && (
<TradingInputError forInput="url" data-testid="team-url-error"> <TradingInputError forInput="url">
{errors.url.message} {errors.url.message}
</TradingInputError> </TradingInputError>
)} )}
@@ -156,86 +153,66 @@ export const TeamForm = ({
message: t('Invalid image URL'), message: t('Invalid image URL'),
}, },
})} })}
data-testid="avatar-url-input"
/> />
{errors.avatarUrl?.message && ( {errors.avatarUrl?.message && (
<TradingInputError <TradingInputError forInput="avatarUrl">
forInput="avatarUrl"
data-testid="avatar-url-error"
>
{errors.avatarUrl.message} {errors.avatarUrl.message}
</TradingInputError> </TradingInputError>
)} )}
</TradingFormGroup> </TradingFormGroup>
{ <TradingFormGroup
// allow changing to private/public if editing, but don't show these options if making a solo team label={t('Make team private')}
(type === TransactionType.UpdateReferralSet || !isCreatingSoloTeam) && ( labelFor="private"
<> hideLabel={true}
<TradingFormGroup >
label={t('Make team private')} <Controller
labelFor="private" name="private"
hideLabel={true} control={control}
> render={({ field }) => {
<Controller return (
name="private" <TradingCheckbox
control={control} label={t('Make team private')}
render={({ field }) => { checked={field.value}
return ( onCheckedChange={(value) => {
<TradingCheckbox field.onChange(value);
label={t('Make team private')}
checked={field.value}
onCheckedChange={(value) => {
field.onChange(value);
}}
data-testid="team-private-checkbox"
/>
);
}} }}
disabled={isSolo}
/> />
</TradingFormGroup> );
{isPrivate && ( }}
<TradingFormGroup />
label={t('Public key allow list')} </TradingFormGroup>
labelFor="allowList" {isPrivate && (
labelDescription={t( <TradingFormGroup
'Use a comma separated list to allow only specific public keys to join the team' label={t('Public key allow list')}
)} labelFor="allowList"
> labelDescription={t(
<TextArea 'Use a comma separated list to allow only specific public keys to join the team'
{...register('allowList', { )}
required: t('Required'), >
validate: { <TextArea
allowList: (value) => { {...register('allowList', {
const publicKeys = parseAllowListText(value); required: t('Required'),
if ( disabled: isSolo,
publicKeys.every((pk) => isValidVegaPublicKey(pk)) validate: {
) { allowList: (value) => {
return true; const publicKeys = parseAllowListText(value);
} if (publicKeys.every((pk) => isValidVegaPublicKey(pk))) {
return t('Invalid public key found in allow list'); return true;
}, }
}, return t('Invalid public key found in allow list');
})} },
data-testid="team-allow-list-textarea" },
/> })}
{errors.allowList?.message && ( />
<TradingInputError {errors.allowList?.message && (
forInput="avatarUrl" <TradingInputError forInput="avatarUrl">
data-testid="team-allow-list-error" {errors.allowList.message}
> </TradingInputError>
{errors.allowList.message} )}
</TradingInputError> </TradingFormGroup>
)}
</TradingFormGroup>
)}
</>
)
}
{err && (
<p className="text-danger text-xs mb-4 first-letter:capitalize">
{err}
</p>
)} )}
{err && <p className="text-danger text-xs mb-4 capitalize">{err}</p>}
<SubmitButton type={type} status={status} /> <SubmitButton type={type} status={status} />
</form> </form>
); );
@@ -256,61 +233,20 @@ const SubmitButton = ({
text = t('Update'); text = t('Update');
} }
let confirmedText = t('Created');
if (type === TransactionType.UpdateReferralSet) {
confirmedText = t('Updated');
}
if (status === 'requested') { if (status === 'requested') {
text = t('Confirm in wallet...'); text = t('Confirm in wallet...');
} else if (status === 'pending') { } else if (status === 'pending') {
text = t('Confirming transaction...'); text = t('Confirming transaction...');
} }
const [showConfirmed, setShowConfirmed] = useState<boolean>(false);
useLayoutEffect(() => {
let to: ReturnType<typeof setTimeout>;
if (status === 'confirmed' && !showConfirmed) {
to = setTimeout(() => {
setShowConfirmed(true);
}, 100);
}
return () => {
clearTimeout(to);
};
}, [showConfirmed, status]);
const confirmed = (
<span
className={classNames('text-sm transition-opacity opacity-0', {
'opacity-100': showConfirmed,
})}
>
<VegaIcon
name={VegaIconNames.TICK}
size={18}
className="text-vega-green-500"
/>{' '}
{confirmedText}
</span>
);
return ( return (
<div className="flex gap-2 items-baseline"> <TradingButton type="submit" intent={Intent.Info} disabled={disabled}>
<TradingButton {text}
type="submit" </TradingButton>
intent={Intent.Info}
disabled={disabled}
data-testid="team-form-submit-button"
>
{text}
</TradingButton>
{status === 'confirmed' && confirmed}
</div>
); );
}; };
const parseAllowListText = (str: string = '') => { const parseAllowListText = (str: string) => {
return str return str
.split(',') .split(',')
.map((v) => v.trim()) .map((v) => v.trim())
@@ -1,30 +1,18 @@
import { type Team } from '../../lib/hooks/use-team';
import { type ComponentProps } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { type Team } from '../../lib/hooks/use-team';
import { Intent, TradingAnchorButton } from '@vegaprotocol/ui-toolkit'; import { Intent, TradingAnchorButton } from '@vegaprotocol/ui-toolkit';
import { Links } from '../../lib/links'; import { Links } from '../../lib/links';
import { useT } from '../../lib/use-t';
export const UpdateTeamButton = ({ export const UpdateTeamButton = ({ team }: { team: Team }) => {
team,
size = 'medium',
}: {
team: Pick<Team, 'teamId' | 'referrer'>;
size?: ComponentProps<typeof TradingAnchorButton>['size'];
}) => {
const t = useT();
const { pubKey, isReadOnly } = useVegaWallet(); const { pubKey, isReadOnly } = useVegaWallet();
if (pubKey && !isReadOnly && pubKey === team.referrer) { if (pubKey && !isReadOnly && pubKey === team.referrer) {
return ( return (
<TradingAnchorButton <TradingAnchorButton
size={size}
data-testid="update-team-button" data-testid="update-team-button"
href={Links.COMPETITIONS_UPDATE_TEAM(team.teamId)} href={Links.COMPETITIONS_UPDATE_TEAM(team.teamId)}
intent={Intent.Info} intent={Intent.Info}
> />
{t('Update team')}
</TradingAnchorButton>
); );
} }
+13 -6
View File
@@ -1,20 +1,27 @@
import { TinyScroll } from '@vegaprotocol/ui-toolkit'; import { useEffect } from 'react';
import { titlefy } from '@vegaprotocol/utils';
import { ErrorBoundary } from '../../components/error-boundary'; import { ErrorBoundary } from '../../components/error-boundary';
import { FeesContainer } from '../../components/fees-container'; import { FeesContainer } from '../../components/fees-container';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { usePageTitle } from '../../lib/hooks/use-page-title'; import { usePageTitleStore } from '../../stores';
export const Fees = () => { export const Fees = () => {
const t = useT(); const t = useT();
const title = t('Fees'); const title = t('Fees');
usePageTitle(title); const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
useEffect(() => {
updateTitle(titlefy([title]));
}, [updateTitle, title]);
return ( return (
<ErrorBoundary feature="fees"> <ErrorBoundary feature="fees">
<TinyScroll className="p-4 max-h-full overflow-auto"> <div className="container p-4 mx-auto">
<h1 className="md:px-4 pb-4 text-2xl">{title}</h1> <h1 className="px-4 pb-4 text-2xl">{title}</h1>
<FeesContainer /> <FeesContainer />
</TinyScroll> </div>
</ErrorBoundary> </ErrorBoundary>
); );
}; };
@@ -19,7 +19,6 @@ import {
useFundingRate, useFundingRate,
useMarketTradingMode, useMarketTradingMode,
useExternalTwap, useExternalTwap,
getQuoteName,
} from '@vegaprotocol/markets'; } from '@vegaprotocol/markets';
import { MarketState as State } from '@vegaprotocol/types'; import { MarketState as State } from '@vegaprotocol/types';
import { HeaderStat } from '../../components/header'; import { HeaderStat } from '../../components/header';
@@ -42,7 +41,6 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore(); const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
const asset = getAsset(market); const asset = getAsset(market);
const quoteUnit = getQuoteName(market);
return ( return (
<> <>
@@ -56,15 +54,12 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
<Last24hPriceChange <Last24hPriceChange
marketId={market.id} marketId={market.id}
decimalPlaces={market.decimalPlaces} decimalPlaces={market.decimalPlaces}
fallback={<span>-</span>}
/> />
</HeaderStat> </HeaderStat>
<HeaderStat heading={t('Volume (24h)')} testId="market-volume"> <HeaderStat heading={t('Volume (24h)')} testId="market-volume">
<Last24hVolume <Last24hVolume
marketId={market.id} marketId={market.id}
positionDecimalPlaces={market.positionDecimalPlaces} positionDecimalPlaces={market.positionDecimalPlaces}
marketDecimals={market.decimalPlaces}
quoteUnit={quoteUnit}
/> />
</HeaderStat> </HeaderStat>
<HeaderStatMarketTradingMode <HeaderStatMarketTradingMode
@@ -113,7 +108,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
heading={`${t('Funding Rate')} / ${t('Countdown')}`} heading={`${t('Funding Rate')} / ${t('Countdown')}`}
testId="market-funding" testId="market-funding"
> >
<div className="flex gap-2"> <div className="flex justify-between gap-2">
<FundingRate marketId={market.id} /> <FundingRate marketId={market.id} />
<FundingCountdown marketId={market.id} /> <FundingCountdown marketId={market.id} />
</div> </div>
@@ -3,6 +3,7 @@ import { type Market } from '@vegaprotocol/markets';
// TODO: handle oracle banner // TODO: handle oracle banner
// import { OracleBanner } from '@vegaprotocol/markets'; // import { OracleBanner } from '@vegaprotocol/markets';
import { useState } from 'react'; import { useState } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import classNames from 'classnames'; import classNames from 'classnames';
import { import {
Popover, Popover,
@@ -11,21 +12,21 @@ import {
VegaIconNames, VegaIconNames,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { MarketBanner } from '../../components/market-banner';
import { ErrorBoundary } from '../../components/error-boundary'; import { ErrorBoundary } from '../../components/error-boundary';
import { type TradingView } from './trade-views'; import { type TradingView } from './trade-views';
import { TradingViews } from './trade-views'; import { TradingViews } from './trade-views';
interface TradePanelsProps { interface TradePanelsProps {
market: Market; market: Market;
pinnedAsset?: PinnedAsset; pinnedAsset?: PinnedAsset;
} }
export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => { export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
const [topView, setTopView] = useState<TradingView>('chart'); const [view, setView] = useState<TradingView>('chart');
const topViewCfg = TradingViews[topView]; const viewCfg = TradingViews[view];
const [bottomView, setBottomView] = useState<TradingView>('positions');
const bottomViewCfg = TradingViews[bottomView];
const renderView = (view: TradingView) => { const renderView = () => {
const Component = TradingViews[view].component; const Component = TradingViews[view].component;
if (!Component) { if (!Component) {
@@ -38,13 +39,12 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
// so watch out for clashes in props // so watch out for clashes in props
return ( return (
<ErrorBoundary feature={view}> <ErrorBoundary feature={view}>
<Component marketId={market?.id} pinnedAsset={pinnedAsset} /> <Component marketId={market?.id} pinnedAsset={pinnedAsset} />;
</ErrorBoundary> </ErrorBoundary>
); );
}; };
// eslint-disable-next-line @typescript-eslint/no-explicit-any const renderMenu = () => {
const renderMenu = (viewCfg: any) => {
if ('menu' in viewCfg || 'settings' in viewCfg) { if ('menu' in viewCfg || 'settings' in viewCfg) {
return ( 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"> <div className="flex items-center justify-end gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default">
@@ -69,80 +69,55 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
}; };
return ( return (
<div className="h-full flex flex-col lg:grid grid-rows-[min-content_min-content_1fr_min-content]"> <div className="h-full grid grid-rows-[min-content_min-content_1fr_min-content]">
<div className="flex flex-col w-full overflow-hidden"> <div>
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default"> <MarketBanner market={market} />
{['chart', 'orderbook', 'trades', 'liquidity', 'fundingPayments']
// filter to control available views for the current market
// e.g. only perpetuals should get the funding views
.filter((_key) => {
const key = _key as TradingView;
const perpOnlyViews = ['funding', 'fundingPayments'];
if (
market?.tradableInstrument.instrument.product.__typename ===
'Perpetual'
) {
return true;
}
if (perpOnlyViews.includes(key)) {
return false;
}
return true;
})
.map((_key) => {
const key = _key as TradingView;
const isActive = topView === key;
return (
<ViewButton
key={key}
view={key}
isActive={isActive}
onClick={() => {
setTopView(key);
}}
/>
);
})}
</div>
<div className="h-[50vh] lg:h-full relative">
<div>{renderMenu(topViewCfg)}</div>
<div className="overflow-auto h-full">{renderView(topView)}</div>
</div>
</div> </div>
<div>{renderMenu()}</div>
<div className="flex flex-col w-full grow"> <div className="h-full relative">
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default"> <AutoSizer>
{[ {({ width, height }) => (
'positions', <div style={{ width, height }} className="overflow-auto">
'activeOrders', {renderView()}
'closedOrders', </div>
'rejectedOrders', )}
'orders', </AutoSizer>
'stopOrders', </div>
'collateral', <div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
'fills', {Object.keys(TradingViews)
].map((_key) => { // filter to control available views for the current market
// eg only perps should get the funding views
.filter((_key) => {
const key = _key as TradingView; const key = _key as TradingView;
const isActive = bottomView === key; const perpOnlyViews = ['funding', 'fundingPayments'];
if (
market?.tradableInstrument.instrument.product.__typename ===
'Perpetual'
) {
return true;
}
if (perpOnlyViews.includes(key)) {
return false;
}
return true;
})
.map((_key) => {
const key = _key as TradingView;
const isActive = view === key;
return ( return (
<ViewButton <ViewButton
key={key} key={key}
view={key} view={key}
isActive={isActive} isActive={isActive}
onClick={() => { onClick={() => {
setBottomView(key); setView(key);
}} }}
/> />
); );
})} })}
</div>
<div className="relative grow">
<div className="flex flex-col">{renderMenu(bottomViewCfg)}</div>
<div className="overflow-auto h-full">{renderView(bottomView)}</div>
</div>
</div> </div>
</div> </div>
); );
@@ -182,7 +157,7 @@ const useViewLabel = (view: TradingView) => {
depth: t('Depth'), depth: t('Depth'),
liquidity: t('Liquidity'), liquidity: t('Liquidity'),
funding: t('Funding'), funding: t('Funding'),
fundingPayments: t('Funding'), fundingPayments: t('Funding Payments'),
orderbook: t('Orderbook'), orderbook: t('Orderbook'),
trades: t('Trades'), trades: t('Trades'),
positions: t('Positions'), positions: t('Positions'),
@@ -5,7 +5,7 @@ import {
useDataGridEvents, useDataGridEvents,
} from '@vegaprotocol/datagrid'; } from '@vegaprotocol/datagrid';
import type { MarketMaybeWithData } from '@vegaprotocol/markets'; import type { MarketMaybeWithData } from '@vegaprotocol/markets';
import { useMarketsColumnDefs } from './use-column-defs'; import { useColumnDefs } from './use-column-defs';
import type { DataGridStore } from '../../stores/datagrid-store-slice'; import type { DataGridStore } from '../../stores/datagrid-store-slice';
import { type StateCreator, create } from 'zustand'; import { type StateCreator, create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
@@ -50,7 +50,7 @@ export const useMarketsStore = create<DataGridSlice>()(
); );
export const MarketListTable = (props: Props) => { export const MarketListTable = (props: Props) => {
const columnDefs = useMarketsColumnDefs(); const columnDefs = useColumnDefs();
const gridStore = useMarketsStore((store) => store.gridStore); const gridStore = useMarketsStore((store) => store.gridStore);
const updateGridStore = useMarketsStore((store) => store.updateGridStore); const updateGridStore = useMarketsStore((store) => store.updateGridStore);
@@ -1,3 +1,5 @@
import React, { useEffect } from 'react';
import { titlefy } from '@vegaprotocol/utils';
import { import {
LocalStoragePersistTabs as Tabs, LocalStoragePersistTabs as Tabs,
Tab, Tab,
@@ -5,6 +7,7 @@ import {
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { OpenMarkets } from './open-markets'; import { OpenMarkets } from './open-markets';
import { Proposed } from './proposed'; import { Proposed } from './proposed';
import { usePageTitleStore } from '../../stores';
import { Closed } from './closed'; import { Closed } from './closed';
import { import {
DApp, DApp,
@@ -14,14 +17,19 @@ import {
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { ErrorBoundary } from '../../components/error-boundary'; import { ErrorBoundary } from '../../components/error-boundary';
import { MarketsSettings } from './markets-settings'; import { MarketsSettings } from './markets-settings';
import { usePageTitle } from '../../lib/hooks/use-page-title';
export const MarketsPage = () => { export const MarketsPage = () => {
const t = useT(); const t = useT();
const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
const governanceLink = useLinks(DApp.Governance); const governanceLink = useLinks(DApp.Governance);
const externalLink = governanceLink(TOKEN_NEW_MARKET_PROPOSAL); const externalLink = governanceLink(TOKEN_NEW_MARKET_PROPOSAL);
usePageTitle(t('Markets')); useEffect(() => {
updateTitle(titlefy([t('Markets')]));
}, [updateTitle, t]);
return ( return (
<div className="h-full pt-0.5 pb-3 px-1.5"> <div className="h-full pt-0.5 pb-3 px-1.5">
@@ -1,235 +0,0 @@
import { Route, Routes } from 'react-router-dom';
import {
Intent,
MobileActionsDropdown,
Tooltip,
TradingButton,
TradingDropdownItem,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { type BarView, ViewType, useSidebar } from '../../components/sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { useT } from '../../lib/use-t';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import { useEffect } from 'react';
import classNames from 'classnames';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
const ViewInitializer = () => {
const currentRouteId = useGetCurrentRouteId();
const { setViews, getView } = useSidebar();
const view = getView(currentRouteId);
const { screenSize } = useScreenDimensions();
const largeScreen = ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize);
useEffect(() => {
if (largeScreen && view === undefined) {
setViews({ type: ViewType.Order }, currentRouteId);
}
}, [setViews, view, currentRouteId, largeScreen]);
return null;
};
export const MarketsMobileSidebar = () => {
const t = useT();
const currentRouteId = useGetCurrentRouteId();
const { pubKeys, isReadOnly } = useVegaWallet();
const openVegaWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
return (
<Routes>
<Route
path=":marketId"
element={
<>
<ViewInitializer />
<div className="grid grid-cols-3 grow md:grow-0 md:flex lg:flex-col items-center gap-2 lg:gap-4 p-1">
{!pubKeys || isReadOnly ? (
<>
<TradingButton
intent={Intent.Primary}
size="medium"
onClick={() => {
openVegaWalletDialog();
}}
>
{t('Connect')}
</TradingButton>
<MobileButton
view={ViewType.Order}
tooltip={t('Trade')}
routeId={currentRouteId}
/>
<MobileBarActionsDropdown currentRouteId={currentRouteId} />
</>
) : (
<>
<MobileButton
view={ViewType.Order}
tooltip={t('Trade')}
routeId={currentRouteId}
/>
<MobileButton
view={ViewType.Deposit}
tooltip={t('Deposit')}
routeId={currentRouteId}
/>
<MobileBarActionsDropdown currentRouteId={currentRouteId} />
</>
)}
</div>
</>
}
/>
</Routes>
);
};
export const MobileButton = ({
view,
tooltip: label,
disabled = false,
onClick,
routeId,
}: {
view?: ViewType;
tooltip: string;
disabled?: boolean;
onClick?: () => void;
routeId: string;
}) => {
const { setViews, getView } = useSidebar((store) => ({
setViews: store.setViews,
getView: store.getView,
}));
const currView = getView(routeId);
const onSelect = (view: BarView['type']) => {
if (view === currView?.type) {
setViews(null, routeId);
} else {
setViews({ type: view }, routeId);
}
};
const buttonClasses = classNames(
'flex items-center p-1 rounded',
'disabled:cursor-not-allowed disabled:text-vega-clight-500 dark:disabled:text-vega-cdark-500',
{
'text-vega-clight-200 dark:text-vega-cdark-200 enabled:hover:bg-vega-clight-500 dark:enabled:hover:bg-vega-cdark-500':
!view || view !== currView?.type,
'bg-vega-yellow enabled:hover:bg-vega-yellow-550 text-black':
view && view === currView?.type,
}
);
return (
<Tooltip description={label} align="center" side="right" sideOffset={10}>
<TradingButton
className={buttonClasses}
data-testid={view}
onClick={onClick || (() => onSelect(view as BarView['type']))}
disabled={disabled}
>
{label}
</TradingButton>
</Tooltip>
);
};
export const MobileDropdownItem = ({
view,
icon,
tooltip,
disabled = false,
onClick,
routeId,
}: {
view?: ViewType;
icon: VegaIconNames;
tooltip: string;
disabled?: boolean;
onClick?: () => void;
routeId: string;
}) => {
const { setViews, getView } = useSidebar((store) => ({
setViews: store.setViews,
getView: store.getView,
}));
const currView = getView(routeId);
const onSelect = (view: BarView['type']) => {
if (view === currView?.type) {
setViews(null, routeId);
} else {
setViews({ type: view }, routeId);
}
};
const buttonClasses = classNames(
'flex items-center p-1 rounded',
'disabled:cursor-not-allowed disabled:text-vega-clight-500 dark:disabled:text-vega-cdark-500',
{
'text-vega-clight-200 dark:text-vega-cdark-200 enabled:hover:bg-vega-clight-500 dark:enabled:hover:bg-vega-cdark-500':
!view || view !== currView?.type,
'bg-vega-yellow enabled:hover:bg-vega-yellow-550 text-black':
view && view === currView?.type,
}
);
return (
<Tooltip description={tooltip} align="center" side="right" sideOffset={10}>
<TradingDropdownItem
className={buttonClasses}
data-testid={view}
onClick={onClick || (() => onSelect(view as BarView['type']))}
disabled={disabled}
>
<VegaIcon name={icon} size={20} />
{tooltip}
</TradingDropdownItem>
</Tooltip>
);
};
export const MobileBarActionsDropdown = ({
currentRouteId,
}: {
currentRouteId: string;
}) => {
const t = useT();
return (
<MobileActionsDropdown>
<MobileDropdownItem
view={ViewType.Deposit}
icon={VegaIconNames.DEPOSIT}
tooltip={t('Deposit')}
routeId={currentRouteId}
/>
<MobileDropdownItem
view={ViewType.Withdraw}
icon={VegaIconNames.WITHDRAW}
tooltip={t('Withdraw')}
routeId={currentRouteId}
/>
<MobileDropdownItem
view={ViewType.Transfer}
icon={VegaIconNames.TRANSFER}
tooltip={t('Transfer')}
routeId={currentRouteId}
/>
<MobileDropdownItem
view={ViewType.Info}
icon={VegaIconNames.BREAKDOWN}
tooltip={t('Market specification')}
routeId={currentRouteId}
/>
<MobileDropdownItem
view={ViewType.Settings}
icon={VegaIconNames.COG}
tooltip={t('Settings')}
routeId={currentRouteId}
/>
</MobileActionsDropdown>
);
};
@@ -7,31 +7,21 @@ import type {
} from '@vegaprotocol/datagrid'; } from '@vegaprotocol/datagrid';
import { COL_DEFS, SetFilter } from '@vegaprotocol/datagrid'; import { COL_DEFS, SetFilter } from '@vegaprotocol/datagrid';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import { import { addDecimalsFormatNumber, toBigNum } from '@vegaprotocol/utils';
addDecimalsFormatNumber,
formatNumber,
toBigNum,
} from '@vegaprotocol/utils';
import { ButtonLink, Tooltip } from '@vegaprotocol/ui-toolkit'; import { ButtonLink, Tooltip } from '@vegaprotocol/ui-toolkit';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets'; import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import type { import type {
MarketFieldsFragment,
MarketMaybeWithData, MarketMaybeWithData,
MarketMaybeWithDataAndCandles, MarketMaybeWithDataAndCandles,
} from '@vegaprotocol/markets'; } from '@vegaprotocol/markets';
import { MarketActionsDropdown } from './market-table-actions'; import { MarketActionsDropdown } from './market-table-actions';
import { import { calcCandleVolume, getAsset } from '@vegaprotocol/markets';
calcCandleVolume,
calcCandleVolumePrice,
getAsset,
getQuoteName,
} from '@vegaprotocol/markets';
import { MarketCodeCell } from './market-code-cell'; import { MarketCodeCell } from './market-code-cell';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
const { MarketTradingMode, AuctionTrigger } = Schema; const { MarketTradingMode, AuctionTrigger } = Schema;
export const useMarketsColumnDefs = () => { export const useColumnDefs = () => {
const t = useT(); const t = useT();
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore(); const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
return useMemo<ColDef[]>( return useMemo<ColDef[]>(
@@ -168,25 +158,11 @@ export const useMarketsColumnDefs = () => {
}: ValueFormatterParams<MarketMaybeWithDataAndCandles, 'candles'>) => { }: ValueFormatterParams<MarketMaybeWithDataAndCandles, 'candles'>) => {
const candles = data?.candles; const candles = data?.candles;
const vol = candles ? calcCandleVolume(candles) : '0'; const vol = candles ? calcCandleVolume(candles) : '0';
const quoteName = getQuoteName(data as MarketFieldsFragment);
const volPrice =
candles &&
calcCandleVolumePrice(
candles,
data.decimalPlaces,
data.positionDecimalPlaces
);
const volume = const volume =
data && vol && vol !== '0' data && vol && vol !== '0'
? addDecimalsFormatNumber(vol, data.positionDecimalPlaces) ? addDecimalsFormatNumber(vol, data.positionDecimalPlaces)
: '0.00'; : '0.00';
const volumePrice = return volume;
volPrice && formatNumber(volPrice, data?.decimalPlaces);
return volumePrice
? `${volume} (${volumePrice} ${quoteName})`
: volume;
}, },
}, },
{ {
@@ -2,7 +2,6 @@ import { VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { SidebarButton, ViewType } from '../../components/sidebar'; import { SidebarButton, ViewType } from '../../components/sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id'; import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { MobileButton } from '../markets/mobile-buttons';
export const PortfolioSidebar = () => { export const PortfolioSidebar = () => {
const t = useT(); const t = useT();
@@ -31,28 +30,3 @@ export const PortfolioSidebar = () => {
</> </>
); );
}; };
export const PortfolioMobileSidebar = () => {
const t = useT();
const currentRouteId = useGetCurrentRouteId();
return (
<div className="grid grid-cols-3 grow md:grow-0 md:flex lg:flex-col items-center gap-2 lg:gap-4 p-1">
<MobileButton
view={ViewType.Deposit}
tooltip={t('Deposit')}
routeId={currentRouteId}
/>
<MobileButton
view={ViewType.Withdraw}
tooltip={t('Withdraw')}
routeId={currentRouteId}
/>
<MobileButton
view={ViewType.Transfer}
tooltip={t('Transfer')}
routeId={currentRouteId}
/>
</div>
);
};
@@ -1,8 +1,10 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { LayoutPriority } from 'allotment'; import { LayoutPriority } from 'allotment';
import { titlefy } from '@vegaprotocol/utils';
import { useIncompleteWithdrawals } from '@vegaprotocol/withdraws'; import { useIncompleteWithdrawals } from '@vegaprotocol/withdraws';
import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit'; import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit';
import { usePageTitleStore } from '../../stores';
import { import {
AccountsContainer, AccountsContainer,
AccountsSettings, AccountsSettings,
@@ -39,7 +41,6 @@ import { WithdrawalsMenu } from '../../components/withdrawals-menu';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id'; import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { ErrorBoundary } from '../../components/error-boundary'; import { ErrorBoundary } from '../../components/error-boundary';
import { usePageTitle } from '../../lib/hooks/use-page-title';
const WithdrawalsIndicator = () => { const WithdrawalsIndicator = () => {
const { ready } = useIncompleteWithdrawals(); const { ready } = useIncompleteWithdrawals();
@@ -68,7 +69,14 @@ const SidebarViewInitializer = () => {
export const Portfolio = () => { export const Portfolio = () => {
const t = useT(); const t = useT();
usePageTitle(t('Portfolio'));
const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
useEffect(() => {
updateTitle(titlefy([t('Portfolio')]));
}, [updateTitle, t]);
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'portfolio' }); const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'portfolio' });
const wrapperClasses = 'p-0.5 h-full max-h-full flex flex-col'; const wrapperClasses = 'p-0.5 h-full max-h-full flex flex-col';
@@ -1,4 +1,4 @@
import { formatNumber } from '@vegaprotocol/utils'; import { getNumberFormat } from '@vegaprotocol/utils';
import sortBy from 'lodash/sortBy'; import sortBy from 'lodash/sortBy';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { useReferralProgramQuery } from './__generated__/CurrentReferralProgram'; import { useReferralProgramQuery } from './__generated__/CurrentReferralProgram';
@@ -107,7 +107,9 @@ export const useReferralProgram = () => {
discountFactor: Number(t.referralDiscountFactor), discountFactor: Number(t.referralDiscountFactor),
discount: BigNumber(t.referralDiscountFactor).times(100).toFixed(2) + '%', discount: BigNumber(t.referralDiscountFactor).times(100).toFixed(2) + '%',
minimumVolume: Number(t.minimumRunningNotionalTakerVolume), minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
volume: formatNumber(t.minimumRunningNotionalTakerVolume, 0), volume: getNumberFormat(0).format(
Number(t.minimumRunningNotionalTakerVolume)
),
epochs: Number(t.minimumEpochs), epochs: Number(t.minimumEpochs),
}; };
}); });
@@ -14,9 +14,9 @@ import {
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { import {
addDecimalsFormatNumber, addDecimalsFormatNumber,
formatNumber,
getDateFormat, getDateFormat,
getDateTimeFormat, getDateTimeFormat,
getNumberFormat,
getUserLocale, getUserLocale,
removePaginationWrapper, removePaginationWrapper,
} from '@vegaprotocol/utils'; } from '@vegaprotocol/utils';
@@ -323,7 +323,7 @@ export const Statistics = ({
} }
description={<QUSDTooltip />} description={<QUSDTooltip />}
> >
{formatNumber(totalCommissionValue, 0)} {getNumberFormat(0).format(Number(totalCommissionValue))}
</StatTile> </StatTile>
); );
@@ -563,8 +563,8 @@ export const RefereesTable = ({
) )
.map((r) => ({ .map((r) => ({
...r, ...r,
volume: formatNumber(r.volume, 0), volume: getNumberFormat(0).format(r.volume),
commission: formatNumber(r.commission, 0), commission: getNumberFormat(0).format(r.commission),
})) }))
.reverse()} .reverse()}
/> />
+11 -5
View File
@@ -1,18 +1,24 @@
import { TinyScroll } from '@vegaprotocol/ui-toolkit'; import { useEffect } from 'react';
import { titlefy } from '@vegaprotocol/utils';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { RewardsContainer } from '../../components/rewards-container'; import { RewardsContainer } from '../../components/rewards-container';
import { usePageTitleStore } from '../../stores';
import { ErrorBoundary } from '../../components/error-boundary'; import { ErrorBoundary } from '../../components/error-boundary';
import { usePageTitle } from '../../lib/hooks/use-page-title'; import { TinyScroll } from '@vegaprotocol/ui-toolkit';
export const Rewards = () => { export const Rewards = () => {
const t = useT(); const t = useT();
const title = t('Rewards'); const title = t('Rewards');
usePageTitle(title); const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
useEffect(() => {
updateTitle(titlefy([title]));
}, [updateTitle, title]);
return ( return (
<ErrorBoundary feature="rewards"> <ErrorBoundary feature="rewards">
<TinyScroll className="p-4 max-h-full overflow-auto"> <TinyScroll className="p-4 max-h-full overflow-auto">
<h1 className="md:px-4 pb-4 text-2xl">{title}</h1> <h1 className="px-4 pb-4 text-2xl">{title}</h1>
<RewardsContainer /> <RewardsContainer />
</TinyScroll> </TinyScroll>
</ErrorBoundary> </ErrorBoundary>
+3 -20
View File
@@ -5,11 +5,7 @@ export const BORDER_COLOR = 'border-vega-clight-500 dark:border-vega-cdark-500';
export const GRADIENT = export const GRADIENT =
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent'; 'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
export const Box = ({ export const Box = (props: HTMLAttributes<HTMLDivElement>) => {
children,
backgroundImage,
...props
}: HTMLAttributes<HTMLDivElement> & { backgroundImage?: string }) => {
return ( return (
<div <div
{...props} {...props}
@@ -17,22 +13,9 @@ export const Box = ({
BORDER_COLOR, BORDER_COLOR,
GRADIENT, GRADIENT,
'border rounded-lg', 'border rounded-lg',
'relative p-6 overflow-hidden', 'p-6',
props.className props.className
)} )}
> />
{Boolean(backgroundImage?.length) && (
<div
className={classNames(
'pointer-events-none',
'bg-no-repeat bg-center bg-[length:500px_500px]',
'absolute top-0 left-0 w-full h-full -z-10 opacity-30 blur-lg'
)}
style={{ backgroundImage: `url("${backgroundImage}")` }}
></div>
)}
{children}
</div>
); );
}; };
@@ -1,6 +1,6 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Splash } from '@vegaprotocol/ui-toolkit'; import { Splash } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '@vegaprotocol/utils'; import { getNumberFormat } from '@vegaprotocol/utils';
import { type useTeams } from '../../lib/hooks/use-teams'; import { type useTeams } from '../../lib/hooks/use-teams';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { Table } from '../table'; import { Table } from '../table';
@@ -15,7 +15,8 @@ export const CompetitionsLeaderboard = ({
}) => { }) => {
const t = useT(); const t = useT();
const num = (n?: number | string) => (!n ? '-' : formatNumber(n, 0)); const num = (n?: number | string) =>
!n ? '-' : getNumberFormat(0).format(Number(n));
if (!data || data.length === 0) { if (!data || data.length === 0) {
return <Splash>{t('Could not find any teams')}</Splash>; return <Splash>{t('Could not find any teams')}</Splash>;
@@ -32,9 +33,9 @@ export const CompetitionsLeaderboard = ({
{ name: 'status', displayName: t('Status') }, { name: 'status', displayName: t('Status') },
{ name: 'volume', displayName: t('Volume') }, { name: 'volume', displayName: t('Volume') },
]} ]}
data={data.map((td) => { data={data.map((td, i) => {
// leaderboard place or medal // leaderboard place or medal
let rank: number | React.ReactNode = td.rank; let rank: number | React.ReactNode = i + 1;
if (rank === 1) rank = <Rank variant="gold" />; if (rank === 1) rank = <Rank variant="gold" />;
if (rank === 2) rank = <Rank variant="silver" />; if (rank === 2) rank = <Rank variant="silver" />;
if (rank === 3) rank = <Rank variant="bronze" />; if (rank === 3) rank = <Rank variant="bronze" />;
@@ -1,11 +1,6 @@
import { type TransferNode } from '@vegaprotocol/types'; import { type TransferNode } from '@vegaprotocol/types';
import { import { ActiveRewardCard } from '../rewards-container/active-rewards';
ActiveRewardCard,
isActiveReward,
} from '../rewards-container/active-rewards';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { useAssetsMapProvider } from '@vegaprotocol/assets';
import { useMarketsMapProvider } from '@vegaprotocol/markets';
export const GamesContainer = ({ export const GamesContainer = ({
data, data,
@@ -15,35 +10,8 @@ export const GamesContainer = ({
currentEpoch: number; currentEpoch: number;
}) => { }) => {
const t = useT(); const t = useT();
// Re-load markets and assets in the games container to ensure that the
// the cards are updated (not grayed out) when the user navigates to the games page
const { data: assets } = useAssetsMapProvider();
const { data: markets } = useMarketsMapProvider();
const enrichedTransfers = data if (!data || data.length === 0) {
.filter((node) => isActiveReward(node, currentEpoch))
.map((node) => {
if (node.transfer.kind.__typename !== 'RecurringTransfer') {
return node;
}
const asset =
assets &&
assets[
node.transfer.kind.dispatchStrategy?.dispatchMetricAssetId || ''
];
const marketsInScope =
node.transfer.kind.dispatchStrategy?.marketIdsInScope?.map(
(id) => markets && markets[id]
);
return { ...node, asset, markets: marketsInScope };
});
if (!enrichedTransfers || !enrichedTransfers.length) return null;
if (!enrichedTransfers || enrichedTransfers.length === 0) {
return ( return (
<p className="mb-6 text-muted"> <p className="mb-6 text-muted">
{t('There are currently no games available.')} {t('There are currently no games available.')}
@@ -53,7 +21,7 @@ export const GamesContainer = ({
return ( return (
<div className="mb-12 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="mb-12 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{enrichedTransfers.map((game, i) => { {data.map((game, i) => {
// TODO: Remove `kind` prop from ActiveRewardCard // TODO: Remove `kind` prop from ActiveRewardCard
const { transfer } = game; const { transfer } = game;
if ( if (
@@ -1,11 +1,9 @@
import { isValidUrl } from '@vegaprotocol/utils';
import classNames from 'classnames'; import classNames from 'classnames';
import { useEffect, useState } from 'react';
const NUM_AVATARS = 20; const NUM_AVATARS = 20;
const AVATAR_PATHNAME_PATTERN = '/team-avatars/{id}.png'; const AVATAR_PATHNAME_PATTERN = '/team-avatars/{id}.png';
export const getFallbackAvatar = (teamId: string) => { const getFallbackAvatar = (teamId: string) => {
const avatarId = ((parseInt(teamId, 16) % NUM_AVATARS) + 1) const avatarId = ((parseInt(teamId, 16) % NUM_AVATARS) + 1)
.toString() .toString()
.padStart(2, '0'); // between 01 - 20 .padStart(2, '0'); // between 01 - 20
@@ -13,26 +11,6 @@ export const getFallbackAvatar = (teamId: string) => {
return AVATAR_PATHNAME_PATTERN.replace('{id}', avatarId); return AVATAR_PATHNAME_PATTERN.replace('{id}', avatarId);
}; };
const useAvatar = (teamId: string, url: string) => {
const fallback = getFallbackAvatar(teamId);
const [avatar, setAvatar] = useState<string>(fallback);
useEffect(() => {
if (!isValidUrl(url)) return;
fetch(url, { cache: 'force-cache' })
.then((response) => {
if (response.ok) {
setAvatar(url);
}
})
.catch(() => {
/** noop */
});
});
return avatar;
};
export const TeamAvatar = ({ export const TeamAvatar = ({
teamId, teamId,
imgUrl, imgUrl,
@@ -44,7 +22,7 @@ export const TeamAvatar = ({
alt?: string; alt?: string;
size?: 'large' | 'small'; size?: 'large' | 'small';
}) => { }) => {
const img = useAvatar(teamId, imgUrl); const img = imgUrl && imgUrl.length > 0 ? imgUrl : getFallbackAvatar(teamId);
return ( return (
// eslint-disable-next-line @next/next/no-img-element // eslint-disable-next-line @next/next/no-img-element
<img <img
@@ -1,154 +0,0 @@
import { type TeamGame, type TeamStats } from '../../lib/hooks/use-team';
import { type TeamsFieldsFragment } from '../../lib/hooks/__generated__/Teams';
import { TeamAvatar, getFallbackAvatar } from './team-avatar';
import { FavoriteGame, Stat } from './team-stats';
import { useT } from '../../lib/use-t';
import { formatNumberRounded } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
import { Box } from './box';
import { Intent, Tooltip, TradingAnchorButton } from '@vegaprotocol/ui-toolkit';
import { Links } from '../../lib/links';
import orderBy from 'lodash/orderBy';
import { take } from 'lodash';
import { DispatchMetricLabels } from '@vegaprotocol/types';
import classNames from 'classnames';
import { UpdateTeamButton } from '../../client-pages/competitions/update-team-button';
export const TeamCard = ({
rank,
team,
stats,
games,
}: {
rank: number;
team: TeamsFieldsFragment;
stats?: TeamStats;
games?: TeamGame[];
}) => {
const t = useT();
const lastGames = take(
orderBy(
games?.map((g) => ({
rank: g.team.rank,
metric: g.team.rewardMetric,
epoch: g.epoch,
})),
(i) => i.epoch,
'desc'
),
5
);
return (
<div
className={classNames(
'gap-6 grid grid-cols-1 grid-rows-1',
'md:grid-cols-3'
)}
>
{/** Card */}
<Box
backgroundImage={team.avatarUrl || getFallbackAvatar(team.teamId)}
className="flex flex-col items-center gap-3 min-w-[80px] lg:min-w-[112px]"
>
<TeamAvatar teamId={team.teamId} imgUrl={team.avatarUrl} />
<h1 className="calt lg:text-2xl" data-testid="team-name">
{team.name}
</h1>
{games && <FavoriteGame games={games} noLabel />}
<TradingAnchorButton
size="extra-small"
intent={Intent.Primary}
href={Links.COMPETITIONS_TEAM(team.teamId)}
>
{t('Profile')}
</TradingAnchorButton>
<UpdateTeamButton team={team} size="extra-small" />
</Box>
{/** Tiles */}
<Box className="w-full md:col-span-2">
<div
className={classNames(
'grid gap-3 w-full mb-4',
'md:grid-cols-3 md:grid-rows-2',
'grid-cols-2 grid-rows-3'
)}
>
<Stat
className="flex flex-col-reverse"
value={rank}
label={t('Rank')}
valueTestId="team-rank"
/>
<Stat
className="flex flex-col-reverse"
value={team.totalMembers || 0}
label={t('Members')}
valueTestId="members-count-stat"
/>
<Stat
className="flex flex-col-reverse"
value={stats?.totalGamesPlayed || 0}
label={t('Total games')}
valueTestId="total-games-stat"
/>
<Stat
className="flex flex-col-reverse"
value={
stats?.totalQuantumVolume
? formatNumberRounded(
new BigNumber(stats.totalQuantumVolume || 0),
'1e3'
)
: 0
}
label={t('Total volume')}
valueTestId="total-volume-stat"
/>
<Stat
className="flex flex-col-reverse"
value={
stats?.totalQuantumRewards
? formatNumberRounded(
new BigNumber(stats.totalQuantumRewards || 0),
'1e3'
)
: 0
}
label={t('Rewards paid out')}
valueTestId="rewards-paid-stat"
/>
</div>
<dl className="w-full pt-4 border-t border-vega-clight-700 dark:border-vega-cdark-700">
<dt className="mb-1 text-sm text-muted">
{t('Last {{games}} games result', {
replace: { games: lastGames.length || '' },
})}
</dt>
<dd className="flex flex-row flex-wrap gap-2">
{lastGames.length === 0 && t('None available')}
{lastGames.map((game, i) => (
<Tooltip key={i} description={DispatchMetricLabels[game.metric]}>
<button className="cursor-help text-sm bg-vega-clight-700 dark:bg-vega-cdark-700 px-2 py-1 rounded-full">
<RankLabel rank={game.rank} />
</button>
</Tooltip>
))}
</dd>
</dl>
</Box>
</div>
);
};
/**
* Sets the english ordinal for given rank only if the current language is set
* to english.
*/
const RankLabel = ({ rank }: { rank: number }) => {
const t = useT();
return t('place', { count: rank, ordinal: true });
};
@@ -15,7 +15,6 @@ import {
} from '../../lib/hooks/use-team'; } from '../../lib/hooks/use-team';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { DispatchMetricLabels, type DispatchMetric } from '@vegaprotocol/types'; import { DispatchMetricLabels, type DispatchMetric } from '@vegaprotocol/types';
import classNames from 'classnames';
export const TeamStats = ({ export const TeamStats = ({
stats, stats,
@@ -103,13 +102,7 @@ const LatestResults = ({ games }: { games: TeamGame[] }) => {
); );
}; };
export const FavoriteGame = ({ const FavoriteGame = ({ games }: { games: TeamGame[] }) => {
games,
noLabel = false,
}: {
games: TeamGame[];
noLabel?: boolean;
}) => {
const t = useT(); const t = useT();
const rewardMetrics = games.map( const rewardMetrics = games.map(
@@ -135,13 +128,7 @@ export const FavoriteGame = ({
return ( return (
<dl className="flex flex-col gap-1"> <dl className="flex flex-col gap-1">
<dt <dt className="text-muted text-sm">{t('Favorite game')}</dt>
className={classNames('text-muted text-sm', {
hidden: noLabel,
})}
>
{t('Favorite game')}
</dt>
<dd> <dd>
<Pill className="inline-flex items-center gap-1 bg-transparent text-sm"> <Pill className="inline-flex items-center gap-1 bg-transparent text-sm">
<VegaIcon <VegaIcon
@@ -155,7 +142,7 @@ export const FavoriteGame = ({
); );
}; };
export const StatSection = ({ children }: { children: ReactNode }) => { const StatSection = ({ children }: { children: ReactNode }) => {
return ( return (
<section className="flex flex-col lg:flex-row gap-4 lg:gap-8"> <section className="flex flex-col lg:flex-row gap-4 lg:gap-8">
{children} {children}
@@ -163,11 +150,11 @@ export const StatSection = ({ children }: { children: ReactNode }) => {
); );
}; };
export const StatSectionSeparator = () => { const StatSectionSeparator = () => {
return <div className="hidden md:block border-r border-default" />; return <div className="hidden md:block border-r border-default" />;
}; };
export const StatList = ({ children }: { children: ReactNode }) => { const StatList = ({ children }: { children: ReactNode }) => {
return ( return (
<dl className="grid grid-cols-2 md:flex gap-4 md:gap-6 lg:gap-8 whitespace-nowrap"> <dl className="grid grid-cols-2 md:flex gap-4 md:gap-6 lg:gap-8 whitespace-nowrap">
{children} {children}
@@ -175,21 +162,19 @@ export const StatList = ({ children }: { children: ReactNode }) => {
); );
}; };
export const Stat = ({ const Stat = ({
value, value,
label, label,
tooltip, tooltip,
valueTestId, valueTestId,
className,
}: { }: {
value: ReactNode; value: ReactNode;
label: ReactNode; label: ReactNode;
tooltip?: string; tooltip?: string;
valueTestId?: string; valueTestId?: string;
className?: classNames.Argument;
}) => { }) => {
return ( return (
<div className={classNames(className)}> <div>
<dd className="text-3xl lg:text-4xl" data-testid={valueTestId}> <dd className="text-3xl lg:text-4xl" data-testid={valueTestId}>
{value} {value}
</dd> </dd>
@@ -3,6 +3,7 @@ import { Outlet } from 'react-router-dom';
import { Sidebar, SidebarContent, useSidebar } from '../sidebar'; import { Sidebar, SidebarContent, useSidebar } from '../sidebar';
import classNames from 'classnames'; import classNames from 'classnames';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id'; import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
export const LayoutWithSidebar = ({ export const LayoutWithSidebar = ({
header, header,
sidebar, sidebar,
@@ -16,7 +17,7 @@ export const LayoutWithSidebar = ({
const sidebarOpen = sidebarView !== null; const sidebarOpen = sidebarView !== null;
const gridClasses = classNames( const gridClasses = classNames(
'h-full relative z-0 grid', 'h-full relative z-0 grid',
'grid-rows-[min-content_1fr_50px]', 'grid-rows-[min-content_1fr_40px]',
'lg:grid-rows-[min-content_1fr]', 'lg:grid-rows-[min-content_1fr]',
'lg:grid-cols-[1fr_280px_40px]', 'lg:grid-cols-[1fr_280px_40px]',
'xxxl:grid-cols-[1fr_320px_40px]' 'xxxl:grid-cols-[1fr_320px_40px]'
@@ -26,13 +27,10 @@ export const LayoutWithSidebar = ({
<div className={gridClasses}> <div className={gridClasses}>
<div className="col-span-full">{header}</div> <div className="col-span-full">{header}</div>
<main <main
className={classNames( className={classNames('col-start-1 col-end-1 overflow-y-auto', {
'col-start-1 col-end-1 overflow-hidden lg:overflow-y-auto grow lg:grow-0', 'lg:col-end-3': !sidebarOpen,
{ 'hidden lg:block lg:col-end-2': sidebarOpen,
'lg:col-end-3': !sidebarOpen, })}
'hidden lg:block lg:col-end-2': sidebarOpen,
}
)}
> >
<Outlet /> <Outlet />
</main> </main>
@@ -1,2 +1 @@
export * from './market-header'; export * from './market-header';
export * from './mobile-market-header';
@@ -1,133 +0,0 @@
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { MarketSelector } from '../market-selector';
import {
Last24hPriceChange,
useMarket,
useMarketList,
} from '@vegaprotocol/markets';
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';
import { MarketHeaderStats } from '../../client-pages/market/market-header-stats';
import { MarketMarkPrice } from '../market-mark-price';
/**
* This is only rendered for the mobile navigation
*/
export const MobileMarketHeader = () => {
const t = useT();
const { marketId } = useParams();
const { data } = useMarket(marketId);
const [openMarket, setOpenMarket] = useState(false);
const [openPrice, setOpenPrice] = useState(false);
// Ensure that markets are kept cached so opening the list
// shows all markets instantly
useMarketList();
if (!marketId) return null;
return (
<div className="pl-3 pr-2 flex justify-between gap-2 h-10 bg-vega-clight-700 dark:bg-vega-cdark-700">
<FullScreenPopover
open={openMarket}
onOpenChange={(x) => {
setOpenMarket(x);
}}
trigger={
<h1 className="flex gap-1 sm:gap-2 md:gap-4 items-center text-base leading-3 md:text-lg whitespace-nowrap">
{data
? data.tradableInstrument.instrument.code
: t('Select market')}
<span
className={classNames(
'transition-transform ease-in-out duration-300 flex',
{
'rotate-180': openMarket,
}
)}
>
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={16} />
</span>
</h1>
}
>
<MarketSelector
currentMarketId={marketId}
onSelect={() => setOpenMarket(false)}
/>
</FullScreenPopover>
<FullScreenPopover
open={openPrice}
onOpenChange={(x) => {
setOpenPrice(x);
}}
trigger={
<span className="flex gap-2 items-end md:text-md whitespace-nowrap leading-3">
{data && (
<>
<span className="text-xs">
<Last24hPriceChange
marketId={data.id}
decimalPlaces={data.decimalPlaces}
/>
</span>
<span className="flex items-center gap-1">
<MarketMarkPrice
marketId={data.id}
decimalPlaces={data.decimalPlaces}
/>
<VegaIcon
name={VegaIconNames.CHEVRON_DOWN}
size={16}
className={classNames(
'transition-transform ease-in-out duration-300',
{
'rotate-180': openPrice,
}
)}
/>
</span>
</>
)}
</span>
}
>
{data && (
<div className="px-3 py-6 text-sm grid grid-cols-2 items-center gap-x-4 gap-y-6">
<MarketHeaderStats market={data} />
</div>
)}
</FullScreenPopover>
</div>
);
};
export interface PopoverProps extends PopoverPrimitive.PopoverProps {
trigger: React.ReactNode | string;
}
export const FullScreenPopover = ({
trigger,
children,
open,
onOpenChange,
}: PopoverProps) => {
return (
<PopoverPrimitive.Root open={open} onOpenChange={onOpenChange}>
<PopoverPrimitive.Trigger data-testid="popover-trigger">
{trigger}
</PopoverPrimitive.Trigger>
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-testid="popover-content"
className="w-screen bg-vega-clight-800 dark:bg-vega-cdark-800 border-y border-default"
sideOffset={0}
>
{children}
</PopoverPrimitive.Content>
</PopoverPrimitive.Portal>
</PopoverPrimitive.Root>
);
};
+1
View File
@@ -1 +1,2 @@
export * from './navbar'; export * from './navbar';
export * from './nav-header';
@@ -0,0 +1,81 @@
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { MarketSelector } from '../market-selector';
import { useMarket, useMarketList } from '@vegaprotocol/markets';
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
*/
export const NavHeader = () => {
const t = useT();
const { marketId } = useParams();
const { data } = useMarket(marketId);
const [open, setOpen] = useState(false);
// Ensure that markets are kept cached so opening the list
// shows all markets instantly
useMarketList();
if (!marketId) return null;
return (
<FullScreenPopover
open={open}
onOpenChange={(x) => {
setOpen(x);
}}
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>
</h1>
}
>
<MarketSelector
currentMarketId={marketId}
onSelect={() => setOpen(false)}
/>
</FullScreenPopover>
);
};
export interface PopoverProps extends PopoverPrimitive.PopoverProps {
trigger: React.ReactNode | string;
}
export const FullScreenPopover = ({
trigger,
children,
open,
onOpenChange,
}: PopoverProps) => {
return (
<PopoverPrimitive.Root open={open} onOpenChange={onOpenChange}>
<PopoverPrimitive.Trigger data-testid="popover-trigger">
{trigger}
</PopoverPrimitive.Trigger>
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-testid="popover-content"
className="w-screen bg-vega-clight-800 dark:bg-vega-cdark-800 text-default border border-default"
sideOffset={5}
>
{children}
</PopoverPrimitive.Content>
</PopoverPrimitive.Portal>
</PopoverPrimitive.Root>
);
};
+9 -1
View File
@@ -34,7 +34,13 @@ import { supportedLngs } from '../../lib/i18n';
type MenuState = 'wallet' | 'nav' | null; type MenuState = 'wallet' | 'nav' | null;
type Theme = 'system' | 'yellow'; type Theme = 'system' | 'yellow';
export const Navbar = ({ theme = 'system' }: { theme?: Theme }) => { export const Navbar = ({
children,
theme = 'system',
}: {
children?: ReactNode;
theme?: Theme;
}) => {
const i18n = useI18n(); const i18n = useI18n();
const t = useT(); const t = useT();
// menu state for small screens // menu state for small screens
@@ -69,6 +75,8 @@ export const Navbar = ({ theme = 'system' }: { theme?: Theme }) => {
> >
<VLogo className="w-4" /> <VLogo className="w-4" />
</NavLink> </NavLink>
{/* Left section */}
<div className="flex items-center lg:hidden">{children}</div>
{/* Used to show header in nav on mobile */} {/* Used to show header in nav on mobile */}
<div className="hidden lg:block"> <div className="hidden lg:block">
<NavbarMenu onClick={() => setMenu(null)} /> <NavbarMenu onClick={() => setMenu(null)} />
@@ -152,11 +152,7 @@ export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
if (!enrichedTransfers || !enrichedTransfers.length) return null; if (!enrichedTransfers || !enrichedTransfers.length) return null;
return ( return (
<Card <Card title={t('Active rewards')} className="lg:col-span-full">
title={t('Active rewards')}
className="lg:col-span-full"
data-testid="active-rewards-card"
>
{enrichedTransfers.length > 1 && ( {enrichedTransfers.length > 1 && (
<TradingInput <TradingInput
onChange={(e) => onChange={(e) =>
@@ -316,30 +312,49 @@ export const ActiveRewardCard = ({
MarketState.STATE_CLOSED, MarketState.STATE_CLOSED,
].includes(m.state) ].includes(m.state)
); );
if (marketSettled) { if (marketSettled) {
return null; return null;
} }
const assetInActiveMarket = const assetInSettledMarket =
allMarkets && allMarkets &&
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => { Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) { if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
return m?.state && MarketState.STATE_ACTIVE === m.state; return (
m?.state &&
[
MarketState.STATE_TRADING_TERMINATED,
MarketState.STATE_SETTLED,
MarketState.STATE_CANCELLED,
MarketState.STATE_CLOSED,
].includes(m.state)
);
} }
return false; return false;
}); });
const marketSuspended = transferNode.markets?.some( // Gray out the cards that are related to suspended markets
const suspended = transferNode.markets?.some(
(m) => (m) =>
m?.state === MarketState.STATE_SUSPENDED || m?.state === MarketState.STATE_SUSPENDED ||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
); );
const assetInSuspendedMarket =
allMarkets &&
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
return (
m?.state === MarketState.STATE_SUSPENDED ||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
);
}
return false;
});
// Gray out the cards that are related to suspended markets // Gray out the cards that are related to suspended markets
// Or settlement assets in markets that are not active and eligible for rewards
const { gradientClassName, mainClassName } = const { gradientClassName, mainClassName } =
marketSuspended || !assetInActiveMarket suspended || assetInSuspendedMarket || assetInSettledMarket
? { ? {
gradientClassName: 'from-vega-cdark-500 to-vega-clight-400', gradientClassName: 'from-vega-cdark-500 to-vega-clight-400',
mainClassName: 'from-vega-cdark-400 dark:from-vega-cdark-600 to-20%', mainClassName: 'from-vega-cdark-400 dark:from-vega-cdark-600 to-20%',
@@ -356,7 +371,6 @@ export const ActiveRewardCard = ({
'rounded-lg', 'rounded-lg',
gradientClassName gradientClassName
)} )}
data-testid="active-rewards-card"
> >
<div <div
className={classNames( className={classNames(
@@ -368,7 +382,7 @@ export const ActiveRewardCard = ({
<div className="flex flex-col gap-2 items-center text-center"> <div className="flex flex-col gap-2 items-center text-center">
<EntityIcon transfer={transfer} /> <EntityIcon transfer={transfer} />
{entityScope && ( {entityScope && (
<span className="text-muted text-xs" data-testid="entity-scope"> <span className="text-muted text-xs">
{EntityScopeLabelMapping[entityScope] || t('Unspecified')} {EntityScopeLabelMapping[entityScope] || t('Unspecified')}
</span> </span>
)} )}
@@ -376,7 +390,7 @@ export const ActiveRewardCard = ({
<div className="flex flex-col gap-2 items-center text-center"> <div className="flex flex-col gap-2 items-center text-center">
<h3 className="flex flex-col gap-1 text-2xl shrink-1 text-center"> <h3 className="flex flex-col gap-1 text-2xl shrink-1 text-center">
<span className="font-glitch" data-testid="reward-value"> <span className="font-glitch">
{addDecimalsFormatNumber( {addDecimalsFormatNumber(
transferNode.transfer.amount, transferNode.transfer.amount,
transferNode.transfer.asset?.decimals || 0, transferNode.transfer.asset?.decimals || 0,
@@ -397,7 +411,7 @@ export const ActiveRewardCard = ({
)} )}
underline={true} underline={true}
> >
<span className="text-xs" data-testid="distribution-strategy"> <span className="text-xs">
{ {
DistributionStrategyMapping[ DistributionStrategyMapping[
dispatchStrategy.distributionStrategy dispatchStrategy.distributionStrategy
@@ -415,10 +429,7 @@ export const ActiveRewardCard = ({
'Number of epochs after distribution to delay vesting of rewards by' 'Number of epochs after distribution to delay vesting of rewards by'
)} )}
/> />
<span <span className="text-muted text-xs whitespace-nowrap">
className="text-muted text-xs whitespace-nowrap"
data-testid="locked-for"
>
{t('numberEpochs', '{{count}} epochs', { {t('numberEpochs', '{{count}} epochs', {
count: kind.dispatchStrategy?.lockPeriod, count: kind.dispatchStrategy?.lockPeriod,
})} })}
@@ -427,15 +438,15 @@ export const ActiveRewardCard = ({
</div> </div>
<span className="border-[0.5px] border-gray-700" /> <span className="border-[0.5px] border-gray-700" />
<span data-testid="dispatch-metric-info"> <span>
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]} {' '} {DispatchMetricLabels[dispatchStrategy.dispatchMetric]} {' '}
<Tooltip <Tooltip
underline={marketSuspended} underline={suspended}
description={ description={
(marketSuspended || !assetInActiveMarket) && (suspended || assetInSuspendedMarket) &&
(specificMarkets (specificMarkets
? t('Eligible market(s) currently suspended') ? t('Eligible market(s) currently suspended')
: !assetInActiveMarket : assetInSuspendedMarket
? t('Currently no markets eligible for reward') ? t('Currently no markets eligible for reward')
: '') : '')
} }
@@ -447,8 +458,8 @@ export const ActiveRewardCard = ({
<div className="flex items-center gap-8 flex-wrap"> <div className="flex items-center gap-8 flex-wrap">
{kind.endEpoch && ( {kind.endEpoch && (
<span className="flex flex-col"> <span className="flex flex-col">
<span className="text-muted text-xs">{t('Ends in')} </span> <span className="text-muted text-xs">{t('Ends in')}</span>
<span data-testid="ends-in"> <span>
{t('numberEpochs', '{{count}} epochs', { {t('numberEpochs', '{{count}} epochs', {
count: kind.endEpoch - currentEpoch, count: kind.endEpoch - currentEpoch,
})} })}
@@ -459,7 +470,7 @@ export const ActiveRewardCard = ({
{ {
<span className="flex flex-col"> <span className="flex flex-col">
<span className="text-muted text-xs">{t('Assessed over')}</span> <span className="text-muted text-xs">{t('Assessed over')}</span>
<span data-testid="assessed-over"> <span>
{t('numberEpochs', '{{count}} epochs', { {t('numberEpochs', '{{count}} epochs', {
count: dispatchStrategy.windowLength, count: dispatchStrategy.windowLength,
})} })}
@@ -468,7 +479,7 @@ export const ActiveRewardCard = ({
} }
</div> </div>
{dispatchStrategy?.dispatchMetric && ( {dispatchStrategy?.dispatchMetric && (
<span className="text-muted text-sm h-[3rem]"> <span className="text-muted text-sm h-[2rem]">
{t(DispatchMetricDescription[dispatchStrategy?.dispatchMetric])} {t(DispatchMetricDescription[dispatchStrategy?.dispatchMetric])}
</span> </span>
)} )}
@@ -502,7 +513,7 @@ const RewardRequirements = ({
entity: EntityScopeLabelMapping[dispatchStrategy.entityScope], entity: EntityScopeLabelMapping[dispatchStrategy.entityScope],
})} })}
</dt> </dt>
<dd className="flex items-center gap-1" data-testid="scope"> <dd className="flex items-center gap-1">
<RewardEntityScope dispatchStrategy={dispatchStrategy} /> <RewardEntityScope dispatchStrategy={dispatchStrategy} />
</dd> </dd>
</div> </div>
@@ -511,10 +522,7 @@ const RewardRequirements = ({
<dt className="flex items-center gap-1 text-muted"> <dt className="flex items-center gap-1 text-muted">
{t('Staked VEGA')} {t('Staked VEGA')}
</dt> </dt>
<dd <dd className="flex items-center gap-1">
className="flex items-center gap-1"
data-testid="staking-requirement"
>
{addDecimalsFormatNumber( {addDecimalsFormatNumber(
dispatchStrategy?.stakingRequirement || 0, dispatchStrategy?.stakingRequirement || 0,
assetDecimalPlaces assetDecimalPlaces
@@ -526,7 +534,7 @@ const RewardRequirements = ({
<dt className="flex items-center gap-1 text-muted"> <dt className="flex items-center gap-1 text-muted">
{t('Average position')} {t('Average position')}
</dt> </dt>
<dd className="flex items-center gap-1" data-testid="average-position"> <dd className="flex items-center gap-1">
{addDecimalsFormatNumber( {addDecimalsFormatNumber(
dispatchStrategy?.notionalTimeWeightedAveragePositionRequirement || dispatchStrategy?.notionalTimeWeightedAveragePositionRequirement ||
0, 0,
@@ -81,7 +81,6 @@ export const Settings = () => {
intent={Intent.Primary} intent={Intent.Primary}
onClick={() => { onClick={() => {
localStorage.clear(); localStorage.clear();
sessionStorage.clear();
window.location.reload(); window.location.reload();
}} }}
> >
+25 -60
View File
@@ -17,7 +17,6 @@ import { useVegaWallet, useViewAsDialog } from '@vegaprotocol/wallet';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id'; import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { ErrorBoundary } from '../error-boundary'; import { ErrorBoundary } from '../error-boundary';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
export enum ViewType { export enum ViewType {
Order = 'Order', Order = 'Order',
@@ -27,10 +26,9 @@ export enum ViewType {
Transfer = 'Transfer', Transfer = 'Transfer',
Settings = 'Settings', Settings = 'Settings',
ViewAs = 'ViewAs', ViewAs = 'ViewAs',
Close = 'Close',
} }
export type BarView = type SidebarView =
| { | {
type: ViewType.Deposit; type: ViewType.Deposit;
assetId?: string; assetId?: string;
@@ -51,9 +49,6 @@ export type BarView =
} }
| { | {
type: ViewType.Settings; type: ViewType.Settings;
}
| {
type: ViewType.Close;
}; };
export const Sidebar = ({ options }: { options?: ReactNode }) => { export const Sidebar = ({ options }: { options?: ReactNode }) => {
@@ -62,52 +57,26 @@ export const Sidebar = ({ options }: { options?: ReactNode }) => {
const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1'; const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1';
const setViewAsDialogOpen = useViewAsDialog((state) => state.setOpen); const setViewAsDialogOpen = useViewAsDialog((state) => state.setOpen);
const { pubKeys } = useVegaWallet(); const { pubKeys } = useVegaWallet();
const { isMobile } = useScreenDimensions();
const { getView } = useSidebar((store) => ({
setViews: store.setViews,
getView: store.getView,
}));
const currView = getView(currentRouteId);
return ( return (
<div className="flex h-full lg:flex-col gap-1" data-testid="sidebar"> <div className="flex h-full p-1 lg:flex-col gap-2" data-testid="sidebar">
{options && ( {options && <nav className={navClasses}>{options}</nav>}
<nav className={classNames(navClasses, 'flex grow')}>{options}</nav> <nav className={classNames(navClasses, 'ml-auto lg:mt-auto lg:ml-0')}>
)} <SidebarButton
<nav view={ViewType.ViewAs}
className={classNames( onClick={() => {
navClasses, setViewAsDialogOpen(true);
'ml-auto lg:mt-auto lg:ml-0 shrink-0' }}
)} icon={VegaIconNames.EYE}
> tooltip={t('View as party')}
{!isMobile ? ( disabled={Boolean(pubKeys)}
<> routeId={currentRouteId}
<SidebarButton />
view={ViewType.ViewAs} <SidebarButton
onClick={() => { view={ViewType.Settings}
setViewAsDialogOpen(true); icon={VegaIconNames.COG}
}} tooltip={t('Settings')}
icon={VegaIconNames.EYE} routeId={currentRouteId}
tooltip={t('View as party')} />
disabled={Boolean(pubKeys)}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Settings}
icon={VegaIconNames.COG}
tooltip={t('Settings')}
routeId={currentRouteId}
/>
</>
) : (
currView && (
<SidebarButton
view={ViewType.Close}
icon={VegaIconNames.ARROW_LEFT}
tooltip={t('Back')}
routeId={currentRouteId}
/>
)
)}
<NodeHealthContainer /> <NodeHealthContainer />
</nav> </nav>
</div> </div>
@@ -134,7 +103,7 @@ export const SidebarButton = ({
getView: store.getView, getView: store.getView,
})); }));
const currView = getView(routeId); const currView = getView(routeId);
const onSelect = (view: BarView['type']) => { const onSelect = (view: SidebarView['type']) => {
if (view === currView?.type) { if (view === currView?.type) {
setViews(null, routeId); setViews(null, routeId);
} else { } else {
@@ -164,7 +133,7 @@ export const SidebarButton = ({
<button <button
className={buttonClasses} className={buttonClasses}
data-testid={view} data-testid={view}
onClick={onClick || (() => onSelect(view as BarView['type']))} onClick={onClick || (() => onSelect(view as SidebarView['type']))}
disabled={disabled} disabled={disabled}
> >
<VegaIcon name={icon} size={20} /> <VegaIcon name={icon} size={20} />
@@ -211,10 +180,6 @@ export const SidebarContent = () => {
} }
} }
if (view.type === ViewType.Close) {
return <CloseSidebar />;
}
if (view.type === ViewType.Info) { if (view.type === ViewType.Info) {
if (params.marketId) { if (params.marketId) {
return ( return (
@@ -302,9 +267,9 @@ const CloseSidebar = () => {
}; };
export const useSidebar = create<{ export const useSidebar = create<{
views: { [key: string]: BarView | null }; views: { [key: string]: SidebarView | null };
setViews: (view: BarView | null, routeId: string) => void; setViews: (view: SidebarView | null, routeId: string) => void;
getView: (routeId: string) => BarView | null | undefined; getView: (routeId: string) => SidebarView | null | undefined;
}>()((set, get) => ({ }>()((set, get) => ({
views: {}, views: {},
setViews: (x, routeId) => setViews: (x, routeId) =>
+1 -1
View File
@@ -1,3 +1,3 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
VEGA_VERSION=v0.74.0-preview.10 VEGA_VERSION=v0.74.0-preview.7
LOCAL_SERVER=false LOCAL_SERVER=false
+2 -27
View File
@@ -111,7 +111,6 @@ def init_vega(request=None):
f"Container {container.id} started", f"Container {container.id} started",
extra={"worker_id": os.environ.get("PYTEST_XDIST_WORKER")}, extra={"worker_id": os.environ.get("PYTEST_XDIST_WORKER")},
) )
vega.container = container
yield vega yield vega
except APIError as e: except APIError as e:
logger.info(f"Container creation failed.") logger.info(f"Container creation failed.")
@@ -178,34 +177,10 @@ def init_page(vega: VegaServiceNull, browser: Browser, request: pytest.FixtureRe
@pytest.fixture @pytest.fixture
def vega(request): def vega(request):
with init_vega(request) as vega_instance: with init_vega(request) as vega:
request.addfinalizer(lambda: cleanup_container(vega_instance)) yield vega
yield vega_instance
def cleanup_container(vega_instance):
try:
# Attempt to stop the container if it's still running
if vega_instance.container.status == 'running':
print(f"Stopping container {vega_instance.container.id}")
vega_instance.container.stop()
else:
print(f"Container {vega_instance.container.id} is not running.")
except docker.errors.NotFound:
print(f"Container {vega_instance.container.id} not found, may have been stopped and removed.")
except Exception as e:
print(f"Error during cleanup: {str(e)}")
try:
# Attempt to remove the container
vega_instance.container.remove()
print(f"Container {vega_instance.container.id} removed.")
except docker.errors.NotFound:
print(f"Container {vega_instance.container.id} not found, may have been removed.")
except Exception as e:
print(f"Error during container removal: {str(e)}")
@pytest.fixture @pytest.fixture
def page(vega, browser, request): def page(vega, browser, request):
with init_page(vega, browser, request) as page_instance: with init_page(vega, browser, request) as page_instance:
@@ -2,7 +2,7 @@ import pytest
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from datetime import datetime, timedelta from datetime import datetime, timedelta
from conftest import init_vega, cleanup_container from conftest import init_vega
from fixtures.market import setup_continuous_market from fixtures.market import setup_continuous_market
from actions.utils import wait_for_toast_confirmation from actions.utils import wait_for_toast_confirmation
@@ -17,10 +17,8 @@ expire = "expire"
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega(request):
with init_vega(request) as vega_instance: with init_vega(request) as vega:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function yield vega
yield vega_instance
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@@ -3,7 +3,7 @@ from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from actions.vega import submit_order from actions.vega import submit_order
from datetime import datetime, timedelta from datetime import datetime, timedelta
from conftest import init_vega, cleanup_container from conftest import init_vega
from fixtures.market import setup_continuous_market from fixtures.market import setup_continuous_market
stop_order_btn = "order-type-Stop" stop_order_btn = "order-type-Stop"
@@ -259,10 +259,9 @@ def test_submit_stop_limit_order_cancel(
class TestStopOcoValidation: class TestStopOcoValidation:
@pytest.fixture(scope="class") @pytest.fixture(scope="class")
def vega(request): def vega(self, request):
with init_vega(request) as vega_instance: with init_vega(request) as vega:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function yield vega
yield vega_instance
@pytest.fixture(scope="class") @pytest.fixture(scope="class")
def continuous_market(self, vega): def continuous_market(self, vega):
@@ -2,7 +2,7 @@ import pytest
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from actions.utils import change_keys from actions.utils import change_keys
from conftest import init_vega, cleanup_container from conftest import init_vega
from fixtures.market import setup_continuous_market from fixtures.market import setup_continuous_market
order_size = "order-size" order_size = "order-size"
@@ -14,9 +14,8 @@ deal_ticket_deposit_dialog_button = "deal-ticket-deposit-dialog-button"
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega(request):
with init_vega(request) as vega_instance: with init_vega(request) as vega:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function yield vega
yield vega_instance
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@@ -34,7 +33,7 @@ def test_should_display_info_and_button_for_deposit(continuous_market, page: Pag
"You may not have enough margin available to open this position.") "You may not have enough margin available to open this position.")
page.get_by_test_id(deal_ticket_warning_margin).hover() page.get_by_test_id(deal_ticket_warning_margin).hover()
expect(page.get_by_test_id("tooltip-content").nth(0)).to_have_text( expect(page.get_by_test_id("tooltip-content").nth(0)).to_have_text(
"1,661,888.12901 tDAI is currently required.You have only 999,991.49731.Deposit tDAI") "1,661,896.6317 tDAI is currently required.You have only 1,000,000.00.Deposit tDAI")
page.get_by_test_id(deal_ticket_deposit_dialog_button).nth(0).click() page.get_by_test_id(deal_ticket_deposit_dialog_button).nth(0).click()
expect(page.get_by_test_id("sidebar-content") expect(page.get_by_test_id("sidebar-content")
).to_contain_text("DepositFrom") ).to_contain_text("DepositFrom")
+6 -11
View File
@@ -4,7 +4,7 @@ from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from actions.vega import submit_order from actions.vega import submit_order
from wallet_config import MM_WALLET from wallet_config import MM_WALLET
from conftest import init_vega, init_page, auth_setup, cleanup_container from conftest import init_vega, init_page, auth_setup
from actions.utils import next_epoch, change_keys, forward_time from actions.utils import next_epoch, change_keys, forward_time
from fixtures.market import market_exists, setup_continuous_market from fixtures.market import market_exists, setup_continuous_market
@@ -82,36 +82,31 @@ def market_ids():
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega_volume_discount_tier_1(request): def vega_volume_discount_tier_1(request):
with init_vega(request) as vega_volume_discount_tier_1: with init_vega(request) as vega_volume_discount_tier_1:
request.addfinalizer(lambda: cleanup_container(vega_volume_discount_tier_1)) # Register the cleanup function yield vega_volume_discount_tier_1
yield vega_volume_discount_tier_1
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega_volume_discount_tier_2(request): def vega_volume_discount_tier_2(request):
with init_vega(request) as vega_volume_discount_tier_2: with init_vega(request) as vega_volume_discount_tier_2:
request.addfinalizer(lambda: cleanup_container(vega_volume_discount_tier_2)) # Register the cleanup function yield vega_volume_discount_tier_2
yield vega_volume_discount_tier_2
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega_referral_discount_tier_1(request): def vega_referral_discount_tier_1(request):
with init_vega(request) as vega_referral_discount_tier_1: with init_vega(request) as vega_referral_discount_tier_1:
request.addfinalizer(lambda: cleanup_container(vega_referral_discount_tier_1)) # Register the cleanup function yield vega_referral_discount_tier_1
yield vega_referral_discount_tier_1
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega_referral_discount_tier_2(request): def vega_referral_discount_tier_2(request):
with init_vega(request) as vega_referral_discount_tier_2: with init_vega(request) as vega_referral_discount_tier_2:
request.addfinalizer(lambda: cleanup_container(vega_referral_discount_tier_2)) # Register the cleanup function yield vega_referral_discount_tier_2
yield vega_referral_discount_tier_2
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega_referral_and_volume_discount(request): def vega_referral_and_volume_discount(request):
with init_vega(request) as vega_referral_and_volume_discount: with init_vega(request) as vega_referral_and_volume_discount:
request.addfinalizer(lambda: cleanup_container(vega_referral_and_volume_discount)) # Register the cleanup function yield vega_referral_and_volume_discount
yield vega_referral_and_volume_discount
@pytest.fixture @pytest.fixture
@@ -3,7 +3,7 @@ from playwright.sync_api import expect, Page
import json import json
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from fixtures.market import setup_simple_market from fixtures.market import setup_simple_market
from conftest import init_vega, cleanup_container from conftest import init_vega
from actions.vega import submit_order from actions.vega import submit_order
from wallet_config import MM_WALLET, TERMINATE_WALLET, wallets from wallet_config import MM_WALLET, TERMINATE_WALLET, wallets
import logging import logging
@@ -12,10 +12,9 @@ logger = logging.getLogger()
@pytest.fixture(scope="class") @pytest.fixture(scope="class")
def vega(request): def vega():
with init_vega(request) as vega_instance: with init_vega() as vega:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function yield vega
yield vega_instance
@pytest.fixture(scope="class") @pytest.fixture(scope="class")
@@ -2,6 +2,8 @@ import pytest
from playwright.sync_api import expect, Page from playwright.sync_api import expect, Page
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from actions.vega import submit_order from actions.vega import submit_order
from conftest import init_vega
from fixtures.market import setup_continuous_market
from wallet_config import MM_WALLET2 from wallet_config import MM_WALLET2
def hover_and_assert_tooltip(page: Page, element_text): def hover_and_assert_tooltip(page: Page, element_text):
@@ -9,30 +11,39 @@ def hover_and_assert_tooltip(page: Page, element_text):
element.hover() element.hover()
expect(page.get_by_role("tooltip")).to_be_visible() expect(page.get_by_role("tooltip")).to_be_visible()
class TestIcebergOrdersValidations:
@pytest.fixture(scope="class")
def vega(self, request):
with init_vega(request) as vega:
yield vega
@pytest.mark.usefixtures("auth", "risk_accepted") @pytest.fixture(scope="class")
def test_iceberg_submit(continuous_market, vega: VegaServiceNull, page: Page): def continuous_market(self, vega):
page.goto(f"/#/markets/{continuous_market}") return setup_continuous_market(vega)
page.get_by_test_id("iceberg").click()
page.get_by_test_id("order-peak-size").type("2")
page.get_by_test_id("order-minimum-size").type("1")
page.get_by_test_id("order-size").type("3")
page.get_by_test_id("order-price").type("107")
page.get_by_test_id("place-order").click()
expect(page.get_by_test_id("toast-content")).to_have_text( @pytest.mark.usefixtures("auth", "risk_accepted")
"Awaiting confirmationPlease wait for your transaction to be confirmedView in block explorer" def test_iceberg_submit(self, continuous_market, vega: VegaServiceNull, page: Page):
) page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id("iceberg").click()
page.get_by_test_id("order-peak-size").type("2")
page.get_by_test_id("order-minimum-size").type("1")
page.get_by_test_id("order-size").type("3")
page.get_by_test_id("order-price").type("107")
page.get_by_test_id("place-order").click()
vega.wait_fn(1) expect(page.get_by_test_id("toast-content")).to_have_text(
vega.wait_for_total_catchup() "Awaiting confirmationPlease wait for your transaction to be confirmedView in block explorer"
expect(page.get_by_test_id("toast-content")).to_have_text( )
"Order filledYour transaction has been confirmedView in block explorerSubmit order - filledBTC:DAI_2023+3 @ 107.00 tDAI"
) vega.wait_fn(1)
page.get_by_test_id("All").click() vega.wait_for_total_catchup()
expect( expect(page.get_by_test_id("toast-content")).to_have_text(
(page.get_by_role("row").locator('[col-id="type"]')).nth(1) "Order filledYour transaction has been confirmedView in block explorerSubmit order - filledBTC:DAI_2023+3 @ 107.00 tDAI"
).to_have_text("Limit (Iceberg)") )
page.get_by_test_id("All").click()
expect(
(page.get_by_role("row").locator('[col-id="type"]')).nth(1)
).to_have_text("Limit (Iceberg)")
@pytest.mark.usefixtures("auth", "risk_accepted") @pytest.mark.usefixtures("auth", "risk_accepted")
def test_iceberg_open_order(continuous_market, vega: VegaServiceNull, page: Page): def test_iceberg_open_order(continuous_market, vega: VegaServiceNull, page: Page):
@@ -1,16 +1,15 @@
import pytest import pytest
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from conftest import init_vega, cleanup_container from conftest import init_vega
from fixtures.market import setup_continuous_market from fixtures.market import setup_continuous_market
from actions.utils import next_epoch, truncate_middle, change_keys from actions.utils import next_epoch, truncate_middle, change_keys
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega(request):
with init_vega(request) as vega_instance: with init_vega(request) as vega:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function yield vega
yield vega_instance
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@@ -4,15 +4,14 @@ import vega_sim.api.governance as governance
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from fixtures.market import setup_continuous_market from fixtures.market import setup_continuous_market
from conftest import init_vega, cleanup_container from conftest import init_vega
from actions.utils import next_epoch from actions.utils import next_epoch
@pytest.fixture(scope="class") @pytest.fixture(scope="class")
def vega(request): def vega():
with init_vega(request) as vega_instance: with init_vega() as vega:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function yield vega
yield vega_instance
@pytest.fixture(scope="class") @pytest.fixture(scope="class")
@@ -3,16 +3,15 @@ import pytest
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from fixtures.market import setup_continuous_market from fixtures.market import setup_continuous_market
from conftest import init_page, init_vega, risk_accepted_setup, cleanup_container from conftest import init_page, init_vega, risk_accepted_setup
market_title_test_id = "accordion-title" market_title_test_id = "accordion-title"
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega():
with init_vega(request) as vega_instance: with init_vega() as vega:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function yield vega
yield vega_instance
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@@ -3,7 +3,7 @@ import vega_sim.api.governance as governance
import re import re
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from conftest import init_vega, cleanup_container from conftest import init_vega
from fixtures.market import setup_simple_market from fixtures.market import setup_simple_market
from wallet_config import MM_WALLET from wallet_config import MM_WALLET
@@ -13,10 +13,8 @@ col_market_id = '[col-id="market"] [data-testid="stack-cell-primary"]'
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega(request):
with init_vega(request) as vega_instance: with init_vega(request) as vega:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function yield vega
yield vega_instance
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@@ -3,7 +3,7 @@ from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from actions.vega import submit_order from actions.vega import submit_order
from fixtures.market import setup_simple_market from fixtures.market import setup_simple_market
from conftest import init_vega, cleanup_container from conftest import init_vega
from actions.utils import wait_for_toast_confirmation, change_keys from actions.utils import wait_for_toast_confirmation, change_keys
from wallet_config import MM_WALLET, MM_WALLET2 from wallet_config import MM_WALLET, MM_WALLET2
@@ -15,10 +15,8 @@ COL_ID_FEE = ".ag-center-cols-container [col-id='fee'] .ag-cell-value"
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega(request):
with init_vega(request) as vega_instance: with init_vega(request) as vega:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function yield vega
yield vega_instance
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@@ -37,7 +37,7 @@ def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page):
# 6002-MDET-004 # 6002-MDET-004
expect(page.get_by_test_id("market-change")).to_have_text("Change (24h)0.00%0.00") expect(page.get_by_test_id("market-change")).to_have_text("Change (24h)0.00%0.00")
# 6002-MDET-005 # 6002-MDET-005
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)- (- BTC)") expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)-")
# 6002-MDET-008 # 6002-MDET-008
expect(page.get_by_test_id("market-settlement-asset")).to_have_text( expect(page.get_by_test_id("market-settlement-asset")).to_have_text(
"Settlement assettDAI" "Settlement assettDAI"
@@ -1,14 +1,13 @@
import pytest import pytest
from playwright.sync_api import Page, expect, Locator from playwright.sync_api import Page, expect, Locator
from conftest import init_page, init_vega, cleanup_container from conftest import init_page, init_vega
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega():
with init_vega(request) as vega_instance: with init_vega() as vega:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function yield vega
yield vega_instance
# we can reuse single page instance in all tests # we can reuse single page instance in all tests
@@ -2,7 +2,7 @@ import pytest
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from vega_sim.service import PeggedOrder from vega_sim.service import PeggedOrder
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from conftest import auth_setup, init_page, init_vega, risk_accepted_setup, cleanup_container from conftest import auth_setup, init_page, init_vega, risk_accepted_setup
from fixtures.market import setup_continuous_market, setup_simple_market from fixtures.market import setup_continuous_market, setup_simple_market
from actions.utils import wait_for_toast_confirmation from actions.utils import wait_for_toast_confirmation
@@ -11,9 +11,8 @@ order_tab = "tab-orders"
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega(request):
with init_vega(request) as vega_instance: with init_vega(request) as vega:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function yield vega
yield vega_instance
@pytest.fixture(scope="module", autouse=True) @pytest.fixture(scope="module", autouse=True)
@@ -2,16 +2,15 @@ import pytest
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from typing import List from typing import List
from actions.vega import submit_order, submit_liquidity, submit_multiple_orders from actions.vega import submit_order, submit_liquidity, submit_multiple_orders
from conftest import init_vega, cleanup_container from conftest import init_vega
from fixtures.market import setup_simple_market from fixtures.market import setup_simple_market
from wallet_config import MM_WALLET, MM_WALLET2 from wallet_config import MM_WALLET, MM_WALLET2
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega():
with init_vega(request) as vega_instance: with init_vega() as vega:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function yield vega
yield vega_instance
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@@ -1,7 +1,7 @@
import pytest import pytest
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from conftest import init_vega, cleanup_container from conftest import init_vega
from fixtures.market import setup_continuous_market from fixtures.market import setup_continuous_market
TOOLTIP_LABEL = "margin-health-tooltip-label" TOOLTIP_LABEL = "margin-health-tooltip-label"
@@ -11,10 +11,8 @@ COL_ID_USED = ".ag-center-cols-container [col-id='used'] .ag-cell-value"
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega(request):
with init_vega(request) as vega_instance: with init_vega(request) as vega:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function yield vega
yield vega_instance
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@@ -1,7 +1,7 @@
import pytest import pytest
from playwright.sync_api import Page from playwright.sync_api import Page
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from conftest import init_vega, cleanup_container from conftest import init_vega
from fixtures.market import setup_continuous_market, setup_simple_market from fixtures.market import setup_continuous_market, setup_simple_market
from actions.utils import change_keys, create_and_faucet_wallet, forward_time, selector_contains_text from actions.utils import change_keys, create_and_faucet_wallet, forward_time, selector_contains_text
from actions.vega import submit_order, submit_liquidity from actions.vega import submit_order, submit_liquidity
@@ -14,10 +14,8 @@ BUY_ORDERS = [[1, 106], [1, 107], [1, 108]]
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega(request):
with init_vega(request) as vega_instance: with init_vega(request) as vega:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function yield vega
yield vega_instance
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@@ -59,7 +59,7 @@ def test_filtered_cards(continuous_market, vega: VegaServiceNull, page: Page):
next_epoch(vega=vega) next_epoch(vega=vega)
page.reload() page.reload()
expect(page.get_by_test_id("active-rewards-card")).to_be_visible(timeout=15000) expect(page.locator(".from-vega-cdark-400")).to_be_visible(timeout=15000)
governance.submit_oracle_data( governance.submit_oracle_data(
wallet=vega.wallet, wallet=vega.wallet,
payload={"trading.terminated": "true"}, payload={"trading.terminated": "true"},
@@ -67,4 +67,4 @@ def test_filtered_cards(continuous_market, vega: VegaServiceNull, page: Page):
) )
next_epoch(vega=vega) next_epoch(vega=vega)
page.reload() page.reload()
expect(page.get_by_test_id("active-rewards-card")).not_to_be_in_viewport() expect(page.locator(".from-vega-cdark-400")).not_to_be_in_viewport()
@@ -2,7 +2,7 @@ import pytest
import vega_sim.proto.vega as vega_protos import vega_sim.proto.vega as vega_protos
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from conftest import init_vega, init_page, auth_setup, cleanup_container from conftest import init_vega, init_page, auth_setup
from fixtures.market import setup_continuous_market, market_exists from fixtures.market import setup_continuous_market, market_exists
from actions.utils import next_epoch, change_keys from actions.utils import next_epoch, change_keys
from wallet_config import MM_WALLET, PARTY_A, PARTY_B, PARTY_C, PARTY_D from wallet_config import MM_WALLET, PARTY_A, PARTY_B, PARTY_C, PARTY_D
@@ -46,42 +46,36 @@ def market_ids():
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega_activity_tier_0(request): def vega_activity_tier_0(request):
with init_vega(request) as vega_activity_tier_0: with init_vega(request) as vega_activity_tier_0:
request.addfinalizer(lambda: cleanup_container(vega_activity_tier_0)) # Register the cleanup function
yield vega_activity_tier_0 yield vega_activity_tier_0
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega_hoarder_tier_0(request): def vega_hoarder_tier_0(request):
with init_vega(request) as vega_hoarder_tier_0: with init_vega(request) as vega_hoarder_tier_0:
request.addfinalizer(lambda: cleanup_container(vega_hoarder_tier_0)) # Register the cleanup function
yield vega_hoarder_tier_0 yield vega_hoarder_tier_0
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega_combo_tier_0(request): def vega_combo_tier_0(request):
with init_vega(request) as vega_combo_tier_0: with init_vega(request) as vega_combo_tier_0:
request.addfinalizer(lambda: cleanup_container(vega_combo_tier_0)) # Register the cleanup function
yield vega_combo_tier_0 yield vega_combo_tier_0
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega_activity_tier_1(request): def vega_activity_tier_1(request):
with init_vega(request) as vega_activity_tier_1: with init_vega(request) as vega_activity_tier_1:
request.addfinalizer(lambda: cleanup_container(vega_activity_tier_1)) # Register the cleanup function
yield vega_activity_tier_1 yield vega_activity_tier_1
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega_hoarder_tier_1(request): def vega_hoarder_tier_1(request):
with init_vega(request) as vega_hoarder_tier_1: with init_vega(request) as vega_hoarder_tier_1:
request.addfinalizer(lambda: cleanup_container(vega_hoarder_tier_1)) # Register the cleanup function
yield vega_hoarder_tier_1 yield vega_hoarder_tier_1
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega_combo_tier_1(request): def vega_combo_tier_1(request):
with init_vega(request) as vega_combo_tier_1: with init_vega(request) as vega_combo_tier_1:
request.addfinalizer(lambda: cleanup_container(vega_combo_tier_1)) # Register the cleanup function
yield vega_combo_tier_1 yield vega_combo_tier_1
@@ -1,176 +0,0 @@
import pytest
import vega_sim.proto.vega as vega_protos
from playwright.sync_api import Page, expect
from conftest import init_vega, init_page, auth_setup, risk_accepted_setup, cleanup_container
from fixtures.market import setup_continuous_market
from actions.utils import next_epoch, change_keys, create_and_faucet_wallet
from wallet_config import MM_WALLET, WalletConfig
from vega_sim.null_service import VegaServiceNull
# region Constants
ACTIVITY = "activity"
HOARDER = "hoarder"
COMBO = "combo"
REWARDS_URL = "/#/rewards"
# test IDs
COMBINED_MULTIPLIERS = "combined-multipliers"
TOTAL_REWARDS = "total-rewards"
PRICE_TAKING_COL_ID = '[col-id="priceTaking"]'
TOTAL_COL_ID = '[col-id="total"]'
ROW = "row"
STREAK_REWARD_MULTIPLIER_VALUE = "streak-reward-multiplier-value"
HOARDER_REWARD_MULTIPLIER_VALUE = "hoarder-reward-multiplier-value"
HOARDER_BONUS_TOTAL_HOARDED = "hoarder-bonus-total-hoarded"
EARNED_BY_ME_BUTTON = "earned-by-me-button"
TRANSFER_AMOUNT = "transfer-amount"
EPOCH_STREAK = "epoch-streak"
# endregion
# Keys
PARTY_A = "PARTY_A"
PARTY_B = "PARTY_B"
PARTY_C = "PARTY_C"
PARTY_D = "PARTY_D"
@pytest.fixture(scope="module")
def vega(request):
with init_vega(request) as vega_instance:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module")
def page(vega, browser, request):
with init_page(vega, browser, request) as page:
risk_accepted_setup(page)
auth_setup(vega, page)
page.goto(REWARDS_URL)
change_keys(page, vega, PARTY_B)
yield page
@pytest.fixture(scope="module", autouse=True)
def setup_market_with_reward_program(vega: VegaServiceNull):
tDAI_market = setup_continuous_market(vega)
PARTY_A, PARTY_B, PARTY_C, PARTY_D = keys(vega)
tDAI_asset_id = vega.find_asset_id(symbol="tDAI")
vega.mint(key_name=PARTY_B.name, asset=tDAI_asset_id, amount=100000)
vega.mint(key_name=PARTY_C.name, asset=tDAI_asset_id, amount=100000)
vega.mint(key_name=PARTY_A.name, asset=tDAI_asset_id, amount=100000)
vega.mint(key_name=PARTY_D.name, asset=tDAI_asset_id, amount=100000)
next_epoch(vega=vega)
vega.update_network_parameter(
proposal_key=MM_WALLET.name,
parameter="rewards.activityStreak.benefitTiers",
new_value=ACTIVITY_STREAKS,
)
print("update_network_parameter activity done")
next_epoch(vega=vega)
tDAI_asset_id = vega.find_asset_id(symbol="tDAI")
vega.update_network_parameter(
MM_WALLET.name, parameter="reward.asset", new_value=tDAI_asset_id
)
next_epoch(vega=vega)
vega.recurring_transfer(
from_key_name=PARTY_A.name,
from_account_type=vega_protos.vega.ACCOUNT_TYPE_GENERAL,
to_account_type=vega_protos.vega.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
asset=tDAI_asset_id,
reference="reward",
asset_for_metric=tDAI_asset_id,
metric=vega_protos.vega.DISPATCH_METRIC_MAKER_FEES_PAID,
amount=100,
factor=1.0,
)
vega.submit_order(
trading_key=PARTY_B.name,
market_id=tDAI_market,
order_type="TYPE_MARKET",
time_in_force="TIME_IN_FORCE_IOC",
side="SIDE_BUY",
volume=1,
)
vega.submit_order(
trading_key=PARTY_A.name,
market_id=tDAI_market,
order_type="TYPE_MARKET",
time_in_force="TIME_IN_FORCE_IOC",
side="SIDE_BUY",
volume=1,
)
vega.wait_for_total_catchup()
next_epoch(vega=vega)
return tDAI_market, tDAI_asset_id
ACTIVITY_STREAKS = """
{
"tiers": [
{
"minimum_activity_streak": 2,
"reward_multiplier": "2.0",
"vesting_multiplier": "1.1"
}
]
}
"""
def keys(vega):
PARTY_A = WalletConfig("PARTY_A", "PARTY_A")
create_and_faucet_wallet(vega=vega, wallet=PARTY_A)
PARTY_B = WalletConfig("PARTY_B", "PARTY_B")
create_and_faucet_wallet(vega=vega, wallet=PARTY_B)
PARTY_C = WalletConfig("PARTY_C", "PARTY_C")
create_and_faucet_wallet(vega=vega, wallet=PARTY_C)
PARTY_D = WalletConfig("PARTY_D", "PARTY_D")
create_and_faucet_wallet(vega=vega, wallet=PARTY_D)
return PARTY_A, PARTY_B, PARTY_C, PARTY_D
@pytest.mark.xdist_group(name="test_rewards_activity_tier_0")
def test_network_reward_pot(
page: Page,
):
expect(page.get_by_test_id(TOTAL_REWARDS)).to_have_text("50.00 tDAI")
@pytest.mark.xdist_group(name="test_rewards_activity_tier_0")
def test_reward_multiplier(
page: Page,
):
expect(page.get_by_test_id(COMBINED_MULTIPLIERS)).to_have_text("1x")
expect(page.get_by_test_id(STREAK_REWARD_MULTIPLIER_VALUE)).to_have_text("1x")
expect(page.get_by_test_id(HOARDER_REWARD_MULTIPLIER_VALUE)).to_have_text("1x")
@pytest.mark.xdist_group(name="test_rewards_activity_tier_0")
def test_activity_streak(
page: Page,
):
expect(page.get_by_test_id(EPOCH_STREAK)).to_have_text(
"Active trader: 1 epochs so far "
)
@pytest.mark.xdist_group(name="test_rewards_activity_tier_0")
def test_reward_history(
page: Page,
):
page.locator('[name="fromEpoch"]').fill("1")
expect((page.get_by_role(ROW).locator(PRICE_TAKING_COL_ID)).nth(1)).to_have_text(
"100.00100.00%"
)
expect((page.get_by_role(ROW).locator(TOTAL_COL_ID)).nth(1)).to_have_text("100.00")
page.get_by_test_id(EARNED_BY_ME_BUTTON).click()
expect((page.get_by_role(ROW).locator(TOTAL_COL_ID)).nth(1)).to_have_text("50.00")
@@ -1,13 +1,12 @@
import pytest import pytest
from playwright.sync_api import expect, Page from playwright.sync_api import expect, Page
from conftest import init_vega, cleanup_container from conftest import init_vega
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega():
with init_vega(request) as vega_instance: with init_vega() as vega:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function yield vega
yield vega_instance
@pytest.mark.usefixtures("risk_accepted") @pytest.mark.usefixtures("risk_accepted")
+47 -165
View File
@@ -2,18 +2,17 @@ import pytest
from playwright.sync_api import expect, Page from playwright.sync_api import expect, Page
import vega_sim.proto.vega as vega_protos import vega_sim.proto.vega as vega_protos
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from conftest import init_vega, cleanup_container from conftest import init_vega
from actions.utils import next_epoch, change_keys from actions.utils import next_epoch
from fixtures.market import setup_continuous_market from fixtures.market import setup_continuous_market
from conftest import auth_setup, init_page, init_vega, risk_accepted_setup from conftest import auth_setup, init_page, init_vega, risk_accepted_setup
from wallet_config import PARTY_A, PARTY_B, PARTY_C, PARTY_D, MM_WALLET from wallet_config import PARTY_A, PARTY_B, PARTY_C, PARTY_D, MM_WALLET
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega(request):
with init_vega(request) as vega_instance: with init_vega(request) as vega:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function yield vega
yield vega_instance
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def team_page(vega, browser, request, setup_teams_and_games): def team_page(vega, browser, request, setup_teams_and_games):
@@ -24,20 +23,9 @@ def team_page(vega, browser, request, setup_teams_and_games):
page.goto(f"/#/competitions/teams/{team_id}") page.goto(f"/#/competitions/teams/{team_id}")
yield page yield page
@pytest.fixture(scope="module")
def competitions_page(vega, browser, request, setup_teams_and_games):
with init_page(vega, browser, request) as page:
risk_accepted_setup(page)
auth_setup(vega, page)
team_id = setup_teams_and_games["team_id"]
page.goto(f"/#/competitions/")
yield page
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def setup_teams_and_games(vega: VegaServiceNull): def setup_teams_and_games(vega: VegaServiceNull):
tDAI_market = setup_continuous_market(vega, custom_quantum=100000) tDAI_market = setup_continuous_market(vega)
tDAI_asset_id = vega.find_asset_id(symbol="tDAI") tDAI_asset_id = vega.find_asset_id(symbol="tDAI")
vega.mint(key_name=PARTY_B.name, asset=tDAI_asset_id, amount=100000) vega.mint(key_name=PARTY_B.name, asset=tDAI_asset_id, amount=100000)
vega.mint(key_name=PARTY_C.name, asset=tDAI_asset_id, amount=100000) vega.mint(key_name=PARTY_C.name, asset=tDAI_asset_id, amount=100000)
@@ -58,18 +46,6 @@ def setup_teams_and_games(vega: VegaServiceNull):
# list_teams actually returns a dictionary {"team_id": Team} # list_teams actually returns a dictionary {"team_id": Team}
team_id = list(teams.keys())[0] team_id = list(teams.keys())[0]
vega.create_referral_set(
key_name="market_maker",
name="test",
team_url="https://vega.xyz",
avatar_url="http://placekitten.com/200/200",
closed=False,
)
next_epoch(vega)
teams = vega.list_teams()
team_id_2 = list(teams.keys())[0]
vega.apply_referral_code("Key 1", team_id_2)
vega.apply_referral_code(PARTY_B.name, team_id) vega.apply_referral_code(PARTY_B.name, team_id)
@@ -87,7 +63,7 @@ def setup_teams_and_games(vega: VegaServiceNull):
current_epoch = vega.statistics().epoch_seq current_epoch = vega.statistics().epoch_seq
game_start = current_epoch + 1 game_start = current_epoch + 1
game_end = current_epoch + 14 game_end = current_epoch + 11
current_epoch = vega.statistics().epoch_seq current_epoch = vega.statistics().epoch_seq
print(f"[EPOCH: {current_epoch}] creating recurring transfer") print(f"[EPOCH: {current_epoch}] creating recurring transfer")
@@ -108,42 +84,9 @@ def setup_teams_and_games(vega: VegaServiceNull):
factor=1.0, factor=1.0,
start_epoch=game_start, start_epoch=game_start,
end_epoch=game_end, end_epoch=game_end,
window_length=15, window_length=10
)
vega.wait_fn(1)
vega.wait_for_total_catchup()
vega.recurring_transfer(
from_key_name=PARTY_B.name,
from_account_type=vega_protos.vega.ACCOUNT_TYPE_GENERAL,
to_account_type=vega_protos.vega.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
asset=tDAI_asset_id,
reference="reward",
asset_for_metric=tDAI_asset_id,
metric=vega_protos.vega.DISPATCH_METRIC_MAKER_FEES_PAID,
entity_scope=vega_protos.vega.ENTITY_SCOPE_INDIVIDUALS,
individual_scope=vega_protos.vega.INDIVIDUAL_SCOPE_IN_TEAM,
n_top_performers=1,
amount=100,
factor=1.0,
window_length=15
)
vega.wait_fn(1)
vega.wait_for_total_catchup()
vega.recurring_transfer(
from_key_name=PARTY_C.name,
from_account_type=vega_protos.vega.ACCOUNT_TYPE_GENERAL,
to_account_type=vega_protos.vega.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
asset=tDAI_asset_id,
reference="reward",
asset_for_metric=tDAI_asset_id,
metric=vega_protos.vega.DISPATCH_METRIC_MAKER_FEES_PAID,
entity_scope=vega_protos.vega.ENTITY_SCOPE_INDIVIDUALS,
individual_scope=vega_protos.vega.INDIVIDUAL_SCOPE_NOT_IN_TEAM,
n_top_performers=1,
amount=100,
factor=1.0,
window_length=15
) )
next_epoch(vega) next_epoch(vega)
print(f"[EPOCH: {vega.statistics().epoch_seq}] starting order activity") print(f"[EPOCH: {vega.statistics().epoch_seq}] starting order activity")
@@ -170,22 +113,6 @@ def setup_teams_and_games(vega: VegaServiceNull):
side="SIDE_BUY", side="SIDE_BUY",
volume=1, volume=1,
) )
vega.submit_order(
trading_key="Key 1",
market_id=tDAI_market,
order_type="TYPE_MARKET",
time_in_force="TIME_IN_FORCE_IOC",
side="SIDE_BUY",
volume=1,
)
vega.submit_order(
trading_key="market_maker",
market_id=tDAI_market,
order_type="TYPE_MARKET",
time_in_force="TIME_IN_FORCE_IOC",
side="SIDE_BUY",
volume=1,
)
next_epoch(vega) next_epoch(vega)
print(f"[EPOCH: {vega.statistics().epoch_seq}] {i} epoch passed") print(f"[EPOCH: {vega.statistics().epoch_seq}] {i} epoch passed")
@@ -193,7 +120,6 @@ def setup_teams_and_games(vega: VegaServiceNull):
"market_id": tDAI_market, "market_id": tDAI_market,
"asset_id": tDAI_asset_id, "asset_id": tDAI_asset_id,
"team_id": team_id, "team_id": team_id,
"team_id_2": team_id_2,
"team_name": team_name, "team_name": team_name,
} }
@@ -210,109 +136,65 @@ def create_team(vega: VegaServiceNull):
return team_name return team_name
def test_team_page_games_table(team_page: Page): def test_team_page_games_table(team_page: Page):
team_page.get_by_test_id("games-toggle").click() team_page.get_by_test_id("games-toggle").click()
expect(team_page.get_by_test_id("games-toggle")).to_have_text("Games (1)") expect(team_page.get_by_test_id("games-toggle")).to_have_text("Games (1)")
expect(team_page.get_by_test_id("rank-0")).to_have_text("2") expect(team_page.get_by_test_id("rank-0")).to_have_text("1")
expect(team_page.get_by_test_id("epoch-0")).to_have_text("19") expect(team_page.get_by_test_id("epoch-0")).to_have_text("18")
expect(team_page.get_by_test_id("type-0") expect(team_page.get_by_test_id("type-0")).to_have_text("Price maker fees paid")
).to_have_text("Price maker fees paid") expect(team_page.get_by_test_id("amount-0")).to_have_text("100,000,000")
expect(team_page.get_by_test_id("amount-0")).to_have_text("74") expect(team_page.get_by_test_id("participatingTeams-0")).to_have_text(
expect(team_page.get_by_test_id("participatingTeams-0")).to_have_text("2") "1"
expect(team_page.get_by_test_id("participatingMembers-0")).to_have_text("4") )
expect(team_page.get_by_test_id("participatingMembers-0")).to_have_text(
"2"
)
def test_team_page_members_table(team_page: Page): def test_team_page_members_table(team_page: Page):
team_page.get_by_test_id("members-toggle").click() team_page.get_by_test_id("members-toggle").click()
expect(team_page.get_by_test_id("members-toggle") expect(team_page.get_by_test_id("members-toggle")).to_have_text("Members (3)")
).to_have_text("Members (4)")
expect(team_page.get_by_test_id("referee-0")).to_be_visible() expect(team_page.get_by_test_id("referee-0")).to_be_visible()
expect(team_page.get_by_test_id("joinedAt-0")).to_be_visible() expect(team_page.get_by_test_id("joinedAt-0")).to_be_visible()
expect(team_page.get_by_test_id("joinedAtEpoch-0")).to_have_text("9") expect(team_page.get_by_test_id("joinedAtEpoch-0")).to_have_text("8")
def test_team_page_headline(team_page: Page, setup_teams_and_games
def test_team_page_headline(team_page: Page, setup_teams_and_games): ):
team_name = setup_teams_and_games["team_name"] team_name = setup_teams_and_games["team_name"]
expect(team_page.get_by_test_id("team-name")).to_have_text(team_name) expect(team_page.get_by_test_id("team-name")).to_have_text(team_name)
expect(team_page.get_by_test_id("members-count-stat")).to_have_text("4") expect(team_page.get_by_test_id("members-count-stat")).to_have_text("3")
expect(team_page.get_by_test_id("total-games-stat")).to_have_text("1") expect(team_page.get_by_test_id("total-games-stat")).to_have_text(
"1"
)
# TODO this still seems wrong as its always 0 # TODO this still seems wrong as its always 0
expect(team_page.get_by_test_id("total-volume-stat")).to_have_text("0") expect(team_page.get_by_test_id("total-volume-stat")).to_have_text(
"0"
)
expect(team_page.get_by_test_id("rewards-paid-stat")).to_have_text("78") expect(team_page.get_by_test_id("rewards-paid-stat")).to_have_text(
"100m"
)
def test_switch_teams(team_page: Page, vega: VegaServiceNull):
team_page.get_by_test_id("switch-team-button").click()
team_page.get_by_test_id("confirm-switch-button").click()
expect(team_page.get_by_test_id("dialog-content").first).to_be_visible()
vega.wait_fn(1)
vega.wait_for_total_catchup()
next_epoch(vega=vega)
team_page.reload()
expect(team_page.get_by_test_id("members-count-stat")).to_have_text("5")
@pytest.fixture(scope="module")
def competitions_page(vega, browser, request):
with init_page(vega, browser, request) as page:
risk_accepted_setup(page)
auth_setup(vega, page)
yield page
def test_leaderboard(competitions_page: Page, setup_teams_and_games): def test_leaderboard(competitions_page: Page, setup_teams_and_games):
team_name = setup_teams_and_games["team_name"] team_name = setup_teams_and_games["team_name"]
competitions_page.reload() competitions_page.goto(f"/#/competitions/")
expect( expect(competitions_page.get_by_test_id("rank-0").locator(".text-yellow-300")).to_have_count(1)
competitions_page.get_by_test_id("rank-0").locator(".text-yellow-300") expect(competitions_page.get_by_test_id("team-0")).to_have_text(team_name)
).to_have_count(1) expect(competitions_page.get_by_test_id("status-0")).to_have_text("Open")
expect(
competitions_page.get_by_test_id(
"rank-1").locator(".text-vega-clight-500")
).to_have_count(1)
expect(competitions_page.get_by_test_id("team-1")).to_have_text(team_name)
expect(competitions_page.get_by_test_id("status-1")).to_have_text("Open")
# FIXME: the numbers are different we need to clarify this with the backend expect(competitions_page.get_by_test_id("earned-0")).to_have_text("100,000,000")
# expect(competitions_page.get_by_test_id("earned-1")).to_have_text("160") expect(competitions_page.get_by_test_id("games-0")).to_have_text("1")
expect(competitions_page.get_by_test_id("games-1")).to_have_text("1")
# TODO still odd that this is 0 # TODO still odd that this is 0
expect(competitions_page.get_by_test_id("volume-0")).to_have_text("-") expect(competitions_page.get_by_test_id("volume-0")).to_have_text("-")
#TODO def test_games(competitions_page: Page):
def test_game_card(competitions_page: Page): #TODO currently no games appear which i think is a bug
expect(competitions_page.get_by_test_id(
"active-rewards-card")).to_have_count(2)
game_1 = competitions_page.get_by_test_id("active-rewards-card").first
expect(game_1).to_be_visible()
expect(game_1.get_by_test_id("entity-scope")).to_have_text("Individual")
expect(game_1.get_by_test_id("locked-for")).to_have_text("1 epoch")
expect(game_1.get_by_test_id("reward-value")).to_have_text("100.00")
expect(game_1.get_by_test_id("distribution-strategy")
).to_have_text("Pro rata")
expect(game_1.get_by_test_id("dispatch-metric-info")
).to_have_text("Price maker fees paid • tDAI")
expect(game_1.get_by_test_id("assessed-over")).to_have_text("15 epochs")
expect(game_1.get_by_test_id("scope")).to_have_text("In team")
expect(game_1.get_by_test_id("staking-requirement")).to_have_text("0.00")
expect(game_1.get_by_test_id("average-position")).to_have_text("0.00")
def test_create_team(competitions_page: Page, vega: VegaServiceNull):
change_keys(competitions_page, vega, "market_maker_2")
competitions_page.get_by_test_id("create-public-team-button").click()
competitions_page.get_by_test_id("team-name-input").fill("e2e")
competitions_page.get_by_test_id("team-url-input").fill("https://vega.xyz")
competitions_page.get_by_test_id("avatar-url-input").fill(
"http://placekitten.com/200/200"
)
competitions_page.get_by_test_id("team-form-submit-button").click()
expect(competitions_page.get_by_test_id("team-form-submit-button")).to_have_text(
"Confirming transaction..."
)
vega.wait_fn(2)
vega.wait_for_total_catchup()
expect(
competitions_page.get_by_test_id("team-creation-success-message")
).to_be_visible()
expect(competitions_page.get_by_test_id("team-id-display")).to_be_visible()
expect(competitions_page.get_by_test_id("team-id-display")).to_be_visible()
competitions_page.get_by_test_id("view-team-button").click()
expect(competitions_page.get_by_test_id("team-name")).to_have_text("e2e")
+1 -18
View File
@@ -17,7 +17,7 @@ fragment TeamStatsFields on TeamStatistics {
totalGamesPlayed totalGamesPlayed
quantumRewards { quantumRewards {
epoch epoch
totalQuantumRewards total_quantum_rewards
} }
gamesPlayed gamesPlayed
} }
@@ -51,13 +51,6 @@ fragment TeamGameFields on Game {
} }
} }
fragment TeamMemberStatsFields on TeamMemberStatistics {
partyId
totalQuantumVolume
totalQuantumRewards
totalGamesPlayed
}
query Team($teamId: ID!, $partyId: ID, $aggregationEpochs: Int) { query Team($teamId: ID!, $partyId: ID, $aggregationEpochs: Int) {
teams(teamId: $teamId) { teams(teamId: $teamId) {
edges { edges {
@@ -94,14 +87,4 @@ query Team($teamId: ID!, $partyId: ID, $aggregationEpochs: Int) {
} }
} }
} }
teamMembersStatistics(
teamId: $teamId
aggregationEpochs: $aggregationEpochs
) {
edges {
node {
...TeamMemberStatsFields
}
}
}
} }
+8 -13
View File
@@ -1,20 +1,15 @@
fragment TeamsFields on Team {
teamId
referrer
name
teamUrl
avatarUrl
createdAt
createdAtEpoch
closed
totalMembers
}
query Teams($teamId: ID, $partyId: ID) { query Teams($teamId: ID, $partyId: ID) {
teams(teamId: $teamId, partyId: $partyId) { teams(teamId: $teamId, partyId: $partyId) {
edges { edges {
node { node {
...TeamsFields teamId
referrer
name
teamUrl
avatarUrl
createdAt
createdAtEpoch
closed
} }
} }
} }
+4 -22
View File
@@ -5,7 +5,7 @@ import * as Apollo from '@apollo/client';
const defaultOptions = {} as const; const defaultOptions = {} as const;
export type TeamFieldsFragment = { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean, allowList: Array<string> }; export type TeamFieldsFragment = { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean, allowList: Array<string> };
export type TeamStatsFieldsFragment = { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string>, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, totalQuantumRewards: string }> }; export type TeamStatsFieldsFragment = { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string>, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, total_quantum_rewards: string }> };
export type TeamRefereeFieldsFragment = { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number }; export type TeamRefereeFieldsFragment = { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number };
@@ -13,8 +13,6 @@ export type TeamEntityFragment = { __typename?: 'TeamGameEntity', rank: number,
export type TeamGameFieldsFragment = { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: Types.DispatchMetric, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } }> }; export type TeamGameFieldsFragment = { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: Types.DispatchMetric, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } }> };
export type TeamMemberStatsFieldsFragment = { __typename?: 'TeamMemberStatistics', partyId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number };
export type TeamQueryVariables = Types.Exact<{ export type TeamQueryVariables = Types.Exact<{
teamId: Types.Scalars['ID']; teamId: Types.Scalars['ID'];
partyId?: Types.InputMaybe<Types.Scalars['ID']>; partyId?: Types.InputMaybe<Types.Scalars['ID']>;
@@ -22,7 +20,7 @@ export type TeamQueryVariables = Types.Exact<{
}>; }>;
export type TeamQuery = { __typename?: 'Query', teams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean, allowList: Array<string> } }> } | null, partyTeams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean, allowList: Array<string> } }> } | null, teamsStatistics?: { __typename?: 'TeamsStatisticsConnection', edges: Array<{ __typename?: 'TeamStatisticsEdge', node: { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string>, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, totalQuantumRewards: string }> } }> } | null, teamReferees?: { __typename?: 'TeamRefereeConnection', edges: Array<{ __typename?: 'TeamRefereeEdge', node: { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number } }> } | null, games: { __typename?: 'GamesConnection', edges?: Array<{ __typename?: 'GameEdge', node: { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: Types.DispatchMetric, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } }> } } | null> | null }, teamMembersStatistics?: { __typename?: 'TeamMembersStatisticsConnection', edges: Array<{ __typename?: 'TeamMemberStatisticsEdge', node: { __typename?: 'TeamMemberStatistics', partyId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number } }> } | null }; export type TeamQuery = { __typename?: 'Query', teams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean, allowList: Array<string> } }> } | null, partyTeams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean, allowList: Array<string> } }> } | null, teamsStatistics?: { __typename?: 'TeamsStatisticsConnection', edges: Array<{ __typename?: 'TeamStatisticsEdge', node: { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string>, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, total_quantum_rewards: string }> } }> } | null, teamReferees?: { __typename?: 'TeamRefereeConnection', edges: Array<{ __typename?: 'TeamRefereeEdge', node: { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number } }> } | null, games: { __typename?: 'GamesConnection', edges?: Array<{ __typename?: 'GameEdge', node: { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: Types.DispatchMetric, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } }> } } | null> | null } };
export const TeamFieldsFragmentDoc = gql` export const TeamFieldsFragmentDoc = gql`
fragment TeamFields on Team { fragment TeamFields on Team {
@@ -45,7 +43,7 @@ export const TeamStatsFieldsFragmentDoc = gql`
totalGamesPlayed totalGamesPlayed
quantumRewards { quantumRewards {
epoch epoch
totalQuantumRewards total_quantum_rewards
} }
gamesPlayed gamesPlayed
} }
@@ -82,14 +80,6 @@ export const TeamGameFieldsFragmentDoc = gql`
} }
} }
${TeamEntityFragmentDoc}`; ${TeamEntityFragmentDoc}`;
export const TeamMemberStatsFieldsFragmentDoc = gql`
fragment TeamMemberStatsFields on TeamMemberStatistics {
partyId
totalQuantumVolume
totalQuantumRewards
totalGamesPlayed
}
`;
export const TeamDocument = gql` export const TeamDocument = gql`
query Team($teamId: ID!, $partyId: ID, $aggregationEpochs: Int) { query Team($teamId: ID!, $partyId: ID, $aggregationEpochs: Int) {
teams(teamId: $teamId) { teams(teamId: $teamId) {
@@ -127,19 +117,11 @@ export const TeamDocument = gql`
} }
} }
} }
teamMembersStatistics(teamId: $teamId, aggregationEpochs: $aggregationEpochs) {
edges {
node {
...TeamMemberStatsFields
}
}
}
} }
${TeamFieldsFragmentDoc} ${TeamFieldsFragmentDoc}
${TeamStatsFieldsFragmentDoc} ${TeamStatsFieldsFragmentDoc}
${TeamRefereeFieldsFragmentDoc} ${TeamRefereeFieldsFragmentDoc}
${TeamGameFieldsFragmentDoc} ${TeamGameFieldsFragmentDoc}`;
${TeamMemberStatsFieldsFragmentDoc}`;
/** /**
* __useTeamQuery__ * __useTeamQuery__
+11 -18
View File
@@ -3,40 +3,33 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client'; import * as Apollo from '@apollo/client';
const defaultOptions = {} as const; const defaultOptions = {} as const;
export type TeamsFieldsFragment = { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean, totalMembers: number };
export type TeamsQueryVariables = Types.Exact<{ export type TeamsQueryVariables = Types.Exact<{
teamId?: Types.InputMaybe<Types.Scalars['ID']>; teamId?: Types.InputMaybe<Types.Scalars['ID']>;
partyId?: Types.InputMaybe<Types.Scalars['ID']>; partyId?: Types.InputMaybe<Types.Scalars['ID']>;
}>; }>;
export type TeamsQuery = { __typename?: 'Query', teams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean, totalMembers: number } }> } | null }; export type TeamsQuery = { __typename?: 'Query', teams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean } }> } | null };
export const TeamsFieldsFragmentDoc = gql`
fragment TeamsFields on Team {
teamId
referrer
name
teamUrl
avatarUrl
createdAt
createdAtEpoch
closed
totalMembers
}
`;
export const TeamsDocument = gql` export const TeamsDocument = gql`
query Teams($teamId: ID, $partyId: ID) { query Teams($teamId: ID, $partyId: ID) {
teams(teamId: $teamId, partyId: $partyId) { teams(teamId: $teamId, partyId: $partyId) {
edges { edges {
node { node {
...TeamsFields teamId
referrer
name
teamUrl
avatarUrl
createdAt
createdAtEpoch
closed
} }
} }
} }
} }
${TeamsFieldsFragmentDoc}`; `;
/** /**
* __useTeamsQuery__ * __useTeamsQuery__
+3 -14
View File
@@ -1,23 +1,12 @@
import compact from 'lodash/compact'; import compact from 'lodash/compact';
import { useActiveRewardsQuery } from '../../components/rewards-container/__generated__/Rewards'; import { useActiveRewardsQuery } from '../../components/rewards-container/__generated__/Rewards';
import { isActiveReward } from '../../components/rewards-container/active-rewards'; import { isActiveReward } from '../../components/rewards-container/active-rewards';
import { import { EntityScope, type TransferNode } from '@vegaprotocol/types';
EntityScope,
IndividualScope,
type TransferNode,
} from '@vegaprotocol/types';
const isScopedToTeams = (node: TransferNode) => const isScopedToTeams = (node: TransferNode) =>
node.transfer.kind.__typename === 'RecurringTransfer' && node.transfer.kind.__typename === 'RecurringTransfer' &&
// scoped to teams node.transfer.kind.dispatchStrategy?.entityScope ===
(node.transfer.kind.dispatchStrategy?.entityScope === EntityScope.ENTITY_SCOPE_TEAMS;
EntityScope.ENTITY_SCOPE_TEAMS ||
// or to individuals
(node.transfer.kind.dispatchStrategy?.entityScope ===
EntityScope.ENTITY_SCOPE_INDIVIDUALS &&
// but they have to be in a team
node.transfer.kind.dispatchStrategy.individualScope ===
IndividualScope.INDIVIDUAL_SCOPE_IN_TEAM));
export const useGames = ({ export const useGames = ({
currentEpoch, currentEpoch,
-25
View File
@@ -1,25 +0,0 @@
import { useVegaWallet } from '@vegaprotocol/wallet';
import compact from 'lodash/compact';
import first from 'lodash/first';
import { useTeamsQuery } from './__generated__/Teams';
import { useTeam } from './use-team';
import { useTeams } from './use-teams';
export const useMyTeam = () => {
const { pubKey } = useVegaWallet();
const { data: teams } = useTeams();
const { data: maybeMyTeam } = useTeamsQuery({
variables: {
partyId: pubKey,
},
skip: !pubKey,
fetchPolicy: 'cache-and-network',
});
const team = first(compact(maybeMyTeam?.teams?.edges.map((n) => n.node)));
const rank = teams.findIndex((t) => t.teamId === team?.teamId) + 1;
const { games, stats } = useTeam(team?.teamId);
return { team, stats, games, rank };
};
+10 -49
View File
@@ -6,24 +6,17 @@ import {
type TeamStatsFieldsFragment, type TeamStatsFieldsFragment,
type TeamRefereeFieldsFragment, type TeamRefereeFieldsFragment,
type TeamEntityFragment, type TeamEntityFragment,
type TeamMemberStatsFieldsFragment,
} from './__generated__/Team'; } from './__generated__/Team';
import { DEFAULT_AGGREGATION_EPOCHS } from './use-teams'; import { DEFAULT_AGGREGATION_EPOCHS } from './use-teams';
export type Team = TeamFieldsFragment; export type Team = TeamFieldsFragment;
export type TeamStats = TeamStatsFieldsFragment; export type TeamStats = TeamStatsFieldsFragment;
export type Member = TeamRefereeFieldsFragment & { export type Member = TeamRefereeFieldsFragment;
isCreator: boolean;
totalGamesPlayed: number;
totalQuantumVolume: string;
totalQuantumRewards: string;
};
export type TeamEntity = TeamEntityFragment; export type TeamEntity = TeamEntityFragment;
export type TeamGame = ReturnType<typeof useTeam>['games'][number]; export type TeamGame = ReturnType<typeof useTeam>['games'][number];
export type MemberStats = TeamMemberStatsFieldsFragment;
export const useTeam = (teamId?: string, partyId?: string) => { export const useTeam = (teamId?: string, partyId?: string) => {
const queryResult = useTeamQuery({ const { data, loading, error, refetch } = useTeamQuery({
variables: { variables: {
teamId: teamId || '', teamId: teamId || '',
partyId, partyId,
@@ -33,11 +26,7 @@ export const useTeam = (teamId?: string, partyId?: string) => {
fetchPolicy: 'cache-and-network', fetchPolicy: 'cache-and-network',
}); });
const { data } = queryResult;
const teamEdge = data?.teams?.edges.find((e) => e.node.teamId === teamId); const teamEdge = data?.teams?.edges.find((e) => e.node.teamId === teamId);
const team = teamEdge?.node;
const partyTeam = data?.partyTeams?.edges?.length const partyTeam = data?.partyTeams?.edges?.length
? data.partyTeams.edges[0].node ? data.partyTeams.edges[0].node
: undefined; : undefined;
@@ -45,40 +34,9 @@ export const useTeam = (teamId?: string, partyId?: string) => {
const teamStatsEdge = data?.teamsStatistics?.edges.find( const teamStatsEdge = data?.teamsStatistics?.edges.find(
(e) => e.node.teamId === teamId (e) => e.node.teamId === teamId
); );
const members = data?.teamReferees?.edges
const memberStats = data?.teamMembersStatistics?.edges.length .filter((e) => e.node.teamId === teamId)
? data.teamMembersStatistics.edges.map((e) => e.node) .map((e) => e.node);
: [];
const members: Member[] = data?.teamReferees?.edges.length
? data.teamReferees.edges
.filter((e) => e.node.teamId === teamId)
.map((e) => {
const member = e.node;
const stats = memberStats.find((m) => m.partyId === member.referee);
return {
...member,
isCreator: false,
totalQuantumVolume: stats ? stats.totalQuantumVolume : '0',
totalQuantumRewards: stats ? stats.totalQuantumRewards : '0',
totalGamesPlayed: stats ? stats.totalGamesPlayed : 0,
};
})
: [];
if (team) {
const ownerStats = memberStats.find((m) => m.partyId === team.referrer);
members.unshift({
teamId: team.teamId,
referee: team.referrer,
joinedAt: team?.createdAt,
joinedAtEpoch: team?.createdAtEpoch,
isCreator: true,
totalQuantumVolume: ownerStats ? ownerStats.totalQuantumVolume : '0',
totalQuantumRewards: ownerStats ? ownerStats.totalQuantumRewards : '0',
totalGamesPlayed: ownerStats ? ownerStats.totalGamesPlayed : 0,
});
}
// Find games where the current team participated in // Find games where the current team participated in
const gamesWithTeam = compact(data?.games.edges).map((edge) => { const gamesWithTeam = compact(data?.games.edges).map((edge) => {
@@ -102,9 +60,12 @@ export const useTeam = (teamId?: string, partyId?: string) => {
const games = orderBy(compact(gamesWithTeam), 'epoch', 'desc'); const games = orderBy(compact(gamesWithTeam), 'epoch', 'desc');
return { return {
...queryResult, data,
loading,
error,
refetch,
stats: teamStatsEdge?.node, stats: teamStatsEdge?.node,
team, team: teamEdge?.node,
members, members,
games, games,
partyTeam, partyTeam,
+33 -10
View File
@@ -1,13 +1,34 @@
import orderBy from 'lodash/orderBy';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useTeamsQuery } from './__generated__/Teams'; import { type TeamsQuery, useTeamsQuery } from './__generated__/Teams';
import { useTeamsStatisticsQuery } from './__generated__/TeamsStatistics'; import {
type TeamsStatisticsQuery,
useTeamsStatisticsQuery,
} from './__generated__/TeamsStatistics';
import compact from 'lodash/compact'; import compact from 'lodash/compact';
import sortBy from 'lodash/sortBy';
import { type ArrayElement } from 'type-fest/source/internal';
// 192 type SortableField = keyof Omit<
export const DEFAULT_AGGREGATION_EPOCHS = 192; ArrayElement<NonNullable<TeamsQuery['teams']>['edges']>['node'] &
ArrayElement<
NonNullable<TeamsStatisticsQuery['teamsStatistics']>['edges']
>['node'],
'__typename'
>;
export const useTeams = (aggregationEpochs = DEFAULT_AGGREGATION_EPOCHS) => { type UseTeamsArgs = {
aggregationEpochs?: number;
sortByField?: SortableField[];
order?: 'asc' | 'desc';
};
export const DEFAULT_AGGREGATION_EPOCHS = 10;
export const useTeams = ({
aggregationEpochs = DEFAULT_AGGREGATION_EPOCHS,
sortByField = ['createdAtEpoch'],
order = 'asc',
}: UseTeamsArgs) => {
const { const {
data: teamsData, data: teamsData,
loading: teamsLoading, loading: teamsLoading,
@@ -36,10 +57,12 @@ export const useTeams = (aggregationEpochs = DEFAULT_AGGREGATION_EPOCHS) => {
...stats.find((s) => s.teamId === t.teamId), ...stats.find((s) => s.teamId === t.teamId),
})); }));
return orderBy(data, (d) => Number(d.totalQuantumRewards || 0), 'desc').map( const sorted = sortBy(data, sortByField);
(d, i) => ({ ...d, rank: i + 1 }) if (order === 'desc') {
); return sorted.reverse();
}, [teams, stats]); }
return sorted;
}, [teams, sortByField, order, stats]);
return { return {
data, data,
+13 -2
View File
@@ -14,7 +14,7 @@ import './styles.css';
import { usePageTitleStore } from '../stores'; import { usePageTitleStore } from '../stores';
import DialogsContainer from './dialogs-container'; import DialogsContainer from './dialogs-container';
import ToastsManager from './toasts-manager'; import ToastsManager from './toasts-manager';
import { HashRouter, useLocation } from 'react-router-dom'; import { HashRouter, useLocation, Route, Routes } from 'react-router-dom';
import { Bootstrapper } from '../components/bootstrapper'; import { Bootstrapper } from '../components/bootstrapper';
import { AnnouncementBanner } from '../components/banner'; import { AnnouncementBanner } from '../components/banner';
import { Navbar } from '../components/navbar'; import { Navbar } from '../components/navbar';
@@ -25,7 +25,9 @@ import {
ProtocolUpgradeProposalNotification, ProtocolUpgradeProposalNotification,
} from '@vegaprotocol/proposals'; } from '@vegaprotocol/proposals';
import { ViewingBanner } from '../components/viewing-banner'; import { ViewingBanner } from '../components/viewing-banner';
import { NavHeader } from '../components/navbar/nav-header';
import { Telemetry } from '../components/telemetry'; import { Telemetry } from '../components/telemetry';
import { Routes as AppRoutes } from '../lib/links';
import { SSRLoader } from './ssr-loader'; import { SSRLoader } from './ssr-loader';
import { PartyActiveOrdersHandler } from './party-active-orders-handler'; import { PartyActiveOrdersHandler } from './party-active-orders-handler';
import { MaybeConnectEagerly } from './maybe-connect-eagerly'; import { MaybeConnectEagerly } from './maybe-connect-eagerly';
@@ -71,7 +73,16 @@ function AppBody({ Component }: AppProps) {
<Title /> <Title />
<div className={gridClasses}> <div className={gridClasses}>
<AnnouncementBanner /> <AnnouncementBanner />
<Navbar theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'system'} /> <Navbar theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'system'}>
<Routes>
<Route
path={AppRoutes.MARKETS}
// render nothing for markets/all, otherwise markets/:marketId will match with markets/all
element={null}
/>
<Route path={AppRoutes.MARKET} element={<NavHeader />} />
</Routes>
</Navbar>
<div data-testid="banners"> <div data-testid="banners">
<ProtocolUpgradeProposalNotification <ProtocolUpgradeProposalNotification
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING} mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
+2 -5
View File
@@ -24,14 +24,11 @@ export default function Document() {
{/* scripts */} {/* scripts */}
<script src="/theme-setter.js" type="text/javascript" async /> <script src="/theme-setter.js" type="text/javascript" async />
{/* manifest */}
<link rel="manifest" href="/apps/trading/public/manifest.json" />
</Head> </Head>
<Html> <Html>
<body <body
// Next.js will set body to display none until js runs. Because the entire app is client rendered // Nextjs will set body to display none until js runs. Because the entire app is client rendered
// and delivered via IPFS we override this to show a server side render loading animation until the // and delivered via ipfs we override this to show a server side render loading animation until the
// js is downloaded and react takes over rendering // js is downloaded and react takes over rendering
style={{ display: 'block' }} style={{ display: 'block' }}
className="bg-white dark:bg-vega-cdark-900 text-default font-alpha" className="bg-white dark:bg-vega-cdark-900 text-default font-alpha"
+12 -28
View File
@@ -23,11 +23,8 @@ import { NotFound as ReferralNotFound } from '../client-pages/referrals/error-bo
import { compact } from 'lodash'; import { compact } from 'lodash';
import { useFeatureFlags } from '@vegaprotocol/environment'; import { useFeatureFlags } from '@vegaprotocol/environment';
import { LiquidityHeader } from '../components/liquidity-header'; import { LiquidityHeader } from '../components/liquidity-header';
import { MarketHeader, MobileMarketHeader } from '../components/market-header'; import { MarketHeader } from '../components/market-header';
import { import { PortfolioSidebar } from '../client-pages/portfolio/portfolio-sidebar';
PortfolioMobileSidebar,
PortfolioSidebar,
} from '../client-pages/portfolio/portfolio-sidebar';
import { LiquiditySidebar } from '../client-pages/liquidity/liquidity-sidebar'; import { LiquiditySidebar } from '../client-pages/liquidity/liquidity-sidebar';
import { MarketsSidebar } from '../client-pages/markets/markets-sidebar'; import { MarketsSidebar } from '../client-pages/markets/markets-sidebar';
import { useT } from '../lib/use-t'; import { useT } from '../lib/use-t';
@@ -36,10 +33,8 @@ import { CompetitionsTeams } from '../client-pages/competitions/competitions-tea
import { CompetitionsTeam } from '../client-pages/competitions/competitions-team'; import { CompetitionsTeam } from '../client-pages/competitions/competitions-team';
import { CompetitionsCreateTeam } from '../client-pages/competitions/competitions-create-team'; import { CompetitionsCreateTeam } from '../client-pages/competitions/competitions-create-team';
import { CompetitionsUpdateTeam } from '../client-pages/competitions/competitions-update-team'; import { CompetitionsUpdateTeam } from '../client-pages/competitions/competitions-update-team';
import { MarketsMobileSidebar } from '../client-pages/markets/mobile-buttons';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
// These must remain dynamically imported as pennant cannot be compiled by Next.js due to ESM // These must remain dynamically imported as pennant cannot be compiled by nextjs due to ESM
// Using dynamic imports is a workaround for this until pennant is published as ESM // Using dynamic imports is a workaround for this until pennant is published as ESM
const MarketPage = lazy(() => import('../client-pages/market')); const MarketPage = lazy(() => import('../client-pages/market'));
const Portfolio = lazy(() => import('../client-pages/portfolio')); const Portfolio = lazy(() => import('../client-pages/portfolio'));
@@ -55,20 +50,6 @@ const NotFound = () => {
export const useRouterConfig = (): RouteObject[] => { export const useRouterConfig = (): RouteObject[] => {
const featureFlags = useFeatureFlags((state) => state.flags); const featureFlags = useFeatureFlags((state) => state.flags);
const { screenSize } = useScreenDimensions();
const largeScreen = ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize);
const marketHeader = largeScreen ? <MarketHeader /> : <MobileMarketHeader />;
const marketsSidebar = largeScreen ? (
<MarketsSidebar />
) : (
<MarketsMobileSidebar />
);
const portfolioSidebar = largeScreen ? (
<PortfolioSidebar />
) : (
<PortfolioMobileSidebar />
);
const routeConfig = compact([ const routeConfig = compact([
{ {
index: true, index: true,
@@ -85,7 +66,7 @@ export const useRouterConfig = (): RouteObject[] => {
featureFlags.REFERRALS featureFlags.REFERRALS
? { ? {
path: AppRoutes.REFERRALS, path: AppRoutes.REFERRALS,
element: <LayoutWithSidebar sidebar={portfolioSidebar} />, element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
children: [ children: [
{ {
element: ( element: (
@@ -118,7 +99,7 @@ export const useRouterConfig = (): RouteObject[] => {
featureFlags.TEAM_COMPETITION featureFlags.TEAM_COMPETITION
? { ? {
path: AppRoutes.COMPETITIONS, path: AppRoutes.COMPETITIONS,
element: <LayoutWithSidebar sidebar={portfolioSidebar} />, element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
children: [ children: [
// pages with planets and stars // pages with planets and stars
{ {
@@ -149,7 +130,7 @@ export const useRouterConfig = (): RouteObject[] => {
: undefined, : undefined,
{ {
path: 'fees/*', path: 'fees/*',
element: <LayoutWithSidebar sidebar={portfolioSidebar} />, element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
children: [ children: [
{ {
index: true, index: true,
@@ -159,7 +140,7 @@ export const useRouterConfig = (): RouteObject[] => {
}, },
{ {
path: 'rewards/*', path: 'rewards/*',
element: <LayoutWithSidebar sidebar={portfolioSidebar} />, element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
children: [ children: [
{ {
index: true, index: true,
@@ -170,7 +151,10 @@ export const useRouterConfig = (): RouteObject[] => {
{ {
path: 'markets/*', path: 'markets/*',
element: ( element: (
<LayoutWithSidebar header={marketHeader} sidebar={marketsSidebar} /> <LayoutWithSidebar
header={<MarketHeader />}
sidebar={<MarketsSidebar />}
/>
), ),
children: [ children: [
{ {
@@ -191,7 +175,7 @@ export const useRouterConfig = (): RouteObject[] => {
}, },
{ {
path: 'portfolio/*', path: 'portfolio/*',
element: <LayoutWithSidebar sidebar={portfolioSidebar} />, element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
children: [ children: [
{ {
index: true, index: true,
-22
View File
@@ -1,22 +0,0 @@
{
"name": "Vega Protocol - Trading",
"short_name": "Console",
"description": "Vega Protocol - Trading dApp",
"start_url": "/",
"display": "standalone",
"orientation": "portrait",
"theme_color": "#000000",
"background_color": "#ffffff",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "cover.png",
"type": "image/png",
"sizes": "192x192"
}
]
}
-18
View File
@@ -1,18 +0,0 @@
query TransferFee(
$fromAccount: ID!
$fromAccountType: AccountType!
$toAccount: ID!
$amount: String!
$assetId: String!
) {
estimateTransferFee(
fromAccount: $fromAccount
fromAccountType: $fromAccountType
toAccount: $toAccount
amount: $amount
assetId: $assetId
) {
fee
discount
}
}
-63
View File
@@ -1,63 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type TransferFeeQueryVariables = Types.Exact<{
fromAccount: Types.Scalars['ID'];
fromAccountType: Types.AccountType;
toAccount: Types.Scalars['ID'];
amount: Types.Scalars['String'];
assetId: Types.Scalars['String'];
}>;
export type TransferFeeQuery = { __typename?: 'Query', estimateTransferFee?: { __typename?: 'EstimatedTransferFee', fee: string, discount: string } | null };
export const TransferFeeDocument = gql`
query TransferFee($fromAccount: ID!, $fromAccountType: AccountType!, $toAccount: ID!, $amount: String!, $assetId: String!) {
estimateTransferFee(
fromAccount: $fromAccount
fromAccountType: $fromAccountType
toAccount: $toAccount
amount: $amount
assetId: $assetId
) {
fee
discount
}
}
`;
/**
* __useTransferFeeQuery__
*
* To run a query within a React component, call `useTransferFeeQuery` and pass it any options that fit your needs.
* When your component renders, `useTransferFeeQuery` 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 } = useTransferFeeQuery({
* variables: {
* fromAccount: // value for 'fromAccount'
* fromAccountType: // value for 'fromAccountType'
* toAccount: // value for 'toAccount'
* amount: // value for 'amount'
* assetId: // value for 'assetId'
* },
* });
*/
export function useTransferFeeQuery(baseOptions: Apollo.QueryHookOptions<TransferFeeQuery, TransferFeeQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<TransferFeeQuery, TransferFeeQueryVariables>(TransferFeeDocument, options);
}
export function useTransferFeeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<TransferFeeQuery, TransferFeeQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<TransferFeeQuery, TransferFeeQueryVariables>(TransferFeeDocument, options);
}
export type TransferFeeQueryHookResult = ReturnType<typeof useTransferFeeQuery>;
export type TransferFeeLazyQueryHookResult = ReturnType<typeof useTransferFeeLazyQuery>;
export type TransferFeeQueryResult = Apollo.QueryResult<TransferFeeQuery, TransferFeeQueryVariables>;
@@ -25,6 +25,7 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
const t = useT(); const t = useT();
const { pubKey, pubKeys, isReadOnly } = useVegaWallet(); const { pubKey, pubKeys, isReadOnly } = useVegaWallet();
const { params } = useNetworkParams([ const { params } = useNetworkParams([
NetworkParams.transfer_fee_factor,
NetworkParams.transfer_minTransferQuantumMultiple, NetworkParams.transfer_minTransferQuantumMultiple,
]); ]);
@@ -71,6 +72,7 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
pubKeys={pubKeys ? pubKeys?.map((pk) => pk.publicKey) : null} pubKeys={pubKeys ? pubKeys?.map((pk) => pk.publicKey) : null}
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
assetId={assetId} assetId={assetId}
feeFactor={params.transfer_fee_factor}
minQuantumMultiple={params.transfer_minTransferQuantumMultiple} minQuantumMultiple={params.transfer_minTransferQuantumMultiple}
submitTransfer={transfer} submitTransfer={transfer}
accounts={sortedAccounts} accounts={sortedAccounts}
+145 -79
View File
@@ -15,30 +15,6 @@ import {
} from './transfer-form'; } from './transfer-form';
import { AccountType, AccountTypeMapping } from '@vegaprotocol/types'; import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
import { removeDecimal } from '@vegaprotocol/utils'; import { removeDecimal } from '@vegaprotocol/utils';
import type { TransferFeeQuery } from './__generated__/TransferFee';
const feeFactor = 0.001;
const mockUseTransferFeeQuery = jest.fn(
({
variables: { amount },
}: {
variables: { amount: string };
}): { data: TransferFeeQuery } => {
return {
data: {
estimateTransferFee: {
discount: '0',
fee: (Number(amount) * feeFactor).toFixed(),
},
},
};
}
);
jest.mock('./__generated__/TransferFee', () => ({
useTransferFeeQuery: (props: { variables: { amount: string } }) =>
mockUseTransferFeeQuery(props),
}));
describe('TransferForm', () => { describe('TransferForm', () => {
const renderComponent = (props: TransferFormProps) => { const renderComponent = (props: TransferFormProps) => {
@@ -80,6 +56,7 @@ describe('TransferForm', () => {
const props = { const props = {
pubKey, pubKey,
pubKeys: [pubKey, '2'.repeat(64)], pubKeys: [pubKey, '2'.repeat(64)],
feeFactor: '0.001',
submitTransfer: jest.fn(), submitTransfer: jest.fn(),
accounts: [ accounts: [
{ {
@@ -102,6 +79,7 @@ describe('TransferForm', () => {
pubKey, pubKey,
'a4b6e3de5d7ef4e31ae1b090be49d1a2ef7bcefff60cccf7658a0d4922651cce', 'a4b6e3de5d7ef4e31ae1b090be49d1a2ef7bcefff60cccf7658a0d4922651cce',
], ],
feeFactor: '0.001',
submitTransfer: jest.fn(), submitTransfer: jest.fn(),
accounts: [], accounts: [],
minQuantumMultiple: '1', minQuantumMultiple: '1',
@@ -118,6 +96,15 @@ describe('TransferForm', () => {
}); });
it.each([ it.each([
{
targetText: 'Include transfer fee',
tooltipText:
'The fee will be taken from the amount you are transferring.',
},
{
targetText: 'Transfer fee',
tooltipText: /transfer\.fee\.factor/,
},
{ {
targetText: 'Amount to be transferred', targetText: 'Amount to be transferred',
tooltipText: /without the fee/, tooltipText: /without the fee/,
@@ -127,6 +114,9 @@ describe('TransferForm', () => {
tooltipText: /total amount taken from your account/, tooltipText: /total amount taken from your account/,
}, },
])('Tooltip for "$targetText" shows', async (o) => { ])('Tooltip for "$targetText" shows', async (o) => {
// 1003-TRAN-015
// 1003-TRAN-016
// 1003-TRAN-017
// 1003-TRAN-018 // 1003-TRAN-018
// 1003-TRAN-019 // 1003-TRAN-019
renderComponent(props); renderComponent(props);
@@ -139,10 +129,6 @@ describe('TransferForm', () => {
// Select asset // Select asset
await selectAsset(asset); await selectAsset(asset);
await userEvent.selectOptions(
screen.getByLabelText('From account'),
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
);
// set valid amount // set valid amount
const amountInput = screen.getByLabelText('Amount'); const amountInput = screen.getByLabelText('Amount');
await userEvent.type(amountInput, amount); await userEvent.type(amountInput, amount);
@@ -233,7 +219,9 @@ describe('TransferForm', () => {
// set valid amount // set valid amount
await userEvent.clear(amountInput); await userEvent.clear(amountInput);
await userEvent.type(amountInput, amount); await userEvent.type(amountInput, amount);
expect(screen.getByTestId('transfer-fee')).toHaveTextContent('1'); expect(screen.getByTestId('transfer-fee')).toHaveTextContent(
new BigNumber(props.feeFactor).times(amount).toFixed()
);
await submit(); await submit();
@@ -288,6 +276,9 @@ describe('TransferForm', () => {
const amountInput = screen.getByLabelText('Amount'); const amountInput = screen.getByLabelText('Amount');
const checkbox = screen.getByTestId('include-transfer-fee');
expect(checkbox).not.toBeChecked();
await userEvent.clear(amountInput); await userEvent.clear(amountInput);
await userEvent.type(amountInput, '50'); await userEvent.type(amountInput, '50');
@@ -297,7 +288,10 @@ describe('TransferForm', () => {
await userEvent.click(screen.getByRole('button', { name: 'Use max' })); await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
expect(amountInput).toHaveValue('100.00'); expect(amountInput).toHaveValue('100.00');
// If transfering from a vested account fees should be 0 // If transfering from a vested account 'include fees' checkbox should
// be disabled and fees should be 0
expect(checkbox).not.toBeChecked();
expect(checkbox).toBeDisabled();
const expectedFee = '0'; const expectedFee = '0';
const total = new BigNumber(amount).plus(expectedFee).toFixed(); const total = new BigNumber(amount).plus(expectedFee).toFixed();
@@ -403,43 +397,120 @@ describe('TransferForm', () => {
}); });
}); });
it('validates fields', async () => { describe('IncludeFeesCheckbox', () => {
renderComponent(props); it('validates fields and submits when checkbox is checked', async () => {
const mockSubmit = jest.fn();
renderComponent({ ...props, submitTransfer: mockSubmit });
// check current pubkey not shown // check current pubkey not shown
const keySelect: HTMLSelectElement = screen.getByLabelText('To Vega key'); const keySelect = screen.getByLabelText<HTMLSelectElement>('To Vega key');
const pubKeyOptions = ['', pubKey, props.pubKeys[1]]; const pubKeyOptions = ['', pubKey, props.pubKeys[1]];
expect(keySelect.children).toHaveLength(pubKeyOptions.length); expect(keySelect.children).toHaveLength(pubKeyOptions.length);
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual( expect(Array.from(keySelect.options).map((o) => o.value)).toEqual(
pubKeyOptions pubKeyOptions
); );
await submit(); await submit();
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value
// Select a pubkey // Select a pubkey
await userEvent.selectOptions( await userEvent.selectOptions(
screen.getByLabelText('To Vega key'), screen.getByLabelText('To Vega key'),
props.pubKeys[1] props.pubKeys[1]
); );
// Select asset // Select asset
await selectAsset(asset); await selectAsset(asset);
await userEvent.selectOptions( await userEvent.selectOptions(
screen.getByLabelText('From account'), screen.getByLabelText('From account'),
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}` `${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
); );
const amountInput = screen.getByLabelText('Amount'); const amountInput = screen.getByLabelText('Amount');
const checkbox = screen.getByTestId('include-transfer-fee');
await userEvent.type(amountInput, amount); // 1003-TRAN-022
const expectedFee = new BigNumber(amount).times(feeFactor).toFixed(); expect(checkbox).not.toBeChecked();
const total = new BigNumber(amount).plus(expectedFee).toFixed();
// 1003-TRAN-021 await userEvent.clear(amountInput);
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee); await userEvent.type(amountInput, amount);
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(amount); await userEvent.click(checkbox);
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total);
expect(checkbox).toBeChecked();
const expectedFee = new BigNumber(amount)
.times(props.feeFactor)
.toFixed();
const expectedAmount = new BigNumber(amount).minus(expectedFee).toFixed();
// 1003-TRAN-020
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(
expectedAmount
);
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(
amount
);
await submit();
await waitFor(() => {
// 1003-TRAN-023
expect(mockSubmit).toHaveBeenCalledTimes(1);
expect(mockSubmit).toHaveBeenCalledWith({
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
to: props.pubKeys[1],
asset: asset.id,
amount: removeDecimal(expectedAmount, asset.decimals),
oneOff: {},
});
});
});
it('validates fields when checkbox is not checked', async () => {
renderComponent(props);
// check current pubkey not shown
const keySelect: HTMLSelectElement = screen.getByLabelText('To Vega key');
const pubKeyOptions = ['', pubKey, props.pubKeys[1]];
expect(keySelect.children).toHaveLength(pubKeyOptions.length);
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual(
pubKeyOptions
);
await submit();
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value
// Select a pubkey
await userEvent.selectOptions(
screen.getByLabelText('To Vega key'),
props.pubKeys[1]
);
// Select asset
await selectAsset(asset);
await userEvent.selectOptions(
screen.getByLabelText('From account'),
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
);
const amountInput = screen.getByLabelText('Amount');
const checkbox = screen.getByTestId('include-transfer-fee');
expect(checkbox).not.toBeChecked();
await userEvent.type(amountInput, amount);
expect(checkbox).not.toBeChecked();
const expectedFee = new BigNumber(amount)
.times(props.feeFactor)
.toFixed();
const total = new BigNumber(amount).plus(expectedFee).toFixed();
// 1003-TRAN-021
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(amount);
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total);
});
}); });
describe('AddressField', () => { describe('AddressField', () => {
@@ -471,29 +542,24 @@ describe('TransferForm', () => {
describe('TransferFee', () => { describe('TransferFee', () => {
const props = { const props = {
amount: '20000', amount: '200',
discount: '0', feeFactor: '0.001',
fee: '20', fee: '0.2',
decimals: 2, transferAmount: '200',
decimals: 8,
}; };
it('calculates and renders amounts and fee', () => { it('calculates and renders the transfer fee', () => {
render(<TransferFee {...props} />); render(<TransferFee {...props} />);
expect(screen.queryByTestId('discount')).not.toBeInTheDocument();
expect(screen.getByTestId('transfer-fee')).toHaveTextContent('0.2');
expect(screen.getByTestId('transfer-amount')).toHaveTextContent('200.00');
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(
'200.20'
);
});
it('calculates and renders amounts, fee and discount', () => { const expected = new BigNumber(props.amount)
render(<TransferFee {...props} discount="10" />); .times(props.feeFactor)
expect(screen.getByTestId('discount')).toHaveTextContent('0.1'); .toFixed();
expect(screen.getByTestId('transfer-fee')).toHaveTextContent('0.2'); const total = new BigNumber(props.amount).plus(expected).toFixed();
expect(screen.getByTestId('transfer-amount')).toHaveTextContent('200.00'); expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expected);
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent( expect(screen.getByTestId('transfer-amount')).toHaveTextContent(
'200.10' props.amount
); );
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total);
}); });
}); });
}); });
+76 -52
View File
@@ -4,9 +4,8 @@ import {
useRequired, useRequired,
useVegaPublicKey, useVegaPublicKey,
addDecimal, addDecimal,
formatNumber,
toBigNum, toBigNum,
removeDecimal,
addDecimalsFormatNumber,
} from '@vegaprotocol/utils'; } from '@vegaprotocol/utils';
import { useT } from './use-t'; import { useT } from './use-t';
import { import {
@@ -16,17 +15,17 @@ import {
TradingRichSelect, TradingRichSelect,
TradingSelect, TradingSelect,
Tooltip, Tooltip,
TradingCheckbox,
TradingButton, TradingButton,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import type { Transfer } from '@vegaprotocol/wallet'; import type { Transfer } from '@vegaprotocol/wallet';
import { normalizeTransfer } from '@vegaprotocol/wallet'; import { normalizeTransfer } from '@vegaprotocol/wallet';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { AssetOption, Balance } from '@vegaprotocol/assets'; import { AssetOption, Balance } from '@vegaprotocol/assets';
import { AccountType, AccountTypeMapping } from '@vegaprotocol/types'; import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
import { useTransferFeeQuery } from './__generated__/TransferFee';
interface FormFields { interface FormFields {
toVegaKey: string; toVegaKey: string;
@@ -53,6 +52,7 @@ export interface TransferFormProps {
asset: Asset; asset: Asset;
}>; }>;
assetId?: string; assetId?: string;
feeFactor: string | null;
minQuantumMultiple: string | null; minQuantumMultiple: string | null;
submitTransfer: (transfer: Transfer) => void; submitTransfer: (transfer: Transfer) => void;
} }
@@ -62,6 +62,7 @@ export const TransferForm = ({
pubKeys, pubKeys,
isReadOnly, isReadOnly,
assetId: initialAssetId, assetId: initialAssetId,
feeFactor,
submitTransfer, submitTransfer,
accounts, accounts,
minQuantumMultiple, minQuantumMultiple,
@@ -134,28 +135,32 @@ export const TransferForm = ({
const accountBalance = const accountBalance =
account && addDecimal(account.balance, account.asset.decimals); account && addDecimal(account.balance, account.asset.decimals);
const [includeFee, setIncludeFee] = useState(false);
// Max amount given selected asset and from account // Max amount given selected asset and from account
const max = accountBalance ? new BigNumber(accountBalance) : new BigNumber(0); const max = accountBalance ? new BigNumber(accountBalance) : new BigNumber(0);
const normalizedAmount =
(amount && asset && removeDecimal(amount, asset.decimals)) || '0';
const transferFeeQuery = useTransferFeeQuery({ const transferAmount = useMemo(() => {
variables: { if (!amount) return undefined;
fromAccount: pubKey || '', if (includeFee && feeFactor) {
fromAccountType: accountType || AccountType.ACCOUNT_TYPE_GENERAL, return new BigNumber(1).minus(feeFactor).times(amount).toString();
amount: normalizedAmount, }
assetId: asset?.id || '', return amount;
toAccount: selectedPubKey, }, [amount, includeFee, feeFactor]);
},
skip: !pubKey || !amount || !asset || !selectedPubKey || fromVested, const fee = useMemo(() => {
}); if (!transferAmount) return undefined;
const transferFee = transferFeeQuery.loading if (includeFee) {
? transferFeeQuery.data || transferFeeQuery.previousData return new BigNumber(amount).minus(transferAmount).toString();
: transferFeeQuery.data; }
return (
feeFactor && new BigNumber(feeFactor).times(transferAmount).toString()
);
}, [amount, includeFee, transferAmount, feeFactor]);
const onSubmit = useCallback( const onSubmit = useCallback(
(fields: FormFields) => { (fields: FormFields) => {
if (!amount) { if (!transferAmount) {
throw new Error('Submitted transfer with no amount selected'); throw new Error('Submitted transfer with no amount selected');
} }
@@ -168,7 +173,7 @@ export const TransferForm = ({
const transfer = normalizeTransfer( const transfer = normalizeTransfer(
fields.toVegaKey, fields.toVegaKey,
amount, transferAmount,
type, type,
AccountType.ACCOUNT_TYPE_GENERAL, // field is readonly in the form AccountType.ACCOUNT_TYPE_GENERAL, // field is readonly in the form
{ {
@@ -178,7 +183,7 @@ export const TransferForm = ({
); );
submitTransfer(transfer); submitTransfer(transfer);
}, },
[submitTransfer, amount, assets] [submitTransfer, transferAmount, assets]
); );
// reset for placeholder workaround https://github.com/radix-ui/primitives/issues/1569 // reset for placeholder workaround https://github.com/radix-ui/primitives/issues/1569
@@ -274,6 +279,7 @@ export const TransferForm = ({
) { ) {
setValue('toVegaKey', pubKey); setValue('toVegaKey', pubKey);
setToVegaKeyMode('select'); setToVegaKeyMode('select');
setIncludeFee(false);
} }
}} }}
> >
@@ -443,14 +449,30 @@ export const TransferForm = ({
</TradingInputError> </TradingInputError>
)} )}
</TradingFormGroup> </TradingFormGroup>
{(transferFee?.estimateTransferFee || fromVested) && amount && asset && ( <div className="mb-4">
<Tooltip
description={t(
`The fee will be taken from the amount you are transferring.`
)}
>
<div>
<TradingCheckbox
name="include-transfer-fee"
disabled={!transferAmount || fromVested}
label={t('Include transfer fee')}
checked={includeFee}
onCheckedChange={() => setIncludeFee((x) => !x)}
/>
</div>
</Tooltip>
</div>
{transferAmount && fee && (
<TransferFee <TransferFee
amount={normalizedAmount} amount={transferAmount}
fee={fromVested ? '0' : transferFee?.estimateTransferFee?.fee} transferAmount={transferAmount}
discount={ feeFactor={feeFactor}
fromVested ? '0' : transferFee?.estimateTransferFee?.discount fee={fromVested ? '0' : fee}
} decimals={asset?.decimals}
decimals={asset.decimals}
/> />
)} )}
<TradingButton type="submit" fill={true} disabled={isReadOnly}> <TradingButton type="submit" fill={true} disabled={isReadOnly}>
@@ -462,44 +484,46 @@ export const TransferForm = ({
export const TransferFee = ({ export const TransferFee = ({
amount, amount,
transferAmount,
feeFactor,
fee, fee,
discount,
decimals, decimals,
}: { }: {
amount: string; amount: string;
transferAmount: string;
feeFactor: string | null;
fee?: string; fee?: string;
discount?: string; decimals?: number;
decimals: number;
}) => { }) => {
const t = useT(); const t = useT();
if (!amount || !fee) return null; if (!feeFactor || !amount || !transferAmount || !fee) return null;
if (isNaN(Number(amount)) || isNaN(Number(fee))) { if (
isNaN(Number(feeFactor)) ||
isNaN(Number(amount)) ||
isNaN(Number(transferAmount)) ||
isNaN(Number(fee))
) {
return null; return null;
} }
const totalValue = ( const totalValue = new BigNumber(transferAmount).plus(fee).toString();
BigInt(amount) +
BigInt(fee) -
BigInt(discount || '0')
).toString();
return ( return (
<div className="mb-4 flex flex-col gap-2 text-xs"> <div className="mb-4 flex flex-col gap-2 text-xs">
<div className="flex flex-wrap items-center justify-between gap-1"> <div className="flex flex-wrap items-center justify-between gap-1">
<div>{t('Transfer fee')}</div> <Tooltip
description={t(
`The transfer fee is set by the network parameter transfer.fee.factor, currently set to {{feeFactor}}`,
{ feeFactor }
)}
>
<div>{t('Transfer fee')}</div>
</Tooltip>
<div data-testid="transfer-fee" className="text-muted"> <div data-testid="transfer-fee" className="text-muted">
{addDecimalsFormatNumber(fee, decimals)} {formatNumber(fee, decimals)}
</div> </div>
</div> </div>
{discount && discount !== '0' && (
<div className="flex flex-wrap items-center justify-between gap-1">
<div>{t('Discount')}</div>
<div data-testid="discount" className="text-muted">
{addDecimalsFormatNumber(discount, decimals)}
</div>
</div>
)}
<div className="flex flex-wrap items-center justify-between gap-1"> <div className="flex flex-wrap items-center justify-between gap-1">
<Tooltip <Tooltip
description={t( description={t(
@@ -510,7 +534,7 @@ export const TransferFee = ({
</Tooltip> </Tooltip>
<div data-testid="transfer-amount" className="text-muted"> <div data-testid="transfer-amount" className="text-muted">
{addDecimalsFormatNumber(amount, decimals)} {formatNumber(amount, decimals)}
</div> </div>
</div> </div>
<div className="flex flex-wrap items-center justify-between gap-1"> <div className="flex flex-wrap items-center justify-between gap-1">
@@ -523,7 +547,7 @@ export const TransferFee = ({
</Tooltip> </Tooltip>
<div data-testid="total-transfer-fee" className="text-muted"> <div data-testid="total-transfer-fee" className="text-muted">
{addDecimalsFormatNumber(totalValue, decimals)} {formatNumber(totalValue, decimals)}
</div> </div>
</div> </div>
</div> </div>
@@ -3,13 +3,27 @@ import { getAsset, getQuoteName } from '@vegaprotocol/markets';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { AccountBreakdownDialog } from '@vegaprotocol/accounts'; import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
import { formatRange, formatValue } from '@vegaprotocol/utils'; import { formatRange, formatValue } from '@vegaprotocol/utils';
import { marketMarginDataProvider } from '@vegaprotocol/accounts';
import { useDataProvider } from '@vegaprotocol/data-provider';
import * as AccordionPrimitive from '@radix-ui/react-accordion';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import { import {
MARGIN_DIFF_TOOLTIP_TEXT,
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
TOTAL_MARGIN_AVAILABLE,
LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT, LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT,
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
MARGIN_ACCOUNT_TOOLTIP_TEXT, MARGIN_ACCOUNT_TOOLTIP_TEXT,
} from '../../constants'; } from '../../constants';
import { KeyValue } from './key-value'; import { KeyValue } from './key-value';
import { ExternalLink } from '@vegaprotocol/ui-toolkit'; import {
Accordion,
AccordionChevron,
AccordionPanel,
ExternalLink,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import { useT, ns } from '../../use-t'; import { useT, ns } from '../../use-t';
import { Trans } from 'react-i18next'; import { Trans } from 'react-i18next';
import type { Market } from '@vegaprotocol/markets'; import type { Market } from '@vegaprotocol/markets';
@@ -17,9 +31,9 @@ import { emptyValue } from './deal-ticket-fee-details';
import type { EstimatePositionQuery } from '@vegaprotocol/positions'; import type { EstimatePositionQuery } from '@vegaprotocol/positions';
export interface DealTicketMarginDetailsProps { export interface DealTicketMarginDetailsProps {
generalAccountBalance: string; generalAccountBalance?: string;
marginAccountBalance: string; marginAccountBalance?: string;
orderMarginAccountBalance: string; orderMarginAccountBalance?: string;
market: Market; market: Market;
onMarketClick?: (marketId: string, metaKey?: boolean) => void; onMarketClick?: (marketId: string, metaKey?: boolean) => void;
assetSymbol: string; assetSymbol: string;
@@ -40,20 +54,118 @@ export const DealTicketMarginDetails = ({
const t = useT(); const t = useT();
const [breakdownDialog, setBreakdownDialog] = useState(false); const [breakdownDialog, setBreakdownDialog] = useState(false);
const { pubKey: partyId } = useVegaWallet(); const { pubKey: partyId } = useVegaWallet();
const { data: currentMargins } = useDataProvider({
dataProvider: marketMarginDataProvider,
variables: { marketId: market.id, partyId: partyId || '' },
skip: !partyId,
});
const liquidationEstimate = positionEstimate?.liquidation; const liquidationEstimate = positionEstimate?.liquidation;
const marginEstimate = positionEstimate?.margin;
const totalMarginAccountBalance = const totalMarginAccountBalance =
BigInt(marginAccountBalance || '0') + BigInt(marginAccountBalance || '0') +
BigInt(orderMarginAccountBalance || '0'); BigInt(orderMarginAccountBalance || '0');
const totalBalance =
BigInt(generalAccountBalance || '0') + totalMarginAccountBalance;
const asset = getAsset(market); const asset = getAsset(market);
const { decimals: assetDecimals, quantum } = asset; const { decimals: assetDecimals, quantum } = asset;
let marginRequiredBestCase: string | undefined = undefined;
let marginRequiredWorstCase: string | undefined = undefined;
const marginEstimateBestCase =
BigInt(marginEstimate?.bestCase.initialLevel ?? 0) +
BigInt(marginEstimate?.bestCase.orderMarginLevel ?? 0);
const marginEstimateWorstCase =
BigInt(marginEstimate?.worstCase.initialLevel ?? 0) +
BigInt(marginEstimate?.worstCase.orderMarginLevel ?? 0);
if (marginEstimate) {
if (currentMargins) {
const currentMargin =
BigInt(currentMargins.initialLevel) +
BigInt(currentMargins.orderMarginLevel);
const collateralIncreaseEstimateBestCase = BigInt( marginRequiredBestCase = (
positionEstimate?.collateralIncreaseEstimate.bestCase ?? '0' marginEstimateBestCase - currentMargin
); ).toString();
const collateralIncreaseEstimateWorstCase = BigInt( if (marginRequiredBestCase.startsWith('-')) {
positionEstimate?.collateralIncreaseEstimate.worstCase ?? '0' marginRequiredBestCase = '0';
); }
marginRequiredWorstCase = (
marginEstimateWorstCase - currentMargin
).toString();
if (marginRequiredWorstCase.startsWith('-')) {
marginRequiredWorstCase = '0';
}
} else {
marginRequiredBestCase = marginEstimateBestCase.toString();
marginRequiredWorstCase = marginEstimateWorstCase.toString();
}
}
const totalMarginAvailable = (
currentMargins
? totalBalance - BigInt(currentMargins.maintenanceLevel)
: totalBalance
).toString();
let deductionFromCollateral = null;
let projectedMargin = null;
if (totalMarginAccountBalance) {
const deductionFromCollateralBestCase =
marginEstimateBestCase - totalMarginAccountBalance;
const deductionFromCollateralWorstCase =
marginEstimateWorstCase - totalMarginAccountBalance;
deductionFromCollateral = (
<KeyValue
indent
label={t('Deduction from collateral')}
value={formatRange(
deductionFromCollateralBestCase > 0
? deductionFromCollateralBestCase.toString()
: '0',
deductionFromCollateralWorstCase > 0
? deductionFromCollateralWorstCase.toString()
: '0',
assetDecimals
)}
formattedValue={formatValue(
deductionFromCollateralWorstCase > 0
? deductionFromCollateralWorstCase.toString()
: '0',
assetDecimals,
quantum
)}
symbol={assetSymbol}
labelDescription={t(
'DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT',
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
{ assetSymbol }
)}
/>
);
projectedMargin = (
<KeyValue
label={t('Projected margin')}
value={formatRange(
marginEstimateBestCase.toString(),
marginEstimateWorstCase.toString(),
assetDecimals
)}
formattedValue={formatValue(
marginEstimateWorstCase.toString(),
assetDecimals,
quantum
)}
symbol={assetSymbol}
labelDescription={t(
'EST_TOTAL_MARGIN_TOOLTIP_TEXT',
EST_TOTAL_MARGIN_TOOLTIP_TEXT
)}
/>
);
}
let liquidationPriceEstimate = emptyValue; let liquidationPriceEstimate = emptyValue;
let liquidationPriceEstimateRange = emptyValue; let liquidationPriceEstimateRange = emptyValue;
@@ -110,50 +222,128 @@ export const DealTicketMarginDetails = ({
const quoteName = getQuoteName(market); const quoteName = getQuoteName(market);
return ( return (
<div className="flex flex-col w-full gap-2 mt-2"> <div className="flex flex-col w-full gap-2">
<Accordion>
<AccordionPanel
itemId="margin"
trigger={
<AccordionPrimitive.Trigger
data-testid="accordion-toggle"
className={classNames(
'w-full pt-2',
'flex items-center gap-2 text-xs',
'group'
)}
>
<div
data-testid={`deal-ticket-fee-margin-required`}
key={'value-dropdown'}
className="flex items-center justify-between w-full gap-2"
>
<div className="flex items-center text-left gap-1">
<Tooltip
description={t(
'MARGIN_DIFF_TOOLTIP_TEXT',
MARGIN_DIFF_TOOLTIP_TEXT,
{ assetSymbol }
)}
>
<span className="text-muted">{t('Margin required')}</span>
</Tooltip>
<AccordionChevron size={10} />
</div>
<Tooltip
description={
formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals
) ?? '-'
}
>
<div className="font-mono text-right">
{formatValue(
marginRequiredWorstCase,
assetDecimals,
quantum
)}{' '}
{assetSymbol || ''}
</div>
</Tooltip>
</div>
</AccordionPrimitive.Trigger>
}
>
<div className="flex flex-col w-full gap-2">
<KeyValue
label={t('Total margin available')}
indent
value={formatValue(totalMarginAvailable, assetDecimals)}
formattedValue={formatValue(
totalMarginAvailable,
assetDecimals,
quantum
)}
symbol={assetSymbol}
labelDescription={t(
'TOTAL_MARGIN_AVAILABLE',
TOTAL_MARGIN_AVAILABLE,
{
generalAccountBalance: formatValue(
generalAccountBalance,
assetDecimals,
quantum
),
marginAccountBalance: formatValue(
marginAccountBalance,
assetDecimals,
quantum
),
orderMarginAccountBalance: formatValue(
orderMarginAccountBalance,
assetDecimals,
quantum
),
marginMaintenance: formatValue(
currentMargins?.maintenanceLevel,
assetDecimals,
quantum
),
assetSymbol,
}
)}
/>
{deductionFromCollateral}
<KeyValue
label={t('Current margin allocation')}
indent
onClick={
generalAccountBalance
? () => setBreakdownDialog(true)
: undefined
}
value={formatValue(
totalMarginAccountBalance.toString(),
assetDecimals
)}
symbol={assetSymbol}
labelDescription={t(
'MARGIN_ACCOUNT_TOOLTIP_TEXT',
MARGIN_ACCOUNT_TOOLTIP_TEXT
)}
formattedValue={formatValue(
totalMarginAccountBalance.toString(),
assetDecimals,
quantum
)}
/>
</div>
</AccordionPanel>
</Accordion>
{projectedMargin}
<KeyValue <KeyValue
label={t('Current margin')} label={t('Liquidation')}
onClick={
generalAccountBalance ? () => setBreakdownDialog(true) : undefined
}
value={formatValue(totalMarginAccountBalance.toString(), assetDecimals)}
symbol={assetSymbol}
labelDescription={t(
'MARGIN_ACCOUNT_TOOLTIP_TEXT',
MARGIN_ACCOUNT_TOOLTIP_TEXT
)}
formattedValue={formatValue(
totalMarginAccountBalance.toString(),
assetDecimals,
quantum
)}
/>
<KeyValue
label={t('Available collateral')}
value={formatValue(generalAccountBalance, assetDecimals)}
formattedValue={formatValue(
generalAccountBalance.toString(),
assetDecimals,
quantum
)}
symbol={assetSymbol}
/>
<KeyValue
label={t('Additional margin required')}
value={formatRange(
collateralIncreaseEstimateBestCase.toString(),
collateralIncreaseEstimateWorstCase.toString(),
assetDecimals
)}
formattedValue={formatValue(
collateralIncreaseEstimateBestCase.toString(),
assetDecimals,
quantum
)}
symbol={assetSymbol}
/>
<KeyValue
label={t('Liquidation estimate')}
value={liquidationPriceEstimateRange} value={liquidationPriceEstimateRange}
formattedValue={liquidationPriceEstimate} formattedValue={liquidationPriceEstimate}
symbol={quoteName} symbol={quoteName}
@@ -73,7 +73,7 @@ import {
} from '../../hooks'; } from '../../hooks';
import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg'; import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg';
import noop from 'lodash/noop'; import noop from 'lodash/noop';
import { isNonPersistentOrder } from '../../utils/time-in-force-persistence'; import { isNonPersistentOrder } from '../../utils/time-in-force-persistance';
import { KeyValue } from './key-value'; import { KeyValue } from './key-value';
import { DocsLinks } from '@vegaprotocol/environment'; import { DocsLinks } from '@vegaprotocol/environment';
import { useT } from '../../use-t'; import { useT } from '../../use-t';
@@ -177,6 +177,12 @@ export const DealTicket = ({
loading: loadingGeneralAccountBalance, loading: loadingGeneralAccountBalance,
} = useAccountBalance(asset.id); } = useAccountBalance(asset.id);
const balance = (
BigInt(marginAccountBalance) +
BigInt(generalAccountBalance) +
BigInt(orderMarginAccountBalance)
).toString();
const { marketState, marketTradingMode } = marketData; const { marketState, marketTradingMode } = marketData;
const timeInForce = watch('timeInForce'); const timeInForce = watch('timeInForce');
@@ -723,11 +729,17 @@ export const DealTicket = ({
error={summaryError} error={summaryError}
asset={asset} asset={asset}
marketTradingMode={marketData.marketTradingMode} marketTradingMode={marketData.marketTradingMode}
balance={generalAccountBalance} balance={balance}
margin={ margin={(
positionEstimate?.estimatePosition?.collateralIncreaseEstimate BigInt(
.bestCase || '0' positionEstimate?.estimatePosition?.margin.bestCase.initialLevel ||
} '0'
) +
BigInt(
positionEstimate?.estimatePosition?.margin.bestCase
.orderMarginLevel || '0'
)
).toString()}
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
pubKey={pubKey} pubKey={pubKey}
onDeposit={onDeposit} onDeposit={onDeposit}
@@ -1,4 +1,5 @@
import { Tooltip } from '@vegaprotocol/ui-toolkit'; import { Tooltip } from '@vegaprotocol/ui-toolkit';
import classnames from 'classnames';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
export interface KeyValuePros { export interface KeyValuePros {
@@ -18,6 +19,7 @@ export const KeyValue = ({
value, value,
labelDescription, labelDescription,
symbol, symbol,
indent,
onClick, onClick,
formattedValue, formattedValue,
}: KeyValuePros) => { }: KeyValuePros) => {
@@ -41,7 +43,10 @@ export const KeyValue = ({
: id : id
}`} }`}
key={typeof label === 'string' ? label : 'value-dropdown'} key={typeof label === 'string' ? label : 'value-dropdown'}
className="text-xs flex justify-between items-center gap-4 flex-wrap text-right" className={classnames(
'text-xs flex justify-between items-center gap-4 flex-wrap text-right',
{ 'ml-2': indent }
)}
> >
<Tooltip description={labelDescription}> <Tooltip description={labelDescription}>
<div className="text-muted text-left">{label}</div> <div className="text-muted text-left">{label}</div>
@@ -29,7 +29,6 @@ import { usePositionEstimate } from '../../hooks/use-position-estimate';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { getAsset, useMarket } from '@vegaprotocol/markets'; import { getAsset, useMarket } from '@vegaprotocol/markets';
import { NoWalletWarning } from './deal-ticket'; import { NoWalletWarning } from './deal-ticket';
import { DealTicketMarginDetails } from './deal-ticket-margin-details';
const defaultLeverage = 10; const defaultLeverage = 10;
@@ -94,78 +93,66 @@ export const MarginChange = ({
}, },
skip skip
); );
if (!asset || !estimateMargin?.estimatePosition) { if (
!asset ||
!estimateMargin?.estimatePosition?.collateralIncreaseEstimate.worstCase ||
estimateMargin.estimatePosition.collateralIncreaseEstimate.worstCase === '0'
) {
return null; return null;
} }
const collateralIncreaseEstimate = BigInt( const collateralIncreaseEstimate = BigInt(
estimateMargin.estimatePosition.collateralIncreaseEstimate.worstCase estimateMargin.estimatePosition.collateralIncreaseEstimate.worstCase
); );
if (!collateralIncreaseEstimate) {
return null;
}
let positionWarning = ''; let positionWarning = '';
if (orders?.length && openVolume !== '0') {
positionWarning = t(
'youHaveOpenPositionAndOrders',
'You have an existing position and open orders on this market.',
{
count: orders.length,
}
);
} else if (!orders?.length) {
positionWarning = t('You have an existing position on this market.');
} else {
positionWarning = t(
'youHaveOpenOrders',
'You have open orders on this market.',
{
count: orders.length,
}
);
}
let marginChangeWarning = ''; let marginChangeWarning = '';
if (collateralIncreaseEstimate) { const amount = addDecimalsFormatNumber(
if (orders?.length && openVolume !== '0') { collateralIncreaseEstimate.toString(),
positionWarning = t( asset?.decimals
'youHaveOpenPositionAndOrders', );
'You have an existing position and open orders on this market.', const { symbol } = asset;
{ const interpolation = { amount, symbol };
count: orders.length, if (marginMode === Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN) {
} marginChangeWarning = t(
); 'Changing the margin mode will move {{amount}} {{symbol}} from your general account to fund the position.',
} else if (!orders?.length) { interpolation
positionWarning = t('You have an existing position on this market.'); );
} else { } else {
positionWarning = t( marginChangeWarning = t(
'youHaveOpenOrders', 'Changing the margin mode and leverage will move {{amount}} {{symbol}} from your general account to fund the position.',
'You have open orders on this market.', interpolation
{
count: orders.length,
}
);
}
const amount = addDecimalsFormatNumber(
collateralIncreaseEstimate.toString(),
asset?.decimals
); );
const { symbol } = asset;
const interpolation = { amount, symbol };
if (marginMode === Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN) {
marginChangeWarning = t(
'Changing the margin mode will move {{amount}} {{symbol}} from your general account to fund the position.',
interpolation
);
} else {
marginChangeWarning = t(
'Changing the margin mode and leverage will move {{amount}} {{symbol}} from your general account to fund the position.',
interpolation
);
}
} }
return ( return (
<div className="mb-2"> <div className="mb-2">
{positionWarning && marginChangeWarning && ( <Notification
<Notification intent={Intent.Warning}
intent={Intent.Warning} message={
message={ <>
<> <p>{positionWarning}</p>
<p>{positionWarning}</p> <p>{marginChangeWarning}</p>
<p>{marginChangeWarning}</p> </>
</>
}
/>
)}
<DealTicketMarginDetails
marginAccountBalance={marginAccountBalance}
generalAccountBalance={generalAccountBalance}
orderMarginAccountBalance={orderMarginAccountBalance}
assetSymbol={asset.symbol}
market={market}
positionEstimate={estimateMargin.estimatePosition}
side={
openVolume.startsWith('-')
? Schema.Side.SIDE_SELL
: Schema.Side.SIDE_BUY
} }
/> />
</div> </div>
@@ -9,7 +9,7 @@ import type {
} from '../hooks/use-form-values'; } from '../hooks/use-form-values';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils'; import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
import { isPersistentOrder } from './time-in-force-persistence'; import { isPersistentOrder } from './time-in-force-persistance';
export const mapFormValuesToOrderSubmission = ( export const mapFormValuesToOrderSubmission = (
order: OrderFormValues, order: OrderFormValues,
@@ -2,9 +2,9 @@ import { OrderTimeInForce } from '@vegaprotocol/types';
import { import {
isNonPersistentOrder, isNonPersistentOrder,
isPersistentOrder, isPersistentOrder,
} from './time-in-force-persistence'; } from './time-in-force-persistance';
it('isNonPersistentOrder', () => { it('isNonPeristentOrder', () => {
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_FOK)).toBe(true); expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_FOK)).toBe(true);
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_IOC)).toBe(true); expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_IOC)).toBe(true);
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTC)).toBe(false); expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTC)).toBe(false);
@@ -13,7 +13,7 @@ it('isNonPersistentOrder', () => {
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GFN)).toBe(false); expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GFN)).toBe(false);
}); });
it('isPersistentOrder', () => { it('isPeristentOrder', () => {
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_FOK)).toBe(false); expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_FOK)).toBe(false);
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_IOC)).toBe(false); expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_IOC)).toBe(false);
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTC)).toBe(true); expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTC)).toBe(true);
@@ -23,6 +23,7 @@ import {
SUBSCRIPTION_TIMEOUT, SUBSCRIPTION_TIMEOUT,
useNodeBasicStatus, useNodeBasicStatus,
useNodeSubscriptionStatus, useNodeSubscriptionStatus,
useResponseTime,
} from './row-data'; } from './row-data';
import { BLOCK_THRESHOLD, RowData } from './row-data'; import { BLOCK_THRESHOLD, RowData } from './row-data';
import { CUSTOM_NODE_KEY } from '../../types'; import { CUSTOM_NODE_KEY } from '../../types';
@@ -161,6 +162,19 @@ describe('useNodeBasicStatus', () => {
}); });
}); });
describe('useResponseTime', () => {
it('returns response time when url is valid', () => {
const { result } = renderHook(() =>
useResponseTime('https://localhost:1234')
);
expect(result.current.responseTime).toBe(50);
});
it('does not return response time when url is invalid', () => {
const { result } = renderHook(() => useResponseTime('nope'));
expect(result.current.responseTime).toBeUndefined();
});
});
describe('RowData', () => { describe('RowData', () => {
const props = { const props = {
id: '0', id: '0',
@@ -1,3 +1,4 @@
import { isValidUrl } from '@vegaprotocol/utils';
import { TradingRadio } from '@vegaprotocol/ui-toolkit'; import { TradingRadio } from '@vegaprotocol/ui-toolkit';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { CUSTOM_NODE_KEY } from '../../types'; import { CUSTOM_NODE_KEY } from '../../types';
@@ -7,7 +8,6 @@ import {
} from '../../utils/__generated__/NodeCheck'; } from '../../utils/__generated__/NodeCheck';
import { LayoutCell } from './layout-cell'; import { LayoutCell } from './layout-cell';
import { useT } from '../../use-t'; import { useT } from '../../use-t';
import { useResponseTime } from '../../utils/time';
export const POLL_INTERVAL = 1000; export const POLL_INTERVAL = 1000;
export const SUBSCRIPTION_TIMEOUT = 3000; export const SUBSCRIPTION_TIMEOUT = 3000;
@@ -108,6 +108,20 @@ export const useNodeBasicStatus = () => {
}; };
}; };
export const useResponseTime = (url: string, trigger?: unknown) => {
const [responseTime, setResponseTime] = useState<number>();
useEffect(() => {
if (!isValidUrl(url)) return;
if (typeof window.performance.getEntriesByName !== 'function') return; // protection for test environment
const requestUrl = new URL(url);
const requests = window.performance.getEntriesByName(requestUrl.href);
const { duration } =
(requests.length && requests[requests.length - 1]) || {};
setResponseTime(duration);
}, [url, trigger]);
return { responseTime };
};
export const RowData = ({ export const RowData = ({
id, id,
url, url,
@@ -10,7 +10,6 @@ import {
getUserEnabledFeatureFlags, getUserEnabledFeatureFlags,
setUserEnabledFeatureFlag, setUserEnabledFeatureFlag,
} from './use-environment'; } from './use-environment';
import { canMeasureResponseTime, measureResponseTime } from '../utils/time';
const noop = () => { const noop = () => {
/* no op*/ /* no op*/
@@ -18,10 +17,6 @@ const noop = () => {
jest.mock('@vegaprotocol/apollo-client'); jest.mock('@vegaprotocol/apollo-client');
jest.mock('zustand'); jest.mock('zustand');
jest.mock('../utils/time');
const mockCanMeasureResponseTime = canMeasureResponseTime as jest.Mock;
const mockMeasureResponseTime = measureResponseTime as jest.Mock;
const mockCreateClient = createClient as jest.Mock; const mockCreateClient = createClient as jest.Mock;
const createDefaultMockClient = () => { const createDefaultMockClient = () => {
@@ -160,14 +155,6 @@ describe('useEnvironment', () => {
const fastNode = 'https://api.n01.foo.vega.xyz'; const fastNode = 'https://api.n01.foo.vega.xyz';
const fastWait = 1000; const fastWait = 1000;
const nodes = [slowNode, fastNode]; const nodes = [slowNode, fastNode];
mockCanMeasureResponseTime.mockImplementation(() => true);
mockMeasureResponseTime.mockImplementation((url: string) => {
if (url === slowNode) return slowWait;
if (url === fastNode) return fastWait;
return Infinity;
});
// @ts-ignore: typscript doesn't recognise the mock implementation // @ts-ignore: typscript doesn't recognise the mock implementation
global.fetch.mockImplementation(setupFetch({ hosts: nodes })); global.fetch.mockImplementation(setupFetch({ hosts: nodes }));
@@ -181,7 +168,7 @@ describe('useEnvironment', () => {
statistics: { statistics: {
chainId: 'chain-id', chainId: 'chain-id',
blockHeight: '100', blockHeight: '100',
vegaTime: new Date(1).toISOString(), vegaTime: new Date().toISOString(),
}, },
}, },
}); });
@@ -209,8 +196,7 @@ describe('useEnvironment', () => {
expect(result.current.nodes).toEqual(nodes); expect(result.current.nodes).toEqual(nodes);
}); });
jest.advanceTimersByTime(2000); jest.runAllTimers();
// jest.runAllTimers();
await waitFor(() => { await waitFor(() => {
expect(result.current.status).toEqual('success'); expect(result.current.status).toEqual('success');
+29 -78
View File
@@ -19,9 +19,6 @@ import { compileErrors } from '../utils/compile-errors';
import { envSchema } from '../utils/validate-environment'; import { envSchema } from '../utils/validate-environment';
import { tomlConfigSchema } from '../utils/validate-configuration'; import { tomlConfigSchema } from '../utils/validate-configuration';
import uniq from 'lodash/uniq'; import uniq from 'lodash/uniq';
import orderBy from 'lodash/orderBy';
import first from 'lodash/first';
import { canMeasureResponseTime, measureResponseTime } from '../utils/time';
type Client = ReturnType<typeof createClient>; type Client = ReturnType<typeof createClient>;
type ClientCollection = { type ClientCollection = {
@@ -41,17 +38,8 @@ export type EnvStore = Env & Actions;
const VERSION = 1; const VERSION = 1;
export const STORAGE_KEY = `vega_url_${VERSION}`; export const STORAGE_KEY = `vega_url_${VERSION}`;
const QUERY_TIMEOUT = 3000;
const SUBSCRIPTION_TIMEOUT = 3000; const SUBSCRIPTION_TIMEOUT = 3000;
const raceAgainst = (timeout: number): Promise<false> =>
new Promise((resolve) => {
setTimeout(() => {
resolve(false);
}, timeout);
});
/** /**
* Fetch and validate a vega node configuration * Fetch and validate a vega node configuration
*/ */
@@ -76,88 +64,53 @@ const fetchConfig = async (url?: string) => {
const findNode = async (clients: ClientCollection): Promise<string | null> => { const findNode = async (clients: ClientCollection): Promise<string | null> => {
const tests = Object.entries(clients).map((args) => testNode(...args)); const tests = Object.entries(clients).map((args) => testNode(...args));
try { try {
const nodes = await Promise.all(tests); const url = await Promise.any(tests);
const responsiveNodes = nodes return url;
.filter(([, q, s]) => q && s) } catch {
.map(([url, q]) => {
return {
url,
...q,
};
});
// more recent and faster at the top
const ordered = orderBy(
responsiveNodes,
[(n) => n.blockHeight, (n) => n.vegaTime, (n) => n.responseTime],
['desc', 'desc', 'asc']
);
const best = first(ordered);
return best ? best.url : null;
} catch (err) {
// All tests rejected, no suitable node found // All tests rejected, no suitable node found
return null; return null;
} }
}; };
type Maybe<T> = T | false;
type QueryTestResult = {
blockHeight: number;
vegaTime: Date;
responseTime: number;
};
type SubscriptionTestResult = true;
type NodeTestResult = [
/** url */
string,
Maybe<QueryTestResult>,
Maybe<SubscriptionTestResult>
];
/** /**
* Test a node for suitability for connection * Test a node for suitability for connection
*/ */
const testNode = async ( const testNode = async (
url: string, url: string,
client: Client client: Client
): Promise<NodeTestResult> => { ): Promise<string | null> => {
const results = await Promise.all([ const results = await Promise.all([
testQuery(client, url), // these promises will only resolve with true/false
testQuery(client),
testSubscription(client), testSubscription(client),
]); ]);
return [url, ...results]; if (results[0] && results[1]) {
return url;
}
const message = `Tests failed for node: ${url}`;
console.warn(message);
// throwing here will mean this tests is ignored and a different
// node that hopefully does resolve will fulfill the Promise.any
throw new Error(message);
}; };
/** /**
* Run a test query on a client * Run a test query on a client
*/ */
const testQuery = ( const testQuery = async (client: Client) => {
client: Client, try {
url: string const result = await client.query<NodeCheckQuery>({
): Promise<Maybe<QueryTestResult>> => { query: NodeCheckDocument,
const test: Promise<Maybe<QueryTestResult>> = new Promise((resolve) => });
client if (!result || result.error) {
.query<NodeCheckQuery>({ return false;
query: NodeCheckDocument, }
}) return true;
.then((result) => { } catch (err) {
if (result && !result.error) { return false;
const res = { }
blockHeight: Number(result.data.statistics.blockHeight),
vegaTime: new Date(result.data.statistics.vegaTime),
// only after a request has been sent we can retrieve the response time
responseTime: canMeasureResponseTime(url)
? measureResponseTime(url) || Infinity
: Infinity,
} as QueryTestResult;
resolve(res);
} else {
resolve(false);
}
})
.catch(() => resolve(false))
);
return Promise.race([test, raceAgainst(QUERY_TIMEOUT)]);
}; };
/** /**
@@ -165,9 +118,7 @@ const testQuery = (
* that takes longer than SUBSCRIPTION_TIMEOUT ms to respond * that takes longer than SUBSCRIPTION_TIMEOUT ms to respond
* is deemed a failure * is deemed a failure
*/ */
const testSubscription = ( const testSubscription = (client: Client) => {
client: Client
): Promise<Maybe<SubscriptionTestResult>> => {
return new Promise((resolve) => { return new Promise((resolve) => {
const sub = client const sub = client
.subscribe<NodeCheckTimeUpdateSubscription>({ .subscribe<NodeCheckTimeUpdateSubscription>({
-1
View File
@@ -86,7 +86,6 @@ export const DocsLinks = VEGA_DOCS_URL
POST_REDUCE_ONLY: `${VEGA_DOCS_URL}/concepts/trading-on-vega/orders#conditional-order-parameters`, POST_REDUCE_ONLY: `${VEGA_DOCS_URL}/concepts/trading-on-vega/orders#conditional-order-parameters`,
QUANTUM: `${VEGA_DOCS_URL}/concepts/assets/asset-framework#quantum`, QUANTUM: `${VEGA_DOCS_URL}/concepts/assets/asset-framework#quantum`,
REFERRALS: `${VEGA_DOCS_URL}/tutorials/proposals/referral-program-proposal`, REFERRALS: `${VEGA_DOCS_URL}/tutorials/proposals/referral-program-proposal`,
LIQUIDITY_FEE_PERCENTAGE: `${VEGA_DOCS_URL}/concepts/liquidity/rewards-penalties#determining-the-liquidity-fee-percentage`,
} }
: undefined; : undefined;

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