Compare commits

...
Author SHA1 Message Date
Matthew Russell a0aa0065b6 chore: align viewing as banner 2024-02-12 11:07:43 -08:00
Matthew Russell bb6ecd2c72 chore: make fees and rewards page layout more consistent 2024-02-12 10:58:47 -08:00
Matthew Russell de62f09fda chore: usePageTitle hook to reduce imports 2024-02-09 15:51:33 -08:00
Matthew Russell 6d70b28f30 fix: title spacing on fees and rewards pages 2024-02-09 15:47:08 -08:00
m.rayandcandida-d 0d850bd8b9 feat(trading): add LP fee settings (#5773)
Co-authored-by: candida-d <62548908+candida-d@users.noreply.github.com>
2024-02-09 19:44:15 +00:00
Bartłomiej Głownia 44189591fc feat(accounts): get trasfer fee from estimateTransferFee API (#5721) 2024-02-09 12:55:17 +00:00
Art 3ed2ec88d7 chore(explorer): remove voting column from proposal table (#5776) 2024-02-09 10:32:54 +00:00
Bartłomiej Głownia a21feea699 chore(utils): improve formatNumber to keep precision (#5761) 2024-02-09 10:31:15 +00:00
m.rayandMatthew Russell 41fd14dd00 feat(trading): mobile layout and buttons (#5751)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2024-02-09 10:30:24 +00:00
Edd c5a27dc6a2 chore(trading): switch eth provider URL (#5779) 2024-02-09 10:26:08 +00:00
Art 0a3b1cadba fix(trading): keep leaderboard rank when filtering (#5775) 2024-02-09 10:00:28 +01:00
Edd b953de953a feat(explorer): update party profile tx (#5719) 2024-02-08 19:06:38 +00:00
Art 5ddcb613e2 chore(trading): create and update team form traversing (#5764) 2024-02-08 14:07:48 +00:00
m.rayandMatthew Russell 76c07992d3 feat(trading): update mobile layout (#5718)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2024-02-08 13:24:48 +00:00
Ben 46e2965fa2 chore(trading): fix test teardown (#5769) 2024-02-08 12:25:16 +00:00
m.rayandbwallacee 532ad3a4b9 feat(trading): show value of the volume traded in the last 24hrs (#5766)
Co-authored-by: bwallacee <ben@vega.xyz>
2024-02-07 11:07:24 +00:00
Madalina Raicu 3a8b40d7a5 Revert "feat(trading): show total value of the valume traded in last 24hrs"
This reverts commit be38813a33.
2024-02-06 22:12:33 +00:00
Madalina Raicu be38813a33 feat(trading): show total value of the valume traded in last 24hrs 2024-02-06 21:52:32 +00:00
bf70dc33ec chore(trading): back merge main to develop (#5749)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
Co-authored-by: Edd <edd@vega.xyz>
Co-authored-by: Bartłomiej Głownia <bglownia@gmail.com>
Co-authored-by: Art <artur@vegaprotocol.io>
2024-02-06 14:41:48 +00:00
Art d6084e75a0 fix(environment): pick best node (#5752) 2024-02-06 14:40:14 +01:00
Art 75e7cea32a fix(trading): trading view on multiple tabs (#5748) 2024-02-06 12:31:12 +01:00
Bartłomiej Głownia b4e98e285e feat(deal-ticket): update fields in margin details (#5722) 2024-02-06 08:42:11 +01:00
Edd f62e29c67f feat(explorer): batch proposal support (#5711) 2024-02-05 17:35:12 +00:00
94e7ad489f feat(trading): main to develop back merge (#5739)
Co-authored-by: m.ray <16125548+MadalinaRaicu@users.noreply.github.com>
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
Co-authored-by: Edd <edd@vega.xyz>
2024-02-05 16:06:33 +01:00
Art 657942d995 feat(trading): team card (#5724) 2024-02-02 20:07:06 +02:00
Matthew Russell a529b9b031 chore(trading): make team stats aggregation epochs 192 (#5723) 2024-02-02 18:01:28 +00:00
Ben 0f311611ba chore(trading): team e2e tests (#5716) 2024-02-01 19:33:25 +00:00
Art 02bd031bee fix(trading): mark as updated when tx confirmed (#5715) 2024-02-01 13:27:21 +00:00
Art bd97651dd3 fix(trading): game cards are not showing (#5713) 2024-02-01 12:04:36 +00:00
Bartłomiej Głownia 516b3e5b93 feat(deal-ticket): fix margin calculations in DealTicketMarginDetails for isolated margin mode (#5709) 2024-02-01 12:52:45 +01:00
Matthew Russellandbwallacee 42a98b6a35 fix(trading): teams snags (#5707)
Co-authored-by: bwallacee <ben@vega.xyz>
2024-02-01 10:38:57 +00:00
148 changed files with 3471 additions and 1618 deletions
+1 -1
View File
@@ -205,7 +205,7 @@ jobs:
# run tests # run tests
#---------------------------------------------- #----------------------------------------------
- name: Run tests - name: Run tests
run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v --numprocesses 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 working-directory: apps/trading/e2e
#---------------------------------------------- #----------------------------------------------
# upload traces # upload traces
+5 -5
View File
@@ -4,7 +4,7 @@ The front-end monorepo provides a toolkit for building apps that interact with V
This repository is managed using [Nx](https://nx.dev). This repository is managed using [Nx](https://nx.dev).
# 🔎 Applications in this repo ## 🔎 Applications in this repo
### [Block explorer](./apps/explorer) ### [Block explorer](./apps/explorer)
@@ -30,7 +30,7 @@ Hosting for static content being shared across apps, for example fonts.
The utility dApp for validators wishing to add or remove themselves as a signer of the multisig contract. The utility dApp for validators wishing to add or remove themselves as a signer of the multisig contract.
# 🧱 Libraries in this repo ## 🧱 Libraries in this repo
### [UI toolkit](./libs/ui-toolkit) ### [UI toolkit](./libs/ui-toolkit)
@@ -53,7 +53,7 @@ A utility library for connecting to the Ethereum network and interacting with Ve
Generic react helpers that can be used across multiple applications, along with other utilities. Generic react helpers that can be used across multiple applications, along with other utilities.
# 💻 Develop ## 💻 Develop
### Set up ### Set up
@@ -103,7 +103,7 @@ In CI linting, formatting and also run. These checks can be seen in the [CI work
Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more. Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more.
# 🐋 Hosting a console ## 🐋 Hosting a console
To host a console there are two possible build scenarios for running the frontends: nx performed **outside** or **inside** docker build. For specific build instructions follow [build instructions](#build-instructions). To host a console there are two possible build scenarios for running the frontends: nx performed **outside** or **inside** docker build. For specific build instructions follow [build instructions](#build-instructions).
@@ -226,6 +226,6 @@ Note: The script is only needed if capsule was built for first time or fresh. To
vega wallet service run -n DV --load-tokens --tokens-passphrase-file passphrase --no-version-check --automatic-consent --home ~/.vegacapsule/testnet/wallet vega wallet service run -n DV --load-tokens --tokens-passphrase-file passphrase --no-version-check --automatic-consent --home ~/.vegacapsule/testnet/wallet
``` ```
# 📑 License ## 📑 License
[MIT](./LICENSE) [MIT](./LICENSE)
@@ -24,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('title').should('have.text', proposalTitle);
cy.get_element_by_col_id('type').should('have.text', 'NewMarket'); cy.get_element_by_col_id('type').should('have.text', 'NewMarket');
cy.get_element_by_col_id('state').should('have.text', 'Enacted'); cy.get_element_by_col_id('state').should('have.text', 'Enacted');
cy.getByTestId('vote-progress').should('be.visible');
cy.getByTestId('vote-progress-bar-for')
.invoke('attr', 'style')
.should('eq', 'width: 100%;');
cy.get('[col-id="cDate"]') cy.get('[col-id="cDate"]')
.invoke('text') .invoke('text')
.should('match', dateTimeRegex); .should('match', dateTimeRegex);
@@ -73,10 +69,6 @@ context('Proposal page', { tags: '@smoke' }, function () {
'have.text', 'have.text',
'Waiting for Node Vote' 'Waiting for Node Vote'
); );
cy.getByTestId('vote-progress').should('be.visible');
cy.getByTestId('vote-progress-bar-against')
.invoke('attr', 'style')
.should('eq', 'width: 100%;');
cy.get('[col-id="cDate"]') cy.get('[col-id="cDate"]')
.invoke('text') .invoke('text')
.should('match', dateTimeRegex); .should('match', dateTimeRegex);
@@ -1,4 +1,4 @@
export type HashProps = { export type HashProps = React.HTMLProps<HTMLSpanElement> & {
text: string; text: string;
truncate?: boolean; truncate?: boolean;
}; };
@@ -2,4 +2,5 @@ export { default as BlockLink } from './block-link/block-link';
export { default as PartyLink } from './party-link/party-link'; export { default as PartyLink } from './party-link/party-link';
export { default as NodeLink } from './node-link/node-link'; export { default as NodeLink } from './node-link/node-link';
export { default as MarketLink } from './market-link/market-link'; export { default as MarketLink } from './market-link/market-link';
export { default as NetworkParameterLink } from './network-parameter-link/network-parameter-link';
export * from './asset-link/asset-link'; export * from './asset-link/asset-link';
@@ -0,0 +1,30 @@
import React from 'react';
import { Routes } from '../../../routes/route-names';
import { Link } from 'react-router-dom';
import type { ComponentProps } from 'react';
import Hash from '../hash';
export type NetworkParameterLinkProps = Partial<ComponentProps<typeof Link>> & {
parameter: string;
};
/**
* Links a given network parameter to the relevant page and anchor on the page
*/
const NetworkParameterLink = ({
parameter,
...props
}: NetworkParameterLinkProps) => {
return (
<Link
className="underline"
{...props}
to={`/${Routes.NETWORK_PARAMETERS}#${parameter}`}
>
<Hash text={parameter} />
</Link>
);
};
export default NetworkParameterLink;
@@ -26,7 +26,7 @@ const ProposalLink = ({ id, text }: ProposalLinkProps) => {
>; >;
const base = ENV.dataSources.governanceUrl; const base = ENV.dataSources.governanceUrl;
const label = proposal?.rationale.title || id; const label = proposal?.rationale?.title || id;
return ( return (
<ExternalLink href={`${base}/proposals/${id}`}> <ExternalLink href={`${base}/proposals/${id}`}>
@@ -1,5 +1,4 @@
import { type ProposalListFieldsFragment } from '@vegaprotocol/proposals'; import { type ProposalListFieldsFragment } from '@vegaprotocol/proposals';
import { VoteProgress } from '@vegaprotocol/proposals';
import { type AgGridReact } from 'ag-grid-react'; import { type AgGridReact } from 'ag-grid-react';
import { ExternalLink } from '@vegaprotocol/ui-toolkit'; import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { AgGrid } from '@vegaprotocol/datagrid'; import { AgGrid } from '@vegaprotocol/datagrid';
@@ -12,12 +11,7 @@ import { type ColDef } from 'ag-grid-community';
import type { RowClickedEvent } from 'ag-grid-community'; import type { RowClickedEvent } from 'ag-grid-community';
import { getDateTimeFormat } from '@vegaprotocol/utils'; import { getDateTimeFormat } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { ProposalStateMapping } from '@vegaprotocol/types'; import { ProposalStateMapping } from '@vegaprotocol/types';
import BigNumber from 'bignumber.js';
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment'; import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
import { BREAKPOINT_MD } from '../../config/breakpoints'; import { BREAKPOINT_MD } from '../../config/breakpoints';
import { JsonViewerDialog } from '../dialogs/json-viewer-dialog'; import { JsonViewerDialog } from '../dialogs/json-viewer-dialog';
@@ -31,15 +25,7 @@ type ProposalsTableProps = {
data: ProposalListFieldsFragment[] | null; data: ProposalListFieldsFragment[] | null;
}; };
export const ProposalsTable = ({ data }: ProposalsTableProps) => { export const ProposalsTable = ({ data }: ProposalsTableProps) => {
const { params } = useNetworkParams([
NetworkParams.governance_proposal_market_requiredMajority,
]);
const tokenLink = useLinks(DApp.Governance); const tokenLink = useLinks(DApp.Governance);
const requiredMajorityPercentage = useMemo(() => {
const requiredMajority =
params?.governance_proposal_market_requiredMajority ?? 1;
return new BigNumber(requiredMajority).times(100);
}, [params?.governance_proposal_market_requiredMajority]);
const gridRef = useRef<AgGridReact>(null); const gridRef = useRef<AgGridReact>(null);
useLayoutEffect(() => { useLayoutEffect(() => {
@@ -90,33 +76,6 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
return value ? ProposalStateMapping[value] : '-'; return value ? ProposalStateMapping[value] : '-';
}, },
}, },
{
colId: 'voting',
maxWidth: 100,
hide: window.innerWidth <= BREAKPOINT_MD,
headerName: t('Voting'),
cellRenderer: ({
data,
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
if (data) {
const yesTokens = new BigNumber(data.votes.yes.totalTokens);
const noTokens = new BigNumber(data.votes.no.totalTokens);
const totalTokensVoted = yesTokens.plus(noTokens);
const yesPercentage = totalTokensVoted.isZero()
? new BigNumber(0)
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
return (
<div className="flex h-full items-center justify-center pt-2 uppercase">
<VoteProgress
threshold={requiredMajorityPercentage}
progress={yesPercentage}
/>
</div>
);
}
return '-';
},
},
{ {
colId: 'cDate', colId: 'cDate',
maxWidth: 150, maxWidth: 150,
@@ -184,7 +143,7 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
}, },
}, },
], ],
[requiredMajorityPercentage, tokenLink] [tokenLink]
); );
return ( return (
<> <>
@@ -5,5 +5,10 @@ query ExplorerProposalStatus($id: ID!) {
state state
rejectionReason rejectionReason
} }
... on BatchProposal {
id
state
rejectionReason
}
} }
} }
@@ -8,7 +8,7 @@ export type ExplorerProposalStatusQueryVariables = Types.Exact<{
}>; }>;
export type ExplorerProposalStatusQuery = { __typename?: 'Query', proposal?: { __typename?: 'BatchProposal' } | { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | null }; export type ExplorerProposalStatusQuery = { __typename?: 'Query', proposal?: { __typename?: 'BatchProposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | null };
export const ExplorerProposalStatusDocument = gql` export const ExplorerProposalStatusDocument = gql`
@@ -19,6 +19,11 @@ export const ExplorerProposalStatusDocument = gql`
state state
rejectionReason rejectionReason
} }
... on BatchProposal {
id
state
rejectionReason
}
} }
} }
`; `;
@@ -0,0 +1,257 @@
import { render, screen } from '@testing-library/react';
import { BatchItem } from './batch-item';
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
import type { components } from '../../../../../types/explorer';
type Item = components['schemas']['vegaBatchProposalTermsChange'];
describe('BatchItem', () => {
it('Renders "Unknown proposal type" by default', () => {
const item = {};
render(<BatchItem item={item} />);
expect(screen.getByText('Unknown proposal type')).toBeInTheDocument();
});
it('Renders "Unknown proposal type" for unknown items', () => {
const item = {
newLochNessMonster: {
location: 'Loch Ness',
},
} as unknown as Item;
render(<BatchItem item={item} />);
expect(screen.getByText('Unknown proposal type')).toBeInTheDocument();
});
it('Renders "New spot market"', () => {
const item = {
newSpotMarket: {},
};
render(<BatchItem item={item} />);
expect(screen.getByText('New spot market')).toBeInTheDocument();
});
it('Renders "Cancel transfer"', () => {
const item = {
cancelTransfer: {
changes: {
transferId: 'transfer123',
},
},
};
render(<BatchItem item={item} />);
expect(screen.getByText('Cancel transfer')).toBeInTheDocument();
expect(screen.getByText('transf')).toBeInTheDocument();
});
it('Renders "Cancel transfer" without an id', () => {
const item = {
cancelTransfer: {
changes: {},
},
};
render(<BatchItem item={item} />);
expect(screen.getByText('Cancel transfer')).toBeInTheDocument();
});
it('Renders "New freeform"', () => {
const item = {
newFreeform: {},
};
render(<BatchItem item={item} />);
expect(screen.getByText('New freeform proposal')).toBeInTheDocument();
});
it('Renders "New market"', () => {
const item = {
newMarket: {},
};
render(<BatchItem item={item} />);
expect(screen.getByText('New market')).toBeInTheDocument();
});
it('Renders "New transfer"', () => {
const item = {
newTransfer: {},
};
render(<BatchItem item={item} />);
expect(screen.getByText('New transfer')).toBeInTheDocument();
});
it('Renders "Update asset" with assetId', () => {
const item = {
updateAsset: {
assetId: 'asset123',
},
};
render(
<MemoryRouter>
<MockedProvider>
<BatchItem item={item} />
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('Update asset')).toBeInTheDocument();
expect(screen.getByText('asset123')).toBeInTheDocument();
});
it('Renders "Update asset" even if assetId is not set', () => {
const item = {
updateAsset: {
assetId: undefined,
},
};
render(<BatchItem item={item} />);
expect(screen.getByText('Update asset')).toBeInTheDocument();
});
it('Renders "Update market state" with marketId', () => {
const item = {
updateMarketState: {
changes: {
marketId: 'market123',
},
},
};
render(
<MemoryRouter>
<MockedProvider>
<BatchItem item={item} />
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('Update market state')).toBeInTheDocument();
expect(screen.getByText('market123')).toBeInTheDocument();
});
it('Renders "Update market state" even if marketId is not set', () => {
const item = {
updateMarketState: {
changes: {
marketId: undefined,
},
},
};
render(
<MemoryRouter>
<MockedProvider>
<BatchItem item={item} />
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('Update market state')).toBeInTheDocument();
});
it('Renders "Update network parameter" with parameter', () => {
const item = {
updateNetworkParameter: {
changes: {
key: 'parameter123',
},
},
};
render(
<MockedProvider>
<MemoryRouter>
<BatchItem item={item} />
</MemoryRouter>
</MockedProvider>
);
expect(screen.getByText('Update network parameter')).toBeInTheDocument();
expect(screen.getByText('parameter123')).toBeInTheDocument();
});
it('Renders "Update network parameter" even if parameter is not set', () => {
const item = {
updateNetworkParameter: {
changes: {
key: undefined,
},
},
};
render(<BatchItem item={item} />);
expect(screen.getByText('Update network parameter')).toBeInTheDocument();
});
it('Renders "Update referral program"', () => {
const item = {
updateReferralProgram: {},
};
render(<BatchItem item={item} />);
expect(screen.getByText('Update referral program')).toBeInTheDocument();
});
it('Renders "Update spot market" with marketId', () => {
const item = {
updateSpotMarket: {
marketId: 'market123',
},
};
render(
<MemoryRouter>
<MockedProvider>
<BatchItem item={item} />
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('Update spot market')).toBeInTheDocument();
expect(screen.getByText('market123')).toBeInTheDocument();
});
it('Renders "Update spot market" even if marketId is not set', () => {
const item = {
updateSpotMarket: {
marketId: undefined,
},
};
render(
<MemoryRouter>
<MockedProvider>
<BatchItem item={item} />
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('Update spot market')).toBeInTheDocument();
});
it('Renders "Update market" with marketId', () => {
const item = {
updateMarket: {
marketId: 'market123',
},
};
render(
<MemoryRouter>
<MockedProvider>
<BatchItem item={item} />
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('Update market')).toBeInTheDocument();
expect(screen.getByText('market123')).toBeInTheDocument();
});
it('Renders "Update market" even if marketId is not set', () => {
const item = {
updateMarket: {
marketId: undefined,
},
};
render(
<MemoryRouter>
<MockedProvider>
<BatchItem item={item} />
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('Update market')).toBeInTheDocument();
});
it('Renders "Update volume discount program"', () => {
const item = {
updateVolumeDiscountProgram: {},
};
render(<BatchItem item={item} />);
expect(
screen.getByText('Update volume discount program')
).toBeInTheDocument();
});
});
@@ -0,0 +1,87 @@
import { t } from '@vegaprotocol/i18n';
import { AssetLink, MarketLink, NetworkParameterLink } from '../../../links';
import type { components } from '../../../../../types/explorer';
import Hash from '../../../links/hash';
type Item = components['schemas']['vegaBatchProposalTermsChange'];
export interface BatchItemProps {
item: Item;
}
/**
* Produces a one line summary for an item in a batch proposal. Could
* easily be adapted to summarise individual proposals, but there is no
* place for that yet.
*
* Details (like IDs) should be shown and linked if available, but handled
* if not available. This is adequate as the ProposalSummary component contains
* a JSON viewer for the full proposal.
*/
export const BatchItem = ({ item }: BatchItemProps) => {
if (item.cancelTransfer) {
const transferId = item?.cancelTransfer?.changes?.transferId || false;
return (
<span>
{t('Cancel transfer')}&nbsp;
{transferId && (
<Hash className="ml-1" truncate={true} text={transferId} />
)}
</span>
);
} else if (item.newFreeform) {
return <span>{t('New freeform proposal')}</span>;
} else if (item.newMarket) {
return <span>{t('New market')}</span>;
} else if (item.newSpotMarket) {
return <span>{t('New spot market')}</span>;
} else if (item.newTransfer) {
return <span>{t('New transfer')}</span>;
} else if (item.updateAsset) {
const assetId = item?.updateAsset?.assetId || false;
return (
<span>
{t('Update asset')}
{assetId && <AssetLink className="ml-1" assetId={assetId} />}
</span>
);
} else if (item.updateMarket) {
const marketId = item?.updateMarket?.marketId || false;
return (
<span>
{t('Update market')}{' '}
{marketId && <MarketLink className="ml-1" id={marketId} />}
</span>
);
} else if (item.updateMarketState) {
const marketId = item?.updateMarketState?.changes?.marketId || false;
return (
<span>
{t('Update market state')}
{marketId && <MarketLink className="ml-1" id={marketId} />}
</span>
);
} else if (item.updateNetworkParameter) {
const param = item?.updateNetworkParameter?.changes?.key || false;
return (
<span>
{t('Update network parameter')}
{param && <NetworkParameterLink className="ml-1" parameter={param} />}
</span>
);
} else if (item.updateReferralProgram) {
return <span>{t('Update referral program')}</span>;
} else if (item.updateSpotMarket) {
const marketId = item?.updateSpotMarket?.marketId || '';
return (
<span>
{t('Update spot market')}
<MarketLink className="ml-1" id={marketId} />
</span>
);
} else if (item.updateVolumeDiscountProgram) {
return <span>{t('Update volume discount program')}</span>;
}
return <span>{t('Unknown proposal type')}</span>;
};
@@ -1,6 +1,4 @@
import type { ProposalTerms } from '../tx-proposal';
import { useState } from 'react'; import { useState } from 'react';
import type { components } from '../../../../../types/explorer';
import { JsonViewerDialog } from '../../../dialogs/json-viewer-dialog'; import { JsonViewerDialog } from '../../../dialogs/json-viewer-dialog';
import ProposalLink from '../../../links/proposal-link/proposal-link'; import ProposalLink from '../../../links/proposal-link/proposal-link';
import truncate from 'lodash/truncate'; import truncate from 'lodash/truncate';
@@ -9,7 +7,12 @@ import ReactMarkdown from 'react-markdown';
import { ProposalDate } from './proposal-date'; import { ProposalDate } from './proposal-date';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import type { ProposalTerms } from '../tx-proposal';
import type { components } from '../../../../../types/explorer';
import { BatchItem } from './batch-item';
type Rationale = components['schemas']['vegaProposalRationale']; type Rationale = components['schemas']['vegaProposalRationale'];
type Batch = components['schemas']['v1BatchProposalSubmissionTerms']['changes'];
type ProposalTermsDialog = { type ProposalTermsDialog = {
open: boolean; open: boolean;
@@ -21,6 +24,7 @@ interface ProposalSummaryProps {
id: string; id: string;
rationale?: Rationale; rationale?: Rationale;
terms?: ProposalTerms; terms?: ProposalTerms;
batch?: Batch;
} }
/** /**
@@ -31,6 +35,7 @@ export const ProposalSummary = ({
id, id,
rationale, rationale,
terms, terms,
batch,
}: ProposalSummaryProps) => { }: ProposalSummaryProps) => {
const [dialog, setDialog] = useState<ProposalTermsDialog>({ const [dialog, setDialog] = useState<ProposalTermsDialog>({
open: false, open: false,
@@ -72,6 +77,18 @@ export const ProposalSummary = ({
</ReactMarkdown> </ReactMarkdown>
</div> </div>
)} )}
{batch && (
<section className="pt-2 text-sm leading-tight my-3">
<h2 className="text-lg pb-1">{t('Changes')}</h2>
<ol>
{batch.map((change, index) => (
<li className="ml-4 list-decimal" key={`batch-${index}`}>
<BatchItem item={change} />
</li>
))}
</ol>
</section>
)}
<div className="pt-5"> <div className="pt-5">
<button className="underline max-md:hidden mr-5" onClick={openDialog}> <button className="underline max-md:hidden mr-5" onClick={openDialog}>
{t('View terms')} {t('View terms')}
@@ -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>
);
};
@@ -0,0 +1,75 @@
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { sharedHeaderProps, TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import type { components } from '../../../../types/explorer';
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
import { ProposalSummary } from './proposal/summary';
import Hash from '../../links/hash';
import { t } from '@vegaprotocol/i18n';
export type Proposal = components['schemas']['v1BatchProposalSubmission'];
export type ProposalTerms = components['schemas']['vegaProposalTerms'];
interface TxBatchProposalProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
*
*/
export const TxBatchProposal = ({
txData,
pubKey,
blockData,
}: TxBatchProposalProps) => {
if (!txData || !txData.command.batchProposalSubmission) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
let deterministicId = '';
const proposal: Proposal = txData.command.batchProposalSubmission;
const sig = txData?.signature?.value;
if (sig) {
deterministicId = txSignatureToDeterministicId(sig);
}
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
<TableCell>{t('Batch proposal')}</TableCell>
</TableRow>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
blockData={blockData}
hideTypeRow={true}
/>
<TableRow modifier="bordered">
<TableCell>{t('Batch size')}</TableCell>
<TableCell>
{proposal.terms?.changes?.length || t('No changes')}
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Proposal ID')}</TableCell>
<TableCell>
<Hash text={deterministicId} />
</TableCell>
</TableRow>
</TableWithTbody>
{proposal && (
<ProposalSummary
id={deterministicId}
rationale={proposal?.rationale}
terms={proposal.terms}
batch={proposal.terms?.changes}
/>
)}
</>
);
};
@@ -33,6 +33,8 @@ import { TxDetailsApplyReferralCode } from './tx-apply-referral-code';
import { TxDetailsUpdateReferralSet } from './tx-update-referral-set'; import { TxDetailsUpdateReferralSet } from './tx-update-referral-set';
import { TxDetailsJoinTeam } from './tx-join-team'; import { TxDetailsJoinTeam } from './tx-join-team';
import { TxDetailsUpdateMarginMode } from './tx-update-margin-mode'; import { TxDetailsUpdateMarginMode } from './tx-update-margin-mode';
import { TxBatchProposal } from './tx-batch-proposal';
import { TxDetailsUpdatePartyProfile } from './proposal/tx-update-party-profile';
interface TxDetailsWrapperProps { interface TxDetailsWrapperProps {
txData: BlockExplorerTransactionResult | undefined; txData: BlockExplorerTransactionResult | undefined;
@@ -136,6 +138,10 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
return TxDetailsJoinTeam; return TxDetailsJoinTeam;
case 'Update Margin Mode': case 'Update Margin Mode':
return TxDetailsUpdateMarginMode; return TxDetailsUpdateMarginMode;
case 'Batch Proposal':
return TxBatchProposal;
case 'Update Party Profile':
return TxDetailsUpdatePartyProfile;
default: default:
return TxDetailsGeneric; return TxDetailsGeneric;
} }
@@ -20,6 +20,7 @@ export type FilterOption =
| 'Amend Order' | 'Amend Order'
| 'Apply Referral Code' | 'Apply Referral Code'
| 'Batch Market Instructions' | 'Batch Market Instructions'
| 'Batch Proposal'
| 'Cancel LiquidityProvision Order' | 'Cancel LiquidityProvision Order'
| 'Cancel Order' | 'Cancel Order'
| 'Cancel Transfer Funds' | 'Cancel Transfer Funds'
@@ -43,6 +44,7 @@ export type FilterOption =
| 'Submit Order' | 'Submit Order'
| 'Transfer Funds' | 'Transfer Funds'
| 'Undelegate' | 'Undelegate'
| 'Update Party Profile'
| 'Update Referral Set' | 'Update Referral Set'
| 'Update Margin Mode' | 'Update Margin Mode'
| 'Validator Heartbeat' | 'Validator Heartbeat'
@@ -67,11 +69,18 @@ export const filterOptions: Record<string, FilterOption[]> = {
'Cancel Transfer Funds', 'Cancel Transfer Funds',
'Withdraw', 'Withdraw',
], ],
Governance: ['Delegate', 'Undelegate', 'Vote on Proposal', 'Proposal'], Governance: [
'Batch Proposal',
'Delegate',
'Undelegate',
'Vote on Proposal',
'Proposal',
],
Referrals: [ Referrals: [
'Apply Referral Code', 'Apply Referral Code',
'Create Referral Set', 'Create Referral Set',
'Join Team', 'Join Team',
'Update Party Profile',
'Update Referral Set', 'Update Referral Set',
], ],
'External Data': ['Chain Event', 'Submit Oracle Data'], 'External Data': ['Chain Event', 'Submit Oracle Data'],
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"short_name": "Mainnet Stats", "short_name": "Explorer VEGA",
"name": "Vega Mainnet statistics", "name": "Vega Protocol - Explorer",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"short_name": "Mainnet Stats", "short_name": "Governance VEGA",
"name": "Vega Mainnet statistics", "name": "Vega Protocol - Governance",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Vega Protocol static asseets</title> <title>Vega Protocol static assets</title>
<link rel="stylesheet" href="fonts.css" /> <link rel="stylesheet" href="fonts.css" />
<link rel="icon" type="image/x-icon" href="favicon.ico" /> <link rel="icon" type="image/x-icon" href="favicon.ico" />
</head> </head>
+1 -1
View File
@@ -1,4 +1,4 @@
NX_ETHEREUM_PROVIDER_URL=https://eth-mainnet.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_ETHERSCAN_URL=https://etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.sentry.io/6300613 NX_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.sentry.io/6300613
+9 -7
View File
@@ -1,6 +1,12 @@
{ {
"short_name": "Mainnet Stats", "name": "Vega Protocol - Trading",
"name": "Vega Mainnet statistics", "short_name": "Console",
"description": "Vega Protocol - Trading dApp",
"start_url": "/",
"display": "standalone",
"orientation": "portrait",
"theme_color": "#000000",
"background_color": "#ffffff",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",
@@ -12,9 +18,5 @@
"type": "image/png", "type": "image/png",
"sizes": "192x192" "sizes": "192x192"
} }
], ]
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
} }
@@ -1,5 +1,10 @@
import { useSearchParams } from 'react-router-dom'; import { Link, useSearchParams } from 'react-router-dom';
import { Intent, TradingAnchorButton } from '@vegaprotocol/ui-toolkit'; import {
Intent,
TradingAnchorButton,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet'; import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
@@ -30,8 +35,21 @@ export const CompetitionsCreateTeam = () => {
<LayoutWithGradient> <LayoutWithGradient>
<div className="mx-auto md:w-2/3 max-w-xl"> <div className="mx-auto md:w-2/3 max-w-xl">
<Box className="flex flex-col gap-4"> <Box className="flex flex-col gap-4">
<Link
to={Links.COMPETITIONS()}
className="text-xs inline-flex items-center gap-1 group"
>
<VegaIcon
name={VegaIconNames.CHEVRON_LEFT}
size={12}
className="text-vega-clight-100 dark:text-vega-cdark-100"
/>{' '}
<span className="group-hover:underline">
{t('Go back to the competitions')}
</span>
</Link>
<h1 className="calt text-2xl lg:text-3xl xl:text-4xl"> <h1 className="calt text-2xl lg:text-3xl xl:text-4xl">
{t('Create a team')} {isSolo ? t('Create solo team') : t('Create a team')}
</h1> </h1>
{pubKey && !isReadOnly ? ( {pubKey && !isReadOnly ? (
<CreateTeamFormContainer isSolo={isSolo} /> <CreateTeamFormContainer isSolo={isSolo} />
@@ -71,18 +89,29 @@ const CreateTeamFormContainer = ({ isSolo }: { isSolo: boolean }) => {
if (status === 'confirmed') { if (status === 'confirmed') {
return ( 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> <p className="text-sm">{t('Team creation transaction successful')}</p>
{code && ( {code && (
<> <>
<p className="text-sm"> <dl>
Your team ID is:{' '} <dt className="text-sm">{t('Your team ID:')}</dt>
<span className="font-mono break-all">{code}</span> <dl>
</p> <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 <TradingAnchorButton
href={Links.COMPETITIONS_TEAM(code)} href={Links.COMPETITIONS_TEAM(code)}
intent={Intent.Info} intent={Intent.Info}
size="small" size="small"
data-testid="view-team-button"
> >
{t('View team')} {t('View team')}
</TradingAnchorButton> </TradingAnchorButton>
@@ -125,7 +154,7 @@ const CreateTeamFormContainer = ({ isSolo }: { isSolo: boolean }) => {
onSubmit={onSubmit} onSubmit={onSubmit}
status={status} status={status}
err={err} err={err}
isSolo={isSolo} isCreatingSoloTeam={isSolo}
/> />
); );
}; };
@@ -16,6 +16,8 @@ import { CompetitionsLeaderboard } from '../../components/competitions/competiti
import { useTeams } from '../../lib/hooks/use-teams'; import { useTeams } from '../../lib/hooks/use-teams';
import take from 'lodash/take'; import take from 'lodash/take';
import { usePageTitle } from '../../lib/hooks/use-page-title'; import { usePageTitle } from '../../lib/hooks/use-page-title';
import { TeamCard } from '../../components/competitions/team-card';
import { useMyTeam } from '../../lib/hooks/use-my-team';
export const CompetitionsHome = () => { export const CompetitionsHome = () => {
const t = useT(); const t = useT();
@@ -31,10 +33,14 @@ export const CompetitionsHome = () => {
currentEpoch, currentEpoch,
}); });
const { data: teamsData, loading: teamsLoading } = useTeams({ const { data: teamsData, loading: teamsLoading } = useTeams();
sortByField: ['totalQuantumRewards'],
order: 'desc', const {
}); team: myTeam,
stats: myTeamStats,
games: myTeamGames,
rank: myTeamRank,
} = useMyTeam();
return ( return (
<ErrorBoundary> <ErrorBoundary>
@@ -46,65 +52,83 @@ export const CompetitionsHome = () => {
</p> </p>
</CompetitionsHeader> </CompetitionsHeader>
{/** Get started */} {/** Team card */}
<h2 className="text-2xl mb-6">{t('Get started')}</h2> {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> <CompetitionsActionsContainer>
<CompetitionsAction <CompetitionsAction
variant="A" variant="A"
title={t('Create a team')} title={t('Create a team')}
description={t( description={t(
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.' 'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
)} )}
actionElement={ actionElement={
<TradingButton <TradingButton
intent={Intent.Primary} intent={Intent.Primary}
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
navigate(Links.COMPETITIONS_CREATE_TEAM()); navigate(Links.COMPETITIONS_CREATE_TEAM());
}} }}
> data-testId="create-public-team-button"
{t('Create a public team')} >
</TradingButton> {t('Create a public team')}
} </TradingButton>
/> }
<CompetitionsAction />
variant="B" <CompetitionsAction
title={t('Solo team / lone wolf')} variant="B"
description={t( title={t('Solo team / lone wolf')}
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.' description={t(
)} 'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
actionElement={ )}
<TradingButton actionElement={
intent={Intent.Primary} <TradingButton
onClick={(e) => { intent={Intent.Primary}
e.preventDefault(); onClick={(e) => {
navigate(Links.COMPETITIONS_CREATE_TEAM_SOLO()); e.preventDefault();
}} navigate(Links.COMPETITIONS_CREATE_TEAM_SOLO());
> }}
{t('Create a private team')} >
</TradingButton> {t('Create a private team')}
} </TradingButton>
/> }
<CompetitionsAction />
variant="C" <CompetitionsAction
title={t('Join a team')} variant="C"
description={t( title={t('Join a team')}
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.' description={t(
)} 'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
actionElement={ )}
<TradingButton actionElement={
intent={Intent.Primary} <TradingButton
onClick={(e) => { intent={Intent.Primary}
e.preventDefault(); onClick={(e) => {
navigate(Links.COMPETITIONS_TEAMS()); e.preventDefault();
}} navigate(Links.COMPETITIONS_TEAMS());
> }}
{t('Choose a team')} >
</TradingButton> {t('Choose a team')}
} </TradingButton>
/> }
</CompetitionsActionsContainer> />
</CompetitionsActionsContainer>
</>
)}
{/** List of available games */} {/** List of available games */}
<h2 className="text-2xl mb-6">{t('Games')}</h2> <h2 className="text-2xl mb-6">{t('Games')}</h2>
@@ -38,12 +38,11 @@ export const CompetitionsTeam = () => {
const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => { const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => {
const t = useT(); const t = useT();
const { pubKey } = useVegaWallet(); const { pubKey } = useVegaWallet();
const { team, partyTeam, stats, members, games, loading, refetch } = useTeam( const { data, team, partyTeam, stats, members, games, loading, refetch } =
teamId, useTeam(teamId, pubKey || undefined);
pubKey || undefined
);
if (loading) { // only show spinner on first load so when users join teams its smoother
if (!data && loading) {
return ( return (
<Splash> <Splash>
<Loader /> <Loader />
@@ -100,8 +99,10 @@ const TeamPage = ({
> >
{team.name} {team.name}
</h1> </h1>
<JoinTeam team={team} partyTeam={partyTeam} refetch={refetch} /> <div className="flex gap-2">
<UpdateTeamButton team={team} /> <JoinTeam team={team} partyTeam={partyTeam} refetch={refetch} />
<UpdateTeamButton team={team} />
</div>
</div> </div>
</header> </header>
<TeamStats stats={stats} members={members} games={games} /> <TeamStats stats={stats} members={members} games={games} />
@@ -184,7 +185,10 @@ const Members = ({ members }: { members?: Member[] }) => {
const data = orderBy( const data = orderBy(
members.map((m) => ({ 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)), joinedAt: getDateTimeFormat().format(new Date(m.joinedAt)),
joinedAtEpoch: Number(m.joinedAtEpoch), joinedAtEpoch: Number(m.joinedAtEpoch),
})), })),
@@ -195,7 +199,10 @@ const Members = ({ members }: { members?: Member[] }) => {
return ( return (
<Table <Table
columns={[ 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', name: 'joinedAt',
displayName: t('Joined at'), 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 linkCreator = useLinks(DApp.Explorer);
const link = linkCreator(EXPLORER_PARTIES.replace(':id', pubkey)); const link = linkCreator(EXPLORER_PARTIES.replace(':id', pubkey));
return ( return (
<Link to={link} target="_blank" className="underline underline-offset-4"> <>
{truncateMiddle(pubkey)} <Link to={link} target="_blank" className="underline underline-offset-4">
</Link> {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')]); usePageTitle([t('Competitions'), t('Teams')]);
const { data: teamsData, loading: teamsLoading } = useTeams({ const { data: teamsData, loading: teamsLoading } = useTeams();
sortByField: ['totalQuantumRewards'],
order: 'desc',
});
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const [filter, setFilter] = useState<string | null | undefined>(undefined); const [filter, setFilter] = useState<string | null | undefined>(undefined);
@@ -3,7 +3,14 @@ import { usePageTitle } from '../../lib/hooks/use-page-title';
import { Box } from '../../components/competitions/box'; import { Box } from '../../components/competitions/box';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet'; import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { Loader, Splash } from '@vegaprotocol/ui-toolkit'; import {
Intent,
Loader,
Splash,
TradingAnchorButton,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { RainbowButton } from '../../components/rainbow-button'; import { RainbowButton } from '../../components/rainbow-button';
import { Link, Navigate, useParams } from 'react-router-dom'; import { Link, Navigate, useParams } from 'react-router-dom';
import { Links } from '../../lib/links'; import { Links } from '../../lib/links';
@@ -11,6 +18,7 @@ import { useReferralSetTransaction } from '../../lib/hooks/use-referral-set-tran
import { type FormFields, TeamForm, TransactionType } from './team-form'; import { type FormFields, TeamForm, TransactionType } from './team-form';
import { useTeam } from '../../lib/hooks/use-team'; import { useTeam } from '../../lib/hooks/use-team';
import { LayoutWithGradient } from '../../components/layouts-inner'; import { LayoutWithGradient } from '../../components/layouts-inner';
import { useEffect, useState } from 'react';
export const CompetitionsUpdateTeam = () => { export const CompetitionsUpdateTeam = () => {
const t = useT(); const t = useT();
@@ -29,6 +37,19 @@ export const CompetitionsUpdateTeam = () => {
<LayoutWithGradient> <LayoutWithGradient>
<div className="mx-auto md:w-2/3 max-w-xl"> <div className="mx-auto md:w-2/3 max-w-xl">
<Box className="flex flex-col gap-4"> <Box className="flex flex-col gap-4">
<Link
to={Links.COMPETITIONS_TEAM(teamId)}
className="text-xs inline-flex items-center gap-1 group"
>
<VegaIcon
name={VegaIconNames.CHEVRON_LEFT}
size={12}
className="text-vega-clight-100 dark:text-vega-cdark-100"
/>{' '}
<span className="group-hover:underline">
{t('Go back to the team profile')}
</span>
</Link>
<h1 className="calt text-2xl lg:text-3xl xl:text-5xl"> <h1 className="calt text-2xl lg:text-3xl xl:text-5xl">
{t('Update a team')} {t('Update a team')}
</h1> </h1>
@@ -57,7 +78,8 @@ const UpdateTeamFormContainer = ({
pubKey: string; pubKey: string;
}) => { }) => {
const t = useT(); 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({ const { err, status, onSubmit } = useReferralSetTransaction({
onSuccess: () => { 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" />; return <Loader size="small" />;
} }
if (error) { if (error) {
@@ -84,6 +114,33 @@ const UpdateTeamFormContainer = ({
return <Navigate to={Links.COMPETITIONS_TEAM(teamId)} />; return <Navigate to={Links.COMPETITIONS_TEAM(teamId)} />;
} }
if (status === 'confirmed') {
return (
<div
className="flex flex-col items-start gap-2"
data-testid="team-creation-success-message"
>
<p className="text-sm">
<VegaIcon
name={VegaIconNames.TICK}
size={18}
className="text-vega-green-500"
/>{' '}
{t('Changes successfully saved to your team.')}
</p>
<TradingAnchorButton
href={Links.COMPETITIONS_TEAM(teamId)}
intent={Intent.Info}
size="small"
data-testid="view-team-button"
>
{t('View team')}
</TradingAnchorButton>
</div>
);
}
const defaultValues: FormFields = { const defaultValues: FormFields = {
id: team.teamId, id: team.teamId,
name: team.name, name: team.name,
@@ -98,7 +155,7 @@ const UpdateTeamFormContainer = ({
type={TransactionType.UpdateReferralSet} type={TransactionType.UpdateReferralSet}
status={status} status={status}
err={err} err={err}
isSolo={team.closed} isCreatingSoloTeam={team.closed}
onSubmit={onSubmit} onSubmit={onSubmit}
defaultValues={defaultValues} defaultValues={defaultValues}
/> />
@@ -6,11 +6,7 @@ import {
VegaIcon, VegaIcon,
VegaIconNames, VegaIconNames,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { import { useSimpleTransaction, useVegaWallet } from '@vegaprotocol/wallet';
useSimpleTransaction,
useVegaWallet,
type Status,
} from '@vegaprotocol/wallet';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { type Team } from '../../lib/hooks/use-team'; import { type Team } from '../../lib/hooks/use-team';
import { useState } from 'react'; import { useState } from 'react';
@@ -27,19 +23,8 @@ export const JoinTeam = ({
refetch: () => void; refetch: () => void;
}) => { }) => {
const { pubKey, isReadOnly } = useVegaWallet(); const { pubKey, isReadOnly } = useVegaWallet();
const { send, status } = useSimpleTransaction({
onSuccess: refetch,
});
const [confirmDialog, setConfirmDialog] = useState<JoinType>(); const [confirmDialog, setConfirmDialog] = useState<JoinType>();
const joinTeam = () => {
send({
joinTeam: {
id: team.teamId,
},
});
};
return ( return (
<> <>
<JoinButton <JoinButton
@@ -56,11 +41,10 @@ export const JoinTeam = ({
{confirmDialog !== undefined && ( {confirmDialog !== undefined && (
<DialogContent <DialogContent
type={confirmDialog} type={confirmDialog}
status={status}
team={team} team={team}
partyTeam={partyTeam} partyTeam={partyTeam}
onConfirm={joinTeam}
onCancel={() => setConfirmDialog(undefined)} onCancel={() => setConfirmDialog(undefined)}
refetch={refetch}
/> />
)} )}
</Dialog> </Dialog>
@@ -110,7 +94,7 @@ export const JoinButton = ({
// Not creator of the team, but still can't switch because // Not creator of the team, but still can't switch because
// creators cannot leave their own team // creators cannot leave their own team
return ( return (
<Tooltip description="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}> <Button intent={Intent.Primary} disabled={true}>
{t('Switch team')}{' '} {t('Switch team')}{' '}
</Button> </Button>
@@ -121,7 +105,11 @@ export const JoinButton = ({
// Party is in a team, but not this one // Party is in a team, but not this one
else if (partyTeam && partyTeam.teamId !== team.teamId) { else if (partyTeam && partyTeam.teamId !== team.teamId) {
return ( return (
<Button onClick={() => onJoin('switch')} intent={Intent.Primary}> <Button
onClick={() => onJoin('switch')}
intent={Intent.Primary}
data-testid="switch-team-button"
>
{t('Switch team')}{' '} {t('Switch team')}{' '}
</Button> </Button>
); );
@@ -149,21 +137,39 @@ export const JoinButton = ({
const DialogContent = ({ const DialogContent = ({
type, type,
status,
team, team,
partyTeam, partyTeam,
onConfirm,
onCancel, onCancel,
refetch,
}: { }: {
type: JoinType; type: JoinType;
status: Status;
team: Team; team: Team;
partyTeam?: Team; partyTeam?: Team;
onConfirm: () => void;
onCancel: () => void; onCancel: () => void;
refetch: () => void;
}) => { }) => {
const t = useT(); const t = useT();
const { send, status, error } = useSimpleTransaction({
onSuccess: refetch,
});
const joinTeam = () => {
send({
joinTeam: {
id: team.teamId,
},
});
};
if (error) {
return (
<p className="text-vega-red break-words first-letter:capitalize">
{error}
</p>
);
}
if (status === 'requested') { if (status === 'requested') {
return <p>{t('Confirm in wallet...')}</p>; return <p>{t('Confirm in wallet...')}</p>;
} }
@@ -213,7 +219,11 @@ const DialogContent = ({
</> </>
)} )}
<div className="flex justify-between gap-2"> <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')} {t('Confirm')}
</Button> </Button>
<Button onClick={onCancel} intent={Intent.Danger}> <Button onClick={onCancel} intent={Intent.Danger}>
@@ -6,6 +6,8 @@ import {
TextArea, TextArea,
TradingButton, TradingButton,
Intent, Intent,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { URL_REGEX, isValidVegaPublicKey } from '@vegaprotocol/utils'; import { URL_REGEX, isValidVegaPublicKey } from '@vegaprotocol/utils';
@@ -17,6 +19,8 @@ import type {
UpdateReferralSet, UpdateReferralSet,
Status, Status,
} from '@vegaprotocol/wallet'; } from '@vegaprotocol/wallet';
import classNames from 'classnames';
import { useLayoutEffect, useState } from 'react';
export type FormFields = { export type FormFields = {
id: string; id: string;
@@ -28,8 +32,8 @@ export type FormFields = {
}; };
export enum TransactionType { export enum TransactionType {
CreateReferralSet, CreateReferralSet = 'CreateReferralSet',
UpdateReferralSet, UpdateReferralSet = 'UpdateReferralSet',
} }
const prepareTransaction = ( const prepareTransaction = (
@@ -75,14 +79,14 @@ export const TeamForm = ({
type, type,
status, status,
err, err,
isSolo, isCreatingSoloTeam,
onSubmit, onSubmit,
defaultValues, defaultValues,
}: { }: {
type: TransactionType; type: TransactionType;
status: ReturnType<typeof useReferralSetTransaction>['status']; status: ReturnType<typeof useReferralSetTransaction>['status'];
err: ReturnType<typeof useReferralSetTransaction>['err']; err: ReturnType<typeof useReferralSetTransaction>['err'];
isSolo: boolean; isCreatingSoloTeam: boolean;
onSubmit: ReturnType<typeof useReferralSetTransaction>['onSubmit']; onSubmit: ReturnType<typeof useReferralSetTransaction>['onSubmit'];
defaultValues?: FormFields; defaultValues?: FormFields;
}) => { }) => {
@@ -96,7 +100,7 @@ export const TeamForm = ({
formState: { errors }, formState: { errors },
} = useForm<FormFields>({ } = useForm<FormFields>({
defaultValues: { defaultValues: {
private: isSolo, private: isCreatingSoloTeam,
...defaultValues, ...defaultValues,
}, },
}); });
@@ -109,16 +113,14 @@ export const TeamForm = ({
return ( return (
<form onSubmit={handleSubmit(sendTransaction)}> <form onSubmit={handleSubmit(sendTransaction)}>
<input <input type="hidden" {...register('id')} />
type="hidden"
{...register('id', {
disabled: true,
})}
/>
<TradingFormGroup label={t('Team name')} labelFor="name"> <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 && ( {errors.name?.message && (
<TradingInputError forInput="name"> <TradingInputError forInput="name" data-testid="team-name-error">
{errors.name.message} {errors.name.message}
</TradingInputError> </TradingInputError>
)} )}
@@ -134,9 +136,10 @@ export const TeamForm = ({
{...register('url', { {...register('url', {
pattern: { value: URL_REGEX, message: t('Invalid URL') }, pattern: { value: URL_REGEX, message: t('Invalid URL') },
})} })}
data-testid="team-url-input"
/> />
{errors.url?.message && ( {errors.url?.message && (
<TradingInputError forInput="url"> <TradingInputError forInput="url" data-testid="team-url-error">
{errors.url.message} {errors.url.message}
</TradingInputError> </TradingInputError>
)} )}
@@ -153,66 +156,86 @@ export const TeamForm = ({
message: t('Invalid image URL'), message: t('Invalid image URL'),
}, },
})} })}
data-testid="avatar-url-input"
/> />
{errors.avatarUrl?.message && ( {errors.avatarUrl?.message && (
<TradingInputError forInput="avatarUrl"> <TradingInputError
forInput="avatarUrl"
data-testid="avatar-url-error"
>
{errors.avatarUrl.message} {errors.avatarUrl.message}
</TradingInputError> </TradingInputError>
)} )}
</TradingFormGroup> </TradingFormGroup>
<TradingFormGroup {
label={t('Make team private')} // allow changing to private/public if editing, but don't show these options if making a solo team
labelFor="private" (type === TransactionType.UpdateReferralSet || !isCreatingSoloTeam) && (
hideLabel={true} <>
> <TradingFormGroup
<Controller label={t('Make team private')}
name="private" labelFor="private"
control={control} hideLabel={true}
render={({ field }) => { >
return ( <Controller
<TradingCheckbox name="private"
label={t('Make team private')} control={control}
checked={field.value} render={({ field }) => {
onCheckedChange={(value) => { return (
field.onChange(value); <TradingCheckbox
label={t('Make team private')}
checked={field.value}
onCheckedChange={(value) => {
field.onChange(value);
}}
data-testid="team-private-checkbox"
/>
);
}} }}
disabled={isSolo}
/> />
); </TradingFormGroup>
}} {isPrivate && (
/> <TradingFormGroup
</TradingFormGroup> label={t('Public key allow list')}
{isPrivate && ( labelFor="allowList"
<TradingFormGroup labelDescription={t(
label={t('Public key allow list')} 'Use a comma separated list to allow only specific public keys to join the team'
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'),
<TextArea validate: {
{...register('allowList', { allowList: (value) => {
required: t('Required'), const publicKeys = parseAllowListText(value);
disabled: isSolo, if (
validate: { publicKeys.every((pk) => isValidVegaPublicKey(pk))
allowList: (value) => { ) {
const publicKeys = parseAllowListText(value); return true;
if (publicKeys.every((pk) => isValidVegaPublicKey(pk))) { }
return true; return t('Invalid public key found in allow list');
} },
return t('Invalid public key found in allow list'); },
}, })}
}, data-testid="team-allow-list-textarea"
})} />
/> {errors.allowList?.message && (
{errors.allowList?.message && ( <TradingInputError
<TradingInputError forInput="avatarUrl"> forInput="avatarUrl"
{errors.allowList.message} data-testid="team-allow-list-error"
</TradingInputError> >
)} {errors.allowList.message}
</TradingFormGroup> </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} /> <SubmitButton type={type} status={status} />
</form> </form>
); );
@@ -233,20 +256,61 @@ const SubmitButton = ({
text = t('Update'); text = t('Update');
} }
let confirmedText = t('Created');
if (type === TransactionType.UpdateReferralSet) {
confirmedText = t('Updated');
}
if (status === 'requested') { if (status === 'requested') {
text = t('Confirm in wallet...'); text = t('Confirm in wallet...');
} else if (status === 'pending') { } else if (status === 'pending') {
text = t('Confirming transaction...'); text = t('Confirming transaction...');
} }
const [showConfirmed, setShowConfirmed] = useState<boolean>(false);
useLayoutEffect(() => {
let to: ReturnType<typeof setTimeout>;
if (status === 'confirmed' && !showConfirmed) {
to = setTimeout(() => {
setShowConfirmed(true);
}, 100);
}
return () => {
clearTimeout(to);
};
}, [showConfirmed, status]);
const confirmed = (
<span
className={classNames('text-sm transition-opacity opacity-0', {
'opacity-100': showConfirmed,
})}
>
<VegaIcon
name={VegaIconNames.TICK}
size={18}
className="text-vega-green-500"
/>{' '}
{confirmedText}
</span>
);
return ( return (
<TradingButton type="submit" intent={Intent.Info} disabled={disabled}> <div className="flex gap-2 items-baseline">
{text} <TradingButton
</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 return str
.split(',') .split(',')
.map((v) => v.trim()) .map((v) => v.trim())
@@ -1,18 +1,30 @@
import { useVegaWallet } from '@vegaprotocol/wallet';
import { type Team } from '../../lib/hooks/use-team'; 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 { Intent, TradingAnchorButton } from '@vegaprotocol/ui-toolkit';
import { Links } from '../../lib/links'; import { Links } from '../../lib/links';
import { useT } from '../../lib/use-t';
export const UpdateTeamButton = ({ 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(); const { pubKey, isReadOnly } = useVegaWallet();
if (pubKey && !isReadOnly && pubKey === team.referrer) { if (pubKey && !isReadOnly && pubKey === team.referrer) {
return ( return (
<TradingAnchorButton <TradingAnchorButton
size={size}
data-testid="update-team-button" data-testid="update-team-button"
href={Links.COMPETITIONS_UPDATE_TEAM(team.teamId)} href={Links.COMPETITIONS_UPDATE_TEAM(team.teamId)}
intent={Intent.Info} intent={Intent.Info}
/> >
{t('Update team')}
</TradingAnchorButton>
); );
} }
+6 -13
View File
@@ -1,27 +1,20 @@
import { useEffect } from 'react'; import { TinyScroll } from '@vegaprotocol/ui-toolkit';
import { titlefy } from '@vegaprotocol/utils';
import { ErrorBoundary } from '../../components/error-boundary'; import { ErrorBoundary } from '../../components/error-boundary';
import { FeesContainer } from '../../components/fees-container'; import { FeesContainer } from '../../components/fees-container';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { usePageTitleStore } from '../../stores'; import { usePageTitle } from '../../lib/hooks/use-page-title';
export const Fees = () => { export const Fees = () => {
const t = useT(); const t = useT();
const title = t('Fees'); const title = t('Fees');
const { updateTitle } = usePageTitleStore((store) => ({ usePageTitle(title);
updateTitle: store.updateTitle,
}));
useEffect(() => {
updateTitle(titlefy([title]));
}, [updateTitle, title]);
return ( return (
<ErrorBoundary feature="fees"> <ErrorBoundary feature="fees">
<div className="container p-4 mx-auto"> <TinyScroll className="p-4 max-h-full overflow-auto">
<h1 className="px-4 pb-4 text-2xl">{title}</h1> <h1 className="md:px-4 pb-4 text-2xl">{title}</h1>
<FeesContainer /> <FeesContainer />
</div> </TinyScroll>
</ErrorBoundary> </ErrorBoundary>
); );
}; };
@@ -19,6 +19,7 @@ import {
useFundingRate, useFundingRate,
useMarketTradingMode, useMarketTradingMode,
useExternalTwap, useExternalTwap,
getQuoteName,
} from '@vegaprotocol/markets'; } from '@vegaprotocol/markets';
import { MarketState as State } from '@vegaprotocol/types'; import { MarketState as State } from '@vegaprotocol/types';
import { HeaderStat } from '../../components/header'; import { HeaderStat } from '../../components/header';
@@ -41,6 +42,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore(); const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
const asset = getAsset(market); const asset = getAsset(market);
const quoteUnit = getQuoteName(market);
return ( return (
<> <>
@@ -54,12 +56,15 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
<Last24hPriceChange <Last24hPriceChange
marketId={market.id} marketId={market.id}
decimalPlaces={market.decimalPlaces} decimalPlaces={market.decimalPlaces}
fallback={<span>-</span>}
/> />
</HeaderStat> </HeaderStat>
<HeaderStat heading={t('Volume (24h)')} testId="market-volume"> <HeaderStat heading={t('Volume (24h)')} testId="market-volume">
<Last24hVolume <Last24hVolume
marketId={market.id} marketId={market.id}
positionDecimalPlaces={market.positionDecimalPlaces} positionDecimalPlaces={market.positionDecimalPlaces}
marketDecimals={market.decimalPlaces}
quoteUnit={quoteUnit}
/> />
</HeaderStat> </HeaderStat>
<HeaderStatMarketTradingMode <HeaderStatMarketTradingMode
@@ -108,7 +113,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
heading={`${t('Funding Rate')} / ${t('Countdown')}`} heading={`${t('Funding Rate')} / ${t('Countdown')}`}
testId="market-funding" testId="market-funding"
> >
<div className="flex justify-between gap-2"> <div className="flex gap-2">
<FundingRate marketId={market.id} /> <FundingRate marketId={market.id} />
<FundingCountdown marketId={market.id} /> <FundingCountdown marketId={market.id} />
</div> </div>
@@ -3,7 +3,6 @@ import { type Market } from '@vegaprotocol/markets';
// TODO: handle oracle banner // TODO: handle oracle banner
// import { OracleBanner } from '@vegaprotocol/markets'; // import { OracleBanner } from '@vegaprotocol/markets';
import { useState } from 'react'; import { useState } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import classNames from 'classnames'; import classNames from 'classnames';
import { import {
Popover, Popover,
@@ -12,21 +11,21 @@ import {
VegaIconNames, VegaIconNames,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { MarketBanner } from '../../components/market-banner';
import { ErrorBoundary } from '../../components/error-boundary'; import { ErrorBoundary } from '../../components/error-boundary';
import { type TradingView } from './trade-views'; import { type TradingView } from './trade-views';
import { TradingViews } from './trade-views'; import { TradingViews } from './trade-views';
interface TradePanelsProps { interface TradePanelsProps {
market: Market; market: Market;
pinnedAsset?: PinnedAsset; pinnedAsset?: PinnedAsset;
} }
export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => { export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
const [view, setView] = useState<TradingView>('chart'); const [topView, setTopView] = useState<TradingView>('chart');
const viewCfg = TradingViews[view]; 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; const Component = TradingViews[view].component;
if (!Component) { if (!Component) {
@@ -39,12 +38,13 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
// so watch out for clashes in props // so watch out for clashes in props
return ( return (
<ErrorBoundary feature={view}> <ErrorBoundary feature={view}>
<Component marketId={market?.id} pinnedAsset={pinnedAsset} />; <Component marketId={market?.id} pinnedAsset={pinnedAsset} />
</ErrorBoundary> </ErrorBoundary>
); );
}; };
const renderMenu = () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any
const renderMenu = (viewCfg: any) => {
if ('menu' in viewCfg || 'settings' in viewCfg) { if ('menu' in viewCfg || 'settings' in viewCfg) {
return ( return (
<div className="flex items-center justify-end gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default"> <div className="flex items-center justify-end gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default">
@@ -69,55 +69,80 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
}; };
return ( return (
<div className="h-full grid grid-rows-[min-content_min-content_1fr_min-content]"> <div className="h-full flex flex-col lg:grid grid-rows-[min-content_min-content_1fr_min-content]">
<div> <div className="flex flex-col w-full overflow-hidden">
<MarketBanner market={market} /> <div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
</div> {['chart', 'orderbook', 'trades', 'liquidity', 'fundingPayments']
<div>{renderMenu()}</div> // filter to control available views for the current market
<div className="h-full relative"> // e.g. only perpetuals should get the funding views
<AutoSizer> .filter((_key) => {
{({ width, height }) => ( const key = _key as TradingView;
<div style={{ width, height }} className="overflow-auto"> const perpOnlyViews = ['funding', 'fundingPayments'];
{renderView()}
</div> if (
)} market?.tradableInstrument.instrument.product.__typename ===
</AutoSizer> 'Perpetual'
</div> ) {
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default"> return true;
{Object.keys(TradingViews) }
// filter to control available views for the current market
// eg only perps should get the funding views if (perpOnlyViews.includes(key)) {
.filter((_key) => { return false;
const key = _key as TradingView; }
const perpOnlyViews = ['funding', 'fundingPayments'];
if (
market?.tradableInstrument.instrument.product.__typename ===
'Perpetual'
) {
return true; 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)) { <div className="flex flex-col w-full grow">
return false; <div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
} {[
'positions',
return true; 'activeOrders',
}) 'closedOrders',
.map((_key) => { 'rejectedOrders',
'orders',
'stopOrders',
'collateral',
'fills',
].map((_key) => {
const key = _key as TradingView; const key = _key as TradingView;
const isActive = view === key; const isActive = bottomView === key;
return ( return (
<ViewButton <ViewButton
key={key} key={key}
view={key} view={key}
isActive={isActive} isActive={isActive}
onClick={() => { 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>
</div> </div>
); );
@@ -157,7 +182,7 @@ const useViewLabel = (view: TradingView) => {
depth: t('Depth'), depth: t('Depth'),
liquidity: t('Liquidity'), liquidity: t('Liquidity'),
funding: t('Funding'), funding: t('Funding'),
fundingPayments: t('Funding Payments'), fundingPayments: t('Funding'),
orderbook: t('Orderbook'), orderbook: t('Orderbook'),
trades: t('Trades'), trades: t('Trades'),
positions: t('Positions'), positions: t('Positions'),
@@ -5,7 +5,7 @@ import {
useDataGridEvents, useDataGridEvents,
} from '@vegaprotocol/datagrid'; } from '@vegaprotocol/datagrid';
import type { MarketMaybeWithData } from '@vegaprotocol/markets'; import type { MarketMaybeWithData } from '@vegaprotocol/markets';
import { useColumnDefs } from './use-column-defs'; import { useMarketsColumnDefs } from './use-column-defs';
import type { DataGridStore } from '../../stores/datagrid-store-slice'; import type { DataGridStore } from '../../stores/datagrid-store-slice';
import { type StateCreator, create } from 'zustand'; import { type StateCreator, create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
@@ -50,7 +50,7 @@ export const useMarketsStore = create<DataGridSlice>()(
); );
export const MarketListTable = (props: Props) => { export const MarketListTable = (props: Props) => {
const columnDefs = useColumnDefs(); const columnDefs = useMarketsColumnDefs();
const gridStore = useMarketsStore((store) => store.gridStore); const gridStore = useMarketsStore((store) => store.gridStore);
const updateGridStore = useMarketsStore((store) => store.updateGridStore); const updateGridStore = useMarketsStore((store) => store.updateGridStore);
@@ -1,5 +1,3 @@
import React, { useEffect } from 'react';
import { titlefy } from '@vegaprotocol/utils';
import { import {
LocalStoragePersistTabs as Tabs, LocalStoragePersistTabs as Tabs,
Tab, Tab,
@@ -7,7 +5,6 @@ import {
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { OpenMarkets } from './open-markets'; import { OpenMarkets } from './open-markets';
import { Proposed } from './proposed'; import { Proposed } from './proposed';
import { usePageTitleStore } from '../../stores';
import { Closed } from './closed'; import { Closed } from './closed';
import { import {
DApp, DApp,
@@ -17,19 +14,14 @@ import {
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { ErrorBoundary } from '../../components/error-boundary'; import { ErrorBoundary } from '../../components/error-boundary';
import { MarketsSettings } from './markets-settings'; import { MarketsSettings } from './markets-settings';
import { usePageTitle } from '../../lib/hooks/use-page-title';
export const MarketsPage = () => { export const MarketsPage = () => {
const t = useT(); const t = useT();
const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
const governanceLink = useLinks(DApp.Governance); const governanceLink = useLinks(DApp.Governance);
const externalLink = governanceLink(TOKEN_NEW_MARKET_PROPOSAL); const externalLink = governanceLink(TOKEN_NEW_MARKET_PROPOSAL);
useEffect(() => { usePageTitle(t('Markets'));
updateTitle(titlefy([t('Markets')]));
}, [updateTitle, t]);
return ( return (
<div className="h-full pt-0.5 pb-3 px-1.5"> <div className="h-full pt-0.5 pb-3 px-1.5">
@@ -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'; } from '@vegaprotocol/datagrid';
import { COL_DEFS, SetFilter } from '@vegaprotocol/datagrid'; import { COL_DEFS, SetFilter } from '@vegaprotocol/datagrid';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import { addDecimalsFormatNumber, toBigNum } from '@vegaprotocol/utils'; import {
addDecimalsFormatNumber,
formatNumber,
toBigNum,
} from '@vegaprotocol/utils';
import { ButtonLink, Tooltip } from '@vegaprotocol/ui-toolkit'; import { ButtonLink, Tooltip } from '@vegaprotocol/ui-toolkit';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets'; import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import type { import type {
MarketFieldsFragment,
MarketMaybeWithData, MarketMaybeWithData,
MarketMaybeWithDataAndCandles, MarketMaybeWithDataAndCandles,
} from '@vegaprotocol/markets'; } from '@vegaprotocol/markets';
import { MarketActionsDropdown } from './market-table-actions'; import { MarketActionsDropdown } from './market-table-actions';
import { calcCandleVolume, getAsset } from '@vegaprotocol/markets'; import {
calcCandleVolume,
calcCandleVolumePrice,
getAsset,
getQuoteName,
} from '@vegaprotocol/markets';
import { MarketCodeCell } from './market-code-cell'; import { MarketCodeCell } from './market-code-cell';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
const { MarketTradingMode, AuctionTrigger } = Schema; const { MarketTradingMode, AuctionTrigger } = Schema;
export const useColumnDefs = () => { export const useMarketsColumnDefs = () => {
const t = useT(); const t = useT();
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore(); const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
return useMemo<ColDef[]>( return useMemo<ColDef[]>(
@@ -158,11 +168,25 @@ export const useColumnDefs = () => {
}: ValueFormatterParams<MarketMaybeWithDataAndCandles, 'candles'>) => { }: ValueFormatterParams<MarketMaybeWithDataAndCandles, 'candles'>) => {
const candles = data?.candles; const candles = data?.candles;
const vol = candles ? calcCandleVolume(candles) : '0'; const vol = candles ? calcCandleVolume(candles) : '0';
const quoteName = getQuoteName(data as MarketFieldsFragment);
const volPrice =
candles &&
calcCandleVolumePrice(
candles,
data.decimalPlaces,
data.positionDecimalPlaces
);
const volume = const volume =
data && vol && vol !== '0' data && vol && vol !== '0'
? addDecimalsFormatNumber(vol, data.positionDecimalPlaces) ? addDecimalsFormatNumber(vol, data.positionDecimalPlaces)
: '0.00'; : '0.00';
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 { SidebarButton, ViewType } from '../../components/sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id'; import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { MobileButton } from '../markets/mobile-buttons';
export const PortfolioSidebar = () => { export const PortfolioSidebar = () => {
const t = useT(); const t = useT();
@@ -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,10 +1,8 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { LayoutPriority } from 'allotment'; import { LayoutPriority } from 'allotment';
import { titlefy } from '@vegaprotocol/utils';
import { useIncompleteWithdrawals } from '@vegaprotocol/withdraws'; import { useIncompleteWithdrawals } from '@vegaprotocol/withdraws';
import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit'; import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit';
import { usePageTitleStore } from '../../stores';
import { import {
AccountsContainer, AccountsContainer,
AccountsSettings, AccountsSettings,
@@ -41,6 +39,7 @@ import { WithdrawalsMenu } from '../../components/withdrawals-menu';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id'; import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { ErrorBoundary } from '../../components/error-boundary'; import { ErrorBoundary } from '../../components/error-boundary';
import { usePageTitle } from '../../lib/hooks/use-page-title';
const WithdrawalsIndicator = () => { const WithdrawalsIndicator = () => {
const { ready } = useIncompleteWithdrawals(); const { ready } = useIncompleteWithdrawals();
@@ -69,14 +68,7 @@ const SidebarViewInitializer = () => {
export const Portfolio = () => { export const Portfolio = () => {
const t = useT(); const t = useT();
usePageTitle(t('Portfolio'));
const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
useEffect(() => {
updateTitle(titlefy([t('Portfolio')]));
}, [updateTitle, t]);
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'portfolio' }); const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'portfolio' });
const wrapperClasses = 'p-0.5 h-full max-h-full flex flex-col'; const wrapperClasses = 'p-0.5 h-full max-h-full flex flex-col';
@@ -1,4 +1,4 @@
import { getNumberFormat } from '@vegaprotocol/utils'; import { formatNumber } from '@vegaprotocol/utils';
import sortBy from 'lodash/sortBy'; import sortBy from 'lodash/sortBy';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { useReferralProgramQuery } from './__generated__/CurrentReferralProgram'; import { useReferralProgramQuery } from './__generated__/CurrentReferralProgram';
@@ -107,9 +107,7 @@ export const useReferralProgram = () => {
discountFactor: Number(t.referralDiscountFactor), discountFactor: Number(t.referralDiscountFactor),
discount: BigNumber(t.referralDiscountFactor).times(100).toFixed(2) + '%', discount: BigNumber(t.referralDiscountFactor).times(100).toFixed(2) + '%',
minimumVolume: Number(t.minimumRunningNotionalTakerVolume), minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
volume: getNumberFormat(0).format( volume: formatNumber(t.minimumRunningNotionalTakerVolume, 0),
Number(t.minimumRunningNotionalTakerVolume)
),
epochs: Number(t.minimumEpochs), epochs: Number(t.minimumEpochs),
}; };
}); });
@@ -14,9 +14,9 @@ import {
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { import {
addDecimalsFormatNumber, addDecimalsFormatNumber,
formatNumber,
getDateFormat, getDateFormat,
getDateTimeFormat, getDateTimeFormat,
getNumberFormat,
getUserLocale, getUserLocale,
removePaginationWrapper, removePaginationWrapper,
} from '@vegaprotocol/utils'; } from '@vegaprotocol/utils';
@@ -323,7 +323,7 @@ export const Statistics = ({
} }
description={<QUSDTooltip />} description={<QUSDTooltip />}
> >
{getNumberFormat(0).format(Number(totalCommissionValue))} {formatNumber(totalCommissionValue, 0)}
</StatTile> </StatTile>
); );
@@ -563,8 +563,8 @@ export const RefereesTable = ({
) )
.map((r) => ({ .map((r) => ({
...r, ...r,
volume: getNumberFormat(0).format(r.volume), volume: formatNumber(r.volume, 0),
commission: getNumberFormat(0).format(r.commission), commission: formatNumber(r.commission, 0),
})) }))
.reverse()} .reverse()}
/> />
+5 -11
View File
@@ -1,24 +1,18 @@
import { useEffect } from 'react'; import { TinyScroll } from '@vegaprotocol/ui-toolkit';
import { titlefy } from '@vegaprotocol/utils';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { RewardsContainer } from '../../components/rewards-container'; import { RewardsContainer } from '../../components/rewards-container';
import { usePageTitleStore } from '../../stores';
import { ErrorBoundary } from '../../components/error-boundary'; import { ErrorBoundary } from '../../components/error-boundary';
import { TinyScroll } from '@vegaprotocol/ui-toolkit'; import { usePageTitle } from '../../lib/hooks/use-page-title';
export const Rewards = () => { export const Rewards = () => {
const t = useT(); const t = useT();
const title = t('Rewards'); const title = t('Rewards');
const { updateTitle } = usePageTitleStore((store) => ({ usePageTitle(title);
updateTitle: store.updateTitle,
}));
useEffect(() => {
updateTitle(titlefy([title]));
}, [updateTitle, title]);
return ( return (
<ErrorBoundary feature="rewards"> <ErrorBoundary feature="rewards">
<TinyScroll className="p-4 max-h-full overflow-auto"> <TinyScroll className="p-4 max-h-full overflow-auto">
<h1 className="px-4 pb-4 text-2xl">{title}</h1> <h1 className="md:px-4 pb-4 text-2xl">{title}</h1>
<RewardsContainer /> <RewardsContainer />
</TinyScroll> </TinyScroll>
</ErrorBoundary> </ErrorBoundary>
+20 -3
View File
@@ -5,7 +5,11 @@ export const BORDER_COLOR = 'border-vega-clight-500 dark:border-vega-cdark-500';
export const GRADIENT = export const GRADIENT =
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent'; 'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
export const Box = (props: HTMLAttributes<HTMLDivElement>) => { export const Box = ({
children,
backgroundImage,
...props
}: HTMLAttributes<HTMLDivElement> & { backgroundImage?: string }) => {
return ( return (
<div <div
{...props} {...props}
@@ -13,9 +17,22 @@ export const Box = (props: HTMLAttributes<HTMLDivElement>) => {
BORDER_COLOR, BORDER_COLOR,
GRADIENT, GRADIENT,
'border rounded-lg', 'border rounded-lg',
'p-6', 'relative p-6 overflow-hidden',
props.className props.className
)} )}
/> >
{Boolean(backgroundImage?.length) && (
<div
className={classNames(
'pointer-events-none',
'bg-no-repeat bg-center bg-[length:500px_500px]',
'absolute top-0 left-0 w-full h-full -z-10 opacity-30 blur-lg'
)}
style={{ backgroundImage: `url("${backgroundImage}")` }}
></div>
)}
{children}
</div>
); );
}; };
@@ -1,6 +1,6 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Splash } from '@vegaprotocol/ui-toolkit'; import { Splash } from '@vegaprotocol/ui-toolkit';
import { getNumberFormat } from '@vegaprotocol/utils'; import { formatNumber } from '@vegaprotocol/utils';
import { type useTeams } from '../../lib/hooks/use-teams'; import { type useTeams } from '../../lib/hooks/use-teams';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { Table } from '../table'; import { Table } from '../table';
@@ -15,8 +15,7 @@ export const CompetitionsLeaderboard = ({
}) => { }) => {
const t = useT(); const t = useT();
const num = (n?: number | string) => const num = (n?: number | string) => (!n ? '-' : formatNumber(n, 0));
!n ? '-' : getNumberFormat(0).format(Number(n));
if (!data || data.length === 0) { if (!data || data.length === 0) {
return <Splash>{t('Could not find any teams')}</Splash>; return <Splash>{t('Could not find any teams')}</Splash>;
@@ -33,9 +32,9 @@ export const CompetitionsLeaderboard = ({
{ name: 'status', displayName: t('Status') }, { name: 'status', displayName: t('Status') },
{ name: 'volume', displayName: t('Volume') }, { name: 'volume', displayName: t('Volume') },
]} ]}
data={data.map((td, i) => { data={data.map((td) => {
// leaderboard place or medal // 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 === 1) rank = <Rank variant="gold" />;
if (rank === 2) rank = <Rank variant="silver" />; if (rank === 2) rank = <Rank variant="silver" />;
if (rank === 3) rank = <Rank variant="bronze" />; if (rank === 3) rank = <Rank variant="bronze" />;
@@ -1,6 +1,11 @@
import { type TransferNode } from '@vegaprotocol/types'; import { type TransferNode } from '@vegaprotocol/types';
import { ActiveRewardCard } from '../rewards-container/active-rewards'; import {
ActiveRewardCard,
isActiveReward,
} from '../rewards-container/active-rewards';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { useAssetsMapProvider } from '@vegaprotocol/assets';
import { useMarketsMapProvider } from '@vegaprotocol/markets';
export const GamesContainer = ({ export const GamesContainer = ({
data, data,
@@ -10,8 +15,35 @@ export const GamesContainer = ({
currentEpoch: number; currentEpoch: number;
}) => { }) => {
const t = useT(); const t = useT();
// Re-load markets and assets in the games container to ensure that the
// the cards are updated (not grayed out) when the user navigates to the games page
const { data: assets } = useAssetsMapProvider();
const { data: markets } = useMarketsMapProvider();
if (!data || data.length === 0) { const enrichedTransfers = data
.filter((node) => isActiveReward(node, currentEpoch))
.map((node) => {
if (node.transfer.kind.__typename !== 'RecurringTransfer') {
return node;
}
const asset =
assets &&
assets[
node.transfer.kind.dispatchStrategy?.dispatchMetricAssetId || ''
];
const marketsInScope =
node.transfer.kind.dispatchStrategy?.marketIdsInScope?.map(
(id) => markets && markets[id]
);
return { ...node, asset, markets: marketsInScope };
});
if (!enrichedTransfers || !enrichedTransfers.length) return null;
if (!enrichedTransfers || enrichedTransfers.length === 0) {
return ( return (
<p className="mb-6 text-muted"> <p className="mb-6 text-muted">
{t('There are currently no games available.')} {t('There are currently no games available.')}
@@ -21,7 +53,7 @@ export const GamesContainer = ({
return ( return (
<div className="mb-12 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="mb-12 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{data.map((game, i) => { {enrichedTransfers.map((game, i) => {
// TODO: Remove `kind` prop from ActiveRewardCard // TODO: Remove `kind` prop from ActiveRewardCard
const { transfer } = game; const { transfer } = game;
if ( if (
@@ -1,9 +1,11 @@
import { isValidUrl } from '@vegaprotocol/utils';
import classNames from 'classnames'; import classNames from 'classnames';
import { useEffect, useState } from 'react';
const NUM_AVATARS = 20; const NUM_AVATARS = 20;
const AVATAR_PATHNAME_PATTERN = '/team-avatars/{id}.png'; const AVATAR_PATHNAME_PATTERN = '/team-avatars/{id}.png';
const getFallbackAvatar = (teamId: string) => { export const getFallbackAvatar = (teamId: string) => {
const avatarId = ((parseInt(teamId, 16) % NUM_AVATARS) + 1) const avatarId = ((parseInt(teamId, 16) % NUM_AVATARS) + 1)
.toString() .toString()
.padStart(2, '0'); // between 01 - 20 .padStart(2, '0'); // between 01 - 20
@@ -11,6 +13,26 @@ const getFallbackAvatar = (teamId: string) => {
return AVATAR_PATHNAME_PATTERN.replace('{id}', avatarId); return AVATAR_PATHNAME_PATTERN.replace('{id}', avatarId);
}; };
const useAvatar = (teamId: string, url: string) => {
const fallback = getFallbackAvatar(teamId);
const [avatar, setAvatar] = useState<string>(fallback);
useEffect(() => {
if (!isValidUrl(url)) return;
fetch(url, { cache: 'force-cache' })
.then((response) => {
if (response.ok) {
setAvatar(url);
}
})
.catch(() => {
/** noop */
});
});
return avatar;
};
export const TeamAvatar = ({ export const TeamAvatar = ({
teamId, teamId,
imgUrl, imgUrl,
@@ -22,7 +44,7 @@ export const TeamAvatar = ({
alt?: string; alt?: string;
size?: 'large' | 'small'; size?: 'large' | 'small';
}) => { }) => {
const img = imgUrl && imgUrl.length > 0 ? imgUrl : getFallbackAvatar(teamId); const img = useAvatar(teamId, imgUrl);
return ( return (
// eslint-disable-next-line @next/next/no-img-element // eslint-disable-next-line @next/next/no-img-element
<img <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'; } from '../../lib/hooks/use-team';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { DispatchMetricLabels, type DispatchMetric } from '@vegaprotocol/types'; import { DispatchMetricLabels, type DispatchMetric } from '@vegaprotocol/types';
import classNames from 'classnames';
export const TeamStats = ({ export const TeamStats = ({
stats, stats,
@@ -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 t = useT();
const rewardMetrics = games.map( const rewardMetrics = games.map(
@@ -128,7 +135,13 @@ const FavoriteGame = ({ games }: { games: TeamGame[] }) => {
return ( return (
<dl className="flex flex-col gap-1"> <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> <dd>
<Pill className="inline-flex items-center gap-1 bg-transparent text-sm"> <Pill className="inline-flex items-center gap-1 bg-transparent text-sm">
<VegaIcon <VegaIcon
@@ -142,7 +155,7 @@ const FavoriteGame = ({ games }: { games: TeamGame[] }) => {
); );
}; };
const StatSection = ({ children }: { children: ReactNode }) => { export const StatSection = ({ children }: { children: ReactNode }) => {
return ( return (
<section className="flex flex-col lg:flex-row gap-4 lg:gap-8"> <section className="flex flex-col lg:flex-row gap-4 lg:gap-8">
{children} {children}
@@ -150,11 +163,11 @@ const StatSection = ({ children }: { children: ReactNode }) => {
); );
}; };
const StatSectionSeparator = () => { export const StatSectionSeparator = () => {
return <div className="hidden md:block border-r border-default" />; return <div className="hidden md:block border-r border-default" />;
}; };
const StatList = ({ children }: { children: ReactNode }) => { export const StatList = ({ children }: { children: ReactNode }) => {
return ( return (
<dl className="grid grid-cols-2 md:flex gap-4 md:gap-6 lg:gap-8 whitespace-nowrap"> <dl className="grid grid-cols-2 md:flex gap-4 md:gap-6 lg:gap-8 whitespace-nowrap">
{children} {children}
@@ -162,19 +175,21 @@ const StatList = ({ children }: { children: ReactNode }) => {
); );
}; };
const Stat = ({ export const Stat = ({
value, value,
label, label,
tooltip, tooltip,
valueTestId, valueTestId,
className,
}: { }: {
value: ReactNode; value: ReactNode;
label: ReactNode; label: ReactNode;
tooltip?: string; tooltip?: string;
valueTestId?: string; valueTestId?: string;
className?: classNames.Argument;
}) => { }) => {
return ( return (
<div> <div className={classNames(className)}>
<dd className="text-3xl lg:text-4xl" data-testid={valueTestId}> <dd className="text-3xl lg:text-4xl" data-testid={valueTestId}>
{value} {value}
</dd> </dd>
@@ -3,7 +3,6 @@ import { Outlet } from 'react-router-dom';
import { Sidebar, SidebarContent, useSidebar } from '../sidebar'; import { Sidebar, SidebarContent, useSidebar } from '../sidebar';
import classNames from 'classnames'; import classNames from 'classnames';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id'; import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
export const LayoutWithSidebar = ({ export const LayoutWithSidebar = ({
header, header,
sidebar, sidebar,
@@ -17,7 +16,7 @@ export const LayoutWithSidebar = ({
const sidebarOpen = sidebarView !== null; const sidebarOpen = sidebarView !== null;
const gridClasses = classNames( const gridClasses = classNames(
'h-full relative z-0 grid', 'h-full relative z-0 grid',
'grid-rows-[min-content_1fr_40px]', 'grid-rows-[min-content_1fr_50px]',
'lg:grid-rows-[min-content_1fr]', 'lg:grid-rows-[min-content_1fr]',
'lg:grid-cols-[1fr_280px_40px]', 'lg:grid-cols-[1fr_280px_40px]',
'xxxl:grid-cols-[1fr_320px_40px]' 'xxxl:grid-cols-[1fr_320px_40px]'
@@ -27,10 +26,13 @@ export const LayoutWithSidebar = ({
<div className={gridClasses}> <div className={gridClasses}>
<div className="col-span-full">{header}</div> <div className="col-span-full">{header}</div>
<main <main
className={classNames('col-start-1 col-end-1 overflow-y-auto', { className={classNames(
'lg:col-end-3': !sidebarOpen, 'col-start-1 col-end-1 overflow-hidden lg:overflow-y-auto grow lg:grow-0',
'hidden lg:block lg:col-end-2': sidebarOpen, {
})} 'lg:col-end-3': !sidebarOpen,
'hidden lg:block lg:col-end-2': sidebarOpen,
}
)}
> >
<Outlet /> <Outlet />
</main> </main>
@@ -1 +1,2 @@
export * from './market-header'; 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
View File
@@ -1,2 +1 @@
export * from './navbar'; 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>
);
};
+1 -9
View File
@@ -34,13 +34,7 @@ import { supportedLngs } from '../../lib/i18n';
type MenuState = 'wallet' | 'nav' | null; type MenuState = 'wallet' | 'nav' | null;
type Theme = 'system' | 'yellow'; type Theme = 'system' | 'yellow';
export const Navbar = ({ export const Navbar = ({ theme = 'system' }: { theme?: Theme }) => {
children,
theme = 'system',
}: {
children?: ReactNode;
theme?: Theme;
}) => {
const i18n = useI18n(); const i18n = useI18n();
const t = useT(); const t = useT();
// menu state for small screens // menu state for small screens
@@ -75,8 +69,6 @@ export const Navbar = ({
> >
<VLogo className="w-4" /> <VLogo className="w-4" />
</NavLink> </NavLink>
{/* Left section */}
<div className="flex items-center lg:hidden">{children}</div>
{/* Used to show header in nav on mobile */} {/* Used to show header in nav on mobile */}
<div className="hidden lg:block"> <div className="hidden lg:block">
<NavbarMenu onClick={() => setMenu(null)} /> <NavbarMenu onClick={() => setMenu(null)} />
@@ -152,7 +152,11 @@ export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
if (!enrichedTransfers || !enrichedTransfers.length) return null; if (!enrichedTransfers || !enrichedTransfers.length) return null;
return ( 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 && ( {enrichedTransfers.length > 1 && (
<TradingInput <TradingInput
onChange={(e) => onChange={(e) =>
@@ -312,49 +316,30 @@ export const ActiveRewardCard = ({
MarketState.STATE_CLOSED, MarketState.STATE_CLOSED,
].includes(m.state) ].includes(m.state)
); );
if (marketSettled) { if (marketSettled) {
return null; return null;
} }
const assetInSettledMarket = const assetInActiveMarket =
allMarkets && allMarkets &&
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => { Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) { if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
return ( return m?.state && MarketState.STATE_ACTIVE === m.state;
m?.state &&
[
MarketState.STATE_TRADING_TERMINATED,
MarketState.STATE_SETTLED,
MarketState.STATE_CANCELLED,
MarketState.STATE_CLOSED,
].includes(m.state)
);
} }
return false; return false;
}); });
// Gray out the cards that are related to suspended markets const marketSuspended = transferNode.markets?.some(
const suspended = transferNode.markets?.some(
(m) => (m) =>
m?.state === MarketState.STATE_SUSPENDED || m?.state === MarketState.STATE_SUSPENDED ||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
); );
const assetInSuspendedMarket =
allMarkets &&
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
return (
m?.state === MarketState.STATE_SUSPENDED ||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
);
}
return false;
});
// Gray out the cards that are related to suspended markets // Gray out the cards that are related to suspended markets
// Or settlement assets in markets that are not active and eligible for rewards
const { gradientClassName, mainClassName } = const { gradientClassName, mainClassName } =
suspended || assetInSuspendedMarket || assetInSettledMarket marketSuspended || !assetInActiveMarket
? { ? {
gradientClassName: 'from-vega-cdark-500 to-vega-clight-400', gradientClassName: 'from-vega-cdark-500 to-vega-clight-400',
mainClassName: 'from-vega-cdark-400 dark:from-vega-cdark-600 to-20%', mainClassName: 'from-vega-cdark-400 dark:from-vega-cdark-600 to-20%',
@@ -371,6 +356,7 @@ export const ActiveRewardCard = ({
'rounded-lg', 'rounded-lg',
gradientClassName gradientClassName
)} )}
data-testid="active-rewards-card"
> >
<div <div
className={classNames( className={classNames(
@@ -382,7 +368,7 @@ export const ActiveRewardCard = ({
<div className="flex flex-col gap-2 items-center text-center"> <div className="flex flex-col gap-2 items-center text-center">
<EntityIcon transfer={transfer} /> <EntityIcon transfer={transfer} />
{entityScope && ( {entityScope && (
<span className="text-muted text-xs"> <span className="text-muted text-xs" data-testid="entity-scope">
{EntityScopeLabelMapping[entityScope] || t('Unspecified')} {EntityScopeLabelMapping[entityScope] || t('Unspecified')}
</span> </span>
)} )}
@@ -390,7 +376,7 @@ export const ActiveRewardCard = ({
<div className="flex flex-col gap-2 items-center text-center"> <div className="flex flex-col gap-2 items-center text-center">
<h3 className="flex flex-col gap-1 text-2xl shrink-1 text-center"> <h3 className="flex flex-col gap-1 text-2xl shrink-1 text-center">
<span className="font-glitch"> <span className="font-glitch" data-testid="reward-value">
{addDecimalsFormatNumber( {addDecimalsFormatNumber(
transferNode.transfer.amount, transferNode.transfer.amount,
transferNode.transfer.asset?.decimals || 0, transferNode.transfer.asset?.decimals || 0,
@@ -411,7 +397,7 @@ export const ActiveRewardCard = ({
)} )}
underline={true} underline={true}
> >
<span className="text-xs"> <span className="text-xs" data-testid="distribution-strategy">
{ {
DistributionStrategyMapping[ DistributionStrategyMapping[
dispatchStrategy.distributionStrategy dispatchStrategy.distributionStrategy
@@ -429,7 +415,10 @@ export const ActiveRewardCard = ({
'Number of epochs after distribution to delay vesting of rewards by' 'Number of epochs after distribution to delay vesting of rewards by'
)} )}
/> />
<span className="text-muted text-xs whitespace-nowrap"> <span
className="text-muted text-xs whitespace-nowrap"
data-testid="locked-for"
>
{t('numberEpochs', '{{count}} epochs', { {t('numberEpochs', '{{count}} epochs', {
count: kind.dispatchStrategy?.lockPeriod, count: kind.dispatchStrategy?.lockPeriod,
})} })}
@@ -438,15 +427,15 @@ export const ActiveRewardCard = ({
</div> </div>
<span className="border-[0.5px] border-gray-700" /> <span className="border-[0.5px] border-gray-700" />
<span> <span data-testid="dispatch-metric-info">
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]} {' '} {DispatchMetricLabels[dispatchStrategy.dispatchMetric]} {' '}
<Tooltip <Tooltip
underline={suspended} underline={marketSuspended}
description={ description={
(suspended || assetInSuspendedMarket) && (marketSuspended || !assetInActiveMarket) &&
(specificMarkets (specificMarkets
? t('Eligible market(s) currently suspended') ? t('Eligible market(s) currently suspended')
: assetInSuspendedMarket : !assetInActiveMarket
? t('Currently no markets eligible for reward') ? t('Currently no markets eligible for reward')
: '') : '')
} }
@@ -458,8 +447,8 @@ export const ActiveRewardCard = ({
<div className="flex items-center gap-8 flex-wrap"> <div className="flex items-center gap-8 flex-wrap">
{kind.endEpoch && ( {kind.endEpoch && (
<span className="flex flex-col"> <span className="flex flex-col">
<span className="text-muted text-xs">{t('Ends in')}</span> <span className="text-muted text-xs">{t('Ends in')} </span>
<span> <span data-testid="ends-in">
{t('numberEpochs', '{{count}} epochs', { {t('numberEpochs', '{{count}} epochs', {
count: kind.endEpoch - currentEpoch, count: kind.endEpoch - currentEpoch,
})} })}
@@ -470,7 +459,7 @@ export const ActiveRewardCard = ({
{ {
<span className="flex flex-col"> <span className="flex flex-col">
<span className="text-muted text-xs">{t('Assessed over')}</span> <span className="text-muted text-xs">{t('Assessed over')}</span>
<span> <span data-testid="assessed-over">
{t('numberEpochs', '{{count}} epochs', { {t('numberEpochs', '{{count}} epochs', {
count: dispatchStrategy.windowLength, count: dispatchStrategy.windowLength,
})} })}
@@ -479,7 +468,7 @@ export const ActiveRewardCard = ({
} }
</div> </div>
{dispatchStrategy?.dispatchMetric && ( {dispatchStrategy?.dispatchMetric && (
<span className="text-muted text-sm h-[2rem]"> <span className="text-muted text-sm h-[3rem]">
{t(DispatchMetricDescription[dispatchStrategy?.dispatchMetric])} {t(DispatchMetricDescription[dispatchStrategy?.dispatchMetric])}
</span> </span>
)} )}
@@ -513,7 +502,7 @@ const RewardRequirements = ({
entity: EntityScopeLabelMapping[dispatchStrategy.entityScope], entity: EntityScopeLabelMapping[dispatchStrategy.entityScope],
})} })}
</dt> </dt>
<dd className="flex items-center gap-1"> <dd className="flex items-center gap-1" data-testid="scope">
<RewardEntityScope dispatchStrategy={dispatchStrategy} /> <RewardEntityScope dispatchStrategy={dispatchStrategy} />
</dd> </dd>
</div> </div>
@@ -522,7 +511,10 @@ const RewardRequirements = ({
<dt className="flex items-center gap-1 text-muted"> <dt className="flex items-center gap-1 text-muted">
{t('Staked VEGA')} {t('Staked VEGA')}
</dt> </dt>
<dd className="flex items-center gap-1"> <dd
className="flex items-center gap-1"
data-testid="staking-requirement"
>
{addDecimalsFormatNumber( {addDecimalsFormatNumber(
dispatchStrategy?.stakingRequirement || 0, dispatchStrategy?.stakingRequirement || 0,
assetDecimalPlaces assetDecimalPlaces
@@ -534,7 +526,7 @@ const RewardRequirements = ({
<dt className="flex items-center gap-1 text-muted"> <dt className="flex items-center gap-1 text-muted">
{t('Average position')} {t('Average position')}
</dt> </dt>
<dd className="flex items-center gap-1"> <dd className="flex items-center gap-1" data-testid="average-position">
{addDecimalsFormatNumber( {addDecimalsFormatNumber(
dispatchStrategy?.notionalTimeWeightedAveragePositionRequirement || dispatchStrategy?.notionalTimeWeightedAveragePositionRequirement ||
0, 0,
@@ -81,6 +81,7 @@ export const Settings = () => {
intent={Intent.Primary} intent={Intent.Primary}
onClick={() => { onClick={() => {
localStorage.clear(); localStorage.clear();
sessionStorage.clear();
window.location.reload(); window.location.reload();
}} }}
> >
+60 -25
View File
@@ -17,6 +17,7 @@ import { useVegaWallet, useViewAsDialog } from '@vegaprotocol/wallet';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id'; import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { ErrorBoundary } from '../error-boundary'; import { ErrorBoundary } from '../error-boundary';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
export enum ViewType { export enum ViewType {
Order = 'Order', Order = 'Order',
@@ -26,9 +27,10 @@ export enum ViewType {
Transfer = 'Transfer', Transfer = 'Transfer',
Settings = 'Settings', Settings = 'Settings',
ViewAs = 'ViewAs', ViewAs = 'ViewAs',
Close = 'Close',
} }
type SidebarView = export type BarView =
| { | {
type: ViewType.Deposit; type: ViewType.Deposit;
assetId?: string; assetId?: string;
@@ -49,6 +51,9 @@ type SidebarView =
} }
| { | {
type: ViewType.Settings; type: ViewType.Settings;
}
| {
type: ViewType.Close;
}; };
export const Sidebar = ({ options }: { options?: ReactNode }) => { 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 navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1';
const setViewAsDialogOpen = useViewAsDialog((state) => state.setOpen); const setViewAsDialogOpen = useViewAsDialog((state) => state.setOpen);
const { pubKeys } = useVegaWallet(); const { pubKeys } = useVegaWallet();
const { isMobile } = useScreenDimensions();
const { getView } = useSidebar((store) => ({
setViews: store.setViews,
getView: store.getView,
}));
const currView = getView(currentRouteId);
return ( return (
<div className="flex h-full p-1 lg:flex-col gap-2" data-testid="sidebar"> <div className="flex h-full lg:flex-col gap-1" data-testid="sidebar">
{options && <nav className={navClasses}>{options}</nav>} {options && (
<nav className={classNames(navClasses, 'ml-auto lg:mt-auto lg:ml-0')}> <nav className={classNames(navClasses, 'flex grow')}>{options}</nav>
<SidebarButton )}
view={ViewType.ViewAs} <nav
onClick={() => { className={classNames(
setViewAsDialogOpen(true); navClasses,
}} 'ml-auto lg:mt-auto lg:ml-0 shrink-0'
icon={VegaIconNames.EYE} )}
tooltip={t('View as party')} >
disabled={Boolean(pubKeys)} {!isMobile ? (
routeId={currentRouteId} <>
/> <SidebarButton
<SidebarButton view={ViewType.ViewAs}
view={ViewType.Settings} onClick={() => {
icon={VegaIconNames.COG} setViewAsDialogOpen(true);
tooltip={t('Settings')} }}
routeId={currentRouteId} 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 /> <NodeHealthContainer />
</nav> </nav>
</div> </div>
@@ -103,7 +134,7 @@ export const SidebarButton = ({
getView: store.getView, getView: store.getView,
})); }));
const currView = getView(routeId); const currView = getView(routeId);
const onSelect = (view: SidebarView['type']) => { const onSelect = (view: BarView['type']) => {
if (view === currView?.type) { if (view === currView?.type) {
setViews(null, routeId); setViews(null, routeId);
} else { } else {
@@ -133,7 +164,7 @@ export const SidebarButton = ({
<button <button
className={buttonClasses} className={buttonClasses}
data-testid={view} data-testid={view}
onClick={onClick || (() => onSelect(view as SidebarView['type']))} onClick={onClick || (() => onSelect(view as BarView['type']))}
disabled={disabled} disabled={disabled}
> >
<VegaIcon name={icon} size={20} /> <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 (view.type === ViewType.Info) {
if (params.marketId) { if (params.marketId) {
return ( return (
@@ -267,9 +302,9 @@ const CloseSidebar = () => {
}; };
export const useSidebar = create<{ export const useSidebar = create<{
views: { [key: string]: SidebarView | null }; views: { [key: string]: BarView | null };
setViews: (view: SidebarView | null, routeId: string) => void; setViews: (view: BarView | null, routeId: string) => void;
getView: (routeId: string) => SidebarView | null | undefined; getView: (routeId: string) => BarView | null | undefined;
}>()((set, get) => ({ }>()((set, get) => ({
views: {}, views: {},
setViews: (x, routeId) => setViews: (x, routeId) =>
+1 -1
View File
@@ -1,3 +1,3 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
VEGA_VERSION=v0.74.0-preview.7 VEGA_VERSION=v0.74.0-preview.10
LOCAL_SERVER=false LOCAL_SERVER=false
+27 -2
View File
@@ -111,6 +111,7 @@ def init_vega(request=None):
f"Container {container.id} started", f"Container {container.id} started",
extra={"worker_id": os.environ.get("PYTEST_XDIST_WORKER")}, extra={"worker_id": os.environ.get("PYTEST_XDIST_WORKER")},
) )
vega.container = container
yield vega yield vega
except APIError as e: except APIError as e:
logger.info(f"Container creation failed.") logger.info(f"Container creation failed.")
@@ -177,10 +178,34 @@ def init_page(vega: VegaServiceNull, browser: Browser, request: pytest.FixtureRe
@pytest.fixture @pytest.fixture
def vega(request): def vega(request):
with init_vega(request) as vega: with init_vega(request) as vega_instance:
yield vega 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 @pytest.fixture
def page(vega, browser, request): def page(vega, browser, request):
with init_page(vega, browser, request) as page_instance: with init_page(vega, browser, request) as page_instance:
@@ -2,7 +2,7 @@ import pytest
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from datetime import datetime, timedelta from datetime import datetime, timedelta
from conftest import init_vega from conftest import init_vega, cleanup_container
from fixtures.market import setup_continuous_market from fixtures.market import setup_continuous_market
from actions.utils import wait_for_toast_confirmation from actions.utils import wait_for_toast_confirmation
@@ -17,8 +17,10 @@ expire = "expire"
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega(request):
with init_vega(request) as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@@ -3,7 +3,7 @@ from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from actions.vega import submit_order from actions.vega import submit_order
from datetime import datetime, timedelta from datetime import datetime, timedelta
from conftest import init_vega from conftest import init_vega, cleanup_container
from fixtures.market import setup_continuous_market from fixtures.market import setup_continuous_market
stop_order_btn = "order-type-Stop" stop_order_btn = "order-type-Stop"
@@ -259,9 +259,10 @@ def test_submit_stop_limit_order_cancel(
class TestStopOcoValidation: class TestStopOcoValidation:
@pytest.fixture(scope="class") @pytest.fixture(scope="class")
def vega(self, request): def vega(request):
with init_vega(request) as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="class") @pytest.fixture(scope="class")
def continuous_market(self, vega): def continuous_market(self, vega):
@@ -2,7 +2,7 @@ import pytest
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from actions.utils import change_keys from actions.utils import change_keys
from conftest import init_vega from conftest import init_vega, cleanup_container
from fixtures.market import setup_continuous_market from fixtures.market import setup_continuous_market
order_size = "order-size" order_size = "order-size"
@@ -14,8 +14,9 @@ deal_ticket_deposit_dialog_button = "deal-ticket-deposit-dialog-button"
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega(request):
with init_vega(request) as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module") @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.") "You may not have enough margin available to open this position.")
page.get_by_test_id(deal_ticket_warning_margin).hover() page.get_by_test_id(deal_ticket_warning_margin).hover()
expect(page.get_by_test_id("tooltip-content").nth(0)).to_have_text( expect(page.get_by_test_id("tooltip-content").nth(0)).to_have_text(
"1,661,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() page.get_by_test_id(deal_ticket_deposit_dialog_button).nth(0).click()
expect(page.get_by_test_id("sidebar-content") expect(page.get_by_test_id("sidebar-content")
).to_contain_text("DepositFrom") ).to_contain_text("DepositFrom")
+11 -6
View File
@@ -4,7 +4,7 @@ from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from actions.vega import submit_order from actions.vega import submit_order
from wallet_config import MM_WALLET from wallet_config import MM_WALLET
from conftest import init_vega, init_page, auth_setup from conftest import init_vega, init_page, auth_setup, cleanup_container
from actions.utils import next_epoch, change_keys, forward_time from actions.utils import next_epoch, change_keys, forward_time
from fixtures.market import market_exists, setup_continuous_market from fixtures.market import market_exists, setup_continuous_market
@@ -82,31 +82,36 @@ def market_ids():
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega_volume_discount_tier_1(request): def vega_volume_discount_tier_1(request):
with init_vega(request) as vega_volume_discount_tier_1: with init_vega(request) as vega_volume_discount_tier_1:
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") @pytest.fixture(scope="module")
def vega_volume_discount_tier_2(request): def vega_volume_discount_tier_2(request):
with init_vega(request) as vega_volume_discount_tier_2: with init_vega(request) as vega_volume_discount_tier_2:
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") @pytest.fixture(scope="module")
def vega_referral_discount_tier_1(request): def vega_referral_discount_tier_1(request):
with init_vega(request) as vega_referral_discount_tier_1: with init_vega(request) as vega_referral_discount_tier_1:
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") @pytest.fixture(scope="module")
def vega_referral_discount_tier_2(request): def vega_referral_discount_tier_2(request):
with init_vega(request) as vega_referral_discount_tier_2: with init_vega(request) as vega_referral_discount_tier_2:
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") @pytest.fixture(scope="module")
def vega_referral_and_volume_discount(request): def vega_referral_and_volume_discount(request):
with init_vega(request) as vega_referral_and_volume_discount: with init_vega(request) as vega_referral_and_volume_discount:
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 @pytest.fixture
@@ -3,7 +3,7 @@ from playwright.sync_api import expect, Page
import json import json
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from fixtures.market import setup_simple_market from fixtures.market import setup_simple_market
from conftest import init_vega from conftest import init_vega, cleanup_container
from actions.vega import submit_order from actions.vega import submit_order
from wallet_config import MM_WALLET, TERMINATE_WALLET, wallets from wallet_config import MM_WALLET, TERMINATE_WALLET, wallets
import logging import logging
@@ -12,9 +12,10 @@ logger = logging.getLogger()
@pytest.fixture(scope="class") @pytest.fixture(scope="class")
def vega(): def vega(request):
with init_vega() as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="class") @pytest.fixture(scope="class")
@@ -2,8 +2,6 @@ import pytest
from playwright.sync_api import expect, Page from playwright.sync_api import expect, Page
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from actions.vega import submit_order from actions.vega import submit_order
from conftest import init_vega
from fixtures.market import setup_continuous_market
from wallet_config import MM_WALLET2 from wallet_config import MM_WALLET2
def hover_and_assert_tooltip(page: Page, element_text): def hover_and_assert_tooltip(page: Page, element_text):
@@ -11,39 +9,30 @@ def hover_and_assert_tooltip(page: Page, element_text):
element.hover() element.hover()
expect(page.get_by_role("tooltip")).to_be_visible() expect(page.get_by_role("tooltip")).to_be_visible()
class TestIcebergOrdersValidations:
@pytest.fixture(scope="class")
def vega(self, request):
with init_vega(request) as vega:
yield vega
@pytest.fixture(scope="class") @pytest.mark.usefixtures("auth", "risk_accepted")
def continuous_market(self, vega): def test_iceberg_submit(continuous_market, vega: VegaServiceNull, page: Page):
return setup_continuous_market(vega) 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") expect(page.get_by_test_id("toast-content")).to_have_text(
def test_iceberg_submit(self, continuous_market, vega: VegaServiceNull, page: Page): "Awaiting confirmationPlease wait for your transaction to be confirmedView in block explorer"
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( vega.wait_fn(1)
"Awaiting confirmationPlease wait for your transaction to be confirmedView in block explorer" 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"
vega.wait_fn(1) )
vega.wait_for_total_catchup() page.get_by_test_id("All").click()
expect(page.get_by_test_id("toast-content")).to_have_text( expect(
"Order filledYour transaction has been confirmedView in block explorerSubmit order - filledBTC:DAI_2023+3 @ 107.00 tDAI" (page.get_by_role("row").locator('[col-id="type"]')).nth(1)
) ).to_have_text("Limit (Iceberg)")
page.get_by_test_id("All").click()
expect(
(page.get_by_role("row").locator('[col-id="type"]')).nth(1)
).to_have_text("Limit (Iceberg)")
@pytest.mark.usefixtures("auth", "risk_accepted") @pytest.mark.usefixtures("auth", "risk_accepted")
def test_iceberg_open_order(continuous_market, vega: VegaServiceNull, page: Page): def test_iceberg_open_order(continuous_market, vega: VegaServiceNull, page: Page):
@@ -1,15 +1,16 @@
import pytest import pytest
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from conftest import init_vega from conftest import init_vega, cleanup_container
from fixtures.market import setup_continuous_market from fixtures.market import setup_continuous_market
from actions.utils import next_epoch, truncate_middle, change_keys from actions.utils import next_epoch, truncate_middle, change_keys
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega(request):
with init_vega(request) as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@@ -4,14 +4,15 @@ import vega_sim.api.governance as governance
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from fixtures.market import setup_continuous_market from fixtures.market import setup_continuous_market
from conftest import init_vega from conftest import init_vega, cleanup_container
from actions.utils import next_epoch from actions.utils import next_epoch
@pytest.fixture(scope="class") @pytest.fixture(scope="class")
def vega(): def vega(request):
with init_vega() as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="class") @pytest.fixture(scope="class")
@@ -3,15 +3,16 @@ import pytest
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from fixtures.market import setup_continuous_market from fixtures.market import setup_continuous_market
from conftest import init_page, init_vega, risk_accepted_setup from conftest import init_page, init_vega, risk_accepted_setup, cleanup_container
market_title_test_id = "accordion-title" market_title_test_id = "accordion-title"
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(): def vega(request):
with init_vega() as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@@ -3,7 +3,7 @@ import vega_sim.api.governance as governance
import re import re
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from conftest import init_vega from conftest import init_vega, cleanup_container
from fixtures.market import setup_simple_market from fixtures.market import setup_simple_market
from wallet_config import MM_WALLET from wallet_config import MM_WALLET
@@ -13,8 +13,10 @@ col_market_id = '[col-id="market"] [data-testid="stack-cell-primary"]'
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega(request):
with init_vega(request) as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@@ -3,7 +3,7 @@ from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from actions.vega import submit_order from actions.vega import submit_order
from fixtures.market import setup_simple_market from fixtures.market import setup_simple_market
from conftest import init_vega from conftest import init_vega, cleanup_container
from actions.utils import wait_for_toast_confirmation, change_keys from actions.utils import wait_for_toast_confirmation, change_keys
from wallet_config import MM_WALLET, MM_WALLET2 from wallet_config import MM_WALLET, MM_WALLET2
@@ -15,8 +15,10 @@ COL_ID_FEE = ".ag-center-cols-container [col-id='fee'] .ag-cell-value"
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega(request):
with init_vega(request) as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@@ -37,7 +37,7 @@ def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page):
# 6002-MDET-004 # 6002-MDET-004
expect(page.get_by_test_id("market-change")).to_have_text("Change (24h)0.00%0.00") expect(page.get_by_test_id("market-change")).to_have_text("Change (24h)0.00%0.00")
# 6002-MDET-005 # 6002-MDET-005
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)-") expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)- (- BTC)")
# 6002-MDET-008 # 6002-MDET-008
expect(page.get_by_test_id("market-settlement-asset")).to_have_text( expect(page.get_by_test_id("market-settlement-asset")).to_have_text(
"Settlement assettDAI" "Settlement assettDAI"
@@ -1,13 +1,14 @@
import pytest import pytest
from playwright.sync_api import Page, expect, Locator from playwright.sync_api import Page, expect, Locator
from conftest import init_page, init_vega from conftest import init_page, init_vega, cleanup_container
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(): def vega(request):
with init_vega() as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
# we can reuse single page instance in all tests # we can reuse single page instance in all tests
@@ -2,7 +2,7 @@ import pytest
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from vega_sim.service import PeggedOrder from vega_sim.service import PeggedOrder
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from conftest import auth_setup, init_page, init_vega, risk_accepted_setup 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 fixtures.market import setup_continuous_market, setup_simple_market
from actions.utils import wait_for_toast_confirmation from actions.utils import wait_for_toast_confirmation
@@ -11,8 +11,9 @@ order_tab = "tab-orders"
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega(request):
with init_vega(request) as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module", autouse=True) @pytest.fixture(scope="module", autouse=True)
@@ -2,15 +2,16 @@ import pytest
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from typing import List from typing import List
from actions.vega import submit_order, submit_liquidity, submit_multiple_orders from actions.vega import submit_order, submit_liquidity, submit_multiple_orders
from conftest import init_vega from conftest import init_vega, cleanup_container
from fixtures.market import setup_simple_market from fixtures.market import setup_simple_market
from wallet_config import MM_WALLET, MM_WALLET2 from wallet_config import MM_WALLET, MM_WALLET2
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(): def vega(request):
with init_vega() as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@@ -1,7 +1,7 @@
import pytest import pytest
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from conftest import init_vega from conftest import init_vega, cleanup_container
from fixtures.market import setup_continuous_market from fixtures.market import setup_continuous_market
TOOLTIP_LABEL = "margin-health-tooltip-label" TOOLTIP_LABEL = "margin-health-tooltip-label"
@@ -11,8 +11,10 @@ COL_ID_USED = ".ag-center-cols-container [col-id='used'] .ag-cell-value"
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega(request):
with init_vega(request) as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@@ -1,7 +1,7 @@
import pytest import pytest
from playwright.sync_api import Page from playwright.sync_api import Page
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from conftest import init_vega from conftest import init_vega, cleanup_container
from fixtures.market import setup_continuous_market, setup_simple_market from fixtures.market import setup_continuous_market, setup_simple_market
from actions.utils import change_keys, create_and_faucet_wallet, forward_time, selector_contains_text from actions.utils import change_keys, create_and_faucet_wallet, forward_time, selector_contains_text
from actions.vega import submit_order, submit_liquidity from actions.vega import submit_order, submit_liquidity
@@ -14,8 +14,10 @@ BUY_ORDERS = [[1, 106], [1, 107], [1, 108]]
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega(request):
with init_vega(request) as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@@ -59,7 +59,7 @@ def test_filtered_cards(continuous_market, vega: VegaServiceNull, page: Page):
next_epoch(vega=vega) next_epoch(vega=vega)
page.reload() page.reload()
expect(page.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( governance.submit_oracle_data(
wallet=vega.wallet, wallet=vega.wallet,
payload={"trading.terminated": "true"}, payload={"trading.terminated": "true"},
@@ -67,4 +67,4 @@ def test_filtered_cards(continuous_market, vega: VegaServiceNull, page: Page):
) )
next_epoch(vega=vega) next_epoch(vega=vega)
page.reload() page.reload()
expect(page.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 import vega_sim.proto.vega as vega_protos
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from conftest import init_vega, init_page, auth_setup from conftest import init_vega, init_page, auth_setup, cleanup_container
from fixtures.market import setup_continuous_market, market_exists from fixtures.market import setup_continuous_market, market_exists
from actions.utils import next_epoch, change_keys from actions.utils import next_epoch, change_keys
from wallet_config import MM_WALLET, PARTY_A, PARTY_B, PARTY_C, PARTY_D from wallet_config import MM_WALLET, PARTY_A, PARTY_B, PARTY_C, PARTY_D
@@ -46,36 +46,42 @@ def market_ids():
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega_activity_tier_0(request): def vega_activity_tier_0(request):
with init_vega(request) as vega_activity_tier_0: with init_vega(request) as vega_activity_tier_0:
request.addfinalizer(lambda: cleanup_container(vega_activity_tier_0)) # Register the cleanup function
yield vega_activity_tier_0 yield vega_activity_tier_0
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega_hoarder_tier_0(request): def vega_hoarder_tier_0(request):
with init_vega(request) as vega_hoarder_tier_0: with init_vega(request) as vega_hoarder_tier_0:
request.addfinalizer(lambda: cleanup_container(vega_hoarder_tier_0)) # Register the cleanup function
yield vega_hoarder_tier_0 yield vega_hoarder_tier_0
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega_combo_tier_0(request): def vega_combo_tier_0(request):
with init_vega(request) as vega_combo_tier_0: with init_vega(request) as vega_combo_tier_0:
request.addfinalizer(lambda: cleanup_container(vega_combo_tier_0)) # Register the cleanup function
yield vega_combo_tier_0 yield vega_combo_tier_0
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega_activity_tier_1(request): def vega_activity_tier_1(request):
with init_vega(request) as vega_activity_tier_1: with init_vega(request) as vega_activity_tier_1:
request.addfinalizer(lambda: cleanup_container(vega_activity_tier_1)) # Register the cleanup function
yield vega_activity_tier_1 yield vega_activity_tier_1
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega_hoarder_tier_1(request): def vega_hoarder_tier_1(request):
with init_vega(request) as vega_hoarder_tier_1: with init_vega(request) as vega_hoarder_tier_1:
request.addfinalizer(lambda: cleanup_container(vega_hoarder_tier_1)) # Register the cleanup function
yield vega_hoarder_tier_1 yield vega_hoarder_tier_1
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega_combo_tier_1(request): def vega_combo_tier_1(request):
with init_vega(request) as vega_combo_tier_1: with init_vega(request) as vega_combo_tier_1:
request.addfinalizer(lambda: cleanup_container(vega_combo_tier_1)) # Register the cleanup function
yield vega_combo_tier_1 yield vega_combo_tier_1
@@ -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 import pytest
from playwright.sync_api import expect, Page from playwright.sync_api import expect, Page
from conftest import init_vega from conftest import init_vega, cleanup_container
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(): def vega(request):
with init_vega() as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.mark.usefixtures("risk_accepted") @pytest.mark.usefixtures("risk_accepted")
+165 -47
View File
@@ -2,17 +2,18 @@ import pytest
from playwright.sync_api import expect, Page from playwright.sync_api import expect, Page
import vega_sim.proto.vega as vega_protos import vega_sim.proto.vega as vega_protos
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from conftest import init_vega from conftest import init_vega, cleanup_container
from actions.utils import next_epoch from actions.utils import next_epoch, change_keys
from fixtures.market import setup_continuous_market from fixtures.market import setup_continuous_market
from conftest import auth_setup, init_page, init_vega, risk_accepted_setup from conftest import auth_setup, init_page, init_vega, risk_accepted_setup
from wallet_config import PARTY_A, PARTY_B, PARTY_C, PARTY_D, MM_WALLET from wallet_config import PARTY_A, PARTY_B, PARTY_C, PARTY_D, MM_WALLET
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega(request):
with init_vega(request) as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def team_page(vega, browser, request, setup_teams_and_games): def team_page(vega, browser, request, setup_teams_and_games):
@@ -23,9 +24,20 @@ def team_page(vega, browser, request, setup_teams_and_games):
page.goto(f"/#/competitions/teams/{team_id}") page.goto(f"/#/competitions/teams/{team_id}")
yield page yield page
@pytest.fixture(scope="module")
def competitions_page(vega, browser, request, setup_teams_and_games):
with init_page(vega, browser, request) as page:
risk_accepted_setup(page)
auth_setup(vega, page)
team_id = setup_teams_and_games["team_id"]
page.goto(f"/#/competitions/")
yield page
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def setup_teams_and_games(vega: VegaServiceNull): def setup_teams_and_games(vega: VegaServiceNull):
tDAI_market = setup_continuous_market(vega) tDAI_market = setup_continuous_market(vega, custom_quantum=100000)
tDAI_asset_id = vega.find_asset_id(symbol="tDAI") tDAI_asset_id = vega.find_asset_id(symbol="tDAI")
vega.mint(key_name=PARTY_B.name, asset=tDAI_asset_id, amount=100000) vega.mint(key_name=PARTY_B.name, asset=tDAI_asset_id, amount=100000)
vega.mint(key_name=PARTY_C.name, asset=tDAI_asset_id, amount=100000) vega.mint(key_name=PARTY_C.name, asset=tDAI_asset_id, amount=100000)
@@ -46,6 +58,18 @@ def setup_teams_and_games(vega: VegaServiceNull):
# list_teams actually returns a dictionary {"team_id": Team} # list_teams actually returns a dictionary {"team_id": Team}
team_id = list(teams.keys())[0] team_id = list(teams.keys())[0]
vega.create_referral_set(
key_name="market_maker",
name="test",
team_url="https://vega.xyz",
avatar_url="http://placekitten.com/200/200",
closed=False,
)
next_epoch(vega)
teams = vega.list_teams()
team_id_2 = list(teams.keys())[0]
vega.apply_referral_code("Key 1", team_id_2)
vega.apply_referral_code(PARTY_B.name, team_id) vega.apply_referral_code(PARTY_B.name, team_id)
@@ -63,7 +87,7 @@ def setup_teams_and_games(vega: VegaServiceNull):
current_epoch = vega.statistics().epoch_seq current_epoch = vega.statistics().epoch_seq
game_start = current_epoch + 1 game_start = current_epoch + 1
game_end = current_epoch + 11 game_end = current_epoch + 14
current_epoch = vega.statistics().epoch_seq current_epoch = vega.statistics().epoch_seq
print(f"[EPOCH: {current_epoch}] creating recurring transfer") print(f"[EPOCH: {current_epoch}] creating recurring transfer")
@@ -84,9 +108,42 @@ def setup_teams_and_games(vega: VegaServiceNull):
factor=1.0, factor=1.0,
start_epoch=game_start, start_epoch=game_start,
end_epoch=game_end, end_epoch=game_end,
window_length=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) next_epoch(vega)
print(f"[EPOCH: {vega.statistics().epoch_seq}] starting order activity") print(f"[EPOCH: {vega.statistics().epoch_seq}] starting order activity")
@@ -113,6 +170,22 @@ def setup_teams_and_games(vega: VegaServiceNull):
side="SIDE_BUY", side="SIDE_BUY",
volume=1, volume=1,
) )
vega.submit_order(
trading_key="Key 1",
market_id=tDAI_market,
order_type="TYPE_MARKET",
time_in_force="TIME_IN_FORCE_IOC",
side="SIDE_BUY",
volume=1,
)
vega.submit_order(
trading_key="market_maker",
market_id=tDAI_market,
order_type="TYPE_MARKET",
time_in_force="TIME_IN_FORCE_IOC",
side="SIDE_BUY",
volume=1,
)
next_epoch(vega) next_epoch(vega)
print(f"[EPOCH: {vega.statistics().epoch_seq}] {i} epoch passed") print(f"[EPOCH: {vega.statistics().epoch_seq}] {i} epoch passed")
@@ -120,6 +193,7 @@ def setup_teams_and_games(vega: VegaServiceNull):
"market_id": tDAI_market, "market_id": tDAI_market,
"asset_id": tDAI_asset_id, "asset_id": tDAI_asset_id,
"team_id": team_id, "team_id": team_id,
"team_id_2": team_id_2,
"team_name": team_name, "team_name": team_name,
} }
@@ -136,65 +210,109 @@ def create_team(vega: VegaServiceNull):
return team_name return team_name
def test_team_page_games_table(team_page: Page): def test_team_page_games_table(team_page: Page):
team_page.get_by_test_id("games-toggle").click() team_page.get_by_test_id("games-toggle").click()
expect(team_page.get_by_test_id("games-toggle")).to_have_text("Games (1)") expect(team_page.get_by_test_id("games-toggle")).to_have_text("Games (1)")
expect(team_page.get_by_test_id("rank-0")).to_have_text("1") 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("18") 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("type-0")
expect(team_page.get_by_test_id("amount-0")).to_have_text("100,000,000") ).to_have_text("Price maker fees paid")
expect(team_page.get_by_test_id("participatingTeams-0")).to_have_text( expect(team_page.get_by_test_id("amount-0")).to_have_text("74")
"1" 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")
expect(team_page.get_by_test_id("participatingMembers-0")).to_have_text(
"2"
)
def test_team_page_members_table(team_page: Page): def test_team_page_members_table(team_page: Page):
team_page.get_by_test_id("members-toggle").click() team_page.get_by_test_id("members-toggle").click()
expect(team_page.get_by_test_id("members-toggle")).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("referee-0")).to_be_visible()
expect(team_page.get_by_test_id("joinedAt-0")).to_be_visible() expect(team_page.get_by_test_id("joinedAt-0")).to_be_visible()
expect(team_page.get_by_test_id("joinedAtEpoch-0")).to_have_text("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"] team_name = setup_teams_and_games["team_name"]
expect(team_page.get_by_test_id("team-name")).to_have_text(team_name) expect(team_page.get_by_test_id("team-name")).to_have_text(team_name)
expect(team_page.get_by_test_id("members-count-stat")).to_have_text("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( expect(team_page.get_by_test_id("total-games-stat")).to_have_text("1")
"1"
)
# TODO this still seems wrong as its always 0 # TODO this still seems wrong as its always 0
expect(team_page.get_by_test_id("total-volume-stat")).to_have_text( expect(team_page.get_by_test_id("total-volume-stat")).to_have_text("0")
"0"
)
expect(team_page.get_by_test_id("rewards-paid-stat")).to_have_text( expect(team_page.get_by_test_id("rewards-paid-stat")).to_have_text("78")
"100m"
)
def test_switch_teams(team_page: Page, vega: VegaServiceNull):
team_page.get_by_test_id("switch-team-button").click()
team_page.get_by_test_id("confirm-switch-button").click()
expect(team_page.get_by_test_id("dialog-content").first).to_be_visible()
vega.wait_fn(1)
vega.wait_for_total_catchup()
next_epoch(vega=vega)
team_page.reload()
expect(team_page.get_by_test_id("members-count-stat")).to_have_text("5")
@pytest.fixture(scope="module")
def competitions_page(vega, browser, request):
with init_page(vega, browser, request) as page:
risk_accepted_setup(page)
auth_setup(vega, page)
yield page
def test_leaderboard(competitions_page: Page, setup_teams_and_games): def test_leaderboard(competitions_page: Page, setup_teams_and_games):
team_name = setup_teams_and_games["team_name"] team_name = setup_teams_and_games["team_name"]
competitions_page.goto(f"/#/competitions/") competitions_page.reload()
expect(competitions_page.get_by_test_id("rank-0").locator(".text-yellow-300")).to_have_count(1) expect(
expect(competitions_page.get_by_test_id("team-0")).to_have_text(team_name) competitions_page.get_by_test_id("rank-0").locator(".text-yellow-300")
expect(competitions_page.get_by_test_id("status-0")).to_have_text("Open") ).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") # FIXME: the numbers are different we need to clarify this with the backend
expect(competitions_page.get_by_test_id("games-0")).to_have_text("1") # 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 # TODO still odd that this is 0
expect(competitions_page.get_by_test_id("volume-0")).to_have_text("-") expect(competitions_page.get_by_test_id("volume-0")).to_have_text("-")
#TODO def test_games(competitions_page: Page):
#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 • tDAI")
expect(game_1.get_by_test_id("assessed-over")).to_have_text("15 epochs")
expect(game_1.get_by_test_id("scope")).to_have_text("In team")
expect(game_1.get_by_test_id("staking-requirement")).to_have_text("0.00")
expect(game_1.get_by_test_id("average-position")).to_have_text("0.00")
def test_create_team(competitions_page: Page, vega: VegaServiceNull):
change_keys(competitions_page, vega, "market_maker_2")
competitions_page.get_by_test_id("create-public-team-button").click()
competitions_page.get_by_test_id("team-name-input").fill("e2e")
competitions_page.get_by_test_id("team-url-input").fill("https://vega.xyz")
competitions_page.get_by_test_id("avatar-url-input").fill(
"http://placekitten.com/200/200"
)
competitions_page.get_by_test_id("team-form-submit-button").click()
expect(competitions_page.get_by_test_id("team-form-submit-button")).to_have_text(
"Confirming transaction..."
)
vega.wait_fn(2)
vega.wait_for_total_catchup()
expect(
competitions_page.get_by_test_id("team-creation-success-message")
).to_be_visible()
expect(competitions_page.get_by_test_id("team-id-display")).to_be_visible()
expect(competitions_page.get_by_test_id("team-id-display")).to_be_visible()
competitions_page.get_by_test_id("view-team-button").click()
expect(competitions_page.get_by_test_id("team-name")).to_have_text("e2e")
+18 -1
View File
@@ -17,7 +17,7 @@ fragment TeamStatsFields on TeamStatistics {
totalGamesPlayed totalGamesPlayed
quantumRewards { quantumRewards {
epoch epoch
total_quantum_rewards totalQuantumRewards
} }
gamesPlayed 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) { query Team($teamId: ID!, $partyId: ID, $aggregationEpochs: Int) {
teams(teamId: $teamId) { teams(teamId: $teamId) {
edges { edges {
@@ -87,4 +94,14 @@ query Team($teamId: ID!, $partyId: ID, $aggregationEpochs: Int) {
} }
} }
} }
teamMembersStatistics(
teamId: $teamId
aggregationEpochs: $aggregationEpochs
) {
edges {
node {
...TeamMemberStatsFields
}
}
}
} }
+13 -8
View File
@@ -1,15 +1,20 @@
fragment TeamsFields on Team {
teamId
referrer
name
teamUrl
avatarUrl
createdAt
createdAtEpoch
closed
totalMembers
}
query Teams($teamId: ID, $partyId: ID) { query Teams($teamId: ID, $partyId: ID) {
teams(teamId: $teamId, partyId: $partyId) { teams(teamId: $teamId, partyId: $partyId) {
edges { edges {
node { node {
teamId ...TeamsFields
referrer
name
teamUrl
avatarUrl
createdAt
createdAtEpoch
closed
} }
} }
} }
+22 -4
View File
@@ -5,7 +5,7 @@ import * as Apollo from '@apollo/client';
const defaultOptions = {} as const; const defaultOptions = {} as const;
export type TeamFieldsFragment = { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean, allowList: Array<string> }; export type TeamFieldsFragment = { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean, allowList: Array<string> };
export type TeamStatsFieldsFragment = { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string>, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, 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 }; 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 TeamGameFieldsFragment = { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: Types.DispatchMetric, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } }> };
export type TeamMemberStatsFieldsFragment = { __typename?: 'TeamMemberStatistics', partyId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number };
export type TeamQueryVariables = Types.Exact<{ export type TeamQueryVariables = Types.Exact<{
teamId: Types.Scalars['ID']; teamId: Types.Scalars['ID'];
partyId?: Types.InputMaybe<Types.Scalars['ID']>; partyId?: Types.InputMaybe<Types.Scalars['ID']>;
@@ -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` export const TeamFieldsFragmentDoc = gql`
fragment TeamFields on Team { fragment TeamFields on Team {
@@ -43,7 +45,7 @@ export const TeamStatsFieldsFragmentDoc = gql`
totalGamesPlayed totalGamesPlayed
quantumRewards { quantumRewards {
epoch epoch
total_quantum_rewards totalQuantumRewards
} }
gamesPlayed gamesPlayed
} }
@@ -80,6 +82,14 @@ export const TeamGameFieldsFragmentDoc = gql`
} }
} }
${TeamEntityFragmentDoc}`; ${TeamEntityFragmentDoc}`;
export const TeamMemberStatsFieldsFragmentDoc = gql`
fragment TeamMemberStatsFields on TeamMemberStatistics {
partyId
totalQuantumVolume
totalQuantumRewards
totalGamesPlayed
}
`;
export const TeamDocument = gql` export const TeamDocument = gql`
query Team($teamId: ID!, $partyId: ID, $aggregationEpochs: Int) { query Team($teamId: ID!, $partyId: ID, $aggregationEpochs: Int) {
teams(teamId: $teamId) { teams(teamId: $teamId) {
@@ -117,11 +127,19 @@ export const TeamDocument = gql`
} }
} }
} }
teamMembersStatistics(teamId: $teamId, aggregationEpochs: $aggregationEpochs) {
edges {
node {
...TeamMemberStatsFields
}
}
}
} }
${TeamFieldsFragmentDoc} ${TeamFieldsFragmentDoc}
${TeamStatsFieldsFragmentDoc} ${TeamStatsFieldsFragmentDoc}
${TeamRefereeFieldsFragmentDoc} ${TeamRefereeFieldsFragmentDoc}
${TeamGameFieldsFragmentDoc}`; ${TeamGameFieldsFragmentDoc}
${TeamMemberStatsFieldsFragmentDoc}`;
/** /**
* __useTeamQuery__ * __useTeamQuery__
+18 -11
View File
@@ -3,33 +3,40 @@ import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client'; import * as Apollo from '@apollo/client';
const defaultOptions = {} as const; const defaultOptions = {} as const;
export type TeamsFieldsFragment = { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean, totalMembers: number };
export type TeamsQueryVariables = Types.Exact<{ export type TeamsQueryVariables = Types.Exact<{
teamId?: Types.InputMaybe<Types.Scalars['ID']>; teamId?: Types.InputMaybe<Types.Scalars['ID']>;
partyId?: Types.InputMaybe<Types.Scalars['ID']>; partyId?: Types.InputMaybe<Types.Scalars['ID']>;
}>; }>;
export type TeamsQuery = { __typename?: 'Query', teams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean } }> } | 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` export const TeamsDocument = gql`
query Teams($teamId: ID, $partyId: ID) { query Teams($teamId: ID, $partyId: ID) {
teams(teamId: $teamId, partyId: $partyId) { teams(teamId: $teamId, partyId: $partyId) {
edges { edges {
node { node {
teamId ...TeamsFields
referrer
name
teamUrl
avatarUrl
createdAt
createdAtEpoch
closed
} }
} }
} }
} }
`; ${TeamsFieldsFragmentDoc}`;
/** /**
* __useTeamsQuery__ * __useTeamsQuery__
+14 -3
View File
@@ -1,12 +1,23 @@
import compact from 'lodash/compact'; import compact from 'lodash/compact';
import { useActiveRewardsQuery } from '../../components/rewards-container/__generated__/Rewards'; import { useActiveRewardsQuery } from '../../components/rewards-container/__generated__/Rewards';
import { isActiveReward } from '../../components/rewards-container/active-rewards'; import { isActiveReward } from '../../components/rewards-container/active-rewards';
import { EntityScope, type TransferNode } from '@vegaprotocol/types'; import {
EntityScope,
IndividualScope,
type TransferNode,
} from '@vegaprotocol/types';
const isScopedToTeams = (node: TransferNode) => const isScopedToTeams = (node: TransferNode) =>
node.transfer.kind.__typename === 'RecurringTransfer' && node.transfer.kind.__typename === 'RecurringTransfer' &&
node.transfer.kind.dispatchStrategy?.entityScope === // scoped to teams
EntityScope.ENTITY_SCOPE_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 = ({ export const useGames = ({
currentEpoch, currentEpoch,
+25
View File
@@ -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 };
};
+49 -10
View File
@@ -6,17 +6,24 @@ import {
type TeamStatsFieldsFragment, type TeamStatsFieldsFragment,
type TeamRefereeFieldsFragment, type TeamRefereeFieldsFragment,
type TeamEntityFragment, type TeamEntityFragment,
type TeamMemberStatsFieldsFragment,
} from './__generated__/Team'; } from './__generated__/Team';
import { DEFAULT_AGGREGATION_EPOCHS } from './use-teams'; import { DEFAULT_AGGREGATION_EPOCHS } from './use-teams';
export type Team = TeamFieldsFragment; export type Team = TeamFieldsFragment;
export type TeamStats = TeamStatsFieldsFragment; export type TeamStats = TeamStatsFieldsFragment;
export type Member = TeamRefereeFieldsFragment; export type Member = TeamRefereeFieldsFragment & {
isCreator: boolean;
totalGamesPlayed: number;
totalQuantumVolume: string;
totalQuantumRewards: string;
};
export type TeamEntity = TeamEntityFragment; export type TeamEntity = TeamEntityFragment;
export type TeamGame = ReturnType<typeof useTeam>['games'][number]; export type TeamGame = ReturnType<typeof useTeam>['games'][number];
export type MemberStats = TeamMemberStatsFieldsFragment;
export const useTeam = (teamId?: string, partyId?: string) => { export const useTeam = (teamId?: string, partyId?: string) => {
const { data, loading, error, refetch } = useTeamQuery({ const queryResult = useTeamQuery({
variables: { variables: {
teamId: teamId || '', teamId: teamId || '',
partyId, partyId,
@@ -26,7 +33,11 @@ export const useTeam = (teamId?: string, partyId?: string) => {
fetchPolicy: 'cache-and-network', fetchPolicy: 'cache-and-network',
}); });
const { data } = queryResult;
const teamEdge = data?.teams?.edges.find((e) => e.node.teamId === teamId); const teamEdge = data?.teams?.edges.find((e) => e.node.teamId === teamId);
const team = teamEdge?.node;
const partyTeam = data?.partyTeams?.edges?.length const partyTeam = data?.partyTeams?.edges?.length
? data.partyTeams.edges[0].node ? data.partyTeams.edges[0].node
: undefined; : undefined;
@@ -34,9 +45,40 @@ export const useTeam = (teamId?: string, partyId?: string) => {
const teamStatsEdge = data?.teamsStatistics?.edges.find( const teamStatsEdge = data?.teamsStatistics?.edges.find(
(e) => e.node.teamId === teamId (e) => e.node.teamId === teamId
); );
const members = data?.teamReferees?.edges
.filter((e) => e.node.teamId === teamId) const memberStats = data?.teamMembersStatistics?.edges.length
.map((e) => e.node); ? 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 // Find games where the current team participated in
const gamesWithTeam = compact(data?.games.edges).map((edge) => { 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'); const games = orderBy(compact(gamesWithTeam), 'epoch', 'desc');
return { return {
data, ...queryResult,
loading,
error,
refetch,
stats: teamStatsEdge?.node, stats: teamStatsEdge?.node,
team: teamEdge?.node, team,
members, members,
games, games,
partyTeam, partyTeam,
+10 -33
View File
@@ -1,34 +1,13 @@
import orderBy from 'lodash/orderBy';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { type TeamsQuery, useTeamsQuery } from './__generated__/Teams'; import { useTeamsQuery } from './__generated__/Teams';
import { import { useTeamsStatisticsQuery } from './__generated__/TeamsStatistics';
type TeamsStatisticsQuery,
useTeamsStatisticsQuery,
} from './__generated__/TeamsStatistics';
import compact from 'lodash/compact'; import compact from 'lodash/compact';
import sortBy from 'lodash/sortBy';
import { type ArrayElement } from 'type-fest/source/internal';
type SortableField = keyof Omit< // 192
ArrayElement<NonNullable<TeamsQuery['teams']>['edges']>['node'] & export const DEFAULT_AGGREGATION_EPOCHS = 192;
ArrayElement<
NonNullable<TeamsStatisticsQuery['teamsStatistics']>['edges']
>['node'],
'__typename'
>;
type UseTeamsArgs = { export const useTeams = (aggregationEpochs = DEFAULT_AGGREGATION_EPOCHS) => {
aggregationEpochs?: number;
sortByField?: SortableField[];
order?: 'asc' | 'desc';
};
export const DEFAULT_AGGREGATION_EPOCHS = 10;
export const useTeams = ({
aggregationEpochs = DEFAULT_AGGREGATION_EPOCHS,
sortByField = ['createdAtEpoch'],
order = 'asc',
}: UseTeamsArgs) => {
const { const {
data: teamsData, data: teamsData,
loading: teamsLoading, loading: teamsLoading,
@@ -57,12 +36,10 @@ export const useTeams = ({
...stats.find((s) => s.teamId === t.teamId), ...stats.find((s) => s.teamId === t.teamId),
})); }));
const sorted = sortBy(data, sortByField); return orderBy(data, (d) => Number(d.totalQuantumRewards || 0), 'desc').map(
if (order === 'desc') { (d, i) => ({ ...d, rank: i + 1 })
return sorted.reverse(); );
} }, [teams, stats]);
return sorted;
}, [teams, sortByField, order, stats]);
return { return {
data, data,
+2 -13
View File
@@ -14,7 +14,7 @@ import './styles.css';
import { usePageTitleStore } from '../stores'; import { usePageTitleStore } from '../stores';
import DialogsContainer from './dialogs-container'; import DialogsContainer from './dialogs-container';
import ToastsManager from './toasts-manager'; import ToastsManager from './toasts-manager';
import { HashRouter, useLocation, Route, Routes } from 'react-router-dom'; import { HashRouter, useLocation } from 'react-router-dom';
import { Bootstrapper } from '../components/bootstrapper'; import { Bootstrapper } from '../components/bootstrapper';
import { AnnouncementBanner } from '../components/banner'; import { AnnouncementBanner } from '../components/banner';
import { Navbar } from '../components/navbar'; import { Navbar } from '../components/navbar';
@@ -25,9 +25,7 @@ import {
ProtocolUpgradeProposalNotification, ProtocolUpgradeProposalNotification,
} from '@vegaprotocol/proposals'; } from '@vegaprotocol/proposals';
import { ViewingBanner } from '../components/viewing-banner'; import { ViewingBanner } from '../components/viewing-banner';
import { NavHeader } from '../components/navbar/nav-header';
import { Telemetry } from '../components/telemetry'; import { Telemetry } from '../components/telemetry';
import { Routes as AppRoutes } from '../lib/links';
import { SSRLoader } from './ssr-loader'; import { SSRLoader } from './ssr-loader';
import { PartyActiveOrdersHandler } from './party-active-orders-handler'; import { PartyActiveOrdersHandler } from './party-active-orders-handler';
import { MaybeConnectEagerly } from './maybe-connect-eagerly'; import { MaybeConnectEagerly } from './maybe-connect-eagerly';
@@ -73,16 +71,7 @@ function AppBody({ Component }: AppProps) {
<Title /> <Title />
<div className={gridClasses}> <div className={gridClasses}>
<AnnouncementBanner /> <AnnouncementBanner />
<Navbar theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'system'}> <Navbar theme={VEGA_ENV === Networks.TESTNET ? 'yellow' : 'system'} />
<Routes>
<Route
path={AppRoutes.MARKETS}
// render nothing for markets/all, otherwise markets/:marketId will match with markets/all
element={null}
/>
<Route path={AppRoutes.MARKET} element={<NavHeader />} />
</Routes>
</Navbar>
<div data-testid="banners"> <div data-testid="banners">
<ProtocolUpgradeProposalNotification <ProtocolUpgradeProposalNotification
mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING} mode={ProtocolUpgradeCountdownMode.IN_ESTIMATED_TIME_REMAINING}
+5 -2
View File
@@ -24,11 +24,14 @@ export default function Document() {
{/* scripts */} {/* scripts */}
<script src="/theme-setter.js" type="text/javascript" async /> <script src="/theme-setter.js" type="text/javascript" async />
{/* manifest */}
<link rel="manifest" href="/apps/trading/public/manifest.json" />
</Head> </Head>
<Html> <Html>
<body <body
// Nextjs will set body to display none until js runs. Because the entire app is client rendered // 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 // and delivered via IPFS we override this to show a server side render loading animation until the
// js is downloaded and react takes over rendering // js is downloaded and react takes over rendering
style={{ display: 'block' }} style={{ display: 'block' }}
className="bg-white dark:bg-vega-cdark-900 text-default font-alpha" className="bg-white dark:bg-vega-cdark-900 text-default font-alpha"
+28 -12
View File
@@ -23,8 +23,11 @@ import { NotFound as ReferralNotFound } from '../client-pages/referrals/error-bo
import { compact } from 'lodash'; import { compact } from 'lodash';
import { useFeatureFlags } from '@vegaprotocol/environment'; import { useFeatureFlags } from '@vegaprotocol/environment';
import { LiquidityHeader } from '../components/liquidity-header'; import { LiquidityHeader } from '../components/liquidity-header';
import { MarketHeader } from '../components/market-header'; import { MarketHeader, MobileMarketHeader } from '../components/market-header';
import { PortfolioSidebar } from '../client-pages/portfolio/portfolio-sidebar'; import {
PortfolioMobileSidebar,
PortfolioSidebar,
} from '../client-pages/portfolio/portfolio-sidebar';
import { LiquiditySidebar } from '../client-pages/liquidity/liquidity-sidebar'; import { LiquiditySidebar } from '../client-pages/liquidity/liquidity-sidebar';
import { MarketsSidebar } from '../client-pages/markets/markets-sidebar'; import { MarketsSidebar } from '../client-pages/markets/markets-sidebar';
import { useT } from '../lib/use-t'; import { useT } from '../lib/use-t';
@@ -33,8 +36,10 @@ import { CompetitionsTeams } from '../client-pages/competitions/competitions-tea
import { CompetitionsTeam } from '../client-pages/competitions/competitions-team'; import { CompetitionsTeam } from '../client-pages/competitions/competitions-team';
import { CompetitionsCreateTeam } from '../client-pages/competitions/competitions-create-team'; import { CompetitionsCreateTeam } from '../client-pages/competitions/competitions-create-team';
import { CompetitionsUpdateTeam } from '../client-pages/competitions/competitions-update-team'; import { CompetitionsUpdateTeam } from '../client-pages/competitions/competitions-update-team';
import { MarketsMobileSidebar } from '../client-pages/markets/mobile-buttons';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
// These must remain dynamically imported as pennant cannot be compiled by 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 // Using dynamic imports is a workaround for this until pennant is published as ESM
const MarketPage = lazy(() => import('../client-pages/market')); const MarketPage = lazy(() => import('../client-pages/market'));
const Portfolio = lazy(() => import('../client-pages/portfolio')); const Portfolio = lazy(() => import('../client-pages/portfolio'));
@@ -50,6 +55,20 @@ const NotFound = () => {
export const useRouterConfig = (): RouteObject[] => { export const useRouterConfig = (): RouteObject[] => {
const featureFlags = useFeatureFlags((state) => state.flags); const featureFlags = useFeatureFlags((state) => state.flags);
const { screenSize } = useScreenDimensions();
const largeScreen = ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize);
const marketHeader = largeScreen ? <MarketHeader /> : <MobileMarketHeader />;
const marketsSidebar = largeScreen ? (
<MarketsSidebar />
) : (
<MarketsMobileSidebar />
);
const portfolioSidebar = largeScreen ? (
<PortfolioSidebar />
) : (
<PortfolioMobileSidebar />
);
const routeConfig = compact([ const routeConfig = compact([
{ {
index: true, index: true,
@@ -66,7 +85,7 @@ export const useRouterConfig = (): RouteObject[] => {
featureFlags.REFERRALS featureFlags.REFERRALS
? { ? {
path: AppRoutes.REFERRALS, path: AppRoutes.REFERRALS,
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />, element: <LayoutWithSidebar sidebar={portfolioSidebar} />,
children: [ children: [
{ {
element: ( element: (
@@ -99,7 +118,7 @@ export const useRouterConfig = (): RouteObject[] => {
featureFlags.TEAM_COMPETITION featureFlags.TEAM_COMPETITION
? { ? {
path: AppRoutes.COMPETITIONS, path: AppRoutes.COMPETITIONS,
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />, element: <LayoutWithSidebar sidebar={portfolioSidebar} />,
children: [ children: [
// pages with planets and stars // pages with planets and stars
{ {
@@ -130,7 +149,7 @@ export const useRouterConfig = (): RouteObject[] => {
: undefined, : undefined,
{ {
path: 'fees/*', path: 'fees/*',
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />, element: <LayoutWithSidebar sidebar={portfolioSidebar} />,
children: [ children: [
{ {
index: true, index: true,
@@ -140,7 +159,7 @@ export const useRouterConfig = (): RouteObject[] => {
}, },
{ {
path: 'rewards/*', path: 'rewards/*',
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />, element: <LayoutWithSidebar sidebar={portfolioSidebar} />,
children: [ children: [
{ {
index: true, index: true,
@@ -151,10 +170,7 @@ export const useRouterConfig = (): RouteObject[] => {
{ {
path: 'markets/*', path: 'markets/*',
element: ( element: (
<LayoutWithSidebar <LayoutWithSidebar header={marketHeader} sidebar={marketsSidebar} />
header={<MarketHeader />}
sidebar={<MarketsSidebar />}
/>
), ),
children: [ children: [
{ {
@@ -175,7 +191,7 @@ export const useRouterConfig = (): RouteObject[] => {
}, },
{ {
path: 'portfolio/*', path: 'portfolio/*',
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />, element: <LayoutWithSidebar sidebar={portfolioSidebar} />,
children: [ children: [
{ {
index: true, index: true,
+22
View File
@@ -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"
}
]
}
+18
View File
@@ -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
View File
@@ -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 t = useT();
const { pubKey, pubKeys, isReadOnly } = useVegaWallet(); const { pubKey, pubKeys, isReadOnly } = useVegaWallet();
const { params } = useNetworkParams([ const { params } = useNetworkParams([
NetworkParams.transfer_fee_factor,
NetworkParams.transfer_minTransferQuantumMultiple, NetworkParams.transfer_minTransferQuantumMultiple,
]); ]);
@@ -72,7 +71,6 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
pubKeys={pubKeys ? pubKeys?.map((pk) => pk.publicKey) : null} pubKeys={pubKeys ? pubKeys?.map((pk) => pk.publicKey) : null}
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
assetId={assetId} assetId={assetId}
feeFactor={params.transfer_fee_factor}
minQuantumMultiple={params.transfer_minTransferQuantumMultiple} minQuantumMultiple={params.transfer_minTransferQuantumMultiple}
submitTransfer={transfer} submitTransfer={transfer}
accounts={sortedAccounts} accounts={sortedAccounts}
+79 -145
View File
@@ -15,6 +15,30 @@ import {
} from './transfer-form'; } from './transfer-form';
import { AccountType, AccountTypeMapping } from '@vegaprotocol/types'; import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
import { removeDecimal } from '@vegaprotocol/utils'; import { removeDecimal } from '@vegaprotocol/utils';
import type { TransferFeeQuery } from './__generated__/TransferFee';
const feeFactor = 0.001;
const mockUseTransferFeeQuery = jest.fn(
({
variables: { amount },
}: {
variables: { amount: string };
}): { data: TransferFeeQuery } => {
return {
data: {
estimateTransferFee: {
discount: '0',
fee: (Number(amount) * feeFactor).toFixed(),
},
},
};
}
);
jest.mock('./__generated__/TransferFee', () => ({
useTransferFeeQuery: (props: { variables: { amount: string } }) =>
mockUseTransferFeeQuery(props),
}));
describe('TransferForm', () => { describe('TransferForm', () => {
const renderComponent = (props: TransferFormProps) => { const renderComponent = (props: TransferFormProps) => {
@@ -56,7 +80,6 @@ describe('TransferForm', () => {
const props = { const props = {
pubKey, pubKey,
pubKeys: [pubKey, '2'.repeat(64)], pubKeys: [pubKey, '2'.repeat(64)],
feeFactor: '0.001',
submitTransfer: jest.fn(), submitTransfer: jest.fn(),
accounts: [ accounts: [
{ {
@@ -79,7 +102,6 @@ describe('TransferForm', () => {
pubKey, pubKey,
'a4b6e3de5d7ef4e31ae1b090be49d1a2ef7bcefff60cccf7658a0d4922651cce', 'a4b6e3de5d7ef4e31ae1b090be49d1a2ef7bcefff60cccf7658a0d4922651cce',
], ],
feeFactor: '0.001',
submitTransfer: jest.fn(), submitTransfer: jest.fn(),
accounts: [], accounts: [],
minQuantumMultiple: '1', minQuantumMultiple: '1',
@@ -96,15 +118,6 @@ describe('TransferForm', () => {
}); });
it.each([ it.each([
{
targetText: 'Include transfer fee',
tooltipText:
'The fee will be taken from the amount you are transferring.',
},
{
targetText: 'Transfer fee',
tooltipText: /transfer\.fee\.factor/,
},
{ {
targetText: 'Amount to be transferred', targetText: 'Amount to be transferred',
tooltipText: /without the fee/, tooltipText: /without the fee/,
@@ -114,9 +127,6 @@ describe('TransferForm', () => {
tooltipText: /total amount taken from your account/, tooltipText: /total amount taken from your account/,
}, },
])('Tooltip for "$targetText" shows', async (o) => { ])('Tooltip for "$targetText" shows', async (o) => {
// 1003-TRAN-015
// 1003-TRAN-016
// 1003-TRAN-017
// 1003-TRAN-018 // 1003-TRAN-018
// 1003-TRAN-019 // 1003-TRAN-019
renderComponent(props); renderComponent(props);
@@ -129,6 +139,10 @@ describe('TransferForm', () => {
// Select asset // Select asset
await selectAsset(asset); await selectAsset(asset);
await userEvent.selectOptions(
screen.getByLabelText('From account'),
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
);
// set valid amount // set valid amount
const amountInput = screen.getByLabelText('Amount'); const amountInput = screen.getByLabelText('Amount');
await userEvent.type(amountInput, amount); await userEvent.type(amountInput, amount);
@@ -219,9 +233,7 @@ describe('TransferForm', () => {
// set valid amount // set valid amount
await userEvent.clear(amountInput); await userEvent.clear(amountInput);
await userEvent.type(amountInput, amount); await userEvent.type(amountInput, amount);
expect(screen.getByTestId('transfer-fee')).toHaveTextContent( expect(screen.getByTestId('transfer-fee')).toHaveTextContent('1');
new BigNumber(props.feeFactor).times(amount).toFixed()
);
await submit(); await submit();
@@ -276,9 +288,6 @@ describe('TransferForm', () => {
const amountInput = screen.getByLabelText('Amount'); const amountInput = screen.getByLabelText('Amount');
const checkbox = screen.getByTestId('include-transfer-fee');
expect(checkbox).not.toBeChecked();
await userEvent.clear(amountInput); await userEvent.clear(amountInput);
await userEvent.type(amountInput, '50'); await userEvent.type(amountInput, '50');
@@ -288,10 +297,7 @@ describe('TransferForm', () => {
await userEvent.click(screen.getByRole('button', { name: 'Use max' })); await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
expect(amountInput).toHaveValue('100.00'); expect(amountInput).toHaveValue('100.00');
// If transfering from a vested account 'include fees' checkbox should // If transfering from a vested account fees should be 0
// be disabled and fees should be 0
expect(checkbox).not.toBeChecked();
expect(checkbox).toBeDisabled();
const expectedFee = '0'; const expectedFee = '0';
const total = new BigNumber(amount).plus(expectedFee).toFixed(); const total = new BigNumber(amount).plus(expectedFee).toFixed();
@@ -397,120 +403,43 @@ describe('TransferForm', () => {
}); });
}); });
describe('IncludeFeesCheckbox', () => { it('validates fields', async () => {
it('validates fields and submits when checkbox is checked', async () => { renderComponent(props);
const mockSubmit = jest.fn();
renderComponent({ ...props, submitTransfer: mockSubmit });
// check current pubkey not shown // check current pubkey not shown
const keySelect = screen.getByLabelText<HTMLSelectElement>('To Vega key'); const keySelect: HTMLSelectElement = screen.getByLabelText('To Vega key');
const pubKeyOptions = ['', pubKey, props.pubKeys[1]]; const pubKeyOptions = ['', pubKey, props.pubKeys[1]];
expect(keySelect.children).toHaveLength(pubKeyOptions.length); expect(keySelect.children).toHaveLength(pubKeyOptions.length);
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual( expect(Array.from(keySelect.options).map((o) => o.value)).toEqual(
pubKeyOptions pubKeyOptions
); );
await submit(); await submit();
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value
// Select a pubkey // Select a pubkey
await userEvent.selectOptions( await userEvent.selectOptions(
screen.getByLabelText('To Vega key'), screen.getByLabelText('To Vega key'),
props.pubKeys[1] props.pubKeys[1]
); );
// Select asset // Select asset
await selectAsset(asset); await selectAsset(asset);
await userEvent.selectOptions( await userEvent.selectOptions(
screen.getByLabelText('From account'), screen.getByLabelText('From account'),
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}` `${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
); );
const amountInput = screen.getByLabelText('Amount'); const amountInput = screen.getByLabelText('Amount');
const checkbox = screen.getByTestId('include-transfer-fee');
// 1003-TRAN-022 await userEvent.type(amountInput, amount);
expect(checkbox).not.toBeChecked(); const expectedFee = new BigNumber(amount).times(feeFactor).toFixed();
const total = new BigNumber(amount).plus(expectedFee).toFixed();
await userEvent.clear(amountInput); // 1003-TRAN-021
await userEvent.type(amountInput, amount); expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
await userEvent.click(checkbox); expect(screen.getByTestId('transfer-amount')).toHaveTextContent(amount);
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total);
expect(checkbox).toBeChecked();
const expectedFee = new BigNumber(amount)
.times(props.feeFactor)
.toFixed();
const expectedAmount = new BigNumber(amount).minus(expectedFee).toFixed();
// 1003-TRAN-020
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(
expectedAmount
);
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(
amount
);
await submit();
await waitFor(() => {
// 1003-TRAN-023
expect(mockSubmit).toHaveBeenCalledTimes(1);
expect(mockSubmit).toHaveBeenCalledWith({
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
to: props.pubKeys[1],
asset: asset.id,
amount: removeDecimal(expectedAmount, asset.decimals),
oneOff: {},
});
});
});
it('validates fields when checkbox is not checked', async () => {
renderComponent(props);
// check current pubkey not shown
const keySelect: HTMLSelectElement = screen.getByLabelText('To Vega key');
const pubKeyOptions = ['', pubKey, props.pubKeys[1]];
expect(keySelect.children).toHaveLength(pubKeyOptions.length);
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual(
pubKeyOptions
);
await submit();
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value
// Select a pubkey
await userEvent.selectOptions(
screen.getByLabelText('To Vega key'),
props.pubKeys[1]
);
// Select asset
await selectAsset(asset);
await userEvent.selectOptions(
screen.getByLabelText('From account'),
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
);
const amountInput = screen.getByLabelText('Amount');
const checkbox = screen.getByTestId('include-transfer-fee');
expect(checkbox).not.toBeChecked();
await userEvent.type(amountInput, amount);
expect(checkbox).not.toBeChecked();
const expectedFee = new BigNumber(amount)
.times(props.feeFactor)
.toFixed();
const total = new BigNumber(amount).plus(expectedFee).toFixed();
// 1003-TRAN-021
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(amount);
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total);
});
}); });
describe('AddressField', () => { describe('AddressField', () => {
@@ -542,24 +471,29 @@ describe('TransferForm', () => {
describe('TransferFee', () => { describe('TransferFee', () => {
const props = { const props = {
amount: '200', amount: '20000',
feeFactor: '0.001', discount: '0',
fee: '0.2', fee: '20',
transferAmount: '200', decimals: 2,
decimals: 8,
}; };
it('calculates and renders the transfer fee', () => { it('calculates and renders amounts and fee', () => {
render(<TransferFee {...props} />); render(<TransferFee {...props} />);
expect(screen.queryByTestId('discount')).not.toBeInTheDocument();
expect(screen.getByTestId('transfer-fee')).toHaveTextContent('0.2');
expect(screen.getByTestId('transfer-amount')).toHaveTextContent('200.00');
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(
'200.20'
);
});
const expected = new BigNumber(props.amount) it('calculates and renders amounts, fee and discount', () => {
.times(props.feeFactor) render(<TransferFee {...props} discount="10" />);
.toFixed(); expect(screen.getByTestId('discount')).toHaveTextContent('0.1');
const total = new BigNumber(props.amount).plus(expected).toFixed(); expect(screen.getByTestId('transfer-fee')).toHaveTextContent('0.2');
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expected); expect(screen.getByTestId('transfer-amount')).toHaveTextContent('200.00');
expect(screen.getByTestId('transfer-amount')).toHaveTextContent( expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(
props.amount '200.10'
); );
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total);
}); });
}); });
}); });
+52 -76
View File
@@ -4,8 +4,9 @@ import {
useRequired, useRequired,
useVegaPublicKey, useVegaPublicKey,
addDecimal, addDecimal,
formatNumber,
toBigNum, toBigNum,
removeDecimal,
addDecimalsFormatNumber,
} from '@vegaprotocol/utils'; } from '@vegaprotocol/utils';
import { useT } from './use-t'; import { useT } from './use-t';
import { import {
@@ -15,17 +16,17 @@ import {
TradingRichSelect, TradingRichSelect,
TradingSelect, TradingSelect,
Tooltip, Tooltip,
TradingCheckbox,
TradingButton, TradingButton,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import type { Transfer } from '@vegaprotocol/wallet'; import type { Transfer } from '@vegaprotocol/wallet';
import { normalizeTransfer } from '@vegaprotocol/wallet'; import { normalizeTransfer } from '@vegaprotocol/wallet';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { AssetOption, Balance } from '@vegaprotocol/assets'; import { AssetOption, Balance } from '@vegaprotocol/assets';
import { AccountType, AccountTypeMapping } from '@vegaprotocol/types'; import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
import { useTransferFeeQuery } from './__generated__/TransferFee';
interface FormFields { interface FormFields {
toVegaKey: string; toVegaKey: string;
@@ -52,7 +53,6 @@ export interface TransferFormProps {
asset: Asset; asset: Asset;
}>; }>;
assetId?: string; assetId?: string;
feeFactor: string | null;
minQuantumMultiple: string | null; minQuantumMultiple: string | null;
submitTransfer: (transfer: Transfer) => void; submitTransfer: (transfer: Transfer) => void;
} }
@@ -62,7 +62,6 @@ export const TransferForm = ({
pubKeys, pubKeys,
isReadOnly, isReadOnly,
assetId: initialAssetId, assetId: initialAssetId,
feeFactor,
submitTransfer, submitTransfer,
accounts, accounts,
minQuantumMultiple, minQuantumMultiple,
@@ -135,32 +134,28 @@ export const TransferForm = ({
const accountBalance = const accountBalance =
account && addDecimal(account.balance, account.asset.decimals); account && addDecimal(account.balance, account.asset.decimals);
const [includeFee, setIncludeFee] = useState(false);
// Max amount given selected asset and from account // Max amount given selected asset and from account
const max = accountBalance ? new BigNumber(accountBalance) : new BigNumber(0); const max = accountBalance ? new BigNumber(accountBalance) : new BigNumber(0);
const normalizedAmount =
(amount && asset && removeDecimal(amount, asset.decimals)) || '0';
const transferAmount = useMemo(() => { const transferFeeQuery = useTransferFeeQuery({
if (!amount) return undefined; variables: {
if (includeFee && feeFactor) { fromAccount: pubKey || '',
return new BigNumber(1).minus(feeFactor).times(amount).toString(); fromAccountType: accountType || AccountType.ACCOUNT_TYPE_GENERAL,
} amount: normalizedAmount,
return amount; assetId: asset?.id || '',
}, [amount, includeFee, feeFactor]); toAccount: selectedPubKey,
},
const fee = useMemo(() => { skip: !pubKey || !amount || !asset || !selectedPubKey || fromVested,
if (!transferAmount) return undefined; });
if (includeFee) { const transferFee = transferFeeQuery.loading
return new BigNumber(amount).minus(transferAmount).toString(); ? transferFeeQuery.data || transferFeeQuery.previousData
} : transferFeeQuery.data;
return (
feeFactor && new BigNumber(feeFactor).times(transferAmount).toString()
);
}, [amount, includeFee, transferAmount, feeFactor]);
const onSubmit = useCallback( const onSubmit = useCallback(
(fields: FormFields) => { (fields: FormFields) => {
if (!transferAmount) { if (!amount) {
throw new Error('Submitted transfer with no amount selected'); throw new Error('Submitted transfer with no amount selected');
} }
@@ -173,7 +168,7 @@ export const TransferForm = ({
const transfer = normalizeTransfer( const transfer = normalizeTransfer(
fields.toVegaKey, fields.toVegaKey,
transferAmount, amount,
type, type,
AccountType.ACCOUNT_TYPE_GENERAL, // field is readonly in the form AccountType.ACCOUNT_TYPE_GENERAL, // field is readonly in the form
{ {
@@ -183,7 +178,7 @@ export const TransferForm = ({
); );
submitTransfer(transfer); submitTransfer(transfer);
}, },
[submitTransfer, transferAmount, assets] [submitTransfer, amount, assets]
); );
// reset for placeholder workaround https://github.com/radix-ui/primitives/issues/1569 // reset for placeholder workaround https://github.com/radix-ui/primitives/issues/1569
@@ -279,7 +274,6 @@ export const TransferForm = ({
) { ) {
setValue('toVegaKey', pubKey); setValue('toVegaKey', pubKey);
setToVegaKeyMode('select'); setToVegaKeyMode('select');
setIncludeFee(false);
} }
}} }}
> >
@@ -449,30 +443,14 @@ export const TransferForm = ({
</TradingInputError> </TradingInputError>
)} )}
</TradingFormGroup> </TradingFormGroup>
<div className="mb-4"> {(transferFee?.estimateTransferFee || fromVested) && amount && asset && (
<Tooltip
description={t(
`The fee will be taken from the amount you are transferring.`
)}
>
<div>
<TradingCheckbox
name="include-transfer-fee"
disabled={!transferAmount || fromVested}
label={t('Include transfer fee')}
checked={includeFee}
onCheckedChange={() => setIncludeFee((x) => !x)}
/>
</div>
</Tooltip>
</div>
{transferAmount && fee && (
<TransferFee <TransferFee
amount={transferAmount} amount={normalizedAmount}
transferAmount={transferAmount} fee={fromVested ? '0' : transferFee?.estimateTransferFee?.fee}
feeFactor={feeFactor} discount={
fee={fromVested ? '0' : fee} fromVested ? '0' : transferFee?.estimateTransferFee?.discount
decimals={asset?.decimals} }
decimals={asset.decimals}
/> />
)} )}
<TradingButton type="submit" fill={true} disabled={isReadOnly}> <TradingButton type="submit" fill={true} disabled={isReadOnly}>
@@ -484,46 +462,44 @@ export const TransferForm = ({
export const TransferFee = ({ export const TransferFee = ({
amount, amount,
transferAmount,
feeFactor,
fee, fee,
discount,
decimals, decimals,
}: { }: {
amount: string; amount: string;
transferAmount: string;
feeFactor: string | null;
fee?: string; fee?: string;
decimals?: number; discount?: string;
decimals: number;
}) => { }) => {
const t = useT(); const t = useT();
if (!feeFactor || !amount || !transferAmount || !fee) return null; if (!amount || !fee) return null;
if ( if (isNaN(Number(amount)) || isNaN(Number(fee))) {
isNaN(Number(feeFactor)) ||
isNaN(Number(amount)) ||
isNaN(Number(transferAmount)) ||
isNaN(Number(fee))
) {
return null; return null;
} }
const totalValue = new BigNumber(transferAmount).plus(fee).toString(); const totalValue = (
BigInt(amount) +
BigInt(fee) -
BigInt(discount || '0')
).toString();
return ( return (
<div className="mb-4 flex flex-col gap-2 text-xs"> <div className="mb-4 flex flex-col gap-2 text-xs">
<div className="flex flex-wrap items-center justify-between gap-1"> <div className="flex flex-wrap items-center justify-between gap-1">
<Tooltip <div>{t('Transfer fee')}</div>
description={t(
`The transfer fee is set by the network parameter transfer.fee.factor, currently set to {{feeFactor}}`,
{ feeFactor }
)}
>
<div>{t('Transfer fee')}</div>
</Tooltip>
<div data-testid="transfer-fee" className="text-muted"> <div data-testid="transfer-fee" className="text-muted">
{formatNumber(fee, decimals)} {addDecimalsFormatNumber(fee, decimals)}
</div> </div>
</div> </div>
{discount && discount !== '0' && (
<div className="flex flex-wrap items-center justify-between gap-1">
<div>{t('Discount')}</div>
<div data-testid="discount" className="text-muted">
{addDecimalsFormatNumber(discount, decimals)}
</div>
</div>
)}
<div className="flex flex-wrap items-center justify-between gap-1"> <div className="flex flex-wrap items-center justify-between gap-1">
<Tooltip <Tooltip
description={t( description={t(
@@ -534,7 +510,7 @@ export const TransferFee = ({
</Tooltip> </Tooltip>
<div data-testid="transfer-amount" className="text-muted"> <div data-testid="transfer-amount" className="text-muted">
{formatNumber(amount, decimals)} {addDecimalsFormatNumber(amount, decimals)}
</div> </div>
</div> </div>
<div className="flex flex-wrap items-center justify-between gap-1"> <div className="flex flex-wrap items-center justify-between gap-1">
@@ -547,7 +523,7 @@ export const TransferFee = ({
</Tooltip> </Tooltip>
<div data-testid="total-transfer-fee" className="text-muted"> <div data-testid="total-transfer-fee" className="text-muted">
{formatNumber(totalValue, decimals)} {addDecimalsFormatNumber(totalValue, decimals)}
</div> </div>
</div> </div>
</div> </div>
@@ -3,27 +3,13 @@ import { getAsset, getQuoteName } from '@vegaprotocol/markets';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import { AccountBreakdownDialog } from '@vegaprotocol/accounts'; import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
import { formatRange, formatValue } from '@vegaprotocol/utils'; import { formatRange, formatValue } from '@vegaprotocol/utils';
import { marketMarginDataProvider } from '@vegaprotocol/accounts';
import { useDataProvider } from '@vegaprotocol/data-provider';
import * as AccordionPrimitive from '@radix-ui/react-accordion';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import { import {
MARGIN_DIFF_TOOLTIP_TEXT,
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
TOTAL_MARGIN_AVAILABLE,
LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT, LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT,
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
MARGIN_ACCOUNT_TOOLTIP_TEXT, MARGIN_ACCOUNT_TOOLTIP_TEXT,
} from '../../constants'; } from '../../constants';
import { KeyValue } from './key-value'; import { KeyValue } from './key-value';
import { import { ExternalLink } from '@vegaprotocol/ui-toolkit';
Accordion,
AccordionChevron,
AccordionPanel,
ExternalLink,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import { useT, ns } from '../../use-t'; import { useT, ns } from '../../use-t';
import { Trans } from 'react-i18next'; import { Trans } from 'react-i18next';
import type { Market } from '@vegaprotocol/markets'; import type { Market } from '@vegaprotocol/markets';
@@ -31,9 +17,9 @@ import { emptyValue } from './deal-ticket-fee-details';
import type { EstimatePositionQuery } from '@vegaprotocol/positions'; import type { EstimatePositionQuery } from '@vegaprotocol/positions';
export interface DealTicketMarginDetailsProps { export interface DealTicketMarginDetailsProps {
generalAccountBalance?: string; generalAccountBalance: string;
marginAccountBalance?: string; marginAccountBalance: string;
orderMarginAccountBalance?: string; orderMarginAccountBalance: string;
market: Market; market: Market;
onMarketClick?: (marketId: string, metaKey?: boolean) => void; onMarketClick?: (marketId: string, metaKey?: boolean) => void;
assetSymbol: string; assetSymbol: string;
@@ -54,118 +40,20 @@ export const DealTicketMarginDetails = ({
const t = useT(); const t = useT();
const [breakdownDialog, setBreakdownDialog] = useState(false); const [breakdownDialog, setBreakdownDialog] = useState(false);
const { pubKey: partyId } = useVegaWallet(); const { pubKey: partyId } = useVegaWallet();
const { data: currentMargins } = useDataProvider({
dataProvider: marketMarginDataProvider,
variables: { marketId: market.id, partyId: partyId || '' },
skip: !partyId,
});
const liquidationEstimate = positionEstimate?.liquidation; const liquidationEstimate = positionEstimate?.liquidation;
const marginEstimate = positionEstimate?.margin;
const totalMarginAccountBalance = const totalMarginAccountBalance =
BigInt(marginAccountBalance || '0') + BigInt(marginAccountBalance || '0') +
BigInt(orderMarginAccountBalance || '0'); BigInt(orderMarginAccountBalance || '0');
const totalBalance =
BigInt(generalAccountBalance || '0') + totalMarginAccountBalance;
const asset = getAsset(market); const asset = getAsset(market);
const { decimals: assetDecimals, quantum } = asset; const { decimals: assetDecimals, quantum } = asset;
let marginRequiredBestCase: string | undefined = undefined;
let marginRequiredWorstCase: string | undefined = undefined;
const marginEstimateBestCase =
BigInt(marginEstimate?.bestCase.initialLevel ?? 0) +
BigInt(marginEstimate?.bestCase.orderMarginLevel ?? 0);
const marginEstimateWorstCase =
BigInt(marginEstimate?.worstCase.initialLevel ?? 0) +
BigInt(marginEstimate?.worstCase.orderMarginLevel ?? 0);
if (marginEstimate) {
if (currentMargins) {
const currentMargin =
BigInt(currentMargins.initialLevel) +
BigInt(currentMargins.orderMarginLevel);
marginRequiredBestCase = ( const collateralIncreaseEstimateBestCase = BigInt(
marginEstimateBestCase - currentMargin positionEstimate?.collateralIncreaseEstimate.bestCase ?? '0'
).toString(); );
if (marginRequiredBestCase.startsWith('-')) { const collateralIncreaseEstimateWorstCase = BigInt(
marginRequiredBestCase = '0'; positionEstimate?.collateralIncreaseEstimate.worstCase ?? '0'
} );
marginRequiredWorstCase = (
marginEstimateWorstCase - currentMargin
).toString();
if (marginRequiredWorstCase.startsWith('-')) {
marginRequiredWorstCase = '0';
}
} else {
marginRequiredBestCase = marginEstimateBestCase.toString();
marginRequiredWorstCase = marginEstimateWorstCase.toString();
}
}
const totalMarginAvailable = (
currentMargins
? totalBalance - BigInt(currentMargins.maintenanceLevel)
: totalBalance
).toString();
let deductionFromCollateral = null;
let projectedMargin = null;
if (totalMarginAccountBalance) {
const deductionFromCollateralBestCase =
marginEstimateBestCase - totalMarginAccountBalance;
const deductionFromCollateralWorstCase =
marginEstimateWorstCase - totalMarginAccountBalance;
deductionFromCollateral = (
<KeyValue
indent
label={t('Deduction from collateral')}
value={formatRange(
deductionFromCollateralBestCase > 0
? deductionFromCollateralBestCase.toString()
: '0',
deductionFromCollateralWorstCase > 0
? deductionFromCollateralWorstCase.toString()
: '0',
assetDecimals
)}
formattedValue={formatValue(
deductionFromCollateralWorstCase > 0
? deductionFromCollateralWorstCase.toString()
: '0',
assetDecimals,
quantum
)}
symbol={assetSymbol}
labelDescription={t(
'DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT',
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
{ assetSymbol }
)}
/>
);
projectedMargin = (
<KeyValue
label={t('Projected margin')}
value={formatRange(
marginEstimateBestCase.toString(),
marginEstimateWorstCase.toString(),
assetDecimals
)}
formattedValue={formatValue(
marginEstimateWorstCase.toString(),
assetDecimals,
quantum
)}
symbol={assetSymbol}
labelDescription={t(
'EST_TOTAL_MARGIN_TOOLTIP_TEXT',
EST_TOTAL_MARGIN_TOOLTIP_TEXT
)}
/>
);
}
let liquidationPriceEstimate = emptyValue; let liquidationPriceEstimate = emptyValue;
let liquidationPriceEstimateRange = emptyValue; let liquidationPriceEstimateRange = emptyValue;
@@ -222,128 +110,50 @@ export const DealTicketMarginDetails = ({
const quoteName = getQuoteName(market); const quoteName = getQuoteName(market);
return ( return (
<div className="flex flex-col w-full gap-2"> <div className="flex flex-col w-full gap-2 mt-2">
<Accordion>
<AccordionPanel
itemId="margin"
trigger={
<AccordionPrimitive.Trigger
data-testid="accordion-toggle"
className={classNames(
'w-full pt-2',
'flex items-center gap-2 text-xs',
'group'
)}
>
<div
data-testid={`deal-ticket-fee-margin-required`}
key={'value-dropdown'}
className="flex items-center justify-between w-full gap-2"
>
<div className="flex items-center text-left gap-1">
<Tooltip
description={t(
'MARGIN_DIFF_TOOLTIP_TEXT',
MARGIN_DIFF_TOOLTIP_TEXT,
{ assetSymbol }
)}
>
<span className="text-muted">{t('Margin required')}</span>
</Tooltip>
<AccordionChevron size={10} />
</div>
<Tooltip
description={
formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals
) ?? '-'
}
>
<div className="font-mono text-right">
{formatValue(
marginRequiredWorstCase,
assetDecimals,
quantum
)}{' '}
{assetSymbol || ''}
</div>
</Tooltip>
</div>
</AccordionPrimitive.Trigger>
}
>
<div className="flex flex-col w-full gap-2">
<KeyValue
label={t('Total margin available')}
indent
value={formatValue(totalMarginAvailable, assetDecimals)}
formattedValue={formatValue(
totalMarginAvailable,
assetDecimals,
quantum
)}
symbol={assetSymbol}
labelDescription={t(
'TOTAL_MARGIN_AVAILABLE',
TOTAL_MARGIN_AVAILABLE,
{
generalAccountBalance: formatValue(
generalAccountBalance,
assetDecimals,
quantum
),
marginAccountBalance: formatValue(
marginAccountBalance,
assetDecimals,
quantum
),
orderMarginAccountBalance: formatValue(
orderMarginAccountBalance,
assetDecimals,
quantum
),
marginMaintenance: formatValue(
currentMargins?.maintenanceLevel,
assetDecimals,
quantum
),
assetSymbol,
}
)}
/>
{deductionFromCollateral}
<KeyValue
label={t('Current margin allocation')}
indent
onClick={
generalAccountBalance
? () => setBreakdownDialog(true)
: undefined
}
value={formatValue(
totalMarginAccountBalance.toString(),
assetDecimals
)}
symbol={assetSymbol}
labelDescription={t(
'MARGIN_ACCOUNT_TOOLTIP_TEXT',
MARGIN_ACCOUNT_TOOLTIP_TEXT
)}
formattedValue={formatValue(
totalMarginAccountBalance.toString(),
assetDecimals,
quantum
)}
/>
</div>
</AccordionPanel>
</Accordion>
{projectedMargin}
<KeyValue <KeyValue
label={t('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} value={liquidationPriceEstimateRange}
formattedValue={liquidationPriceEstimate} formattedValue={liquidationPriceEstimate}
symbol={quoteName} symbol={quoteName}
@@ -73,7 +73,7 @@ import {
} from '../../hooks'; } from '../../hooks';
import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg'; import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg';
import noop from 'lodash/noop'; import noop from 'lodash/noop';
import { isNonPersistentOrder } from '../../utils/time-in-force-persistance'; import { isNonPersistentOrder } from '../../utils/time-in-force-persistence';
import { KeyValue } from './key-value'; import { KeyValue } from './key-value';
import { DocsLinks } from '@vegaprotocol/environment'; import { DocsLinks } from '@vegaprotocol/environment';
import { useT } from '../../use-t'; import { useT } from '../../use-t';
@@ -177,12 +177,6 @@ export const DealTicket = ({
loading: loadingGeneralAccountBalance, loading: loadingGeneralAccountBalance,
} = useAccountBalance(asset.id); } = useAccountBalance(asset.id);
const balance = (
BigInt(marginAccountBalance) +
BigInt(generalAccountBalance) +
BigInt(orderMarginAccountBalance)
).toString();
const { marketState, marketTradingMode } = marketData; const { marketState, marketTradingMode } = marketData;
const timeInForce = watch('timeInForce'); const timeInForce = watch('timeInForce');
@@ -729,17 +723,11 @@ export const DealTicket = ({
error={summaryError} error={summaryError}
asset={asset} asset={asset}
marketTradingMode={marketData.marketTradingMode} marketTradingMode={marketData.marketTradingMode}
balance={balance} balance={generalAccountBalance}
margin={( margin={
BigInt( positionEstimate?.estimatePosition?.collateralIncreaseEstimate
positionEstimate?.estimatePosition?.margin.bestCase.initialLevel || .bestCase || '0'
'0' }
) +
BigInt(
positionEstimate?.estimatePosition?.margin.bestCase
.orderMarginLevel || '0'
)
).toString()}
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
pubKey={pubKey} pubKey={pubKey}
onDeposit={onDeposit} onDeposit={onDeposit}

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