Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8db286fa93 | ||
|
|
44189591fc | ||
|
|
3ed2ec88d7 | ||
|
|
a21feea699 | ||
|
|
41fd14dd00 | ||
|
|
c5a27dc6a2 | ||
|
|
0a3b1cadba | ||
|
|
b953de953a | ||
|
|
5ddcb613e2 | ||
|
|
76c07992d3 | ||
|
|
46e2965fa2 | ||
|
|
532ad3a4b9 | ||
|
|
3a8b40d7a5 | ||
|
|
be38813a33 | ||
|
|
bf70dc33ec | ||
|
|
d6084e75a0 | ||
|
|
75e7cea32a | ||
|
|
b4e98e285e | ||
|
|
f62e29c67f | ||
|
|
94e7ad489f | ||
|
|
657942d995 | ||
|
|
a529b9b031 | ||
|
|
0f311611ba | ||
|
|
02bd031bee | ||
|
|
bd97651dd3 | ||
|
|
516b3e5b93 | ||
|
|
42a98b6a35 |
@@ -205,7 +205,7 @@ jobs:
|
||||
# run tests
|
||||
#----------------------------------------------
|
||||
- name: Run tests
|
||||
run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v --numprocesses 1 --dist loadfile --durations=45
|
||||
run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v --numprocesses 4 --dist loadfile --durations=45
|
||||
working-directory: apps/trading/e2e
|
||||
#----------------------------------------------
|
||||
# upload traces
|
||||
|
||||
@@ -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).
|
||||
|
||||
# 🔎 Applications in this repo
|
||||
## 🔎 Applications in this repo
|
||||
|
||||
### [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.
|
||||
|
||||
# 🧱 Libraries in this repo
|
||||
## 🧱 Libraries in this repo
|
||||
|
||||
### [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.
|
||||
|
||||
# 💻 Develop
|
||||
## 💻 Develop
|
||||
|
||||
### 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.
|
||||
|
||||
# 🐋 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).
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
# 📑 License
|
||||
## 📑 License
|
||||
|
||||
[MIT](./LICENSE)
|
||||
|
||||
@@ -24,10 +24,6 @@ context('Proposal page', { tags: '@smoke' }, function () {
|
||||
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('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"]')
|
||||
.invoke('text')
|
||||
.should('match', dateTimeRegex);
|
||||
@@ -73,10 +69,6 @@ context('Proposal page', { tags: '@smoke' }, function () {
|
||||
'have.text',
|
||||
'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"]')
|
||||
.invoke('text')
|
||||
.should('match', dateTimeRegex);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { type ProposalListFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { VoteProgress } from '@vegaprotocol/proposals';
|
||||
import { type AgGridReact } from 'ag-grid-react';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGrid } from '@vegaprotocol/datagrid';
|
||||
@@ -12,12 +11,7 @@ import { type ColDef } from 'ag-grid-community';
|
||||
import type { RowClickedEvent } from 'ag-grid-community';
|
||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
import { ProposalStateMapping } from '@vegaprotocol/types';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
|
||||
import { BREAKPOINT_MD } from '../../config/breakpoints';
|
||||
import { JsonViewerDialog } from '../dialogs/json-viewer-dialog';
|
||||
@@ -31,15 +25,7 @@ type ProposalsTableProps = {
|
||||
data: ProposalListFieldsFragment[] | null;
|
||||
};
|
||||
export const ProposalsTable = ({ data }: ProposalsTableProps) => {
|
||||
const { params } = useNetworkParams([
|
||||
NetworkParams.governance_proposal_market_requiredMajority,
|
||||
]);
|
||||
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);
|
||||
useLayoutEffect(() => {
|
||||
@@ -90,33 +76,6 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
|
||||
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',
|
||||
maxWidth: 150,
|
||||
@@ -184,7 +143,7 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
|
||||
},
|
||||
},
|
||||
],
|
||||
[requiredMajorityPercentage, tokenLink]
|
||||
[tokenLink]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
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,6 +34,7 @@ import { TxDetailsUpdateReferralSet } from './tx-update-referral-set';
|
||||
import { TxDetailsJoinTeam } from './tx-join-team';
|
||||
import { TxDetailsUpdateMarginMode } from './tx-update-margin-mode';
|
||||
import { TxBatchProposal } from './tx-batch-proposal';
|
||||
import { TxDetailsUpdatePartyProfile } from './proposal/tx-update-party-profile';
|
||||
|
||||
interface TxDetailsWrapperProps {
|
||||
txData: BlockExplorerTransactionResult | undefined;
|
||||
@@ -139,6 +140,8 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
|
||||
return TxDetailsUpdateMarginMode;
|
||||
case 'Batch Proposal':
|
||||
return TxBatchProposal;
|
||||
case 'Update Party Profile':
|
||||
return TxDetailsUpdatePartyProfile;
|
||||
default:
|
||||
return TxDetailsGeneric;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ export type FilterOption =
|
||||
| 'Submit Order'
|
||||
| 'Transfer Funds'
|
||||
| 'Undelegate'
|
||||
| 'Update Party Profile'
|
||||
| 'Update Referral Set'
|
||||
| 'Update Margin Mode'
|
||||
| 'Validator Heartbeat'
|
||||
@@ -79,6 +80,7 @@ export const filterOptions: Record<string, FilterOption[]> = {
|
||||
'Apply Referral Code',
|
||||
'Create Referral Set',
|
||||
'Join Team',
|
||||
'Update Party Profile',
|
||||
'Update Referral Set',
|
||||
],
|
||||
'External Data': ['Chain Event', 'Submit Oracle Data'],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"short_name": "Mainnet Stats",
|
||||
"name": "Vega Mainnet statistics",
|
||||
"short_name": "Explorer VEGA",
|
||||
"name": "Vega Protocol - Explorer",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"short_name": "Mainnet Stats",
|
||||
"name": "Vega Mainnet statistics",
|
||||
"short_name": "Governance VEGA",
|
||||
"name": "Vega Protocol - Governance",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<title>Vega Protocol static asseets</title>
|
||||
<title>Vega Protocol static assets</title>
|
||||
<link rel="stylesheet" href="fonts.css" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
</head>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
NX_ETHEREUM_PROVIDER_URL=https://eth-mainnet.gateway.pokt.network/v1/lb/af6a2d529a11f8158bc8ca2a
|
||||
NX_ETHEREUM_PROVIDER_URL=https://eth-mainnet.rpc.grove.city/v1/af6a2d529a11f8158bc8ca2a
|
||||
NX_ETHERSCAN_URL=https://etherscan.io
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.sentry.io/6300613
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
{
|
||||
"short_name": "Mainnet Stats",
|
||||
"name": "Vega Mainnet statistics",
|
||||
"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",
|
||||
@@ -12,9 +18,5 @@
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { Intent, TradingAnchorButton } from '@vegaprotocol/ui-toolkit';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
import {
|
||||
Intent,
|
||||
TradingAnchorButton,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { useT } from '../../lib/use-t';
|
||||
@@ -30,8 +35,21 @@ export const CompetitionsCreateTeam = () => {
|
||||
<LayoutWithGradient>
|
||||
<div className="mx-auto md:w-2/3 max-w-xl">
|
||||
<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">
|
||||
{t('Create a team')}
|
||||
{isSolo ? t('Create solo team') : t('Create a team')}
|
||||
</h1>
|
||||
{pubKey && !isReadOnly ? (
|
||||
<CreateTeamFormContainer isSolo={isSolo} />
|
||||
@@ -71,18 +89,29 @@ const CreateTeamFormContainer = ({ isSolo }: { isSolo: boolean }) => {
|
||||
|
||||
if (status === 'confirmed') {
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<div
|
||||
className="flex flex-col items-start gap-2"
|
||||
data-testid="team-creation-success-message"
|
||||
>
|
||||
<p className="text-sm">{t('Team creation transaction successful')}</p>
|
||||
{code && (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
Your team ID is:{' '}
|
||||
<span className="font-mono break-all">{code}</span>
|
||||
</p>
|
||||
<dl>
|
||||
<dt className="text-sm">{t('Your team ID:')}</dt>
|
||||
<dl>
|
||||
<span
|
||||
className="font-mono break-all bg-rainbow bg-clip-text text-transparent text-2xl"
|
||||
data-testid="team-id-display"
|
||||
>
|
||||
{code}
|
||||
</span>
|
||||
</dl>
|
||||
</dl>
|
||||
<TradingAnchorButton
|
||||
href={Links.COMPETITIONS_TEAM(code)}
|
||||
intent={Intent.Info}
|
||||
size="small"
|
||||
data-testid="view-team-button"
|
||||
>
|
||||
{t('View team')}
|
||||
</TradingAnchorButton>
|
||||
@@ -125,7 +154,7 @@ const CreateTeamFormContainer = ({ isSolo }: { isSolo: boolean }) => {
|
||||
onSubmit={onSubmit}
|
||||
status={status}
|
||||
err={err}
|
||||
isSolo={isSolo}
|
||||
isCreatingSoloTeam={isSolo}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,6 +16,8 @@ import { CompetitionsLeaderboard } from '../../components/competitions/competiti
|
||||
import { useTeams } from '../../lib/hooks/use-teams';
|
||||
import take from 'lodash/take';
|
||||
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 = () => {
|
||||
const t = useT();
|
||||
@@ -31,10 +33,14 @@ export const CompetitionsHome = () => {
|
||||
currentEpoch,
|
||||
});
|
||||
|
||||
const { data: teamsData, loading: teamsLoading } = useTeams({
|
||||
sortByField: ['totalQuantumRewards'],
|
||||
order: 'desc',
|
||||
});
|
||||
const { data: teamsData, loading: teamsLoading } = useTeams();
|
||||
|
||||
const {
|
||||
team: myTeam,
|
||||
stats: myTeamStats,
|
||||
games: myTeamGames,
|
||||
rank: myTeamRank,
|
||||
} = useMyTeam();
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
@@ -46,65 +52,83 @@ export const CompetitionsHome = () => {
|
||||
</p>
|
||||
</CompetitionsHeader>
|
||||
|
||||
{/** Get started */}
|
||||
<h2 className="text-2xl mb-6">{t('Get started')}</h2>
|
||||
{/** Team card */}
|
||||
{myTeam ? (
|
||||
<>
|
||||
<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>
|
||||
<CompetitionsAction
|
||||
variant="A"
|
||||
title={t('Create a team')}
|
||||
description={t(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||
)}
|
||||
actionElement={
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(Links.COMPETITIONS_CREATE_TEAM());
|
||||
}}
|
||||
>
|
||||
{t('Create a public team')}
|
||||
</TradingButton>
|
||||
}
|
||||
/>
|
||||
<CompetitionsAction
|
||||
variant="B"
|
||||
title={t('Solo team / lone wolf')}
|
||||
description={t(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||
)}
|
||||
actionElement={
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(Links.COMPETITIONS_CREATE_TEAM_SOLO());
|
||||
}}
|
||||
>
|
||||
{t('Create a private team')}
|
||||
</TradingButton>
|
||||
}
|
||||
/>
|
||||
<CompetitionsAction
|
||||
variant="C"
|
||||
title={t('Join a team')}
|
||||
description={t(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||
)}
|
||||
actionElement={
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(Links.COMPETITIONS_TEAMS());
|
||||
}}
|
||||
>
|
||||
{t('Choose a team')}
|
||||
</TradingButton>
|
||||
}
|
||||
/>
|
||||
</CompetitionsActionsContainer>
|
||||
<CompetitionsActionsContainer>
|
||||
<CompetitionsAction
|
||||
variant="A"
|
||||
title={t('Create a team')}
|
||||
description={t(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||
)}
|
||||
actionElement={
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(Links.COMPETITIONS_CREATE_TEAM());
|
||||
}}
|
||||
data-testId="create-public-team-button"
|
||||
>
|
||||
{t('Create a public team')}
|
||||
</TradingButton>
|
||||
}
|
||||
/>
|
||||
<CompetitionsAction
|
||||
variant="B"
|
||||
title={t('Solo team / lone wolf')}
|
||||
description={t(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||
)}
|
||||
actionElement={
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(Links.COMPETITIONS_CREATE_TEAM_SOLO());
|
||||
}}
|
||||
>
|
||||
{t('Create a private team')}
|
||||
</TradingButton>
|
||||
}
|
||||
/>
|
||||
<CompetitionsAction
|
||||
variant="C"
|
||||
title={t('Join a team')}
|
||||
description={t(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||
)}
|
||||
actionElement={
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(Links.COMPETITIONS_TEAMS());
|
||||
}}
|
||||
>
|
||||
{t('Choose a team')}
|
||||
</TradingButton>
|
||||
}
|
||||
/>
|
||||
</CompetitionsActionsContainer>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/** List of available games */}
|
||||
<h2 className="text-2xl mb-6">{t('Games')}</h2>
|
||||
|
||||
@@ -38,12 +38,11 @@ export const CompetitionsTeam = () => {
|
||||
const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => {
|
||||
const t = useT();
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { team, partyTeam, stats, members, games, loading, refetch } = useTeam(
|
||||
teamId,
|
||||
pubKey || undefined
|
||||
);
|
||||
const { data, team, partyTeam, stats, members, games, loading, refetch } =
|
||||
useTeam(teamId, pubKey || undefined);
|
||||
|
||||
if (loading) {
|
||||
// only show spinner on first load so when users join teams its smoother
|
||||
if (!data && loading) {
|
||||
return (
|
||||
<Splash>
|
||||
<Loader />
|
||||
@@ -100,8 +99,10 @@ const TeamPage = ({
|
||||
>
|
||||
{team.name}
|
||||
</h1>
|
||||
<JoinTeam team={team} partyTeam={partyTeam} refetch={refetch} />
|
||||
<UpdateTeamButton team={team} />
|
||||
<div className="flex gap-2">
|
||||
<JoinTeam team={team} partyTeam={partyTeam} refetch={refetch} />
|
||||
<UpdateTeamButton team={team} />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<TeamStats stats={stats} members={members} games={games} />
|
||||
@@ -184,7 +185,10 @@ const Members = ({ members }: { members?: Member[] }) => {
|
||||
|
||||
const data = orderBy(
|
||||
members.map((m) => ({
|
||||
referee: <RefereeLink pubkey={m.referee} />,
|
||||
referee: <RefereeLink pubkey={m.referee} isCreator={m.isCreator} />,
|
||||
rewards: formatNumber(m.totalQuantumRewards),
|
||||
volume: formatNumber(m.totalQuantumVolume),
|
||||
gamesPlayed: formatNumber(m.totalGamesPlayed),
|
||||
joinedAt: getDateTimeFormat().format(new Date(m.joinedAt)),
|
||||
joinedAtEpoch: Number(m.joinedAtEpoch),
|
||||
})),
|
||||
@@ -195,7 +199,10 @@ const Members = ({ members }: { members?: Member[] }) => {
|
||||
return (
|
||||
<Table
|
||||
columns={[
|
||||
{ name: 'referee', displayName: t('Referee') },
|
||||
{ name: 'referee', displayName: t('Member ID') },
|
||||
{ name: 'rewards', displayName: t('Rewards earned') },
|
||||
{ name: 'volume', displayName: t('Total volume') },
|
||||
{ name: 'gamesPlayed', displayName: t('Games played') },
|
||||
{
|
||||
name: 'joinedAt',
|
||||
displayName: t('Joined at'),
|
||||
@@ -211,14 +218,24 @@ const Members = ({ members }: { members?: Member[] }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const RefereeLink = ({ pubkey }: { pubkey: string }) => {
|
||||
const RefereeLink = ({
|
||||
pubkey,
|
||||
isCreator,
|
||||
}: {
|
||||
pubkey: string;
|
||||
isCreator: boolean;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const linkCreator = useLinks(DApp.Explorer);
|
||||
const link = linkCreator(EXPLORER_PARTIES.replace(':id', pubkey));
|
||||
|
||||
return (
|
||||
<Link to={link} target="_blank" className="underline underline-offset-4">
|
||||
{truncateMiddle(pubkey)}
|
||||
</Link>
|
||||
<>
|
||||
<Link to={link} target="_blank" className="underline underline-offset-4">
|
||||
{truncateMiddle(pubkey)}
|
||||
</Link>{' '}
|
||||
<span className="text-muted text-xs">{isCreator ? t('Owner') : ''}</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -17,10 +17,7 @@ export const CompetitionsTeams = () => {
|
||||
|
||||
usePageTitle([t('Competitions'), t('Teams')]);
|
||||
|
||||
const { data: teamsData, loading: teamsLoading } = useTeams({
|
||||
sortByField: ['totalQuantumRewards'],
|
||||
order: 'desc',
|
||||
});
|
||||
const { data: teamsData, loading: teamsLoading } = useTeams();
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [filter, setFilter] = useState<string | null | undefined>(undefined);
|
||||
|
||||
@@ -3,7 +3,14 @@ import { usePageTitle } from '../../lib/hooks/use-page-title';
|
||||
import { Box } from '../../components/competitions/box';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
Intent,
|
||||
Loader,
|
||||
Splash,
|
||||
TradingAnchorButton,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { RainbowButton } from '../../components/rainbow-button';
|
||||
import { Link, Navigate, useParams } from 'react-router-dom';
|
||||
import { Links } from '../../lib/links';
|
||||
@@ -11,6 +18,7 @@ import { useReferralSetTransaction } from '../../lib/hooks/use-referral-set-tran
|
||||
import { type FormFields, TeamForm, TransactionType } from './team-form';
|
||||
import { useTeam } from '../../lib/hooks/use-team';
|
||||
import { LayoutWithGradient } from '../../components/layouts-inner';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const CompetitionsUpdateTeam = () => {
|
||||
const t = useT();
|
||||
@@ -29,6 +37,19 @@ export const CompetitionsUpdateTeam = () => {
|
||||
<LayoutWithGradient>
|
||||
<div className="mx-auto md:w-2/3 max-w-xl">
|
||||
<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">
|
||||
{t('Update a team')}
|
||||
</h1>
|
||||
@@ -57,7 +78,8 @@ const UpdateTeamFormContainer = ({
|
||||
pubKey: string;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const { team, loading, error } = useTeam(teamId, pubKey);
|
||||
const [refetching, setRefetching] = useState<boolean>(false);
|
||||
const { team, loading, error, refetch } = useTeam(teamId, pubKey);
|
||||
|
||||
const { err, status, onSubmit } = useReferralSetTransaction({
|
||||
onSuccess: () => {
|
||||
@@ -65,7 +87,15 @@ const UpdateTeamFormContainer = ({
|
||||
},
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
// refetch when saved
|
||||
useEffect(() => {
|
||||
if (refetch && status === 'confirmed') {
|
||||
refetch();
|
||||
setRefetching(true);
|
||||
}
|
||||
}, [refetch, status]);
|
||||
|
||||
if (loading && !refetching) {
|
||||
return <Loader size="small" />;
|
||||
}
|
||||
if (error) {
|
||||
@@ -84,6 +114,33 @@ const UpdateTeamFormContainer = ({
|
||||
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 = {
|
||||
id: team.teamId,
|
||||
name: team.name,
|
||||
@@ -98,7 +155,7 @@ const UpdateTeamFormContainer = ({
|
||||
type={TransactionType.UpdateReferralSet}
|
||||
status={status}
|
||||
err={err}
|
||||
isSolo={team.closed}
|
||||
isCreatingSoloTeam={team.closed}
|
||||
onSubmit={onSubmit}
|
||||
defaultValues={defaultValues}
|
||||
/>
|
||||
|
||||
@@ -6,11 +6,7 @@ import {
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
useSimpleTransaction,
|
||||
useVegaWallet,
|
||||
type Status,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import { useSimpleTransaction, useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { type Team } from '../../lib/hooks/use-team';
|
||||
import { useState } from 'react';
|
||||
@@ -27,19 +23,8 @@ export const JoinTeam = ({
|
||||
refetch: () => void;
|
||||
}) => {
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
const { send, status } = useSimpleTransaction({
|
||||
onSuccess: refetch,
|
||||
});
|
||||
const [confirmDialog, setConfirmDialog] = useState<JoinType>();
|
||||
|
||||
const joinTeam = () => {
|
||||
send({
|
||||
joinTeam: {
|
||||
id: team.teamId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<JoinButton
|
||||
@@ -56,11 +41,10 @@ export const JoinTeam = ({
|
||||
{confirmDialog !== undefined && (
|
||||
<DialogContent
|
||||
type={confirmDialog}
|
||||
status={status}
|
||||
team={team}
|
||||
partyTeam={partyTeam}
|
||||
onConfirm={joinTeam}
|
||||
onCancel={() => setConfirmDialog(undefined)}
|
||||
refetch={refetch}
|
||||
/>
|
||||
)}
|
||||
</Dialog>
|
||||
@@ -110,7 +94,7 @@ export const JoinButton = ({
|
||||
// Not creator of the team, but still can't switch because
|
||||
// creators cannot leave their own team
|
||||
return (
|
||||
<Tooltip description="As a team creator, you cannot switch teams">
|
||||
<Tooltip description={t('As a team creator, you cannot switch teams')}>
|
||||
<Button intent={Intent.Primary} disabled={true}>
|
||||
{t('Switch team')}{' '}
|
||||
</Button>
|
||||
@@ -121,7 +105,11 @@ export const JoinButton = ({
|
||||
// Party is in a team, but not this one
|
||||
else if (partyTeam && partyTeam.teamId !== team.teamId) {
|
||||
return (
|
||||
<Button onClick={() => onJoin('switch')} intent={Intent.Primary}>
|
||||
<Button
|
||||
onClick={() => onJoin('switch')}
|
||||
intent={Intent.Primary}
|
||||
data-testid="switch-team-button"
|
||||
>
|
||||
{t('Switch team')}{' '}
|
||||
</Button>
|
||||
);
|
||||
@@ -149,21 +137,39 @@ export const JoinButton = ({
|
||||
|
||||
const DialogContent = ({
|
||||
type,
|
||||
status,
|
||||
team,
|
||||
partyTeam,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
refetch,
|
||||
}: {
|
||||
type: JoinType;
|
||||
status: Status;
|
||||
team: Team;
|
||||
partyTeam?: Team;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
refetch: () => void;
|
||||
}) => {
|
||||
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') {
|
||||
return <p>{t('Confirm in wallet...')}</p>;
|
||||
}
|
||||
@@ -213,7 +219,11 @@ const DialogContent = ({
|
||||
</>
|
||||
)}
|
||||
<div className="flex justify-between gap-2">
|
||||
<Button onClick={onConfirm} intent={Intent.Success}>
|
||||
<Button
|
||||
onClick={joinTeam}
|
||||
intent={Intent.Success}
|
||||
data-testid="confirm-switch-button"
|
||||
>
|
||||
{t('Confirm')}
|
||||
</Button>
|
||||
<Button onClick={onCancel} intent={Intent.Danger}>
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
TextArea,
|
||||
TradingButton,
|
||||
Intent,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { URL_REGEX, isValidVegaPublicKey } from '@vegaprotocol/utils';
|
||||
|
||||
@@ -17,6 +19,8 @@ import type {
|
||||
UpdateReferralSet,
|
||||
Status,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import classNames from 'classnames';
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
|
||||
export type FormFields = {
|
||||
id: string;
|
||||
@@ -28,8 +32,8 @@ export type FormFields = {
|
||||
};
|
||||
|
||||
export enum TransactionType {
|
||||
CreateReferralSet,
|
||||
UpdateReferralSet,
|
||||
CreateReferralSet = 'CreateReferralSet',
|
||||
UpdateReferralSet = 'UpdateReferralSet',
|
||||
}
|
||||
|
||||
const prepareTransaction = (
|
||||
@@ -75,14 +79,14 @@ export const TeamForm = ({
|
||||
type,
|
||||
status,
|
||||
err,
|
||||
isSolo,
|
||||
isCreatingSoloTeam,
|
||||
onSubmit,
|
||||
defaultValues,
|
||||
}: {
|
||||
type: TransactionType;
|
||||
status: ReturnType<typeof useReferralSetTransaction>['status'];
|
||||
err: ReturnType<typeof useReferralSetTransaction>['err'];
|
||||
isSolo: boolean;
|
||||
isCreatingSoloTeam: boolean;
|
||||
onSubmit: ReturnType<typeof useReferralSetTransaction>['onSubmit'];
|
||||
defaultValues?: FormFields;
|
||||
}) => {
|
||||
@@ -96,7 +100,7 @@ export const TeamForm = ({
|
||||
formState: { errors },
|
||||
} = useForm<FormFields>({
|
||||
defaultValues: {
|
||||
private: isSolo,
|
||||
private: isCreatingSoloTeam,
|
||||
...defaultValues,
|
||||
},
|
||||
});
|
||||
@@ -109,16 +113,14 @@ export const TeamForm = ({
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(sendTransaction)}>
|
||||
<input
|
||||
type="hidden"
|
||||
{...register('id', {
|
||||
disabled: true,
|
||||
})}
|
||||
/>
|
||||
<input type="hidden" {...register('id')} />
|
||||
<TradingFormGroup label={t('Team name')} labelFor="name">
|
||||
<TradingInput {...register('name', { required: t('Required') })} />
|
||||
<TradingInput
|
||||
{...register('name', { required: t('Required') })}
|
||||
data-testid="team-name-input"
|
||||
/>
|
||||
{errors.name?.message && (
|
||||
<TradingInputError forInput="name">
|
||||
<TradingInputError forInput="name" data-testid="team-name-error">
|
||||
{errors.name.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
@@ -134,9 +136,10 @@ export const TeamForm = ({
|
||||
{...register('url', {
|
||||
pattern: { value: URL_REGEX, message: t('Invalid URL') },
|
||||
})}
|
||||
data-testid="team-url-input"
|
||||
/>
|
||||
{errors.url?.message && (
|
||||
<TradingInputError forInput="url">
|
||||
<TradingInputError forInput="url" data-testid="team-url-error">
|
||||
{errors.url.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
@@ -153,66 +156,86 @@ export const TeamForm = ({
|
||||
message: t('Invalid image URL'),
|
||||
},
|
||||
})}
|
||||
data-testid="avatar-url-input"
|
||||
/>
|
||||
{errors.avatarUrl?.message && (
|
||||
<TradingInputError forInput="avatarUrl">
|
||||
<TradingInputError
|
||||
forInput="avatarUrl"
|
||||
data-testid="avatar-url-error"
|
||||
>
|
||||
{errors.avatarUrl.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup
|
||||
label={t('Make team private')}
|
||||
labelFor="private"
|
||||
hideLabel={true}
|
||||
>
|
||||
<Controller
|
||||
name="private"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<TradingCheckbox
|
||||
label={t('Make team private')}
|
||||
checked={field.value}
|
||||
onCheckedChange={(value) => {
|
||||
field.onChange(value);
|
||||
{
|
||||
// allow changing to private/public if editing, but don't show these options if making a solo team
|
||||
(type === TransactionType.UpdateReferralSet || !isCreatingSoloTeam) && (
|
||||
<>
|
||||
<TradingFormGroup
|
||||
label={t('Make team private')}
|
||||
labelFor="private"
|
||||
hideLabel={true}
|
||||
>
|
||||
<Controller
|
||||
name="private"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<TradingCheckbox
|
||||
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')}
|
||||
labelFor="allowList"
|
||||
labelDescription={t(
|
||||
'Use a comma separated list to allow only specific public keys to join the team'
|
||||
)}
|
||||
>
|
||||
<TextArea
|
||||
{...register('allowList', {
|
||||
required: t('Required'),
|
||||
disabled: isSolo,
|
||||
validate: {
|
||||
allowList: (value) => {
|
||||
const publicKeys = parseAllowListText(value);
|
||||
if (publicKeys.every((pk) => isValidVegaPublicKey(pk))) {
|
||||
return true;
|
||||
}
|
||||
return t('Invalid public key found in allow list');
|
||||
},
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.allowList?.message && (
|
||||
<TradingInputError forInput="avatarUrl">
|
||||
{errors.allowList.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
</TradingFormGroup>
|
||||
{isPrivate && (
|
||||
<TradingFormGroup
|
||||
label={t('Public key allow list')}
|
||||
labelFor="allowList"
|
||||
labelDescription={t(
|
||||
'Use a comma separated list to allow only specific public keys to join the team'
|
||||
)}
|
||||
>
|
||||
<TextArea
|
||||
{...register('allowList', {
|
||||
required: t('Required'),
|
||||
validate: {
|
||||
allowList: (value) => {
|
||||
const publicKeys = parseAllowListText(value);
|
||||
if (
|
||||
publicKeys.every((pk) => isValidVegaPublicKey(pk))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return t('Invalid public key found in allow list');
|
||||
},
|
||||
},
|
||||
})}
|
||||
data-testid="team-allow-list-textarea"
|
||||
/>
|
||||
{errors.allowList?.message && (
|
||||
<TradingInputError
|
||||
forInput="avatarUrl"
|
||||
data-testid="team-allow-list-error"
|
||||
>
|
||||
{errors.allowList.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</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} />
|
||||
</form>
|
||||
);
|
||||
@@ -233,20 +256,61 @@ const SubmitButton = ({
|
||||
text = t('Update');
|
||||
}
|
||||
|
||||
let confirmedText = t('Created');
|
||||
if (type === TransactionType.UpdateReferralSet) {
|
||||
confirmedText = t('Updated');
|
||||
}
|
||||
|
||||
if (status === 'requested') {
|
||||
text = t('Confirm in wallet...');
|
||||
} else if (status === 'pending') {
|
||||
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 (
|
||||
<TradingButton type="submit" intent={Intent.Info} disabled={disabled}>
|
||||
{text}
|
||||
</TradingButton>
|
||||
<div className="flex gap-2 items-baseline">
|
||||
<TradingButton
|
||||
type="submit"
|
||||
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
|
||||
.split(',')
|
||||
.map((v) => v.trim())
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { type Team } from '../../lib/hooks/use-team';
|
||||
import { type ComponentProps } from 'react';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { Intent, TradingAnchorButton } from '@vegaprotocol/ui-toolkit';
|
||||
import { Links } from '../../lib/links';
|
||||
import { useT } from '../../lib/use-t';
|
||||
|
||||
export const UpdateTeamButton = ({ team }: { team: Team }) => {
|
||||
export const UpdateTeamButton = ({
|
||||
team,
|
||||
size = 'medium',
|
||||
}: {
|
||||
team: Pick<Team, 'teamId' | 'referrer'>;
|
||||
size?: ComponentProps<typeof TradingAnchorButton>['size'];
|
||||
}) => {
|
||||
const t = useT();
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
|
||||
if (pubKey && !isReadOnly && pubKey === team.referrer) {
|
||||
return (
|
||||
<TradingAnchorButton
|
||||
size={size}
|
||||
data-testid="update-team-button"
|
||||
href={Links.COMPETITIONS_UPDATE_TEAM(team.teamId)}
|
||||
intent={Intent.Info}
|
||||
/>
|
||||
>
|
||||
{t('Update team')}
|
||||
</TradingAnchorButton>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
useFundingRate,
|
||||
useMarketTradingMode,
|
||||
useExternalTwap,
|
||||
getQuoteName,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { MarketState as State } from '@vegaprotocol/types';
|
||||
import { HeaderStat } from '../../components/header';
|
||||
@@ -41,6 +42,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
|
||||
const asset = getAsset(market);
|
||||
const quoteUnit = getQuoteName(market);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -54,12 +56,15 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
|
||||
<Last24hPriceChange
|
||||
marketId={market.id}
|
||||
decimalPlaces={market.decimalPlaces}
|
||||
fallback={<span>-</span>}
|
||||
/>
|
||||
</HeaderStat>
|
||||
<HeaderStat heading={t('Volume (24h)')} testId="market-volume">
|
||||
<Last24hVolume
|
||||
marketId={market.id}
|
||||
positionDecimalPlaces={market.positionDecimalPlaces}
|
||||
marketDecimals={market.decimalPlaces}
|
||||
quoteUnit={quoteUnit}
|
||||
/>
|
||||
</HeaderStat>
|
||||
<HeaderStatMarketTradingMode
|
||||
@@ -108,7 +113,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
|
||||
heading={`${t('Funding Rate')} / ${t('Countdown')}`}
|
||||
testId="market-funding"
|
||||
>
|
||||
<div className="flex justify-between gap-2">
|
||||
<div className="flex gap-2">
|
||||
<FundingRate marketId={market.id} />
|
||||
<FundingCountdown marketId={market.id} />
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,6 @@ import { type Market } from '@vegaprotocol/markets';
|
||||
// TODO: handle oracle banner
|
||||
// import { OracleBanner } from '@vegaprotocol/markets';
|
||||
import { useState } from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
Popover,
|
||||
@@ -12,21 +11,21 @@ import {
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { MarketBanner } from '../../components/market-banner';
|
||||
import { ErrorBoundary } from '../../components/error-boundary';
|
||||
import { type TradingView } from './trade-views';
|
||||
import { TradingViews } from './trade-views';
|
||||
|
||||
interface TradePanelsProps {
|
||||
market: Market;
|
||||
pinnedAsset?: PinnedAsset;
|
||||
}
|
||||
|
||||
export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
||||
const [view, setView] = useState<TradingView>('chart');
|
||||
const viewCfg = TradingViews[view];
|
||||
const [topView, setTopView] = useState<TradingView>('chart');
|
||||
const topViewCfg = TradingViews[topView];
|
||||
const [bottomView, setBottomView] = useState<TradingView>('positions');
|
||||
const bottomViewCfg = TradingViews[bottomView];
|
||||
|
||||
const renderView = () => {
|
||||
const renderView = (view: TradingView) => {
|
||||
const Component = TradingViews[view].component;
|
||||
|
||||
if (!Component) {
|
||||
@@ -39,12 +38,13 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
||||
// so watch out for clashes in props
|
||||
return (
|
||||
<ErrorBoundary feature={view}>
|
||||
<Component marketId={market?.id} pinnedAsset={pinnedAsset} />;
|
||||
<Component marketId={market?.id} pinnedAsset={pinnedAsset} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMenu = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const renderMenu = (viewCfg: any) => {
|
||||
if ('menu' in viewCfg || 'settings' in viewCfg) {
|
||||
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">
|
||||
@@ -69,55 +69,80 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full grid grid-rows-[min-content_min-content_1fr_min-content]">
|
||||
<div>
|
||||
<MarketBanner market={market} />
|
||||
</div>
|
||||
<div>{renderMenu()}</div>
|
||||
<div className="h-full relative">
|
||||
<AutoSizer>
|
||||
{({ width, height }) => (
|
||||
<div style={{ width, height }} className="overflow-auto">
|
||||
{renderView()}
|
||||
</div>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
|
||||
{Object.keys(TradingViews)
|
||||
// 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 perpOnlyViews = ['funding', 'fundingPayments'];
|
||||
<div className="h-full flex flex-col lg:grid grid-rows-[min-content_min-content_1fr_min-content]">
|
||||
<div className="flex flex-col w-full overflow-hidden">
|
||||
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
|
||||
{['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;
|
||||
}
|
||||
|
||||
if (
|
||||
market?.tradableInstrument.instrument.product.__typename ===
|
||||
'Perpetual'
|
||||
) {
|
||||
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>
|
||||
|
||||
if (perpOnlyViews.includes(key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.map((_key) => {
|
||||
<div className="flex flex-col w-full grow">
|
||||
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
|
||||
{[
|
||||
'positions',
|
||||
'activeOrders',
|
||||
'closedOrders',
|
||||
'rejectedOrders',
|
||||
'orders',
|
||||
'stopOrders',
|
||||
'collateral',
|
||||
'fills',
|
||||
].map((_key) => {
|
||||
const key = _key as TradingView;
|
||||
const isActive = view === key;
|
||||
const isActive = bottomView === key;
|
||||
return (
|
||||
<ViewButton
|
||||
key={key}
|
||||
view={key}
|
||||
isActive={isActive}
|
||||
onClick={() => {
|
||||
setView(key);
|
||||
setBottomView(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>
|
||||
);
|
||||
@@ -157,7 +182,7 @@ const useViewLabel = (view: TradingView) => {
|
||||
depth: t('Depth'),
|
||||
liquidity: t('Liquidity'),
|
||||
funding: t('Funding'),
|
||||
fundingPayments: t('Funding Payments'),
|
||||
fundingPayments: t('Funding'),
|
||||
orderbook: t('Orderbook'),
|
||||
trades: t('Trades'),
|
||||
positions: t('Positions'),
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
useDataGridEvents,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import type { MarketMaybeWithData } from '@vegaprotocol/markets';
|
||||
import { useColumnDefs } from './use-column-defs';
|
||||
import { useMarketsColumnDefs } from './use-column-defs';
|
||||
import type { DataGridStore } from '../../stores/datagrid-store-slice';
|
||||
import { type StateCreator, create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
@@ -50,7 +50,7 @@ export const useMarketsStore = create<DataGridSlice>()(
|
||||
);
|
||||
|
||||
export const MarketListTable = (props: Props) => {
|
||||
const columnDefs = useColumnDefs();
|
||||
const columnDefs = useMarketsColumnDefs();
|
||||
const gridStore = useMarketsStore((store) => store.gridStore);
|
||||
const updateGridStore = useMarketsStore((store) => store.updateGridStore);
|
||||
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
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,21 +7,31 @@ import type {
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import { COL_DEFS, SetFilter } from '@vegaprotocol/datagrid';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { addDecimalsFormatNumber, toBigNum } from '@vegaprotocol/utils';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumber,
|
||||
toBigNum,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { ButtonLink, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
import type {
|
||||
MarketFieldsFragment,
|
||||
MarketMaybeWithData,
|
||||
MarketMaybeWithDataAndCandles,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { MarketActionsDropdown } from './market-table-actions';
|
||||
import { calcCandleVolume, getAsset } from '@vegaprotocol/markets';
|
||||
import {
|
||||
calcCandleVolume,
|
||||
calcCandleVolumePrice,
|
||||
getAsset,
|
||||
getQuoteName,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { MarketCodeCell } from './market-code-cell';
|
||||
import { useT } from '../../lib/use-t';
|
||||
|
||||
const { MarketTradingMode, AuctionTrigger } = Schema;
|
||||
|
||||
export const useColumnDefs = () => {
|
||||
export const useMarketsColumnDefs = () => {
|
||||
const t = useT();
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
return useMemo<ColDef[]>(
|
||||
@@ -158,11 +168,25 @@ export const useColumnDefs = () => {
|
||||
}: ValueFormatterParams<MarketMaybeWithDataAndCandles, 'candles'>) => {
|
||||
const candles = data?.candles;
|
||||
const vol = candles ? calcCandleVolume(candles) : '0';
|
||||
const quoteName = getQuoteName(data as MarketFieldsFragment);
|
||||
const volPrice =
|
||||
candles &&
|
||||
calcCandleVolumePrice(
|
||||
candles,
|
||||
data.decimalPlaces,
|
||||
data.positionDecimalPlaces
|
||||
);
|
||||
|
||||
const volume =
|
||||
data && vol && vol !== '0'
|
||||
? addDecimalsFormatNumber(vol, data.positionDecimalPlaces)
|
||||
: '0.00';
|
||||
return volume;
|
||||
const volumePrice =
|
||||
volPrice && formatNumber(volPrice, data?.decimalPlaces);
|
||||
|
||||
return volumePrice
|
||||
? `${volume} (${volumePrice} ${quoteName})`
|
||||
: volume;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ import { VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
||||
import { SidebarButton, ViewType } from '../../components/sidebar';
|
||||
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { MobileButton } from '../markets/mobile-buttons';
|
||||
|
||||
export const PortfolioSidebar = () => {
|
||||
const t = useT();
|
||||
@@ -30,3 +31,28 @@ 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,4 +1,4 @@
|
||||
import { getNumberFormat } from '@vegaprotocol/utils';
|
||||
import { formatNumber } from '@vegaprotocol/utils';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import omit from 'lodash/omit';
|
||||
import { useReferralProgramQuery } from './__generated__/CurrentReferralProgram';
|
||||
@@ -107,9 +107,7 @@ export const useReferralProgram = () => {
|
||||
discountFactor: Number(t.referralDiscountFactor),
|
||||
discount: BigNumber(t.referralDiscountFactor).times(100).toFixed(2) + '%',
|
||||
minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
|
||||
volume: getNumberFormat(0).format(
|
||||
Number(t.minimumRunningNotionalTakerVolume)
|
||||
),
|
||||
volume: formatNumber(t.minimumRunningNotionalTakerVolume, 0),
|
||||
epochs: Number(t.minimumEpochs),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -14,9 +14,9 @@ import {
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumber,
|
||||
getDateFormat,
|
||||
getDateTimeFormat,
|
||||
getNumberFormat,
|
||||
getUserLocale,
|
||||
removePaginationWrapper,
|
||||
} from '@vegaprotocol/utils';
|
||||
@@ -323,7 +323,7 @@ export const Statistics = ({
|
||||
}
|
||||
description={<QUSDTooltip />}
|
||||
>
|
||||
{getNumberFormat(0).format(Number(totalCommissionValue))}
|
||||
{formatNumber(totalCommissionValue, 0)}
|
||||
</StatTile>
|
||||
);
|
||||
|
||||
@@ -563,8 +563,8 @@ export const RefereesTable = ({
|
||||
)
|
||||
.map((r) => ({
|
||||
...r,
|
||||
volume: getNumberFormat(0).format(r.volume),
|
||||
commission: getNumberFormat(0).format(r.commission),
|
||||
volume: formatNumber(r.volume, 0),
|
||||
commission: formatNumber(r.commission, 0),
|
||||
}))
|
||||
.reverse()}
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,11 @@ export const BORDER_COLOR = 'border-vega-clight-500 dark:border-vega-cdark-500';
|
||||
export const GRADIENT =
|
||||
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
|
||||
|
||||
export const Box = (props: HTMLAttributes<HTMLDivElement>) => {
|
||||
export const Box = ({
|
||||
children,
|
||||
backgroundImage,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLDivElement> & { backgroundImage?: string }) => {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
@@ -13,9 +17,22 @@ export const Box = (props: HTMLAttributes<HTMLDivElement>) => {
|
||||
BORDER_COLOR,
|
||||
GRADIENT,
|
||||
'border rounded-lg',
|
||||
'p-6',
|
||||
'relative p-6 overflow-hidden',
|
||||
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 { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { getNumberFormat } from '@vegaprotocol/utils';
|
||||
import { formatNumber } from '@vegaprotocol/utils';
|
||||
import { type useTeams } from '../../lib/hooks/use-teams';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { Table } from '../table';
|
||||
@@ -15,8 +15,7 @@ export const CompetitionsLeaderboard = ({
|
||||
}) => {
|
||||
const t = useT();
|
||||
|
||||
const num = (n?: number | string) =>
|
||||
!n ? '-' : getNumberFormat(0).format(Number(n));
|
||||
const num = (n?: number | string) => (!n ? '-' : formatNumber(n, 0));
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return <Splash>{t('Could not find any teams')}</Splash>;
|
||||
@@ -33,9 +32,9 @@ export const CompetitionsLeaderboard = ({
|
||||
{ name: 'status', displayName: t('Status') },
|
||||
{ name: 'volume', displayName: t('Volume') },
|
||||
]}
|
||||
data={data.map((td, i) => {
|
||||
data={data.map((td) => {
|
||||
// leaderboard place or medal
|
||||
let rank: number | React.ReactNode = i + 1;
|
||||
let rank: number | React.ReactNode = td.rank;
|
||||
if (rank === 1) rank = <Rank variant="gold" />;
|
||||
if (rank === 2) rank = <Rank variant="silver" />;
|
||||
if (rank === 3) rank = <Rank variant="bronze" />;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { isValidUrl } from '@vegaprotocol/utils';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const NUM_AVATARS = 20;
|
||||
const AVATAR_PATHNAME_PATTERN = '/team-avatars/{id}.png';
|
||||
|
||||
const getFallbackAvatar = (teamId: string) => {
|
||||
export const getFallbackAvatar = (teamId: string) => {
|
||||
const avatarId = ((parseInt(teamId, 16) % NUM_AVATARS) + 1)
|
||||
.toString()
|
||||
.padStart(2, '0'); // between 01 - 20
|
||||
@@ -11,6 +13,26 @@ const getFallbackAvatar = (teamId: string) => {
|
||||
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 = ({
|
||||
teamId,
|
||||
imgUrl,
|
||||
@@ -22,7 +44,7 @@ export const TeamAvatar = ({
|
||||
alt?: string;
|
||||
size?: 'large' | 'small';
|
||||
}) => {
|
||||
const img = imgUrl && imgUrl.length > 0 ? imgUrl : getFallbackAvatar(teamId);
|
||||
const img = useAvatar(teamId, imgUrl);
|
||||
return (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
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,6 +15,7 @@ import {
|
||||
} from '../../lib/hooks/use-team';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { DispatchMetricLabels, type DispatchMetric } from '@vegaprotocol/types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export const TeamStats = ({
|
||||
stats,
|
||||
@@ -102,7 +103,13 @@ const LatestResults = ({ games }: { games: TeamGame[] }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const FavoriteGame = ({ games }: { games: TeamGame[] }) => {
|
||||
export const FavoriteGame = ({
|
||||
games,
|
||||
noLabel = false,
|
||||
}: {
|
||||
games: TeamGame[];
|
||||
noLabel?: boolean;
|
||||
}) => {
|
||||
const t = useT();
|
||||
|
||||
const rewardMetrics = games.map(
|
||||
@@ -128,7 +135,13 @@ const FavoriteGame = ({ games }: { games: TeamGame[] }) => {
|
||||
|
||||
return (
|
||||
<dl className="flex flex-col gap-1">
|
||||
<dt className="text-muted text-sm">{t('Favorite game')}</dt>
|
||||
<dt
|
||||
className={classNames('text-muted text-sm', {
|
||||
hidden: noLabel,
|
||||
})}
|
||||
>
|
||||
{t('Favorite game')}
|
||||
</dt>
|
||||
<dd>
|
||||
<Pill className="inline-flex items-center gap-1 bg-transparent text-sm">
|
||||
<VegaIcon
|
||||
@@ -142,7 +155,7 @@ const FavoriteGame = ({ games }: { games: TeamGame[] }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const StatSection = ({ children }: { children: ReactNode }) => {
|
||||
export const StatSection = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<section className="flex flex-col lg:flex-row gap-4 lg:gap-8">
|
||||
{children}
|
||||
@@ -150,11 +163,11 @@ const StatSection = ({ children }: { children: ReactNode }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const StatSectionSeparator = () => {
|
||||
export const StatSectionSeparator = () => {
|
||||
return <div className="hidden md:block border-r border-default" />;
|
||||
};
|
||||
|
||||
const StatList = ({ children }: { children: ReactNode }) => {
|
||||
export const StatList = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<dl className="grid grid-cols-2 md:flex gap-4 md:gap-6 lg:gap-8 whitespace-nowrap">
|
||||
{children}
|
||||
@@ -162,19 +175,21 @@ const StatList = ({ children }: { children: ReactNode }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const Stat = ({
|
||||
export const Stat = ({
|
||||
value,
|
||||
label,
|
||||
tooltip,
|
||||
valueTestId,
|
||||
className,
|
||||
}: {
|
||||
value: ReactNode;
|
||||
label: ReactNode;
|
||||
tooltip?: string;
|
||||
valueTestId?: string;
|
||||
className?: classNames.Argument;
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<div className={classNames(className)}>
|
||||
<dd className="text-3xl lg:text-4xl" data-testid={valueTestId}>
|
||||
{value}
|
||||
</dd>
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Outlet } from 'react-router-dom';
|
||||
import { Sidebar, SidebarContent, useSidebar } from '../sidebar';
|
||||
import classNames from 'classnames';
|
||||
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
||||
|
||||
export const LayoutWithSidebar = ({
|
||||
header,
|
||||
sidebar,
|
||||
@@ -17,7 +16,7 @@ export const LayoutWithSidebar = ({
|
||||
const sidebarOpen = sidebarView !== null;
|
||||
const gridClasses = classNames(
|
||||
'h-full relative z-0 grid',
|
||||
'grid-rows-[min-content_1fr_40px]',
|
||||
'grid-rows-[min-content_1fr_50px]',
|
||||
'lg:grid-rows-[min-content_1fr]',
|
||||
'lg:grid-cols-[1fr_280px_40px]',
|
||||
'xxxl:grid-cols-[1fr_320px_40px]'
|
||||
@@ -27,10 +26,13 @@ export const LayoutWithSidebar = ({
|
||||
<div className={gridClasses}>
|
||||
<div className="col-span-full">{header}</div>
|
||||
<main
|
||||
className={classNames('col-start-1 col-end-1 overflow-y-auto', {
|
||||
'lg:col-end-3': !sidebarOpen,
|
||||
'hidden lg:block lg:col-end-2': sidebarOpen,
|
||||
})}
|
||||
className={classNames(
|
||||
'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,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Outlet />
|
||||
</main>
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './market-header';
|
||||
export * from './mobile-market-header';
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
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,2 +1 @@
|
||||
export * from './navbar';
|
||||
export * from './nav-header';
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -34,13 +34,7 @@ import { supportedLngs } from '../../lib/i18n';
|
||||
type MenuState = 'wallet' | 'nav' | null;
|
||||
type Theme = 'system' | 'yellow';
|
||||
|
||||
export const Navbar = ({
|
||||
children,
|
||||
theme = 'system',
|
||||
}: {
|
||||
children?: ReactNode;
|
||||
theme?: Theme;
|
||||
}) => {
|
||||
export const Navbar = ({ theme = 'system' }: { theme?: Theme }) => {
|
||||
const i18n = useI18n();
|
||||
const t = useT();
|
||||
// menu state for small screens
|
||||
@@ -75,8 +69,6 @@ export const Navbar = ({
|
||||
>
|
||||
<VLogo className="w-4" />
|
||||
</NavLink>
|
||||
{/* Left section */}
|
||||
<div className="flex items-center lg:hidden">{children}</div>
|
||||
{/* Used to show header in nav on mobile */}
|
||||
<div className="hidden lg:block">
|
||||
<NavbarMenu onClick={() => setMenu(null)} />
|
||||
|
||||
@@ -152,7 +152,11 @@ export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
|
||||
if (!enrichedTransfers || !enrichedTransfers.length) return null;
|
||||
|
||||
return (
|
||||
<Card title={t('Active rewards')} className="lg:col-span-full">
|
||||
<Card
|
||||
title={t('Active rewards')}
|
||||
className="lg:col-span-full"
|
||||
data-testid="active-rewards-card"
|
||||
>
|
||||
{enrichedTransfers.length > 1 && (
|
||||
<TradingInput
|
||||
onChange={(e) =>
|
||||
@@ -312,49 +316,30 @@ export const ActiveRewardCard = ({
|
||||
MarketState.STATE_CLOSED,
|
||||
].includes(m.state)
|
||||
);
|
||||
|
||||
if (marketSettled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const assetInSettledMarket =
|
||||
const assetInActiveMarket =
|
||||
allMarkets &&
|
||||
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
|
||||
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
|
||||
return (
|
||||
m?.state &&
|
||||
[
|
||||
MarketState.STATE_TRADING_TERMINATED,
|
||||
MarketState.STATE_SETTLED,
|
||||
MarketState.STATE_CANCELLED,
|
||||
MarketState.STATE_CLOSED,
|
||||
].includes(m.state)
|
||||
);
|
||||
return m?.state && MarketState.STATE_ACTIVE === m.state;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Gray out the cards that are related to suspended markets
|
||||
const suspended = transferNode.markets?.some(
|
||||
const marketSuspended = transferNode.markets?.some(
|
||||
(m) =>
|
||||
m?.state === MarketState.STATE_SUSPENDED ||
|
||||
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
|
||||
// Or settlement assets in markets that are not active and eligible for rewards
|
||||
const { gradientClassName, mainClassName } =
|
||||
suspended || assetInSuspendedMarket || assetInSettledMarket
|
||||
marketSuspended || !assetInActiveMarket
|
||||
? {
|
||||
gradientClassName: 'from-vega-cdark-500 to-vega-clight-400',
|
||||
mainClassName: 'from-vega-cdark-400 dark:from-vega-cdark-600 to-20%',
|
||||
@@ -371,6 +356,7 @@ export const ActiveRewardCard = ({
|
||||
'rounded-lg',
|
||||
gradientClassName
|
||||
)}
|
||||
data-testid="active-rewards-card"
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -382,7 +368,7 @@ export const ActiveRewardCard = ({
|
||||
<div className="flex flex-col gap-2 items-center text-center">
|
||||
<EntityIcon transfer={transfer} />
|
||||
{entityScope && (
|
||||
<span className="text-muted text-xs">
|
||||
<span className="text-muted text-xs" data-testid="entity-scope">
|
||||
{EntityScopeLabelMapping[entityScope] || t('Unspecified')}
|
||||
</span>
|
||||
)}
|
||||
@@ -390,7 +376,7 @@ export const ActiveRewardCard = ({
|
||||
|
||||
<div className="flex flex-col gap-2 items-center text-center">
|
||||
<h3 className="flex flex-col gap-1 text-2xl shrink-1 text-center">
|
||||
<span className="font-glitch">
|
||||
<span className="font-glitch" data-testid="reward-value">
|
||||
{addDecimalsFormatNumber(
|
||||
transferNode.transfer.amount,
|
||||
transferNode.transfer.asset?.decimals || 0,
|
||||
@@ -411,7 +397,7 @@ export const ActiveRewardCard = ({
|
||||
)}
|
||||
underline={true}
|
||||
>
|
||||
<span className="text-xs">
|
||||
<span className="text-xs" data-testid="distribution-strategy">
|
||||
{
|
||||
DistributionStrategyMapping[
|
||||
dispatchStrategy.distributionStrategy
|
||||
@@ -429,7 +415,10 @@ export const ActiveRewardCard = ({
|
||||
'Number of epochs after distribution to delay vesting of rewards by'
|
||||
)}
|
||||
/>
|
||||
<span className="text-muted text-xs whitespace-nowrap">
|
||||
<span
|
||||
className="text-muted text-xs whitespace-nowrap"
|
||||
data-testid="locked-for"
|
||||
>
|
||||
{t('numberEpochs', '{{count}} epochs', {
|
||||
count: kind.dispatchStrategy?.lockPeriod,
|
||||
})}
|
||||
@@ -438,15 +427,15 @@ export const ActiveRewardCard = ({
|
||||
</div>
|
||||
|
||||
<span className="border-[0.5px] border-gray-700" />
|
||||
<span>
|
||||
<span data-testid="dispatch-metric-info">
|
||||
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]} •{' '}
|
||||
<Tooltip
|
||||
underline={suspended}
|
||||
underline={marketSuspended}
|
||||
description={
|
||||
(suspended || assetInSuspendedMarket) &&
|
||||
(marketSuspended || !assetInActiveMarket) &&
|
||||
(specificMarkets
|
||||
? t('Eligible market(s) currently suspended')
|
||||
: assetInSuspendedMarket
|
||||
: !assetInActiveMarket
|
||||
? t('Currently no markets eligible for reward')
|
||||
: '')
|
||||
}
|
||||
@@ -458,8 +447,8 @@ export const ActiveRewardCard = ({
|
||||
<div className="flex items-center gap-8 flex-wrap">
|
||||
{kind.endEpoch && (
|
||||
<span className="flex flex-col">
|
||||
<span className="text-muted text-xs">{t('Ends in')}</span>
|
||||
<span>
|
||||
<span className="text-muted text-xs">{t('Ends in')} </span>
|
||||
<span data-testid="ends-in">
|
||||
{t('numberEpochs', '{{count}} epochs', {
|
||||
count: kind.endEpoch - currentEpoch,
|
||||
})}
|
||||
@@ -470,7 +459,7 @@ export const ActiveRewardCard = ({
|
||||
{
|
||||
<span className="flex flex-col">
|
||||
<span className="text-muted text-xs">{t('Assessed over')}</span>
|
||||
<span>
|
||||
<span data-testid="assessed-over">
|
||||
{t('numberEpochs', '{{count}} epochs', {
|
||||
count: dispatchStrategy.windowLength,
|
||||
})}
|
||||
@@ -513,7 +502,7 @@ const RewardRequirements = ({
|
||||
entity: EntityScopeLabelMapping[dispatchStrategy.entityScope],
|
||||
})}
|
||||
</dt>
|
||||
<dd className="flex items-center gap-1">
|
||||
<dd className="flex items-center gap-1" data-testid="scope">
|
||||
<RewardEntityScope dispatchStrategy={dispatchStrategy} />
|
||||
</dd>
|
||||
</div>
|
||||
@@ -522,7 +511,10 @@ const RewardRequirements = ({
|
||||
<dt className="flex items-center gap-1 text-muted">
|
||||
{t('Staked VEGA')}
|
||||
</dt>
|
||||
<dd className="flex items-center gap-1">
|
||||
<dd
|
||||
className="flex items-center gap-1"
|
||||
data-testid="staking-requirement"
|
||||
>
|
||||
{addDecimalsFormatNumber(
|
||||
dispatchStrategy?.stakingRequirement || 0,
|
||||
assetDecimalPlaces
|
||||
@@ -534,7 +526,7 @@ const RewardRequirements = ({
|
||||
<dt className="flex items-center gap-1 text-muted">
|
||||
{t('Average position')}
|
||||
</dt>
|
||||
<dd className="flex items-center gap-1">
|
||||
<dd className="flex items-center gap-1" data-testid="average-position">
|
||||
{addDecimalsFormatNumber(
|
||||
dispatchStrategy?.notionalTimeWeightedAveragePositionRequirement ||
|
||||
0,
|
||||
|
||||
@@ -81,6 +81,7 @@ export const Settings = () => {
|
||||
intent={Intent.Primary}
|
||||
onClick={() => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useVegaWallet, useViewAsDialog } from '@vegaprotocol/wallet';
|
||||
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { ErrorBoundary } from '../error-boundary';
|
||||
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export enum ViewType {
|
||||
Order = 'Order',
|
||||
@@ -26,9 +27,10 @@ export enum ViewType {
|
||||
Transfer = 'Transfer',
|
||||
Settings = 'Settings',
|
||||
ViewAs = 'ViewAs',
|
||||
Close = 'Close',
|
||||
}
|
||||
|
||||
type SidebarView =
|
||||
export type BarView =
|
||||
| {
|
||||
type: ViewType.Deposit;
|
||||
assetId?: string;
|
||||
@@ -49,6 +51,9 @@ type SidebarView =
|
||||
}
|
||||
| {
|
||||
type: ViewType.Settings;
|
||||
}
|
||||
| {
|
||||
type: ViewType.Close;
|
||||
};
|
||||
|
||||
export const Sidebar = ({ options }: { options?: ReactNode }) => {
|
||||
@@ -57,26 +62,52 @@ export const Sidebar = ({ options }: { options?: ReactNode }) => {
|
||||
const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1';
|
||||
const setViewAsDialogOpen = useViewAsDialog((state) => state.setOpen);
|
||||
const { pubKeys } = useVegaWallet();
|
||||
const { isMobile } = useScreenDimensions();
|
||||
const { getView } = useSidebar((store) => ({
|
||||
setViews: store.setViews,
|
||||
getView: store.getView,
|
||||
}));
|
||||
const currView = getView(currentRouteId);
|
||||
return (
|
||||
<div className="flex h-full p-1 lg:flex-col gap-2" data-testid="sidebar">
|
||||
{options && <nav className={navClasses}>{options}</nav>}
|
||||
<nav className={classNames(navClasses, 'ml-auto lg:mt-auto lg:ml-0')}>
|
||||
<SidebarButton
|
||||
view={ViewType.ViewAs}
|
||||
onClick={() => {
|
||||
setViewAsDialogOpen(true);
|
||||
}}
|
||||
icon={VegaIconNames.EYE}
|
||||
tooltip={t('View as party')}
|
||||
disabled={Boolean(pubKeys)}
|
||||
routeId={currentRouteId}
|
||||
/>
|
||||
<SidebarButton
|
||||
view={ViewType.Settings}
|
||||
icon={VegaIconNames.COG}
|
||||
tooltip={t('Settings')}
|
||||
routeId={currentRouteId}
|
||||
/>
|
||||
<div className="flex h-full lg:flex-col gap-1" data-testid="sidebar">
|
||||
{options && (
|
||||
<nav className={classNames(navClasses, 'flex grow')}>{options}</nav>
|
||||
)}
|
||||
<nav
|
||||
className={classNames(
|
||||
navClasses,
|
||||
'ml-auto lg:mt-auto lg:ml-0 shrink-0'
|
||||
)}
|
||||
>
|
||||
{!isMobile ? (
|
||||
<>
|
||||
<SidebarButton
|
||||
view={ViewType.ViewAs}
|
||||
onClick={() => {
|
||||
setViewAsDialogOpen(true);
|
||||
}}
|
||||
icon={VegaIconNames.EYE}
|
||||
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 />
|
||||
</nav>
|
||||
</div>
|
||||
@@ -103,7 +134,7 @@ export const SidebarButton = ({
|
||||
getView: store.getView,
|
||||
}));
|
||||
const currView = getView(routeId);
|
||||
const onSelect = (view: SidebarView['type']) => {
|
||||
const onSelect = (view: BarView['type']) => {
|
||||
if (view === currView?.type) {
|
||||
setViews(null, routeId);
|
||||
} else {
|
||||
@@ -133,7 +164,7 @@ export const SidebarButton = ({
|
||||
<button
|
||||
className={buttonClasses}
|
||||
data-testid={view}
|
||||
onClick={onClick || (() => onSelect(view as SidebarView['type']))}
|
||||
onClick={onClick || (() => onSelect(view as BarView['type']))}
|
||||
disabled={disabled}
|
||||
>
|
||||
<VegaIcon name={icon} size={20} />
|
||||
@@ -180,6 +211,10 @@ export const SidebarContent = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (view.type === ViewType.Close) {
|
||||
return <CloseSidebar />;
|
||||
}
|
||||
|
||||
if (view.type === ViewType.Info) {
|
||||
if (params.marketId) {
|
||||
return (
|
||||
@@ -267,9 +302,9 @@ const CloseSidebar = () => {
|
||||
};
|
||||
|
||||
export const useSidebar = create<{
|
||||
views: { [key: string]: SidebarView | null };
|
||||
setViews: (view: SidebarView | null, routeId: string) => void;
|
||||
getView: (routeId: string) => SidebarView | null | undefined;
|
||||
views: { [key: string]: BarView | null };
|
||||
setViews: (view: BarView | null, routeId: string) => void;
|
||||
getView: (routeId: string) => BarView | null | undefined;
|
||||
}>()((set, get) => ({
|
||||
views: {},
|
||||
setViews: (x, routeId) =>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
|
||||
VEGA_VERSION=v0.74.0-preview.7
|
||||
VEGA_VERSION=v0.74.0-preview.10
|
||||
LOCAL_SERVER=false
|
||||
|
||||
@@ -111,6 +111,7 @@ def init_vega(request=None):
|
||||
f"Container {container.id} started",
|
||||
extra={"worker_id": os.environ.get("PYTEST_XDIST_WORKER")},
|
||||
)
|
||||
vega.container = container
|
||||
yield vega
|
||||
except APIError as e:
|
||||
logger.info(f"Container creation failed.")
|
||||
@@ -177,10 +178,34 @@ def init_page(vega: VegaServiceNull, browser: Browser, request: pytest.FixtureRe
|
||||
|
||||
@pytest.fixture
|
||||
def vega(request):
|
||||
with init_vega(request) as vega:
|
||||
yield vega
|
||||
with init_vega(request) as vega_instance:
|
||||
request.addfinalizer(lambda: cleanup_container(vega_instance))
|
||||
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
|
||||
def page(vega, browser, request):
|
||||
with init_page(vega, browser, request) as page_instance:
|
||||
|
||||
@@ -2,7 +2,7 @@ import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from datetime import datetime, timedelta
|
||||
from conftest import init_vega
|
||||
from conftest import init_vega, cleanup_container
|
||||
from fixtures.market import setup_continuous_market
|
||||
from actions.utils import wait_for_toast_confirmation
|
||||
|
||||
@@ -17,8 +17,10 @@ expire = "expire"
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega(request):
|
||||
with init_vega(request) as vega:
|
||||
yield vega
|
||||
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")
|
||||
|
||||
@@ -3,7 +3,7 @@ from playwright.sync_api import Page, expect
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from actions.vega import submit_order
|
||||
from datetime import datetime, timedelta
|
||||
from conftest import init_vega
|
||||
from conftest import init_vega, cleanup_container
|
||||
from fixtures.market import setup_continuous_market
|
||||
|
||||
stop_order_btn = "order-type-Stop"
|
||||
@@ -259,9 +259,10 @@ def test_submit_stop_limit_order_cancel(
|
||||
|
||||
class TestStopOcoValidation:
|
||||
@pytest.fixture(scope="class")
|
||||
def vega(self, request):
|
||||
with init_vega(request) as vega:
|
||||
yield vega
|
||||
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="class")
|
||||
def continuous_market(self, vega):
|
||||
|
||||
@@ -2,7 +2,7 @@ import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from actions.utils import change_keys
|
||||
from conftest import init_vega
|
||||
from conftest import init_vega, cleanup_container
|
||||
from fixtures.market import setup_continuous_market
|
||||
|
||||
order_size = "order-size"
|
||||
@@ -14,8 +14,9 @@ deal_ticket_deposit_dialog_button = "deal-ticket-deposit-dialog-button"
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega(request):
|
||||
with init_vega(request) as vega:
|
||||
yield vega
|
||||
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")
|
||||
@@ -33,7 +34,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.")
|
||||
page.get_by_test_id(deal_ticket_warning_margin).hover()
|
||||
expect(page.get_by_test_id("tooltip-content").nth(0)).to_have_text(
|
||||
"1,661,896.6317 tDAI is currently required.You have only 1,000,000.00.Deposit tDAI")
|
||||
"1,661,888.12901 tDAI is currently required.You have only 999,991.49731.Deposit tDAI")
|
||||
page.get_by_test_id(deal_ticket_deposit_dialog_button).nth(0).click()
|
||||
expect(page.get_by_test_id("sidebar-content")
|
||||
).to_contain_text("DepositFrom")
|
||||
|
||||
@@ -4,7 +4,7 @@ from playwright.sync_api import Page, expect
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from actions.vega import submit_order
|
||||
from wallet_config import MM_WALLET
|
||||
from conftest import init_vega, init_page, auth_setup
|
||||
from conftest import init_vega, init_page, auth_setup, cleanup_container
|
||||
from actions.utils import next_epoch, change_keys, forward_time
|
||||
from fixtures.market import market_exists, setup_continuous_market
|
||||
|
||||
@@ -82,31 +82,36 @@ def market_ids():
|
||||
@pytest.fixture(scope="module")
|
||||
def vega_volume_discount_tier_1(request):
|
||||
with init_vega(request) as vega_volume_discount_tier_1:
|
||||
yield 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
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega_volume_discount_tier_2(request):
|
||||
with init_vega(request) as vega_volume_discount_tier_2:
|
||||
yield 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
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega_referral_discount_tier_1(request):
|
||||
with init_vega(request) as vega_referral_discount_tier_1:
|
||||
yield 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
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega_referral_discount_tier_2(request):
|
||||
with init_vega(request) as vega_referral_discount_tier_2:
|
||||
yield 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
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega_referral_and_volume_discount(request):
|
||||
with init_vega(request) as vega_referral_and_volume_discount:
|
||||
yield 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
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -3,7 +3,7 @@ from playwright.sync_api import expect, Page
|
||||
import json
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from fixtures.market import setup_simple_market
|
||||
from conftest import init_vega
|
||||
from conftest import init_vega, cleanup_container
|
||||
from actions.vega import submit_order
|
||||
from wallet_config import MM_WALLET, TERMINATE_WALLET, wallets
|
||||
import logging
|
||||
@@ -12,9 +12,10 @@ logger = logging.getLogger()
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def vega():
|
||||
with init_vega() as vega:
|
||||
yield vega
|
||||
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="class")
|
||||
|
||||
@@ -2,8 +2,6 @@ import pytest
|
||||
from playwright.sync_api import expect, Page
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from actions.vega import submit_order
|
||||
from conftest import init_vega
|
||||
from fixtures.market import setup_continuous_market
|
||||
from wallet_config import MM_WALLET2
|
||||
|
||||
def hover_and_assert_tooltip(page: Page, element_text):
|
||||
@@ -11,39 +9,30 @@ def hover_and_assert_tooltip(page: Page, element_text):
|
||||
element.hover()
|
||||
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.fixture(scope="class")
|
||||
def continuous_market(self, vega):
|
||||
return setup_continuous_market(vega)
|
||||
@pytest.mark.usefixtures("auth", "risk_accepted")
|
||||
def test_iceberg_submit(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()
|
||||
|
||||
@pytest.mark.usefixtures("auth", "risk_accepted")
|
||||
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()
|
||||
expect(page.get_by_test_id("toast-content")).to_have_text(
|
||||
"Awaiting confirmationPlease wait for your transaction to be confirmedView in block explorer"
|
||||
)
|
||||
|
||||
expect(page.get_by_test_id("toast-content")).to_have_text(
|
||||
"Awaiting confirmationPlease wait for your transaction to be confirmedView in block explorer"
|
||||
)
|
||||
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
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"
|
||||
)
|
||||
page.get_by_test_id("All").click()
|
||||
expect(
|
||||
(page.get_by_role("row").locator('[col-id="type"]')).nth(1)
|
||||
).to_have_text("Limit (Iceberg)")
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
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"
|
||||
)
|
||||
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")
|
||||
def test_iceberg_open_order(continuous_market, vega: VegaServiceNull, page: Page):
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from conftest import init_vega
|
||||
from conftest import init_vega, cleanup_container
|
||||
from fixtures.market import setup_continuous_market
|
||||
from actions.utils import next_epoch, truncate_middle, change_keys
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega(request):
|
||||
with init_vega(request) as vega:
|
||||
yield vega
|
||||
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")
|
||||
|
||||
@@ -4,14 +4,15 @@ import vega_sim.api.governance as governance
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from playwright.sync_api import Page, expect
|
||||
from fixtures.market import setup_continuous_market
|
||||
from conftest import init_vega
|
||||
from conftest import init_vega, cleanup_container
|
||||
from actions.utils import next_epoch
|
||||
|
||||
|
||||
@pytest.fixture(scope="class")
|
||||
def vega():
|
||||
with init_vega() as vega:
|
||||
yield vega
|
||||
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="class")
|
||||
|
||||
@@ -3,15 +3,16 @@ import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from fixtures.market import setup_continuous_market
|
||||
from conftest import init_page, init_vega, risk_accepted_setup
|
||||
from conftest import init_page, init_vega, risk_accepted_setup, cleanup_container
|
||||
|
||||
market_title_test_id = "accordion-title"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega():
|
||||
with init_vega() as vega:
|
||||
yield vega
|
||||
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")
|
||||
|
||||
@@ -3,7 +3,7 @@ import vega_sim.api.governance as governance
|
||||
import re
|
||||
from playwright.sync_api import Page, expect
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from conftest import init_vega
|
||||
from conftest import init_vega, cleanup_container
|
||||
from fixtures.market import setup_simple_market
|
||||
from wallet_config import MM_WALLET
|
||||
|
||||
@@ -13,8 +13,10 @@ col_market_id = '[col-id="market"] [data-testid="stack-cell-primary"]'
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega(request):
|
||||
with init_vega(request) as vega:
|
||||
yield vega
|
||||
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")
|
||||
|
||||
@@ -3,7 +3,7 @@ from playwright.sync_api import Page, expect
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from actions.vega import submit_order
|
||||
from fixtures.market import setup_simple_market
|
||||
from conftest import init_vega
|
||||
from conftest import init_vega, cleanup_container
|
||||
from actions.utils import wait_for_toast_confirmation, change_keys
|
||||
from wallet_config import MM_WALLET, MM_WALLET2
|
||||
|
||||
@@ -15,8 +15,10 @@ COL_ID_FEE = ".ag-center-cols-container [col-id='fee'] .ag-cell-value"
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega(request):
|
||||
with init_vega(request) as vega:
|
||||
yield vega
|
||||
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")
|
||||
|
||||
@@ -37,7 +37,7 @@ def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page):
|
||||
# 6002-MDET-004
|
||||
expect(page.get_by_test_id("market-change")).to_have_text("Change (24h)0.00%0.00")
|
||||
# 6002-MDET-005
|
||||
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)-")
|
||||
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)- (- BTC)")
|
||||
# 6002-MDET-008
|
||||
expect(page.get_by_test_id("market-settlement-asset")).to_have_text(
|
||||
"Settlement assettDAI"
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import pytest
|
||||
from playwright.sync_api import Page, expect, Locator
|
||||
|
||||
from conftest import init_page, init_vega
|
||||
from conftest import init_page, init_vega, cleanup_container
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega():
|
||||
with init_vega() as vega:
|
||||
yield vega
|
||||
def vega(request):
|
||||
with init_vega(request) as vega_instance:
|
||||
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
|
||||
yield vega_instance
|
||||
|
||||
|
||||
# we can reuse single page instance in all tests
|
||||
|
||||
@@ -2,7 +2,7 @@ import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
from vega_sim.service import PeggedOrder
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from conftest import auth_setup, init_page, init_vega, risk_accepted_setup
|
||||
from conftest import auth_setup, init_page, init_vega, risk_accepted_setup, cleanup_container
|
||||
from fixtures.market import setup_continuous_market, setup_simple_market
|
||||
from actions.utils import wait_for_toast_confirmation
|
||||
|
||||
@@ -11,8 +11,9 @@ order_tab = "tab-orders"
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega(request):
|
||||
with init_vega(request) as vega:
|
||||
yield vega
|
||||
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", autouse=True)
|
||||
|
||||
@@ -2,15 +2,16 @@ import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
from typing import List
|
||||
from actions.vega import submit_order, submit_liquidity, submit_multiple_orders
|
||||
from conftest import init_vega
|
||||
from conftest import init_vega, cleanup_container
|
||||
from fixtures.market import setup_simple_market
|
||||
from wallet_config import MM_WALLET, MM_WALLET2
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega():
|
||||
with init_vega() as vega:
|
||||
yield vega
|
||||
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")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from conftest import init_vega
|
||||
from conftest import init_vega, cleanup_container
|
||||
from fixtures.market import setup_continuous_market
|
||||
|
||||
TOOLTIP_LABEL = "margin-health-tooltip-label"
|
||||
@@ -11,8 +11,10 @@ COL_ID_USED = ".ag-center-cols-container [col-id='used'] .ag-cell-value"
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega(request):
|
||||
with init_vega(request) as vega:
|
||||
yield vega
|
||||
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")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
from playwright.sync_api import Page
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from conftest import init_vega
|
||||
from conftest import init_vega, cleanup_container
|
||||
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.vega import submit_order, submit_liquidity
|
||||
@@ -14,8 +14,10 @@ BUY_ORDERS = [[1, 106], [1, 107], [1, 108]]
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega(request):
|
||||
with init_vega(request) as vega:
|
||||
yield vega
|
||||
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")
|
||||
|
||||
@@ -59,7 +59,7 @@ def test_filtered_cards(continuous_market, vega: VegaServiceNull, page: Page):
|
||||
next_epoch(vega=vega)
|
||||
|
||||
page.reload()
|
||||
expect(page.locator(".from-vega-cdark-400")).to_be_visible(timeout=15000)
|
||||
expect(page.get_by_test_id("active-rewards-card")).to_be_visible(timeout=15000)
|
||||
governance.submit_oracle_data(
|
||||
wallet=vega.wallet,
|
||||
payload={"trading.terminated": "true"},
|
||||
@@ -67,4 +67,4 @@ def test_filtered_cards(continuous_market, vega: VegaServiceNull, page: Page):
|
||||
)
|
||||
next_epoch(vega=vega)
|
||||
page.reload()
|
||||
expect(page.locator(".from-vega-cdark-400")).not_to_be_in_viewport()
|
||||
expect(page.get_by_test_id("active-rewards-card")).not_to_be_in_viewport()
|
||||
|
||||
@@ -2,7 +2,7 @@ import pytest
|
||||
|
||||
import vega_sim.proto.vega as vega_protos
|
||||
from playwright.sync_api import Page, expect
|
||||
from conftest import init_vega, init_page, auth_setup
|
||||
from conftest import init_vega, init_page, auth_setup, cleanup_container
|
||||
from fixtures.market import setup_continuous_market, market_exists
|
||||
from actions.utils import next_epoch, change_keys
|
||||
from wallet_config import MM_WALLET, PARTY_A, PARTY_B, PARTY_C, PARTY_D
|
||||
@@ -46,36 +46,42 @@ def market_ids():
|
||||
@pytest.fixture(scope="module")
|
||||
def vega_activity_tier_0(request):
|
||||
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
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega_hoarder_tier_0(request):
|
||||
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
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega_combo_tier_0(request):
|
||||
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
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega_activity_tier_1(request):
|
||||
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
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega_hoarder_tier_1(request):
|
||||
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
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega_combo_tier_1(request):
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
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,12 +1,13 @@
|
||||
import pytest
|
||||
from playwright.sync_api import expect, Page
|
||||
from conftest import init_vega
|
||||
from conftest import init_vega, cleanup_container
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega():
|
||||
with init_vega() as vega:
|
||||
yield vega
|
||||
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.mark.usefixtures("risk_accepted")
|
||||
|
||||
@@ -2,17 +2,18 @@ import pytest
|
||||
from playwright.sync_api import expect, Page
|
||||
import vega_sim.proto.vega as vega_protos
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from conftest import init_vega
|
||||
from actions.utils import next_epoch
|
||||
from conftest import init_vega, cleanup_container
|
||||
from actions.utils import next_epoch, change_keys
|
||||
from fixtures.market import setup_continuous_market
|
||||
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
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def vega(request):
|
||||
with init_vega(request) as vega:
|
||||
yield vega
|
||||
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 team_page(vega, browser, request, setup_teams_and_games):
|
||||
@@ -23,9 +24,20 @@ def team_page(vega, browser, request, setup_teams_and_games):
|
||||
page.goto(f"/#/competitions/teams/{team_id}")
|
||||
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")
|
||||
def setup_teams_and_games(vega: VegaServiceNull):
|
||||
tDAI_market = setup_continuous_market(vega)
|
||||
tDAI_market = setup_continuous_market(vega, custom_quantum=100000)
|
||||
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)
|
||||
@@ -46,6 +58,18 @@ def setup_teams_and_games(vega: VegaServiceNull):
|
||||
|
||||
# list_teams actually returns a dictionary {"team_id": Team}
|
||||
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)
|
||||
|
||||
@@ -63,7 +87,7 @@ def setup_teams_and_games(vega: VegaServiceNull):
|
||||
|
||||
current_epoch = vega.statistics().epoch_seq
|
||||
game_start = current_epoch + 1
|
||||
game_end = current_epoch + 11
|
||||
game_end = current_epoch + 14
|
||||
|
||||
current_epoch = vega.statistics().epoch_seq
|
||||
print(f"[EPOCH: {current_epoch}] creating recurring transfer")
|
||||
@@ -84,9 +108,42 @@ def setup_teams_and_games(vega: VegaServiceNull):
|
||||
factor=1.0,
|
||||
start_epoch=game_start,
|
||||
end_epoch=game_end,
|
||||
window_length=10
|
||||
window_length=15,
|
||||
)
|
||||
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)
|
||||
print(f"[EPOCH: {vega.statistics().epoch_seq}] starting order activity")
|
||||
|
||||
@@ -113,6 +170,22 @@ def setup_teams_and_games(vega: VegaServiceNull):
|
||||
side="SIDE_BUY",
|
||||
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)
|
||||
print(f"[EPOCH: {vega.statistics().epoch_seq}] {i} epoch passed")
|
||||
|
||||
@@ -120,6 +193,7 @@ def setup_teams_and_games(vega: VegaServiceNull):
|
||||
"market_id": tDAI_market,
|
||||
"asset_id": tDAI_asset_id,
|
||||
"team_id": team_id,
|
||||
"team_id_2": team_id_2,
|
||||
"team_name": team_name,
|
||||
}
|
||||
|
||||
@@ -136,65 +210,109 @@ def create_team(vega: VegaServiceNull):
|
||||
|
||||
return team_name
|
||||
|
||||
|
||||
def test_team_page_games_table(team_page: Page):
|
||||
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("rank-0")).to_have_text("1")
|
||||
expect(team_page.get_by_test_id("epoch-0")).to_have_text("18")
|
||||
expect(team_page.get_by_test_id("type-0")).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("participatingTeams-0")).to_have_text(
|
||||
"1"
|
||||
)
|
||||
expect(team_page.get_by_test_id("participatingMembers-0")).to_have_text(
|
||||
"2"
|
||||
)
|
||||
expect(team_page.get_by_test_id("rank-0")).to_have_text("2")
|
||||
expect(team_page.get_by_test_id("epoch-0")).to_have_text("19")
|
||||
expect(team_page.get_by_test_id("type-0")
|
||||
).to_have_text("Price maker fees paid")
|
||||
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("2")
|
||||
expect(team_page.get_by_test_id("participatingMembers-0")).to_have_text("4")
|
||||
|
||||
|
||||
def test_team_page_members_table(team_page: Page):
|
||||
team_page.get_by_test_id("members-toggle").click()
|
||||
expect(team_page.get_by_test_id("members-toggle")).to_have_text("Members (3)")
|
||||
expect(team_page.get_by_test_id("members-toggle")
|
||||
).to_have_text("Members (4)")
|
||||
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("joinedAtEpoch-0")).to_have_text("8")
|
||||
expect(team_page.get_by_test_id("joinedAtEpoch-0")).to_have_text("9")
|
||||
|
||||
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"]
|
||||
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("3")
|
||||
expect(team_page.get_by_test_id("members-count-stat")).to_have_text("4")
|
||||
|
||||
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
|
||||
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(
|
||||
"100m"
|
||||
)
|
||||
expect(team_page.get_by_test_id("rewards-paid-stat")).to_have_text("78")
|
||||
|
||||
|
||||
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):
|
||||
team_name = setup_teams_and_games["team_name"]
|
||||
competitions_page.goto(f"/#/competitions/")
|
||||
expect(competitions_page.get_by_test_id("rank-0").locator(".text-yellow-300")).to_have_count(1)
|
||||
expect(competitions_page.get_by_test_id("team-0")).to_have_text(team_name)
|
||||
expect(competitions_page.get_by_test_id("status-0")).to_have_text("Open")
|
||||
competitions_page.reload()
|
||||
expect(
|
||||
competitions_page.get_by_test_id("rank-0").locator(".text-yellow-300")
|
||||
).to_have_count(1)
|
||||
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")
|
||||
|
||||
expect(competitions_page.get_by_test_id("earned-0")).to_have_text("100,000,000")
|
||||
expect(competitions_page.get_by_test_id("games-0")).to_have_text("1")
|
||||
# FIXME: the numbers are different we need to clarify this with the backend
|
||||
# expect(competitions_page.get_by_test_id("earned-1")).to_have_text("160")
|
||||
expect(competitions_page.get_by_test_id("games-1")).to_have_text("1")
|
||||
|
||||
# TODO still odd that this is 0
|
||||
expect(competitions_page.get_by_test_id("volume-0")).to_have_text("-")
|
||||
|
||||
#TODO def test_games(competitions_page: Page):
|
||||
#TODO currently no games appear which i think is a bug
|
||||
|
||||
def test_game_card(competitions_page: Page):
|
||||
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 • ")
|
||||
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")
|
||||
|
||||
@@ -17,7 +17,7 @@ fragment TeamStatsFields on TeamStatistics {
|
||||
totalGamesPlayed
|
||||
quantumRewards {
|
||||
epoch
|
||||
total_quantum_rewards
|
||||
totalQuantumRewards
|
||||
}
|
||||
gamesPlayed
|
||||
}
|
||||
@@ -51,6 +51,13 @@ fragment TeamGameFields on Game {
|
||||
}
|
||||
}
|
||||
|
||||
fragment TeamMemberStatsFields on TeamMemberStatistics {
|
||||
partyId
|
||||
totalQuantumVolume
|
||||
totalQuantumRewards
|
||||
totalGamesPlayed
|
||||
}
|
||||
|
||||
query Team($teamId: ID!, $partyId: ID, $aggregationEpochs: Int) {
|
||||
teams(teamId: $teamId) {
|
||||
edges {
|
||||
@@ -87,4 +94,14 @@ query Team($teamId: ID!, $partyId: ID, $aggregationEpochs: Int) {
|
||||
}
|
||||
}
|
||||
}
|
||||
teamMembersStatistics(
|
||||
teamId: $teamId
|
||||
aggregationEpochs: $aggregationEpochs
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
...TeamMemberStatsFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
fragment TeamsFields on Team {
|
||||
teamId
|
||||
referrer
|
||||
name
|
||||
teamUrl
|
||||
avatarUrl
|
||||
createdAt
|
||||
createdAtEpoch
|
||||
closed
|
||||
totalMembers
|
||||
}
|
||||
|
||||
query Teams($teamId: ID, $partyId: ID) {
|
||||
teams(teamId: $teamId, partyId: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
teamId
|
||||
referrer
|
||||
name
|
||||
teamUrl
|
||||
avatarUrl
|
||||
createdAt
|
||||
createdAtEpoch
|
||||
closed
|
||||
...TeamsFields
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
-4
@@ -5,7 +5,7 @@ import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type TeamFieldsFragment = { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean, 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, total_quantum_rewards: 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 TeamRefereeFieldsFragment = { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number };
|
||||
|
||||
@@ -13,6 +13,8 @@ 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 TeamMemberStatsFieldsFragment = { __typename?: 'TeamMemberStatistics', partyId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number };
|
||||
|
||||
export type TeamQueryVariables = Types.Exact<{
|
||||
teamId: Types.Scalars['ID'];
|
||||
partyId?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||
@@ -20,7 +22,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, 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 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 const TeamFieldsFragmentDoc = gql`
|
||||
fragment TeamFields on Team {
|
||||
@@ -43,7 +45,7 @@ export const TeamStatsFieldsFragmentDoc = gql`
|
||||
totalGamesPlayed
|
||||
quantumRewards {
|
||||
epoch
|
||||
total_quantum_rewards
|
||||
totalQuantumRewards
|
||||
}
|
||||
gamesPlayed
|
||||
}
|
||||
@@ -80,6 +82,14 @@ export const TeamGameFieldsFragmentDoc = gql`
|
||||
}
|
||||
}
|
||||
${TeamEntityFragmentDoc}`;
|
||||
export const TeamMemberStatsFieldsFragmentDoc = gql`
|
||||
fragment TeamMemberStatsFields on TeamMemberStatistics {
|
||||
partyId
|
||||
totalQuantumVolume
|
||||
totalQuantumRewards
|
||||
totalGamesPlayed
|
||||
}
|
||||
`;
|
||||
export const TeamDocument = gql`
|
||||
query Team($teamId: ID!, $partyId: ID, $aggregationEpochs: Int) {
|
||||
teams(teamId: $teamId) {
|
||||
@@ -117,11 +127,19 @@ export const TeamDocument = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
teamMembersStatistics(teamId: $teamId, aggregationEpochs: $aggregationEpochs) {
|
||||
edges {
|
||||
node {
|
||||
...TeamMemberStatsFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${TeamFieldsFragmentDoc}
|
||||
${TeamStatsFieldsFragmentDoc}
|
||||
${TeamRefereeFieldsFragmentDoc}
|
||||
${TeamGameFieldsFragmentDoc}`;
|
||||
${TeamGameFieldsFragmentDoc}
|
||||
${TeamMemberStatsFieldsFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useTeamQuery__
|
||||
|
||||
+18
-11
@@ -3,33 +3,40 @@ import * as Types from '@vegaprotocol/types';
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
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<{
|
||||
teamId?: 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 } }> } | 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, totalMembers: number } }> } | null };
|
||||
|
||||
export const TeamsFieldsFragmentDoc = gql`
|
||||
fragment TeamsFields on Team {
|
||||
teamId
|
||||
referrer
|
||||
name
|
||||
teamUrl
|
||||
avatarUrl
|
||||
createdAt
|
||||
createdAtEpoch
|
||||
closed
|
||||
totalMembers
|
||||
}
|
||||
`;
|
||||
export const TeamsDocument = gql`
|
||||
query Teams($teamId: ID, $partyId: ID) {
|
||||
teams(teamId: $teamId, partyId: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
teamId
|
||||
referrer
|
||||
name
|
||||
teamUrl
|
||||
avatarUrl
|
||||
createdAt
|
||||
createdAtEpoch
|
||||
closed
|
||||
...TeamsFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
${TeamsFieldsFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useTeamsQuery__
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
import compact from 'lodash/compact';
|
||||
import { useActiveRewardsQuery } from '../../components/rewards-container/__generated__/Rewards';
|
||||
import { isActiveReward } from '../../components/rewards-container/active-rewards';
|
||||
import { EntityScope, type TransferNode } from '@vegaprotocol/types';
|
||||
import {
|
||||
EntityScope,
|
||||
IndividualScope,
|
||||
type TransferNode,
|
||||
} from '@vegaprotocol/types';
|
||||
|
||||
const isScopedToTeams = (node: TransferNode) =>
|
||||
node.transfer.kind.__typename === 'RecurringTransfer' &&
|
||||
node.transfer.kind.dispatchStrategy?.entityScope ===
|
||||
EntityScope.ENTITY_SCOPE_TEAMS;
|
||||
// scoped to teams
|
||||
(node.transfer.kind.dispatchStrategy?.entityScope ===
|
||||
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 = ({
|
||||
currentEpoch,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
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 };
|
||||
};
|
||||
@@ -6,17 +6,24 @@ import {
|
||||
type TeamStatsFieldsFragment,
|
||||
type TeamRefereeFieldsFragment,
|
||||
type TeamEntityFragment,
|
||||
type TeamMemberStatsFieldsFragment,
|
||||
} from './__generated__/Team';
|
||||
import { DEFAULT_AGGREGATION_EPOCHS } from './use-teams';
|
||||
|
||||
export type Team = TeamFieldsFragment;
|
||||
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 TeamGame = ReturnType<typeof useTeam>['games'][number];
|
||||
export type MemberStats = TeamMemberStatsFieldsFragment;
|
||||
|
||||
export const useTeam = (teamId?: string, partyId?: string) => {
|
||||
const { data, loading, error, refetch } = useTeamQuery({
|
||||
const queryResult = useTeamQuery({
|
||||
variables: {
|
||||
teamId: teamId || '',
|
||||
partyId,
|
||||
@@ -26,7 +33,11 @@ export const useTeam = (teamId?: string, partyId?: string) => {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
const { data } = queryResult;
|
||||
|
||||
const teamEdge = data?.teams?.edges.find((e) => e.node.teamId === teamId);
|
||||
const team = teamEdge?.node;
|
||||
|
||||
const partyTeam = data?.partyTeams?.edges?.length
|
||||
? data.partyTeams.edges[0].node
|
||||
: undefined;
|
||||
@@ -34,9 +45,40 @@ export const useTeam = (teamId?: string, partyId?: string) => {
|
||||
const teamStatsEdge = data?.teamsStatistics?.edges.find(
|
||||
(e) => e.node.teamId === teamId
|
||||
);
|
||||
const members = data?.teamReferees?.edges
|
||||
.filter((e) => e.node.teamId === teamId)
|
||||
.map((e) => e.node);
|
||||
|
||||
const memberStats = data?.teamMembersStatistics?.edges.length
|
||||
? data.teamMembersStatistics.edges.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
|
||||
const gamesWithTeam = compact(data?.games.edges).map((edge) => {
|
||||
@@ -60,12 +102,9 @@ export const useTeam = (teamId?: string, partyId?: string) => {
|
||||
const games = orderBy(compact(gamesWithTeam), 'epoch', 'desc');
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
refetch,
|
||||
...queryResult,
|
||||
stats: teamStatsEdge?.node,
|
||||
team: teamEdge?.node,
|
||||
team,
|
||||
members,
|
||||
games,
|
||||
partyTeam,
|
||||
|
||||
@@ -1,34 +1,13 @@
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { useMemo } from 'react';
|
||||
import { type TeamsQuery, useTeamsQuery } from './__generated__/Teams';
|
||||
import {
|
||||
type TeamsStatisticsQuery,
|
||||
useTeamsStatisticsQuery,
|
||||
} from './__generated__/TeamsStatistics';
|
||||
import { useTeamsQuery } from './__generated__/Teams';
|
||||
import { useTeamsStatisticsQuery } from './__generated__/TeamsStatistics';
|
||||
import compact from 'lodash/compact';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { type ArrayElement } from 'type-fest/source/internal';
|
||||
|
||||
type SortableField = keyof Omit<
|
||||
ArrayElement<NonNullable<TeamsQuery['teams']>['edges']>['node'] &
|
||||
ArrayElement<
|
||||
NonNullable<TeamsStatisticsQuery['teamsStatistics']>['edges']
|
||||
>['node'],
|
||||
'__typename'
|
||||
>;
|
||||
// 192
|
||||
export const DEFAULT_AGGREGATION_EPOCHS = 192;
|
||||
|
||||
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) => {
|
||||
export const useTeams = (aggregationEpochs = DEFAULT_AGGREGATION_EPOCHS) => {
|
||||
const {
|
||||
data: teamsData,
|
||||
loading: teamsLoading,
|
||||
@@ -57,12 +36,10 @@ export const useTeams = ({
|
||||
...stats.find((s) => s.teamId === t.teamId),
|
||||
}));
|
||||
|
||||
const sorted = sortBy(data, sortByField);
|
||||
if (order === 'desc') {
|
||||
return sorted.reverse();
|
||||
}
|
||||
return sorted;
|
||||
}, [teams, sortByField, order, stats]);
|
||||
return orderBy(data, (d) => Number(d.totalQuantumRewards || 0), 'desc').map(
|
||||
(d, i) => ({ ...d, rank: i + 1 })
|
||||
);
|
||||
}, [teams, stats]);
|
||||
|
||||
return {
|
||||
data,
|
||||
|
||||
@@ -14,7 +14,7 @@ import './styles.css';
|
||||
import { usePageTitleStore } from '../stores';
|
||||
import DialogsContainer from './dialogs-container';
|
||||
import ToastsManager from './toasts-manager';
|
||||
import { HashRouter, useLocation, Route, Routes } from 'react-router-dom';
|
||||
import { HashRouter, useLocation } from 'react-router-dom';
|
||||
import { Bootstrapper } from '../components/bootstrapper';
|
||||
import { AnnouncementBanner } from '../components/banner';
|
||||
import { Navbar } from '../components/navbar';
|
||||
@@ -25,9 +25,7 @@ import {
|
||||
ProtocolUpgradeProposalNotification,
|
||||
} from '@vegaprotocol/proposals';
|
||||
import { ViewingBanner } from '../components/viewing-banner';
|
||||
import { NavHeader } from '../components/navbar/nav-header';
|
||||
import { Telemetry } from '../components/telemetry';
|
||||
import { Routes as AppRoutes } from '../lib/links';
|
||||
import { SSRLoader } from './ssr-loader';
|
||||
import { PartyActiveOrdersHandler } from './party-active-orders-handler';
|
||||
import { MaybeConnectEagerly } from './maybe-connect-eagerly';
|
||||
@@ -73,16 +71,7 @@ function AppBody({ Component }: AppProps) {
|
||||
<Title />
|
||||
<div className={gridClasses}>
|
||||
<AnnouncementBanner />
|
||||
<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>
|
||||
<Navbar theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'system'} />
|
||||
<div data-testid="banners">
|
||||
<ProtocolUpgradeProposalNotification
|
||||
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
|
||||
|
||||
@@ -24,11 +24,14 @@ export default function Document() {
|
||||
|
||||
{/* scripts */}
|
||||
<script src="/theme-setter.js" type="text/javascript" async />
|
||||
|
||||
{/* manifest */}
|
||||
<link rel="manifest" href="/apps/trading/public/manifest.json" />
|
||||
</Head>
|
||||
<Html>
|
||||
<body
|
||||
// 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
|
||||
// Next.js 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
|
||||
// js is downloaded and react takes over rendering
|
||||
style={{ display: 'block' }}
|
||||
className="bg-white dark:bg-vega-cdark-900 text-default font-alpha"
|
||||
|
||||
@@ -23,8 +23,11 @@ import { NotFound as ReferralNotFound } from '../client-pages/referrals/error-bo
|
||||
import { compact } from 'lodash';
|
||||
import { useFeatureFlags } from '@vegaprotocol/environment';
|
||||
import { LiquidityHeader } from '../components/liquidity-header';
|
||||
import { MarketHeader } from '../components/market-header';
|
||||
import { PortfolioSidebar } from '../client-pages/portfolio/portfolio-sidebar';
|
||||
import { MarketHeader, MobileMarketHeader } from '../components/market-header';
|
||||
import {
|
||||
PortfolioMobileSidebar,
|
||||
PortfolioSidebar,
|
||||
} from '../client-pages/portfolio/portfolio-sidebar';
|
||||
import { LiquiditySidebar } from '../client-pages/liquidity/liquidity-sidebar';
|
||||
import { MarketsSidebar } from '../client-pages/markets/markets-sidebar';
|
||||
import { useT } from '../lib/use-t';
|
||||
@@ -33,8 +36,10 @@ import { CompetitionsTeams } from '../client-pages/competitions/competitions-tea
|
||||
import { CompetitionsTeam } from '../client-pages/competitions/competitions-team';
|
||||
import { CompetitionsCreateTeam } from '../client-pages/competitions/competitions-create-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 nextjs due to ESM
|
||||
// These must remain dynamically imported as pennant cannot be compiled by Next.js due to ESM
|
||||
// Using dynamic imports is a workaround for this until pennant is published as ESM
|
||||
const MarketPage = lazy(() => import('../client-pages/market'));
|
||||
const Portfolio = lazy(() => import('../client-pages/portfolio'));
|
||||
@@ -50,6 +55,20 @@ const NotFound = () => {
|
||||
|
||||
export const useRouterConfig = (): RouteObject[] => {
|
||||
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([
|
||||
{
|
||||
index: true,
|
||||
@@ -66,7 +85,7 @@ export const useRouterConfig = (): RouteObject[] => {
|
||||
featureFlags.REFERRALS
|
||||
? {
|
||||
path: AppRoutes.REFERRALS,
|
||||
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
|
||||
element: <LayoutWithSidebar sidebar={portfolioSidebar} />,
|
||||
children: [
|
||||
{
|
||||
element: (
|
||||
@@ -99,7 +118,7 @@ export const useRouterConfig = (): RouteObject[] => {
|
||||
featureFlags.TEAM_COMPETITION
|
||||
? {
|
||||
path: AppRoutes.COMPETITIONS,
|
||||
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
|
||||
element: <LayoutWithSidebar sidebar={portfolioSidebar} />,
|
||||
children: [
|
||||
// pages with planets and stars
|
||||
{
|
||||
@@ -130,7 +149,7 @@ export const useRouterConfig = (): RouteObject[] => {
|
||||
: undefined,
|
||||
{
|
||||
path: 'fees/*',
|
||||
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
|
||||
element: <LayoutWithSidebar sidebar={portfolioSidebar} />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
@@ -140,7 +159,7 @@ export const useRouterConfig = (): RouteObject[] => {
|
||||
},
|
||||
{
|
||||
path: 'rewards/*',
|
||||
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
|
||||
element: <LayoutWithSidebar sidebar={portfolioSidebar} />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
@@ -151,10 +170,7 @@ export const useRouterConfig = (): RouteObject[] => {
|
||||
{
|
||||
path: 'markets/*',
|
||||
element: (
|
||||
<LayoutWithSidebar
|
||||
header={<MarketHeader />}
|
||||
sidebar={<MarketsSidebar />}
|
||||
/>
|
||||
<LayoutWithSidebar header={marketHeader} sidebar={marketsSidebar} />
|
||||
),
|
||||
children: [
|
||||
{
|
||||
@@ -175,7 +191,7 @@ export const useRouterConfig = (): RouteObject[] => {
|
||||
},
|
||||
{
|
||||
path: 'portfolio/*',
|
||||
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
|
||||
element: <LayoutWithSidebar sidebar={portfolioSidebar} />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
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
@@ -0,0 +1,63 @@
|
||||
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,7 +25,6 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
||||
const t = useT();
|
||||
const { pubKey, pubKeys, isReadOnly } = useVegaWallet();
|
||||
const { params } = useNetworkParams([
|
||||
NetworkParams.transfer_fee_factor,
|
||||
NetworkParams.transfer_minTransferQuantumMultiple,
|
||||
]);
|
||||
|
||||
@@ -72,7 +71,6 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
||||
pubKeys={pubKeys ? pubKeys?.map((pk) => pk.publicKey) : null}
|
||||
isReadOnly={isReadOnly}
|
||||
assetId={assetId}
|
||||
feeFactor={params.transfer_fee_factor}
|
||||
minQuantumMultiple={params.transfer_minTransferQuantumMultiple}
|
||||
submitTransfer={transfer}
|
||||
accounts={sortedAccounts}
|
||||
|
||||
@@ -15,6 +15,30 @@ import {
|
||||
} from './transfer-form';
|
||||
import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
|
||||
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', () => {
|
||||
const renderComponent = (props: TransferFormProps) => {
|
||||
@@ -56,7 +80,6 @@ describe('TransferForm', () => {
|
||||
const props = {
|
||||
pubKey,
|
||||
pubKeys: [pubKey, '2'.repeat(64)],
|
||||
feeFactor: '0.001',
|
||||
submitTransfer: jest.fn(),
|
||||
accounts: [
|
||||
{
|
||||
@@ -79,7 +102,6 @@ describe('TransferForm', () => {
|
||||
pubKey,
|
||||
'a4b6e3de5d7ef4e31ae1b090be49d1a2ef7bcefff60cccf7658a0d4922651cce',
|
||||
],
|
||||
feeFactor: '0.001',
|
||||
submitTransfer: jest.fn(),
|
||||
accounts: [],
|
||||
minQuantumMultiple: '1',
|
||||
@@ -96,15 +118,6 @@ describe('TransferForm', () => {
|
||||
});
|
||||
|
||||
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',
|
||||
tooltipText: /without the fee/,
|
||||
@@ -114,9 +127,6 @@ describe('TransferForm', () => {
|
||||
tooltipText: /total amount taken from your account/,
|
||||
},
|
||||
])('Tooltip for "$targetText" shows', async (o) => {
|
||||
// 1003-TRAN-015
|
||||
// 1003-TRAN-016
|
||||
// 1003-TRAN-017
|
||||
// 1003-TRAN-018
|
||||
// 1003-TRAN-019
|
||||
renderComponent(props);
|
||||
@@ -129,6 +139,10 @@ describe('TransferForm', () => {
|
||||
// Select asset
|
||||
await selectAsset(asset);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('From account'),
|
||||
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
|
||||
);
|
||||
// set valid amount
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
await userEvent.type(amountInput, amount);
|
||||
@@ -219,9 +233,7 @@ describe('TransferForm', () => {
|
||||
// set valid amount
|
||||
await userEvent.clear(amountInput);
|
||||
await userEvent.type(amountInput, amount);
|
||||
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(
|
||||
new BigNumber(props.feeFactor).times(amount).toFixed()
|
||||
);
|
||||
expect(screen.getByTestId('transfer-fee')).toHaveTextContent('1');
|
||||
|
||||
await submit();
|
||||
|
||||
@@ -276,9 +288,6 @@ describe('TransferForm', () => {
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
|
||||
const checkbox = screen.getByTestId('include-transfer-fee');
|
||||
expect(checkbox).not.toBeChecked();
|
||||
|
||||
await userEvent.clear(amountInput);
|
||||
await userEvent.type(amountInput, '50');
|
||||
|
||||
@@ -288,10 +297,7 @@ describe('TransferForm', () => {
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
|
||||
expect(amountInput).toHaveValue('100.00');
|
||||
|
||||
// If transfering from a vested account 'include fees' checkbox should
|
||||
// be disabled and fees should be 0
|
||||
expect(checkbox).not.toBeChecked();
|
||||
expect(checkbox).toBeDisabled();
|
||||
// If transfering from a vested account fees should be 0
|
||||
const expectedFee = '0';
|
||||
const total = new BigNumber(amount).plus(expectedFee).toFixed();
|
||||
|
||||
@@ -397,120 +403,43 @@ describe('TransferForm', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('IncludeFeesCheckbox', () => {
|
||||
it('validates fields and submits when checkbox is checked', async () => {
|
||||
const mockSubmit = jest.fn();
|
||||
renderComponent({ ...props, submitTransfer: mockSubmit });
|
||||
it('validates fields', async () => {
|
||||
renderComponent(props);
|
||||
|
||||
// check current pubkey not shown
|
||||
const keySelect = screen.getByLabelText<HTMLSelectElement>('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
|
||||
);
|
||||
// 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
|
||||
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 a pubkey
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('To Vega key'),
|
||||
props.pubKeys[1]
|
||||
);
|
||||
|
||||
// Select asset
|
||||
await selectAsset(asset);
|
||||
// Select asset
|
||||
await selectAsset(asset);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('From account'),
|
||||
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
|
||||
);
|
||||
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');
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
|
||||
// 1003-TRAN-022
|
||||
expect(checkbox).not.toBeChecked();
|
||||
|
||||
await userEvent.clear(amountInput);
|
||||
await userEvent.type(amountInput, amount);
|
||||
await userEvent.click(checkbox);
|
||||
|
||||
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);
|
||||
});
|
||||
await userEvent.type(amountInput, amount);
|
||||
const expectedFee = new BigNumber(amount).times(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', () => {
|
||||
@@ -542,24 +471,29 @@ describe('TransferForm', () => {
|
||||
|
||||
describe('TransferFee', () => {
|
||||
const props = {
|
||||
amount: '200',
|
||||
feeFactor: '0.001',
|
||||
fee: '0.2',
|
||||
transferAmount: '200',
|
||||
decimals: 8,
|
||||
amount: '20000',
|
||||
discount: '0',
|
||||
fee: '20',
|
||||
decimals: 2,
|
||||
};
|
||||
it('calculates and renders the transfer fee', () => {
|
||||
it('calculates and renders amounts and fee', () => {
|
||||
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'
|
||||
);
|
||||
});
|
||||
|
||||
const expected = new BigNumber(props.amount)
|
||||
.times(props.feeFactor)
|
||||
.toFixed();
|
||||
const total = new BigNumber(props.amount).plus(expected).toFixed();
|
||||
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expected);
|
||||
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(
|
||||
props.amount
|
||||
it('calculates and renders amounts, fee and discount', () => {
|
||||
render(<TransferFee {...props} discount="10" />);
|
||||
expect(screen.getByTestId('discount')).toHaveTextContent('0.1');
|
||||
expect(screen.getByTestId('transfer-fee')).toHaveTextContent('0.2');
|
||||
expect(screen.getByTestId('transfer-amount')).toHaveTextContent('200.00');
|
||||
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(
|
||||
'200.10'
|
||||
);
|
||||
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,8 +4,9 @@ import {
|
||||
useRequired,
|
||||
useVegaPublicKey,
|
||||
addDecimal,
|
||||
formatNumber,
|
||||
toBigNum,
|
||||
removeDecimal,
|
||||
addDecimalsFormatNumber,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { useT } from './use-t';
|
||||
import {
|
||||
@@ -15,17 +16,17 @@ import {
|
||||
TradingRichSelect,
|
||||
TradingSelect,
|
||||
Tooltip,
|
||||
TradingCheckbox,
|
||||
TradingButton,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { Transfer } from '@vegaprotocol/wallet';
|
||||
import { normalizeTransfer } from '@vegaprotocol/wallet';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { AssetOption, Balance } from '@vegaprotocol/assets';
|
||||
import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
|
||||
import { useTransferFeeQuery } from './__generated__/TransferFee';
|
||||
|
||||
interface FormFields {
|
||||
toVegaKey: string;
|
||||
@@ -52,7 +53,6 @@ export interface TransferFormProps {
|
||||
asset: Asset;
|
||||
}>;
|
||||
assetId?: string;
|
||||
feeFactor: string | null;
|
||||
minQuantumMultiple: string | null;
|
||||
submitTransfer: (transfer: Transfer) => void;
|
||||
}
|
||||
@@ -62,7 +62,6 @@ export const TransferForm = ({
|
||||
pubKeys,
|
||||
isReadOnly,
|
||||
assetId: initialAssetId,
|
||||
feeFactor,
|
||||
submitTransfer,
|
||||
accounts,
|
||||
minQuantumMultiple,
|
||||
@@ -135,32 +134,28 @@ export const TransferForm = ({
|
||||
const accountBalance =
|
||||
account && addDecimal(account.balance, account.asset.decimals);
|
||||
|
||||
const [includeFee, setIncludeFee] = useState(false);
|
||||
|
||||
// Max amount given selected asset and from account
|
||||
const max = accountBalance ? new BigNumber(accountBalance) : new BigNumber(0);
|
||||
const normalizedAmount =
|
||||
(amount && asset && removeDecimal(amount, asset.decimals)) || '0';
|
||||
|
||||
const transferAmount = useMemo(() => {
|
||||
if (!amount) return undefined;
|
||||
if (includeFee && feeFactor) {
|
||||
return new BigNumber(1).minus(feeFactor).times(amount).toString();
|
||||
}
|
||||
return amount;
|
||||
}, [amount, includeFee, feeFactor]);
|
||||
|
||||
const fee = useMemo(() => {
|
||||
if (!transferAmount) return undefined;
|
||||
if (includeFee) {
|
||||
return new BigNumber(amount).minus(transferAmount).toString();
|
||||
}
|
||||
return (
|
||||
feeFactor && new BigNumber(feeFactor).times(transferAmount).toString()
|
||||
);
|
||||
}, [amount, includeFee, transferAmount, feeFactor]);
|
||||
const transferFeeQuery = useTransferFeeQuery({
|
||||
variables: {
|
||||
fromAccount: pubKey || '',
|
||||
fromAccountType: accountType || AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
amount: normalizedAmount,
|
||||
assetId: asset?.id || '',
|
||||
toAccount: selectedPubKey,
|
||||
},
|
||||
skip: !pubKey || !amount || !asset || !selectedPubKey || fromVested,
|
||||
});
|
||||
const transferFee = transferFeeQuery.loading
|
||||
? transferFeeQuery.data || transferFeeQuery.previousData
|
||||
: transferFeeQuery.data;
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(fields: FormFields) => {
|
||||
if (!transferAmount) {
|
||||
if (!amount) {
|
||||
throw new Error('Submitted transfer with no amount selected');
|
||||
}
|
||||
|
||||
@@ -173,7 +168,7 @@ export const TransferForm = ({
|
||||
|
||||
const transfer = normalizeTransfer(
|
||||
fields.toVegaKey,
|
||||
transferAmount,
|
||||
amount,
|
||||
type,
|
||||
AccountType.ACCOUNT_TYPE_GENERAL, // field is readonly in the form
|
||||
{
|
||||
@@ -183,7 +178,7 @@ export const TransferForm = ({
|
||||
);
|
||||
submitTransfer(transfer);
|
||||
},
|
||||
[submitTransfer, transferAmount, assets]
|
||||
[submitTransfer, amount, assets]
|
||||
);
|
||||
|
||||
// reset for placeholder workaround https://github.com/radix-ui/primitives/issues/1569
|
||||
@@ -279,7 +274,6 @@ export const TransferForm = ({
|
||||
) {
|
||||
setValue('toVegaKey', pubKey);
|
||||
setToVegaKeyMode('select');
|
||||
setIncludeFee(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -449,30 +443,14 @@ export const TransferForm = ({
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<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?.estimateTransferFee || fromVested) && amount && asset && (
|
||||
<TransferFee
|
||||
amount={transferAmount}
|
||||
transferAmount={transferAmount}
|
||||
feeFactor={feeFactor}
|
||||
fee={fromVested ? '0' : fee}
|
||||
decimals={asset?.decimals}
|
||||
amount={normalizedAmount}
|
||||
fee={fromVested ? '0' : transferFee?.estimateTransferFee?.fee}
|
||||
discount={
|
||||
fromVested ? '0' : transferFee?.estimateTransferFee?.discount
|
||||
}
|
||||
decimals={asset.decimals}
|
||||
/>
|
||||
)}
|
||||
<TradingButton type="submit" fill={true} disabled={isReadOnly}>
|
||||
@@ -484,46 +462,44 @@ export const TransferForm = ({
|
||||
|
||||
export const TransferFee = ({
|
||||
amount,
|
||||
transferAmount,
|
||||
feeFactor,
|
||||
fee,
|
||||
discount,
|
||||
decimals,
|
||||
}: {
|
||||
amount: string;
|
||||
transferAmount: string;
|
||||
feeFactor: string | null;
|
||||
fee?: string;
|
||||
decimals?: number;
|
||||
discount?: string;
|
||||
decimals: number;
|
||||
}) => {
|
||||
const t = useT();
|
||||
if (!feeFactor || !amount || !transferAmount || !fee) return null;
|
||||
if (
|
||||
isNaN(Number(feeFactor)) ||
|
||||
isNaN(Number(amount)) ||
|
||||
isNaN(Number(transferAmount)) ||
|
||||
isNaN(Number(fee))
|
||||
) {
|
||||
if (!amount || !fee) return null;
|
||||
if (isNaN(Number(amount)) || isNaN(Number(fee))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const totalValue = new BigNumber(transferAmount).plus(fee).toString();
|
||||
const totalValue = (
|
||||
BigInt(amount) +
|
||||
BigInt(fee) -
|
||||
BigInt(discount || '0')
|
||||
).toString();
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex flex-col gap-2 text-xs">
|
||||
<div className="flex flex-wrap items-center justify-between gap-1">
|
||||
<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>{t('Transfer fee')}</div>
|
||||
<div data-testid="transfer-fee" className="text-muted">
|
||||
{formatNumber(fee, decimals)}
|
||||
{addDecimalsFormatNumber(fee, decimals)}
|
||||
</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">
|
||||
<Tooltip
|
||||
description={t(
|
||||
@@ -534,7 +510,7 @@ export const TransferFee = ({
|
||||
</Tooltip>
|
||||
|
||||
<div data-testid="transfer-amount" className="text-muted">
|
||||
{formatNumber(amount, decimals)}
|
||||
{addDecimalsFormatNumber(amount, decimals)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-between gap-1">
|
||||
@@ -547,7 +523,7 @@ export const TransferFee = ({
|
||||
</Tooltip>
|
||||
|
||||
<div data-testid="total-transfer-fee" className="text-muted">
|
||||
{formatNumber(totalValue, decimals)}
|
||||
{addDecimalsFormatNumber(totalValue, decimals)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,27 +3,13 @@ import { getAsset, getQuoteName } from '@vegaprotocol/markets';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
|
||||
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 {
|
||||
MARGIN_DIFF_TOOLTIP_TEXT,
|
||||
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
|
||||
TOTAL_MARGIN_AVAILABLE,
|
||||
LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT,
|
||||
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
|
||||
MARGIN_ACCOUNT_TOOLTIP_TEXT,
|
||||
} from '../../constants';
|
||||
import { KeyValue } from './key-value';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionChevron,
|
||||
AccordionPanel,
|
||||
ExternalLink,
|
||||
Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { useT, ns } from '../../use-t';
|
||||
import { Trans } from 'react-i18next';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
@@ -31,9 +17,9 @@ import { emptyValue } from './deal-ticket-fee-details';
|
||||
import type { EstimatePositionQuery } from '@vegaprotocol/positions';
|
||||
|
||||
export interface DealTicketMarginDetailsProps {
|
||||
generalAccountBalance?: string;
|
||||
marginAccountBalance?: string;
|
||||
orderMarginAccountBalance?: string;
|
||||
generalAccountBalance: string;
|
||||
marginAccountBalance: string;
|
||||
orderMarginAccountBalance: string;
|
||||
market: Market;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
assetSymbol: string;
|
||||
@@ -54,118 +40,20 @@ export const DealTicketMarginDetails = ({
|
||||
const t = useT();
|
||||
const [breakdownDialog, setBreakdownDialog] = useState(false);
|
||||
const { pubKey: partyId } = useVegaWallet();
|
||||
const { data: currentMargins } = useDataProvider({
|
||||
dataProvider: marketMarginDataProvider,
|
||||
variables: { marketId: market.id, partyId: partyId || '' },
|
||||
skip: !partyId,
|
||||
});
|
||||
const liquidationEstimate = positionEstimate?.liquidation;
|
||||
const marginEstimate = positionEstimate?.margin;
|
||||
const totalMarginAccountBalance =
|
||||
BigInt(marginAccountBalance || '0') +
|
||||
BigInt(orderMarginAccountBalance || '0');
|
||||
const totalBalance =
|
||||
BigInt(generalAccountBalance || '0') + totalMarginAccountBalance;
|
||||
|
||||
const asset = getAsset(market);
|
||||
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);
|
||||
|
||||
marginRequiredBestCase = (
|
||||
marginEstimateBestCase - currentMargin
|
||||
).toString();
|
||||
if (marginRequiredBestCase.startsWith('-')) {
|
||||
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
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const collateralIncreaseEstimateBestCase = BigInt(
|
||||
positionEstimate?.collateralIncreaseEstimate.bestCase ?? '0'
|
||||
);
|
||||
const collateralIncreaseEstimateWorstCase = BigInt(
|
||||
positionEstimate?.collateralIncreaseEstimate.worstCase ?? '0'
|
||||
);
|
||||
|
||||
let liquidationPriceEstimate = emptyValue;
|
||||
let liquidationPriceEstimateRange = emptyValue;
|
||||
@@ -222,128 +110,50 @@ export const DealTicketMarginDetails = ({
|
||||
const quoteName = getQuoteName(market);
|
||||
|
||||
return (
|
||||
<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}
|
||||
<div className="flex flex-col w-full gap-2 mt-2">
|
||||
<KeyValue
|
||||
label={t('Liquidation')}
|
||||
label={t('Current margin')}
|
||||
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}
|
||||
formattedValue={liquidationPriceEstimate}
|
||||
symbol={quoteName}
|
||||
|
||||
@@ -73,7 +73,7 @@ import {
|
||||
} from '../../hooks';
|
||||
import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg';
|
||||
import noop from 'lodash/noop';
|
||||
import { isNonPersistentOrder } from '../../utils/time-in-force-persistance';
|
||||
import { isNonPersistentOrder } from '../../utils/time-in-force-persistence';
|
||||
import { KeyValue } from './key-value';
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
import { useT } from '../../use-t';
|
||||
@@ -177,12 +177,6 @@ export const DealTicket = ({
|
||||
loading: loadingGeneralAccountBalance,
|
||||
} = useAccountBalance(asset.id);
|
||||
|
||||
const balance = (
|
||||
BigInt(marginAccountBalance) +
|
||||
BigInt(generalAccountBalance) +
|
||||
BigInt(orderMarginAccountBalance)
|
||||
).toString();
|
||||
|
||||
const { marketState, marketTradingMode } = marketData;
|
||||
const timeInForce = watch('timeInForce');
|
||||
|
||||
@@ -287,7 +281,7 @@ export const DealTicket = ({
|
||||
marginFactor: margin?.marginFactor || '1',
|
||||
marginMode:
|
||||
margin?.marginMode || Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN,
|
||||
includeCollateralIncreaseInAvailableCollateral: true,
|
||||
includeRequiredPositionMarginInAvailableCollateral: true,
|
||||
},
|
||||
!normalizedOrder ||
|
||||
(normalizedOrder.type !== Schema.OrderType.TYPE_MARKET &&
|
||||
@@ -729,17 +723,11 @@ export const DealTicket = ({
|
||||
error={summaryError}
|
||||
asset={asset}
|
||||
marketTradingMode={marketData.marketTradingMode}
|
||||
balance={balance}
|
||||
margin={(
|
||||
BigInt(
|
||||
positionEstimate?.estimatePosition?.margin.bestCase.initialLevel ||
|
||||
'0'
|
||||
) +
|
||||
BigInt(
|
||||
positionEstimate?.estimatePosition?.margin.bestCase
|
||||
.orderMarginLevel || '0'
|
||||
)
|
||||
).toString()}
|
||||
balance={generalAccountBalance}
|
||||
margin={
|
||||
positionEstimate?.estimatePosition?.collateralIncreaseEstimate
|
||||
.bestCase || '0'
|
||||
}
|
||||
isReadOnly={isReadOnly}
|
||||
pubKey={pubKey}
|
||||
onDeposit={onDeposit}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import classnames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface KeyValuePros {
|
||||
@@ -19,7 +18,6 @@ export const KeyValue = ({
|
||||
value,
|
||||
labelDescription,
|
||||
symbol,
|
||||
indent,
|
||||
onClick,
|
||||
formattedValue,
|
||||
}: KeyValuePros) => {
|
||||
@@ -43,10 +41,7 @@ export const KeyValue = ({
|
||||
: id
|
||||
}`}
|
||||
key={typeof label === 'string' ? label : 'value-dropdown'}
|
||||
className={classnames(
|
||||
'text-xs flex justify-between items-center gap-4 flex-wrap text-right',
|
||||
{ 'ml-2': indent }
|
||||
)}
|
||||
className="text-xs flex justify-between items-center gap-4 flex-wrap text-right"
|
||||
>
|
||||
<Tooltip description={labelDescription}>
|
||||
<div className="text-muted text-left">{label}</div>
|
||||
|
||||
@@ -29,6 +29,7 @@ import { usePositionEstimate } from '../../hooks/use-position-estimate';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { getAsset, useMarket } from '@vegaprotocol/markets';
|
||||
import { NoWalletWarning } from './deal-ticket';
|
||||
import { DealTicketMarginDetails } from './deal-ticket-margin-details';
|
||||
|
||||
const defaultLeverage = 10;
|
||||
|
||||
@@ -88,71 +89,83 @@ export const MarginChange = ({
|
||||
openVolume,
|
||||
marketId,
|
||||
orderMarginAccountBalance: orderMarginAccountBalance || '0',
|
||||
includeCollateralIncreaseInAvailableCollateral: true,
|
||||
includeRequiredPositionMarginInAvailableCollateral: true,
|
||||
orders,
|
||||
},
|
||||
skip
|
||||
);
|
||||
if (
|
||||
!asset ||
|
||||
!estimateMargin?.estimatePosition?.collateralIncreaseEstimate.worstCase ||
|
||||
estimateMargin.estimatePosition.collateralIncreaseEstimate.worstCase === '0'
|
||||
) {
|
||||
if (!asset || !estimateMargin?.estimatePosition) {
|
||||
return null;
|
||||
}
|
||||
const collateralIncreaseEstimate = BigInt(
|
||||
estimateMargin.estimatePosition.collateralIncreaseEstimate.worstCase
|
||||
);
|
||||
if (!collateralIncreaseEstimate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 = '';
|
||||
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
|
||||
if (collateralIncreaseEstimate) {
|
||||
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,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="mb-2">
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
message={
|
||||
<>
|
||||
<p>{positionWarning}</p>
|
||||
<p>{marginChangeWarning}</p>
|
||||
</>
|
||||
{positionWarning && marginChangeWarning && (
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
message={
|
||||
<>
|
||||
<p>{positionWarning}</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>
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
} from '../hooks/use-form-values';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
|
||||
import { isPersistentOrder } from './time-in-force-persistance';
|
||||
import { isPersistentOrder } from './time-in-force-persistence';
|
||||
|
||||
export const mapFormValuesToOrderSubmission = (
|
||||
order: OrderFormValues,
|
||||
|
||||
+3
-3
@@ -2,9 +2,9 @@ import { OrderTimeInForce } from '@vegaprotocol/types';
|
||||
import {
|
||||
isNonPersistentOrder,
|
||||
isPersistentOrder,
|
||||
} from './time-in-force-persistance';
|
||||
} from './time-in-force-persistence';
|
||||
|
||||
it('isNonPeristentOrder', () => {
|
||||
it('isNonPersistentOrder', () => {
|
||||
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_FOK)).toBe(true);
|
||||
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_IOC)).toBe(true);
|
||||
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTC)).toBe(false);
|
||||
@@ -13,7 +13,7 @@ it('isNonPeristentOrder', () => {
|
||||
expect(isNonPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GFN)).toBe(false);
|
||||
});
|
||||
|
||||
it('isPeristentOrder', () => {
|
||||
it('isPersistentOrder', () => {
|
||||
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_FOK)).toBe(false);
|
||||
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_IOC)).toBe(false);
|
||||
expect(isPersistentOrder(OrderTimeInForce.TIME_IN_FORCE_GTC)).toBe(true);
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
SUBSCRIPTION_TIMEOUT,
|
||||
useNodeBasicStatus,
|
||||
useNodeSubscriptionStatus,
|
||||
useResponseTime,
|
||||
} from './row-data';
|
||||
import { BLOCK_THRESHOLD, RowData } from './row-data';
|
||||
import { CUSTOM_NODE_KEY } from '../../types';
|
||||
@@ -162,19 +161,6 @@ 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', () => {
|
||||
const props = {
|
||||
id: '0',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { isValidUrl } from '@vegaprotocol/utils';
|
||||
import { TradingRadio } from '@vegaprotocol/ui-toolkit';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CUSTOM_NODE_KEY } from '../../types';
|
||||
@@ -8,6 +7,7 @@ import {
|
||||
} from '../../utils/__generated__/NodeCheck';
|
||||
import { LayoutCell } from './layout-cell';
|
||||
import { useT } from '../../use-t';
|
||||
import { useResponseTime } from '../../utils/time';
|
||||
|
||||
export const POLL_INTERVAL = 1000;
|
||||
export const SUBSCRIPTION_TIMEOUT = 3000;
|
||||
@@ -108,20 +108,6 @@ 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 = ({
|
||||
id,
|
||||
url,
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
getUserEnabledFeatureFlags,
|
||||
setUserEnabledFeatureFlag,
|
||||
} from './use-environment';
|
||||
import { canMeasureResponseTime, measureResponseTime } from '../utils/time';
|
||||
|
||||
const noop = () => {
|
||||
/* no op*/
|
||||
@@ -17,6 +18,10 @@ const noop = () => {
|
||||
|
||||
jest.mock('@vegaprotocol/apollo-client');
|
||||
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 createDefaultMockClient = () => {
|
||||
@@ -155,6 +160,14 @@ describe('useEnvironment', () => {
|
||||
const fastNode = 'https://api.n01.foo.vega.xyz';
|
||||
const fastWait = 1000;
|
||||
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
|
||||
global.fetch.mockImplementation(setupFetch({ hosts: nodes }));
|
||||
|
||||
@@ -168,7 +181,7 @@ describe('useEnvironment', () => {
|
||||
statistics: {
|
||||
chainId: 'chain-id',
|
||||
blockHeight: '100',
|
||||
vegaTime: new Date().toISOString(),
|
||||
vegaTime: new Date(1).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -196,7 +209,8 @@ describe('useEnvironment', () => {
|
||||
expect(result.current.nodes).toEqual(nodes);
|
||||
});
|
||||
|
||||
jest.runAllTimers();
|
||||
jest.advanceTimersByTime(2000);
|
||||
// jest.runAllTimers();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.status).toEqual('success');
|
||||
|
||||
@@ -19,6 +19,9 @@ import { compileErrors } from '../utils/compile-errors';
|
||||
import { envSchema } from '../utils/validate-environment';
|
||||
import { tomlConfigSchema } from '../utils/validate-configuration';
|
||||
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 ClientCollection = {
|
||||
@@ -38,8 +41,17 @@ export type EnvStore = Env & Actions;
|
||||
|
||||
const VERSION = 1;
|
||||
export const STORAGE_KEY = `vega_url_${VERSION}`;
|
||||
|
||||
const QUERY_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
|
||||
*/
|
||||
@@ -64,53 +76,88 @@ const fetchConfig = async (url?: string) => {
|
||||
const findNode = async (clients: ClientCollection): Promise<string | null> => {
|
||||
const tests = Object.entries(clients).map((args) => testNode(...args));
|
||||
try {
|
||||
const url = await Promise.any(tests);
|
||||
return url;
|
||||
} catch {
|
||||
const nodes = await Promise.all(tests);
|
||||
const responsiveNodes = nodes
|
||||
.filter(([, q, s]) => q && s)
|
||||
.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
|
||||
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
|
||||
*/
|
||||
const testNode = async (
|
||||
url: string,
|
||||
client: Client
|
||||
): Promise<string | null> => {
|
||||
): Promise<NodeTestResult> => {
|
||||
const results = await Promise.all([
|
||||
// these promises will only resolve with true/false
|
||||
testQuery(client),
|
||||
testQuery(client, url),
|
||||
testSubscription(client),
|
||||
]);
|
||||
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);
|
||||
return [url, ...results];
|
||||
};
|
||||
|
||||
/**
|
||||
* Run a test query on a client
|
||||
*/
|
||||
const testQuery = async (client: Client) => {
|
||||
try {
|
||||
const result = await client.query<NodeCheckQuery>({
|
||||
query: NodeCheckDocument,
|
||||
});
|
||||
if (!result || result.error) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
const testQuery = (
|
||||
client: Client,
|
||||
url: string
|
||||
): Promise<Maybe<QueryTestResult>> => {
|
||||
const test: Promise<Maybe<QueryTestResult>> = new Promise((resolve) =>
|
||||
client
|
||||
.query<NodeCheckQuery>({
|
||||
query: NodeCheckDocument,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result && !result.error) {
|
||||
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)]);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -118,7 +165,9 @@ const testQuery = async (client: Client) => {
|
||||
* that takes longer than SUBSCRIPTION_TIMEOUT ms to respond
|
||||
* is deemed a failure
|
||||
*/
|
||||
const testSubscription = (client: Client) => {
|
||||
const testSubscription = (
|
||||
client: Client
|
||||
): Promise<Maybe<SubscriptionTestResult>> => {
|
||||
return new Promise((resolve) => {
|
||||
const sub = client
|
||||
.subscribe<NodeCheckTimeUpdateSubscription>({
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useResponseTime } from './time';
|
||||
|
||||
const mockResponseTime = 50;
|
||||
global.performance.getEntriesByName = jest.fn().mockReturnValue([
|
||||
{
|
||||
duration: mockResponseTime,
|
||||
},
|
||||
]);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { isValidUrl } from '@vegaprotocol/utils';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const useResponseTime = (url: string, trigger?: unknown) => {
|
||||
const [responseTime, setResponseTime] = useState<number>();
|
||||
useEffect(() => {
|
||||
if (!canMeasureResponseTime(url)) return;
|
||||
const duration = measureResponseTime(url);
|
||||
setResponseTime(duration);
|
||||
}, [url, trigger]);
|
||||
return { responseTime };
|
||||
};
|
||||
|
||||
export const canMeasureResponseTime = (url: string) => {
|
||||
if (!isValidUrl(url)) return false;
|
||||
if (typeof window.performance.getEntriesByName !== 'function') return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
export const measureResponseTime = (url: string) => {
|
||||
const requestUrl = new URL(url);
|
||||
const requests = window.performance.getEntriesByName(requestUrl.href);
|
||||
const { duration } = (requests.length && requests[requests.length - 1]) || {};
|
||||
return duration;
|
||||
};
|
||||
@@ -21,7 +21,6 @@
|
||||
"Deposited on the network, but not allocated to a market. Free to use for placing orders or providing liquidity.": "Deposited on the network, but not allocated to a market. Free to use for placing orders or providing liquidity.",
|
||||
"Enter manually": "Enter manually",
|
||||
"From account": "From account",
|
||||
"Include transfer fee": "Include transfer fee",
|
||||
"initial level": "initial level",
|
||||
"maintenance level": "maintenance level",
|
||||
"Margin health": "Margin health",
|
||||
@@ -33,11 +32,9 @@
|
||||
"release level": "release level",
|
||||
"search level": "search level",
|
||||
"Select from wallet": "Select from wallet",
|
||||
"The fee will be taken from the amount you are transferring.": "The fee will be taken from the amount you are transferring.",
|
||||
"The total amount of each asset on this key. Includes used and available collateral.": "The total amount of each asset on this key. Includes used and available collateral.",
|
||||
"The total amount taken from your account. The amount to be transferred plus the fee.": "The total amount taken from your account. The amount to be transferred plus the fee.",
|
||||
"The total amount to be transferred (without the fee)": "The total amount to be transferred (without the fee)",
|
||||
"The transfer fee is set by the network parameter transfer.fee.factor, currently set to {{feeFactor}}": "The transfer fee is set by the network parameter transfer.fee.factor, currently set to {{feeFactor}}",
|
||||
"To account": "To account",
|
||||
"To Vega key": "To Vega key",
|
||||
"Total": "Total",
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"Create": "Create",
|
||||
"Create a team": "Create a team",
|
||||
"Create a simple referral code to enjoy the referrer commission outlined in the current referral program": "Create a simple referral code to enjoy the referrer commission outlined in the current referral program",
|
||||
"Create solo team": "Create solo team",
|
||||
"Make your referral code a Team to compete in Competitions with your friends, appear in leaderboards on the <0>Competitions Homepage</0>, and earn rewards": "Make your referral code a Team to compete in Competitions with your friends, appear in leaderboards on the <0>Competitions Homepage</0>, and earn rewards",
|
||||
"Current tier": "Current tier",
|
||||
"DISCLAIMER_P1": "Vega is a decentralised peer-to-peer protocol that can be used to trade derivatives with cryptoassets. The Vega Protocol is an implementation layer (layer one) protocol made of free, public, open-source or source-available software. Use of the Vega Protocol involves various risks, including but not limited to, losses while digital assets are supplied to the Vega Protocol and losses due to the fluctuation of prices of assets.",
|
||||
@@ -181,6 +182,7 @@
|
||||
"Markets": "Markets",
|
||||
"Members": "Members",
|
||||
"Members ({{count}})": "Members ({{count}})",
|
||||
"Member ID": "Member ID",
|
||||
"Menu": "Menu",
|
||||
"Metamask Snap <0>quick start</0>": "Metamask Snap <0>quick start</0>",
|
||||
"Min. epochs": "Min. epochs",
|
||||
@@ -363,6 +365,7 @@
|
||||
"Type": "Type",
|
||||
"Unknown": "Unknown",
|
||||
"Unknown settlement date": "Unknown settlement date",
|
||||
"Update team": "Update team",
|
||||
"URL": "URL",
|
||||
"Use a comma separated list to allow only specific public keys to join the team": "Use a comma separated list to allow only specific public keys to join the team",
|
||||
"Vega chart": "Vega chart",
|
||||
@@ -414,6 +417,7 @@
|
||||
"myVolume_other": "My volume (last {{count}} epochs)",
|
||||
"numberEpochs": "{{count}} epochs",
|
||||
"numberEpochs_one": "{{count}} epoch",
|
||||
"Rewards earned": "Rewards earned",
|
||||
"Rewards paid out": "Rewards paid out",
|
||||
"{{reward}}x": "{{reward}}x",
|
||||
"userActive": "{{active}} trader: {{count}} epochs so far",
|
||||
@@ -428,5 +432,23 @@
|
||||
"{{assetSymbol}} Reward pot": "{{assetSymbol}} Reward pot",
|
||||
"{{checkedAssets}} Assets": "{{checkedAssets}} Assets",
|
||||
"{{distance}} ago": "{{distance}} ago",
|
||||
"{{instrumentCode}} liquidity provision": "{{instrumentCode}} liquidity provision"
|
||||
"{{instrumentCode}} liquidity provision": "{{instrumentCode}} liquidity provision",
|
||||
"My team": "My team",
|
||||
"Profile": "Profile",
|
||||
"Last {{games}} games result_one": "Last game result",
|
||||
"Last {{games}} games result_other": "Last {{games}} games result",
|
||||
"Leaderboard": "Leaderboard",
|
||||
"View all teams": "View all teams",
|
||||
"Competitions": "Competitions",
|
||||
"Be a team player! Participate in games and work together to rake in as much profit to win.": "Be a team player! Participate in games and work together to rake in as much profit to win.",
|
||||
"Create a public team": "Create a public team",
|
||||
"Create a private team": "Create a private team",
|
||||
"Choose a team": "Choose a team",
|
||||
"Join a team": "Join a team",
|
||||
"Solo team / lone wolf": "Solo team / lone wolf",
|
||||
"Choose a team to get involved": "Choose a team to get involved",
|
||||
"Go back to the team's profile": "Go back to the team's profile",
|
||||
"Go back to the competitions": "Go back to the competitions",
|
||||
"Your team ID:": "Your team ID:",
|
||||
"Changes successfully saved to your team.": "Changes successfully saved to your team."
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ export const LiquidityTable = ({
|
||||
return `${addDecimalsFormatNumberQuantum(
|
||||
value,
|
||||
assetDecimalPlaces ?? 0,
|
||||
quantum ?? 0
|
||||
quantum ?? 1
|
||||
)}`;
|
||||
};
|
||||
|
||||
@@ -165,7 +165,7 @@ export const LiquidityTable = ({
|
||||
return `${addDecimalsFormatNumberQuantum(
|
||||
newValue,
|
||||
assetDecimalPlaces ?? 0,
|
||||
quantum ?? 0
|
||||
quantum ?? 1
|
||||
)}`;
|
||||
};
|
||||
|
||||
@@ -227,7 +227,7 @@ export const LiquidityTable = ({
|
||||
addDecimalsFormatNumberQuantum(
|
||||
pendingCommitmentAmount,
|
||||
assetDecimalPlaces ?? 0,
|
||||
quantum ?? 0
|
||||
quantum ?? 1
|
||||
);
|
||||
|
||||
if (
|
||||
@@ -238,7 +238,7 @@ export const LiquidityTable = ({
|
||||
addDecimalsFormatNumberQuantum(
|
||||
currentCommitmentAmount,
|
||||
assetDecimalPlaces ?? 0,
|
||||
quantum ?? 0
|
||||
quantum ?? 1
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -286,7 +286,7 @@ export const LiquidityTable = ({
|
||||
addDecimalsFormatNumberQuantum(
|
||||
pendingCommitmentAmount,
|
||||
assetDecimalPlaces ?? 0,
|
||||
quantum ?? 0
|
||||
quantum ?? 1
|
||||
);
|
||||
|
||||
if (
|
||||
@@ -297,7 +297,7 @@ export const LiquidityTable = ({
|
||||
addDecimalsFormatNumberQuantum(
|
||||
currentCommitmentAmount,
|
||||
assetDecimalPlaces ?? 0,
|
||||
quantum ?? 0
|
||||
quantum ?? 1
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DepthChart } from 'pennant';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import { addDecimal, getNumberFormat } from '@vegaprotocol/utils';
|
||||
import { addDecimal, formatNumber } from '@vegaprotocol/utils';
|
||||
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { marketDepthProvider } from './market-depth-provider';
|
||||
@@ -216,13 +216,12 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
|
||||
|
||||
const volumeFormat = useCallback(
|
||||
(volume: number) =>
|
||||
getNumberFormat(market?.positionDecimalPlaces || 0).format(volume),
|
||||
formatNumber(volume, market?.positionDecimalPlaces || 0),
|
||||
[market?.positionDecimalPlaces]
|
||||
);
|
||||
|
||||
const priceFormat = useCallback(
|
||||
(price: number) =>
|
||||
getNumberFormat(market?.decimalPlaces || 0).format(price),
|
||||
(price: number) => formatNumber(price, market?.decimalPlaces || 0),
|
||||
[market?.decimalPlaces]
|
||||
);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user