Compare commits

...
Author SHA1 Message Date
asiaznik 7176ff63f1 fix: align headers 2024-02-15 15:48:13 +00:00
bwallacee fb7860ec4f chore(trading): fix test 2024-02-15 15:48:13 +00:00
asiaznik 034afe7e54 chore: add reward asset id to query 2024-02-15 15:48:13 +00:00
asiaznik 725b5f1b23 fix: remove debug id column 2024-02-15 15:48:13 +00:00
asiaznik f50e813ed2 fix: transfer picking 2024-02-15 15:48:13 +00:00
asiaznik 80620abaff feat(trading): game results table 2024-02-15 15:47:57 +00:00
Matthew RussellandBen 8268acac6d fix(trading): add missing funding panel, fix 24 hr price/volume change (#5788)
Co-authored-by: Ben <ben@vega.xyz>
2024-02-15 15:41:04 +00:00
daro-majandBen 4ad1a47ded chore(trading): add cross isolated margin info test (#5786)
Co-authored-by: Ben <ben@vega.xyz>
2024-02-15 08:17:38 +00:00
daro-majandbwallacee fea97aac19 chore(trading): update vega version for sim tests - 0.74.0 (#5784)
Co-authored-by: bwallacee <ben@vega.xyz>
2024-02-14 09:31:28 +00:00
Ben f652292e02 chore(trading): add missed vega cleanup (#5794) 2024-02-14 09:19:52 +00:00
JonRay15 9195bf8c91 Merge pull request #5789 from vegaprotocol/fix/5677-gov-rewards-table
fix(governance): dont show all reward types in gov rewards table
2024-02-13 16:18:02 +00:00
Jeremy Letang cd481512f3 Merge pull request #5771 from vegaprotocol/feat/4306-transfer-status
feat(explorer): transfer status and games overview
2024-02-13 16:17:14 +00:00
Matthew Russell 8fe6f3f5f2 chore(trading): misc fixes (#5785) 2024-02-13 16:17:00 +00:00
Art 1c2389dee5 fix(trading): game cards colour, missing market info, other tweaks (#5791) 2024-02-13 13:38:01 +00:00
Jeremy Letang 1e6a2debfc Merge pull request #5783 from vegaprotocol/5781-update-EstimatePosition-argument
feat(deal-ticket): rename EstimatePosition argument
2024-02-13 10:26:02 +00:00
Matthew Russell fc2773d748 fix: make sure asset is not added to list if it doesn't container correct reward types 2024-02-12 13:42:49 -08:00
Matthew Russell b2860121e5 fix: dont show unrelated rewards in gov rewards table 2024-02-12 13:32:45 -08:00
Matthew Russell db5e5ee782 chore(trading, governance, explorer): update snap to 1.0.1 (#5768) 2024-02-12 18:38:31 +00:00
Art 0d3bcf05a1 chore(trading): team page refactor, games with specified epoch from (#5772) 2024-02-12 15:03:21 +01:00
Matthew RussellandMadalina Raicu c7dd5e846a fix(trading): validate whitespace team names, show empty in list (#5727)
Co-authored-by: Madalina Raicu <madalina@raygroup.uk>
2024-02-12 13:23:12 +00:00
Dariusz Majcherczyk 61e5941751 fix(deal-ticket): margin test 2024-02-09 21:14:36 +01: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 8db286fa93 feat(deal-ticket): rename EstimatePosition argument 2024-02-09 15:02:09 +01: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
Edd e5635cae61 test(explorer): add tests for games display 2024-02-08 12:26:32 +00:00
Ben 46e2965fa2 chore(trading): fix test teardown (#5769) 2024-02-08 12:25:16 +00:00
Edd b01c67ced5 chore(explorer): update transfer status query to match latest preview 2024-02-08 10:54:00 +00:00
Edd 636b1f98db chore(explorer): relayout transfers 2024-02-08 10:51:10 +00:00
Edd c31a927526 chore(explorer): progress on rank table 2024-02-08 10:51:10 +00:00
Edd 5803d6e890 feat(explorer): complete transfer status 2024-02-08 10:51:10 +00:00
Edd cb0dd17839 chore(explorer): more fields for rewards 2024-02-08 10:51:10 +00:00
Edd 496b0b5a90 feat(explorer): extend transfers view 2024-02-08 10:51:10 +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
184 changed files with 4788 additions and 1819 deletions
+2 -2
View File
@@ -10,7 +10,7 @@ on:
inputs:
console-test-branch:
type: choice
description: 'main: v0.73.5, develop: v0.73.5'
description: 'main: v0.73.13, develop: v0.74.0'
options:
- main
- develop
@@ -205,7 +205,7 @@ jobs:
# run tests
#----------------------------------------------
- name: Run tests
run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v --numprocesses 1 --dist loadfile --durations=45
run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v --numprocesses 4 --dist loadfile --durations=45
working-directory: apps/trading/e2e
#----------------------------------------------
# upload traces
+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).
# 🔎 Applications in this repo
## 🔎 Applications in this repo
### [Block explorer](./apps/explorer)
@@ -30,7 +30,7 @@ Hosting for static content being shared across apps, for example fonts.
The utility dApp for validators wishing to add or remove themselves as a signer of the multisig contract.
# 🧱 Libraries in this repo
## 🧱 Libraries in this repo
### [UI toolkit](./libs/ui-toolkit)
@@ -53,7 +53,7 @@ A utility library for connecting to the Ethereum network and interacting with Ve
Generic react helpers that can be used across multiple applications, along with other utilities.
# 💻 Develop
## 💻 Develop
### Set up
@@ -103,7 +103,7 @@ In CI linting, formatting and also run. These checks can be seen in the [CI work
Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more.
# 🐋 Hosting a console
## 🐋 Hosting a console
To host a console there are two possible build scenarios for running the frontends: nx performed **outside** or **inside** docker build. For specific build instructions follow [build instructions](#build-instructions).
@@ -226,6 +226,6 @@ Note: The script is only needed if capsule was built for first time or fresh. To
vega wallet service run -n DV --load-tokens --tokens-passphrase-file passphrase --no-version-check --automatic-consent --home ~/.vegacapsule/testnet/wallet
```
# 📑 License
## 📑 License
[MIT](./LICENSE)
@@ -24,10 +24,6 @@ context('Proposal page', { tags: '@smoke' }, function () {
cy.get_element_by_col_id('title').should('have.text', proposalTitle);
cy.get_element_by_col_id('type').should('have.text', 'NewMarket');
cy.get_element_by_col_id('state').should('have.text', 'Enacted');
cy.getByTestId('vote-progress').should('be.visible');
cy.getByTestId('vote-progress-bar-for')
.invoke('attr', 'style')
.should('eq', 'width: 100%;');
cy.get('[col-id="cDate"]')
.invoke('text')
.should('match', dateTimeRegex);
@@ -73,10 +69,6 @@ context('Proposal page', { tags: '@smoke' }, function () {
'have.text',
'Waiting for Node Vote'
);
cy.getByTestId('vote-progress').should('be.visible');
cy.getByTestId('vote-progress-bar-against')
.invoke('attr', 'style')
.should('eq', 'width: 100%;');
cy.get('[col-id="cDate"]')
.invoke('text')
.should('match', dateTimeRegex);
@@ -1,4 +1,4 @@
export type HashProps = {
export type HashProps = React.HTMLProps<HTMLSpanElement> & {
text: string;
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 NodeLink } from './node-link/node-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';
@@ -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 label = proposal?.rationale.title || id;
const label = proposal?.rationale?.title || id;
return (
<ExternalLink href={`${base}/proposals/${id}`}>
@@ -1,5 +1,4 @@
import { type ProposalListFieldsFragment } from '@vegaprotocol/proposals';
import { VoteProgress } from '@vegaprotocol/proposals';
import { type AgGridReact } from 'ag-grid-react';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { AgGrid } from '@vegaprotocol/datagrid';
@@ -12,12 +11,7 @@ import { type ColDef } from 'ag-grid-community';
import type { RowClickedEvent } from 'ag-grid-community';
import { getDateTimeFormat } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { ProposalStateMapping } from '@vegaprotocol/types';
import BigNumber from 'bignumber.js';
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
import { BREAKPOINT_MD } from '../../config/breakpoints';
import { JsonViewerDialog } from '../dialogs/json-viewer-dialog';
@@ -31,15 +25,7 @@ type ProposalsTableProps = {
data: ProposalListFieldsFragment[] | null;
};
export const ProposalsTable = ({ data }: ProposalsTableProps) => {
const { params } = useNetworkParams([
NetworkParams.governance_proposal_market_requiredMajority,
]);
const tokenLink = useLinks(DApp.Governance);
const requiredMajorityPercentage = useMemo(() => {
const requiredMajority =
params?.governance_proposal_market_requiredMajority ?? 1;
return new BigNumber(requiredMajority).times(100);
}, [params?.governance_proposal_market_requiredMajority]);
const gridRef = useRef<AgGridReact>(null);
useLayoutEffect(() => {
@@ -90,33 +76,6 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
return value ? ProposalStateMapping[value] : '-';
},
},
{
colId: 'voting',
maxWidth: 100,
hide: window.innerWidth <= BREAKPOINT_MD,
headerName: t('Voting'),
cellRenderer: ({
data,
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
if (data) {
const yesTokens = new BigNumber(data.votes.yes.totalTokens);
const noTokens = new BigNumber(data.votes.no.totalTokens);
const totalTokensVoted = yesTokens.plus(noTokens);
const yesPercentage = totalTokensVoted.isZero()
? new BigNumber(0)
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
return (
<div className="flex h-full items-center justify-center pt-2 uppercase">
<VoteProgress
threshold={requiredMajorityPercentage}
progress={yesPercentage}
/>
</div>
);
}
return '-';
},
},
{
colId: 'cDate',
maxWidth: 150,
@@ -184,7 +143,7 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
},
},
],
[requiredMajorityPercentage, tokenLink]
[tokenLink]
);
return (
<>
@@ -5,5 +5,10 @@ query ExplorerProposalStatus($id: ID!) {
state
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`
@@ -19,6 +19,11 @@ export const ExplorerProposalStatusDocument = gql`
state
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 type { components } from '../../../../../types/explorer';
import { JsonViewerDialog } from '../../../dialogs/json-viewer-dialog';
import ProposalLink from '../../../links/proposal-link/proposal-link';
import truncate from 'lodash/truncate';
@@ -9,7 +7,12 @@ import ReactMarkdown from 'react-markdown';
import { ProposalDate } from './proposal-date';
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 Batch = components['schemas']['v1BatchProposalSubmissionTerms']['changes'];
type ProposalTermsDialog = {
open: boolean;
@@ -21,6 +24,7 @@ interface ProposalSummaryProps {
id: string;
rationale?: Rationale;
terms?: ProposalTerms;
batch?: Batch;
}
/**
@@ -31,6 +35,7 @@ export const ProposalSummary = ({
id,
rationale,
terms,
batch,
}: ProposalSummaryProps) => {
const [dialog, setDialog] = useState<ProposalTermsDialog>({
open: false,
@@ -59,7 +64,9 @@ export const ProposalSummary = ({
return (
<div className="w-auto max-w-lg border-2 border-solid border-vega-light-100 dark:border-vega-dark-200 p-5">
{id && <ProposalStatusIcon id={id} />}
{rationale?.title && <h1 className="text-xl pb-1">{rationale.title}</h1>}
{rationale?.title && (
<h1 className="text-xl pb-1 break-all">{rationale.title}</h1>
)}
{rationale?.description && (
<div className="pt-2 text-sm leading-tight">
<ReactMarkdown
@@ -72,6 +79,18 @@ export const ProposalSummary = ({
</ReactMarkdown>
</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">
<button className="underline max-md:hidden mr-5" onClick={openDialog}>
{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,18 @@
query ExplorerTransferStatus($id: ID!) {
transfer(id: $id) {
transfer {
reference
timestamp
status
reason
fromAccountType
from
to
toAccountType
asset {
id
}
amount
}
}
}
@@ -0,0 +1,61 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerTransferStatusQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
export type ExplorerTransferStatusQuery = { __typename?: 'Query', transfer?: { __typename?: 'TransferNode', transfer: { __typename?: 'Transfer', reference?: string | null, timestamp: any, status: Types.TransferStatus, reason?: string | null, fromAccountType: Types.AccountType, from: string, to: string, toAccountType: Types.AccountType, amount: string, asset?: { __typename?: 'Asset', id: string } | null } } | null };
export const ExplorerTransferStatusDocument = gql`
query ExplorerTransferStatus($id: ID!) {
transfer(id: $id) {
transfer {
reference
timestamp
status
reason
fromAccountType
from
to
toAccountType
asset {
id
}
amount
}
}
}
`;
/**
* __useExplorerTransferStatusQuery__
*
* To run a query within a React component, call `useExplorerTransferStatusQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerTransferStatusQuery` 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 } = useExplorerTransferStatusQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useExplorerTransferStatusQuery(baseOptions: Apollo.QueryHookOptions<ExplorerTransferStatusQuery, ExplorerTransferStatusQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerTransferStatusQuery, ExplorerTransferStatusQueryVariables>(ExplorerTransferStatusDocument, options);
}
export function useExplorerTransferStatusLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerTransferStatusQuery, ExplorerTransferStatusQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerTransferStatusQuery, ExplorerTransferStatusQueryVariables>(ExplorerTransferStatusDocument, options);
}
export type ExplorerTransferStatusQueryHookResult = ReturnType<typeof useExplorerTransferStatusQuery>;
export type ExplorerTransferStatusLazyQueryHookResult = ReturnType<typeof useExplorerTransferStatusLazyQuery>;
export type ExplorerTransferStatusQueryResult = Apollo.QueryResult<ExplorerTransferStatusQuery, ExplorerTransferStatusQueryVariables>;
@@ -111,7 +111,7 @@ export function TransferParticipants({
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 9"
className="fill-vega-light-100 dark:fill-black"
className="fill-white dark:fill-black"
>
<path d="M0,0L8,9l8,-9Z" />
</svg>
@@ -120,7 +120,7 @@ export function TransferParticipants({
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 9"
className="fill-vega-light-100 dark:fill-vega-dark-200"
className="fill-vega-light-200 dark:fill-vega-dark-200"
>
<path d="M0,0L8,9l8,-9Z" />
</svg>
@@ -1,97 +1,223 @@
import { t } from '@vegaprotocol/i18n';
import { AssetLink, MarketLink } from '../../../../links';
import { headerClasses, wrapperClasses } from '../transfer-details';
import type { components } from '../../../../../../types/explorer';
import type { Recurring } from '../transfer-details';
import { DispatchMetricLabels } from '@vegaprotocol/types';
import {
DispatchMetricLabels,
DistributionStrategy,
} from '@vegaprotocol/types';
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '@vegaprotocol/utils';
export type Metric = components['schemas']['vegaDispatchMetric'];
export type Strategy = components['schemas']['vegaDispatchStrategy'];
export const wrapperClasses = 'border pv-2 w-full flex-auto basis-full';
export const headerClasses =
'bg-solid bg-vega-light-150 dark:bg-vega-dark-150 text-center text-xl py-2 font-alpha calt';
const metricLabels: Record<Metric, string> = {
DISPATCH_METRIC_UNSPECIFIED: 'Unknown metric',
...DispatchMetricLabels,
};
// Maps the two (non-null) values of entityScope to the icon that represents it
const entityScopeIcons: Record<
string,
typeof VegaIconNames[keyof typeof VegaIconNames]
> = {
ENTITY_SCOPE_INDIVIDUALS: VegaIconNames.MAN,
ENTITY_SCOPE_TEAMS: VegaIconNames.TEAM,
};
const distributionStrategyLabel: Record<DistributionStrategy, string> = {
[DistributionStrategy.DISTRIBUTION_STRATEGY_PRO_RATA]: 'Pro Rata',
[DistributionStrategy.DISTRIBUTION_STRATEGY_RANK]: 'Ranked',
};
interface TransferRewardsProps {
recurring: Recurring;
}
/**
* Renderer for a transfer. These can vary quite
* widely, essentially every field can be null.
* Renders recurring transfers/game details in a way that is, perhaps, easy to understand
*
* @param transfer A recurring transfer object
*/
export function TransferRewards({ recurring }: TransferRewardsProps) {
const metric =
recurring?.dispatchStrategy?.metric || 'DISPATCH_METRIC_UNSPECIFIED';
if (!recurring || !recurring.dispatchStrategy) {
return null;
}
// Destructure to make things a bit more readable
const {
entityScope,
individualScope,
teamScope,
distributionStrategy,
lockPeriod,
markets,
stakingRequirement,
windowLength,
notionalTimeWeightedAveragePositionRequirement,
rankTable,
nTopPerformers,
} = recurring.dispatchStrategy;
return (
<div className={wrapperClasses}>
<h2 className={headerClasses}>{t('Reward metrics')}</h2>
<ul className="relative block rounded-lg py-6 text-center p-6">
{recurring.dispatchStrategy.assetForMetric ? (
<h2 className={headerClasses}>{getRewardTitle(entityScope)}</h2>
<ul className="relative block rounded-lg py-6 text-left p-6">
{entityScope && entityScopeIcons[entityScope] ? (
<li>
<strong>{t('Asset')}</strong>:{' '}
<AssetLink assetId={recurring.dispatchStrategy.assetForMetric} />
<strong>{t('Scope')}</strong>:{' '}
<VegaIcon name={entityScopeIcons[entityScope]} />
&nbsp;
{individualScope ? individualScopeLabels[individualScope] : null}
{getScopeLabel(entityScope, teamScope)}
</li>
) : null}
<li>
<strong>{t('Metric')}</strong>: {metricLabels[metric]}
</li>
{recurring.dispatchStrategy.markets &&
recurring.dispatchStrategy.markets.length > 0 ? (
{recurring.dispatchStrategy &&
recurring.dispatchStrategy.assetForMetric && (
<li>
<strong>{t('Asset for metric')}</strong>:{' '}
<AssetLink assetId={recurring.dispatchStrategy.assetForMetric} />
</li>
)}
{recurring.dispatchStrategy.metric &&
metricLabels[recurring.dispatchStrategy.metric] && (
<li>
<strong>{t('Metric')}</strong>:{' '}
{metricLabels[recurring.dispatchStrategy.metric]}
</li>
)}
{lockPeriod && (
<li>
<strong>{t('Reward lock')}</strong>:&nbsp;
{recurring.dispatchStrategy.lockPeriod}{' '}
{recurring.dispatchStrategy.lockPeriod === '1'
? t('epoch')
: t('epochs')}
</li>
)}
{markets && markets.length > 0 ? (
<li>
<strong>{t('Markets in scope')}</strong>:
<ul>
{recurring.dispatchStrategy.markets.map((m) => (
<li key={m}>
<ul className="inline-block ml-1">
{markets.map((m) => (
<li key={m} className="inline-block mr-2">
<MarketLink id={m} />
</li>
))}
</ul>
</li>
) : null}
<li>
<strong>{t('Factor')}</strong>: {recurring.factor}
</li>
{stakingRequirement && stakingRequirement !== '0' ? (
<li>
<strong>{t('Staking requirement')}</strong>: {stakingRequirement}
</li>
) : null}
{windowLength && windowLength !== '0' ? (
<li>
<strong>{t('Window length')}</strong>:{' '}
{recurring.dispatchStrategy.windowLength}{' '}
{recurring.dispatchStrategy.windowLength === '1'
? t('epoch')
: t('epochs')}
</li>
) : null}
{notionalTimeWeightedAveragePositionRequirement &&
notionalTimeWeightedAveragePositionRequirement !== '' ? (
<li>
<strong>{t('Notional TWAP')}</strong>:{' '}
{notionalTimeWeightedAveragePositionRequirement}
</li>
) : null}
{nTopPerformers && (
<li>
<strong>{t('Elligible team members:')}</strong> top{' '}
{`${formatNumber(Number(nTopPerformers) * 100, 0)}%`}
</li>
)}
{distributionStrategy &&
distributionStrategy !== 'DISTRIBUTION_STRATEGY_UNSPECIFIED' && (
<li>
<strong>{t('Distribution strategy')}</strong>:{' '}
{distributionStrategyLabel[distributionStrategy]}
</li>
)}
</ul>
<div className="px-6 pt-1 pb-5">
{rankTable && rankTable.length > 0 ? (
<table className="border-collapse border border-gray-400 ">
<thead>
<tr>
<th className="border border-gray-300 bg-gray-300 px-3">
<strong>{t('Start rank')}</strong>
</th>
<th className="border border-gray-300 bg-gray-300 px-3">
<strong>{t('Share of reward pool')}</strong>
</th>
</tr>
</thead>
<tbody>
{rankTable.map((row, i) => {
return (
<tr key={`rank-${i}`}>
<td className="border border-slate-300 text-center">
{row.startRank}
</td>
<td className="border border-slate-300 text-center">
{row.shareRatio}
</td>
</tr>
);
})}
</tbody>
</table>
) : null}
</div>
</div>
);
}
interface TransferRecurringStrategyProps {
strategy: Strategy;
}
/**
* Simple renderer for a dispatch strategy in a recurring transfer
*
* @param strategy Dispatch strategy object
*/
export function TransferRecurringStrategy({
strategy,
}: TransferRecurringStrategyProps) {
if (!strategy) {
return null;
export function getScopeLabel(
scope: components['schemas']['vegaEntityScope'] | undefined,
teamScope: readonly string[] | undefined
): string {
if (scope === 'ENTITY_SCOPE_TEAMS') {
if (teamScope && teamScope.length !== 0) {
return ` ${teamScope.length} teams`;
} else {
return t('All teams');
}
} else if (scope === 'ENTITY_SCOPE_INDIVIDUALS') {
return t('Individuals');
} else {
return '';
}
return (
<>
{strategy.assetForMetric ? (
<li>
<strong>{t('Asset for metric')}</strong>:{' '}
<AssetLink assetId={strategy.assetForMetric} />
</li>
) : null}
<li>
<strong>{t('Metric')}</strong>: {strategy.metric}
</li>
</>
);
}
export function getRewardTitle(
scope?: components['schemas']['vegaEntityScope']
) {
if (scope === 'ENTITY_SCOPE_TEAMS') {
return t('Game');
}
return t('Reward metrics');
}
const individualScopeLabels: Record<
components['schemas']['vegaIndividualScope'],
string
> = {
// Unspecified and All are not rendered
INDIVIDUAL_SCOPE_UNSPECIFIED: '',
INDIVIDUAL_SCOPE_ALL: '',
INDIVIDUAL_SCOPE_IN_TEAM: '(in team)',
INDIVIDUAL_SCOPE_NOT_IN_TEAM: '(not in team)',
};
@@ -0,0 +1,84 @@
import { t } from '@vegaprotocol/i18n';
import { headerClasses, wrapperClasses } from '../transfer-details';
import { Icon, Loader } from '@vegaprotocol/ui-toolkit';
import type { IconName } from '@vegaprotocol/ui-toolkit';
import type { ApolloError } from '@apollo/client';
import { TransferStatus, TransferStatusMapping } from '@vegaprotocol/types';
import { IconNames } from '@blueprintjs/icons';
interface TransferStatusProps {
status: TransferStatus | undefined;
error: ApolloError | undefined;
loading: boolean;
}
/**
* Renderer for a transfer. These can vary quite
* widely, essentially every field can be null.
*
* @param transfer A recurring transfer object
*/
export function TransferStatusView({ status, loading }: TransferStatusProps) {
if (!status) {
status = TransferStatus.STATUS_PENDING;
}
return (
<div className={wrapperClasses}>
<h2 className={headerClasses}>{t('Status')}</h2>
<div className="relative block rounded-lg py-6 text-center p-6">
{loading ? (
<div className="leading-10 mt-12">
<Loader size={'small'} />
</div>
) : (
<>
<p className="leading-10 my-2">
<Icon
name={getIconForStatus(status)}
className={getColourForStatus(status)}
/>
</p>
<p className="leading-10 my-2">{TransferStatusMapping[status]}</p>
</>
)}
</div>
</div>
);
}
/**
* Simple mapping from status to icon name
* @param status TransferStatus
* @returns IconName
*/
export function getIconForStatus(status: TransferStatus): IconName {
switch (status) {
case TransferStatus.STATUS_PENDING:
return IconNames.TIME;
case TransferStatus.STATUS_DONE:
return IconNames.TICK;
case TransferStatus.STATUS_REJECTED:
return IconNames.CROSS;
default:
return IconNames.TIME;
}
}
/**
* Simple mapping from status to colour
* @param status TransferStatus
* @returns string Tailwind classname
*/
export function getColourForStatus(status: TransferStatus): string {
switch (status) {
case TransferStatus.STATUS_PENDING:
return 'text-yellow-500';
case TransferStatus.STATUS_DONE:
return 'text-green-500';
case TransferStatus.STATUS_REJECTED:
return 'text-red-500';
default:
return 'text-yellow-500';
}
}
@@ -2,12 +2,15 @@ import type { components } from '../../../../../types/explorer';
import { TransferRepeat } from './blocks/transfer-repeat';
import { TransferRewards } from './blocks/transfer-rewards';
import { TransferParticipants } from './blocks/transfer-participants';
import { useExplorerTransferStatusQuery } from './__generated__/Transfer';
import { TransferStatusView } from './blocks/transfer-status';
import { TransferStatus } from '@vegaprotocol/types';
export type Recurring = components['schemas']['commandsv1RecurringTransfer'];
export type Metric = components['schemas']['vegaDispatchMetric'];
export const wrapperClasses =
'border border-vega-light-150 dark:border-vega-dark-200 rounded-md pv-2 mb-5 w-full sm:w-1/4 min-w-[200px] ';
'border border-vega-light-150 dark:border-vega-dark-200 pv-2 w-full sm:w-1/3 basis-1/3';
export const headerClasses =
'bg-solid bg-vega-light-150 dark:bg-vega-dark-150 border-vega-light-150 text-center text-xl py-2 font-alpha calt';
@@ -16,6 +19,7 @@ export type Transfer = components['schemas']['commandsv1Transfer'];
interface TransferDetailsProps {
transfer: Transfer;
from: string;
id: string;
}
/**
@@ -24,13 +28,24 @@ interface TransferDetailsProps {
*
* @param transfer A recurring transfer object
*/
export function TransferDetails({ transfer, from }: TransferDetailsProps) {
export function TransferDetails({ transfer, from, id }: TransferDetailsProps) {
const recurring = transfer.recurring;
// Currently all this is passed in to TransferStatus, but the extra details
// may be useful in the future.
const { data, error, loading } = useExplorerTransferStatusQuery({
variables: { id },
});
const status = error
? TransferStatus.STATUS_REJECTED
: data?.transfer?.transfer.status;
return (
<div className="flex gap-5 flex-wrap">
<div className="flex flex-wrap">
<TransferParticipants from={from} transfer={transfer} />
{recurring ? <TransferRepeat recurring={transfer.recurring} /> : null}
<TransferStatusView status={status} error={error} loading={loading} />
{recurring && recurring.dispatchStrategy ? (
<TransferRewards recurring={transfer.recurring} />
) : null}
@@ -0,0 +1,172 @@
import {
getScopeLabel,
getRewardTitle,
TransferRewards,
} from './blocks/transfer-rewards';
import { render } from '@testing-library/react';
import type { components } from '../../../../../types/explorer';
import type { Recurring } from './transfer-details';
import {
DispatchMetric,
DistributionStrategy,
EntityScope,
IndividualScope,
} from '@vegaprotocol/types';
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
describe('getScopeLabel', () => {
it('should return the correct label for ENTITY_SCOPE_TEAMS with teamScope', () => {
const scope = 'ENTITY_SCOPE_TEAMS';
const teamScope = ['team1', 'team2', 'team3'];
const expectedLabel = ' 3 teams';
const result = getScopeLabel(scope, teamScope);
expect(result).toEqual(expectedLabel);
});
it('should return the correct label for ENTITY_SCOPE_TEAMS without teamScope', () => {
const scope = 'ENTITY_SCOPE_TEAMS';
const teamScope = undefined;
const expectedLabel = 'All teams';
const result = getScopeLabel(scope, teamScope);
expect(result).toEqual(expectedLabel);
});
it('should return the correct label for ENTITY_SCOPE_INDIVIDUALS', () => {
const scope = 'ENTITY_SCOPE_INDIVIDUALS';
const teamScope = undefined;
const expectedLabel = 'Individuals';
const result = getScopeLabel(scope, teamScope);
expect(result).toEqual(expectedLabel);
});
it('should return an empty string for unknown scope', () => {
const scope = 'UNKNOWN_SCOPE';
const teamScope = undefined;
const expectedLabel = '';
const result = getScopeLabel(
scope as unknown as components['schemas']['vegaEntityScope'],
teamScope
);
expect(result).toEqual(expectedLabel);
});
});
describe('getRewardTitle', () => {
it('should return the correct title for ENTITY_SCOPE_TEAMS', () => {
const scope = 'ENTITY_SCOPE_TEAMS';
const expectedTitle = 'Game';
const result = getRewardTitle(scope);
expect(result).toEqual(expectedTitle);
});
it('should return the correct title for other scopes', () => {
const scope = 'ENTITY_SCOPE_INDIVIDUALS';
const expectedTitle = 'Reward metrics';
const result = getRewardTitle(scope);
expect(result).toEqual(expectedTitle);
});
});
describe('TransferRewards', () => {
it('should render nothing if recurring dispatchStrategy is not provided', () => {
const { container } = render(
<TransferRewards recurring={null as unknown as Recurring} />
);
expect(container.firstChild).toBeNull();
});
it('should render nothing if recurring.dispatchStrategy is not provided', () => {
const { container } = render(
<TransferRewards recurring={{} as unknown as Recurring} />
);
expect(container.firstChild).toBeNull();
});
it('should render the reward details correctly', () => {
const recurring = {
dispatchStrategy: {
metric: DispatchMetric.DISPATCH_METRIC_AVERAGE_POSITION,
assetForMetric: '123',
entityScope: EntityScope.ENTITY_SCOPE_TEAMS,
individualScope: IndividualScope.INDIVIDUAL_SCOPE_IN_TEAM,
teamScope: [],
distributionStrategy:
DistributionStrategy.DISTRIBUTION_STRATEGY_PRO_RATA,
lockPeriod: 'lockPeriod',
markets: ['market1', 'market2'],
stakingRequirement: '1',
windowLength: 'windowLength',
notionalTimeWeightedAveragePositionRequirement:
'notionalTimeWeightedAveragePositionRequirement',
rankTable: [
{ startRank: 1, shareRatio: 0.2 },
{ startRank: 2, shareRatio: 0.3 },
],
nTopPerformers: 'nTopPerformers',
},
};
const { getByText } = render(
<MemoryRouter>
<MockedProvider>
<TransferRewards recurring={recurring} />
</MockedProvider>
</MemoryRouter>
);
expect(getByText('Game')).toBeInTheDocument();
expect(getByText('Scope')).toBeInTheDocument();
expect(getByText('Asset for metric')).toBeInTheDocument();
expect(getByText('Metric')).toBeInTheDocument();
expect(getByText('Reward lock')).toBeInTheDocument();
expect(getByText('Markets in scope')).toBeInTheDocument();
expect(getByText('Staking requirement')).toBeInTheDocument();
expect(getByText('Window length')).toBeInTheDocument();
expect(getByText('Notional TWAP')).toBeInTheDocument();
expect(getByText('Elligible team members:')).toBeInTheDocument();
expect(getByText('Distribution strategy')).toBeInTheDocument();
expect(getByText('Start rank')).toBeInTheDocument();
expect(getByText('Share of reward pool')).toBeInTheDocument();
});
it('should not render a rank table if recurring.dispatchStrategy.rankTable is not provided', () => {
const recurring = {
dispatchStrategy: {
entityScope: EntityScope.ENTITY_SCOPE_INDIVIDUALS,
individualScope: IndividualScope.INDIVIDUAL_SCOPE_ALL,
teamScope: ['team1', 'team2', 'team3'],
distributionStrategy:
DistributionStrategy.DISTRIBUTION_STRATEGY_PRO_RATA,
lockPeriod: 'lockPeriod',
markets: ['market1', 'market2'],
stakingRequirement: 'stakingRequirement',
windowLength: 'windowLength',
notionalTimeWeightedAveragePositionRequirement:
'notionalTimeWeightedAveragePositionRequirement',
nTopPerformers: 'nTopPerformers',
},
};
const { container } = render(
<MemoryRouter>
<MockedProvider>
<TransferRewards recurring={recurring} />
</MockedProvider>
</MemoryRouter>
);
expect(container.querySelector('table')).toBeNull();
});
});
@@ -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 { TxDetailsJoinTeam } from './tx-join-team';
import { TxDetailsUpdateMarginMode } from './tx-update-margin-mode';
import { TxBatchProposal } from './tx-batch-proposal';
import { TxDetailsUpdatePartyProfile } from './proposal/tx-update-party-profile';
interface TxDetailsWrapperProps {
txData: BlockExplorerTransactionResult | undefined;
@@ -136,6 +138,10 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
return TxDetailsJoinTeam;
case 'Update Margin Mode':
return TxDetailsUpdateMarginMode;
case 'Batch Proposal':
return TxBatchProposal;
case 'Update Party Profile':
return TxDetailsUpdatePartyProfile;
default:
return TxDetailsGeneric;
}
@@ -12,6 +12,8 @@ import { ProposalSignatureBundleNewAsset } from './proposal/signature-bundle-new
import { ProposalSignatureBundleUpdateAsset } from './proposal/signature-bundle-update';
import { MarketLink } from '../../links';
import { formatNumber } from '@vegaprotocol/utils';
import { TransferDetails } from './transfer/transfer-details';
import { proposalToTransfer } from '../lib/proposal-to-transfer';
export type Proposal = components['schemas']['v1ProposalSubmission'];
export type ProposalTerms = components['schemas']['vegaProposalTerms'];
@@ -104,6 +106,12 @@ export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => {
? ProposalSignatureBundleNewAsset
: ProposalSignatureBundleUpdateAsset;
let transfer, from;
if (proposal.terms?.newTransfer?.changes) {
transfer = proposalToTransfer(proposal.terms?.newTransfer.changes);
from = proposal.terms.newTransfer.changes.source;
}
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
@@ -149,14 +157,26 @@ export const TxProposal = ({ txData, pubKey, blockData }: TxProposalProps) => {
</>
) : null}
</TableWithTbody>
<ProposalSummary
id={deterministicId}
rationale={proposal.rationale}
terms={proposal?.terms}
/>
{proposalRequiresSignatureBundle(proposal) && (
<SignatureBundleComponent id={deterministicId} tx={tx} />
)}
{transfer && (
<div className="mt-8">
<TransferDetails
transfer={transfer}
from={from || ''}
id={deterministicId}
/>
</div>
)}
</>
);
};
@@ -13,6 +13,7 @@ import {
SPECIAL_CASE_NETWORK_ID,
} from '../../links/party-link/party-link';
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
import Hash from '../../links/hash';
type Transfer = components['schemas']['commandsv1Transfer'];
@@ -60,7 +61,7 @@ export const TxDetailsTransfer = ({
}
const from = txData.submitter;
const id = txSignatureToDeterministicId(txData.signature.value);
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
@@ -71,7 +72,7 @@ export const TxDetailsTransfer = ({
<TableRow modifier="bordered" data-testid="id">
<TableCell {...sharedHeaderProps}>{t('Transfer ID')}</TableCell>
<TableCell>
{txSignatureToDeterministicId(txData.signature.value)}
<Hash text={id} />
</TableCell>
</TableRow>
<TxDetailsShared
@@ -105,7 +106,7 @@ export const TxDetailsTransfer = ({
</TableRow>
) : null}
</TableWithTbody>
<TransferDetails from={from} transfer={transfer} />
<TransferDetails from={from} transfer={transfer} id={id} />
</>
);
};
@@ -0,0 +1,29 @@
import type { components } from '../../../../types/explorer';
type TransferProposal = components['schemas']['vegaNewTransferConfiguration'];
type ActualTransfer = components['schemas']['commandsv1Transfer'];
/**
* Converts a governance proposal for a transfer in to a transfer command that the
* TransferDetails component can then render. The types are very similar, but do not
* map precisely to each other due to some missing fields and some different field
* names.
*
* @param proposal Governance proposal for a transfer
* @returns transfer a Transfer object as if it had been submitted
*/
export function proposalToTransfer(proposal: TransferProposal): ActualTransfer {
return {
amount: proposal.amount,
asset: proposal.asset,
// On a transfer, 'from' is determined by the submitter, so there is no 'from' field
// fromAccountType does exist and is just named differently on the proposal
fromAccountType: proposal.sourceType,
oneOff: proposal.oneOff,
recurring: proposal.recurring,
// There is no reference applied on governance initiated transfers
reference: '',
to: proposal.destination,
toAccountType: proposal.destinationType,
};
}
@@ -20,6 +20,7 @@ export type FilterOption =
| 'Amend Order'
| 'Apply Referral Code'
| 'Batch Market Instructions'
| 'Batch Proposal'
| 'Cancel LiquidityProvision Order'
| 'Cancel Order'
| 'Cancel Transfer Funds'
@@ -43,6 +44,7 @@ export type FilterOption =
| 'Submit Order'
| 'Transfer Funds'
| 'Undelegate'
| 'Update Party Profile'
| 'Update Referral Set'
| 'Update Margin Mode'
| 'Validator Heartbeat'
@@ -67,11 +69,18 @@ export const filterOptions: Record<string, FilterOption[]> = {
'Cancel Transfer Funds',
'Withdraw',
],
Governance: ['Delegate', 'Undelegate', 'Vote on Proposal', 'Proposal'],
Governance: [
'Batch Proposal',
'Delegate',
'Undelegate',
'Vote on Proposal',
'Proposal',
],
Referrals: [
'Apply Referral Code',
'Create Referral Set',
'Join Team',
'Update Party Profile',
'Update Referral Set',
],
'External Data': ['Chain Event', 'Submit Oracle Data'],
+2 -2
View File
@@ -1,6 +1,6 @@
{
"short_name": "Mainnet Stats",
"name": "Vega Mainnet statistics",
"short_name": "Explorer VEGA",
"name": "Vega Protocol - Explorer",
"icons": [
{
"src": "favicon.ico",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"short_name": "Mainnet Stats",
"name": "Vega Mainnet statistics",
"short_name": "Governance VEGA",
"name": "Vega Protocol - Governance",
"icons": [
{
"src": "favicon.ico",
@@ -241,6 +241,16 @@ describe('generateEpochAssetRewardsList', () => {
amount: '5',
},
},
{
// This should not be included in the result
node: {
epoch: 2,
assetId: '3',
decimals: 18,
rewardType: AccountType.ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY,
amount: '5',
},
},
],
},
epoch: {
@@ -83,6 +83,12 @@ export const generateEpochTotalRewardsList = ({
(Number(rewardItem?.amount) || 0) + Number(reward.amount)
).toString();
// only RowAccountTypes are relevant for this table, others should
// be discarded
if (!Object.keys(RowAccountTypes).includes(reward.rewardType)) {
return acc;
}
rewards?.set(reward.rewardType, {
rewardType: reward.rewardType,
amount,
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Vega Protocol static asseets</title>
<title>Vega Protocol static assets</title>
<link rel="stylesheet" href="fonts.css" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
+1 -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_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.sentry.io/6300613
+9 -7
View File
@@ -1,6 +1,12 @@
{
"short_name": "Mainnet Stats",
"name": "Vega Mainnet statistics",
"name": "Vega Protocol - Trading",
"short_name": "Console",
"description": "Vega Protocol - Trading dApp",
"start_url": "/",
"display": "standalone",
"orientation": "portrait",
"theme_color": "#000000",
"background_color": "#ffffff",
"icons": [
{
"src": "favicon.ico",
@@ -12,9 +18,5 @@
"type": "image/png",
"sizes": "192x192"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
]
}
@@ -1,5 +1,10 @@
import { useSearchParams } from 'react-router-dom';
import { Intent, TradingAnchorButton } from '@vegaprotocol/ui-toolkit';
import { Link, useSearchParams } from 'react-router-dom';
import {
Intent,
TradingAnchorButton,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { useT } from '../../lib/use-t';
@@ -30,6 +35,19 @@ export const CompetitionsCreateTeam = () => {
<LayoutWithGradient>
<div className="mx-auto md:w-2/3 max-w-xl">
<Box className="flex flex-col gap-4">
<Link
to={Links.COMPETITIONS()}
className="text-xs inline-flex items-center gap-1 group"
>
<VegaIcon
name={VegaIconNames.CHEVRON_LEFT}
size={12}
className="text-vega-clight-100 dark:text-vega-cdark-100"
/>{' '}
<span className="group-hover:underline">
{t('Go back to the competitions')}
</span>
</Link>
<h1 className="calt text-2xl lg:text-3xl xl:text-4xl">
{isSolo ? t('Create solo team') : t('Create a team')}
</h1>
@@ -71,18 +89,29 @@ const CreateTeamFormContainer = ({ isSolo }: { isSolo: boolean }) => {
if (status === 'confirmed') {
return (
<div className="flex flex-col items-start gap-2">
<div
className="flex flex-col items-start gap-2"
data-testid="team-creation-success-message"
>
<p className="text-sm">{t('Team creation transaction successful')}</p>
{code && (
<>
<p className="text-sm">
Your team ID is:{' '}
<span className="font-mono break-all">{code}</span>
</p>
<dl>
<dt className="text-sm">{t('Your team ID:')}</dt>
<dl>
<span
className="font-mono break-all bg-rainbow bg-clip-text text-transparent text-2xl"
data-testid="team-id-display"
>
{code}
</span>
</dl>
</dl>
<TradingAnchorButton
href={Links.COMPETITIONS_TEAM(code)}
intent={Intent.Info}
size="small"
data-testid="view-team-button"
>
{t('View team')}
</TradingAnchorButton>
@@ -3,8 +3,8 @@ import { ErrorBoundary } from '@sentry/react';
import { CompetitionsHeader } from '../../components/competitions/competitions-header';
import { Intent, Loader, TradingButton } from '@vegaprotocol/ui-toolkit';
import { useGames } from '../../lib/hooks/use-games';
import { useCurrentEpochInfoQuery } from '../referrals/hooks/__generated__/Epoch';
import { useGameCards } from '../../lib/hooks/use-game-cards';
import { useEpochInfoQuery } from '../../lib/hooks/__generated__/Epoch';
import { Link, useNavigate } from 'react-router-dom';
import { Links } from '../../lib/links';
import {
@@ -16,6 +16,8 @@ import { CompetitionsLeaderboard } from '../../components/competitions/competiti
import { useTeams } from '../../lib/hooks/use-teams';
import take from 'lodash/take';
import { usePageTitle } from '../../lib/hooks/use-page-title';
import { TeamCard } from '../../components/competitions/team-card';
import { useMyTeam } from '../../lib/hooks/use-my-team';
export const CompetitionsHome = () => {
const t = useT();
@@ -23,16 +25,23 @@ export const CompetitionsHome = () => {
usePageTitle(t('Competitions'));
const { data: epochData } = useCurrentEpochInfoQuery();
const { data: epochData } = useEpochInfoQuery();
const currentEpoch = Number(epochData?.epoch.id);
const { data: gamesData, loading: gamesLoading } = useGames({
const { data: gamesData, loading: gamesLoading } = useGameCards({
onlyActive: true,
currentEpoch,
});
const { data: teamsData, loading: teamsLoading } = useTeams();
const {
team: myTeam,
stats: myTeamStats,
games: myTeamGames,
rank: myTeamRank,
} = useMyTeam();
return (
<ErrorBoundary>
<CompetitionsHeader title={t('Competitions')}>
@@ -43,65 +52,83 @@ export const CompetitionsHome = () => {
</p>
</CompetitionsHeader>
{/** Get started */}
<h2 className="text-2xl mb-6">{t('Get started')}</h2>
{/** Team card */}
{myTeam ? (
<>
<h2 className="text-2xl mb-6">{t('My team')}</h2>
<div className="mb-12">
<TeamCard
team={myTeam}
rank={myTeamRank}
stats={myTeamStats}
games={myTeamGames}
/>
</div>
</>
) : (
<>
{/** Get started */}
<h2 className="text-2xl mb-6">{t('Get started')}</h2>
<CompetitionsActionsContainer>
<CompetitionsAction
variant="A"
title={t('Create a team')}
description={t(
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
)}
actionElement={
<TradingButton
intent={Intent.Primary}
onClick={(e) => {
e.preventDefault();
navigate(Links.COMPETITIONS_CREATE_TEAM());
}}
>
{t('Create a public team')}
</TradingButton>
}
/>
<CompetitionsAction
variant="B"
title={t('Solo team / lone wolf')}
description={t(
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
)}
actionElement={
<TradingButton
intent={Intent.Primary}
onClick={(e) => {
e.preventDefault();
navigate(Links.COMPETITIONS_CREATE_TEAM_SOLO());
}}
>
{t('Create a private team')}
</TradingButton>
}
/>
<CompetitionsAction
variant="C"
title={t('Join a team')}
description={t(
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
)}
actionElement={
<TradingButton
intent={Intent.Primary}
onClick={(e) => {
e.preventDefault();
navigate(Links.COMPETITIONS_TEAMS());
}}
>
{t('Choose a team')}
</TradingButton>
}
/>
</CompetitionsActionsContainer>
<CompetitionsActionsContainer>
<CompetitionsAction
variant="A"
title={t('Create a team')}
description={t(
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
)}
actionElement={
<TradingButton
intent={Intent.Primary}
onClick={(e) => {
e.preventDefault();
navigate(Links.COMPETITIONS_CREATE_TEAM());
}}
data-testId="create-public-team-button"
>
{t('Create a public team')}
</TradingButton>
}
/>
<CompetitionsAction
variant="B"
title={t('Solo team / lone wolf')}
description={t(
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
)}
actionElement={
<TradingButton
intent={Intent.Primary}
onClick={(e) => {
e.preventDefault();
navigate(Links.COMPETITIONS_CREATE_TEAM_SOLO());
}}
>
{t('Create a private team')}
</TradingButton>
}
/>
<CompetitionsAction
variant="C"
title={t('Join a team')}
description={t(
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
)}
actionElement={
<TradingButton
intent={Intent.Primary}
onClick={(e) => {
e.preventDefault();
navigate(Links.COMPETITIONS_TEAMS());
}}
>
{t('Choose a team')}
</TradingButton>
}
/>
</CompetitionsActionsContainer>
</>
)}
{/** List of available games */}
<h2 className="text-2xl mb-6">{t('Games')}</h2>
@@ -1,18 +1,33 @@
import { useState, type ButtonHTMLAttributes } from 'react';
import { useState, type ButtonHTMLAttributes, useRef } from 'react';
import { Link, useParams } from 'react-router-dom';
import orderBy from 'lodash/orderBy';
import { Splash, truncateMiddle, Loader } from '@vegaprotocol/ui-toolkit';
import { DispatchMetricLabels, type DispatchMetric } from '@vegaprotocol/types';
import {
Splash,
truncateMiddle,
Loader,
Dialog,
Button,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import {
TransferStatus,
type Asset,
type RecurringTransfer,
} from '@vegaprotocol/types';
import classNames from 'classnames';
import { useT } from '../../lib/use-t';
import { Table } from '../../components/table';
import { formatNumber, getDateTimeFormat } from '@vegaprotocol/utils';
import {
addDecimalsFormatNumberQuantum,
formatNumber,
getDateTimeFormat,
} from '@vegaprotocol/utils';
import {
useTeam,
type TeamStats as ITeamStats,
type Team as TeamType,
type Member,
type TeamGame,
} from '../../lib/hooks/use-team';
import { DApp, EXPLORER_PARTIES, useLinks } from '@vegaprotocol/environment';
import { TeamAvatar } from '../../components/competitions/team-avatar';
@@ -23,6 +38,24 @@ import { LayoutWithGradient } from '../../components/layouts-inner';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { JoinTeam } from './join-team';
import { UpdateTeamButton } from './update-team-button';
import {
type TeamGame,
useGames,
areTeamGames,
} from '../../lib/hooks/use-games';
import { useEpochInfoQuery } from '../../lib/hooks/__generated__/Epoch';
import {
type EnrichedTransfer,
isScopedToTeams,
useGameCards,
} from '../../lib/hooks/use-game-cards';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import {
ActiveRewardCard,
DispatchMetricInfo,
} from '../../components/rewards-container/active-rewards';
import { type MarketMap, useMarketsMapProvider } from '@vegaprotocol/markets';
import format from 'date-fns/format';
export const CompetitionsTeam = () => {
const t = useT();
@@ -38,8 +71,20 @@ export const CompetitionsTeam = () => {
const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => {
const t = useT();
const { pubKey } = useVegaWallet();
const { data, team, partyTeam, stats, members, games, loading, refetch } =
useTeam(teamId, pubKey || undefined);
const { data, team, partyTeam, stats, members, loading, refetch } = useTeam(
teamId,
pubKey || undefined
);
const { data: games, loading: gamesLoading } = useGames(teamId);
const { data: epochData, loading: epochLoading } = useEpochInfoQuery();
const { data: transfersData, loading: transfersLoading } = useGameCards({
currentEpoch: Number(epochData?.epoch.id),
onlyActive: false,
});
const { data: markets } = useMarketsMapProvider();
// only show spinner on first load so when users join teams its smoother
if (!data && loading) {
@@ -64,7 +109,11 @@ const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => {
partyTeam={partyTeam}
stats={stats}
members={members}
games={games}
games={areTeamGames(games) ? games : undefined}
gamesLoading={gamesLoading}
transfers={transfersData}
transfersLoading={epochLoading || transfersLoading}
allMarkets={markets || undefined}
refetch={refetch}
/>
);
@@ -76,6 +125,10 @@ const TeamPage = ({
stats,
members,
games,
gamesLoading,
transfers,
transfersLoading,
allMarkets,
refetch,
}: {
team: TeamType;
@@ -83,6 +136,10 @@ const TeamPage = ({
stats?: ITeamStats;
members?: Member[];
games?: TeamGame[];
gamesLoading?: boolean;
transfers?: EnrichedTransfer[];
transfersLoading?: boolean;
allMarkets?: MarketMap;
refetch: () => void;
}) => {
const t = useT();
@@ -113,7 +170,11 @@ const TeamPage = ({
onClick={() => setShowGames(true)}
data-testid="games-toggle"
>
{t('Games ({{count}})', { count: games ? games.length : 0 })}
{t('Results {{games}}', {
replace: {
games: gamesLoading ? '' : games ? `(${games.length})` : '(0)',
},
})}
</ToggleButton>
<ToggleButton
active={!showGames}
@@ -125,53 +186,149 @@ const TeamPage = ({
})}
</ToggleButton>
</div>
{showGames ? <Games games={games} /> : <Members members={members} />}
{showGames ? (
<Games
games={games}
gamesLoading={gamesLoading}
transfers={transfers}
transfersLoading={transfersLoading}
allMarkets={allMarkets}
/>
) : (
<Members members={members} />
)}
</section>
</LayoutWithGradient>
);
};
const Games = ({ games }: { games?: TeamGame[] }) => {
const Games = ({
games,
gamesLoading,
transfers,
transfersLoading,
allMarkets,
}: {
games?: TeamGame[];
gamesLoading?: boolean;
transfers?: EnrichedTransfer[];
transfersLoading?: boolean;
allMarkets?: MarketMap;
}) => {
const t = useT();
if (!games?.length) {
return <p>{t('No games')}</p>;
if (gamesLoading) {
return (
<div className="w-[15px]">
<Loader size="small" />
</div>
);
}
if (!games?.length) {
return <p>{t('No game results available')}</p>;
}
const dependable = (value: string | JSX.Element) => {
if (transfersLoading) return <Loader size="small" />;
return value;
};
return (
<Table
columns={[
{ name: 'rank', displayName: t('Rank') },
{
name: 'epoch',
displayName: t('Epoch'),
headerClassName: 'hidden md:table-cell',
className: 'hidden md:table-cell',
},
{
name: 'endtime',
displayName: t('End time'),
},
{ name: 'type', displayName: t('Type') },
{ name: 'amount', displayName: t('Amount earned') },
{
name: 'asset',
displayName: t('Reward asset'),
},
{ name: 'daily', displayName: t('Daily reward amount') },
{ name: 'rank', displayName: t('Rank') },
{ name: 'amount', displayName: t('Amount earned this epoch') },
{ name: 'total', displayName: t('Cumulative amount earned') },
{
name: 'participatingTeams',
displayName: t('No. of participating teams'),
headerClassName: 'hidden md:table-cell',
className: 'hidden md:table-cell',
},
{
name: 'participatingMembers',
displayName: t('No. of participating members'),
headerClassName: 'hidden md:table-cell',
className: 'hidden md:table-cell',
},
]}
data={games.map((game) => ({
rank: game.team.rank,
epoch: game.epoch,
type: DispatchMetricLabels[game.team.rewardMetric as DispatchMetric],
amount: formatNumber(game.team.totalRewardsEarned),
participatingTeams: game.entities.length,
participatingMembers: game.numberOfParticipants,
}))}
noCollapse={true}
].map((c) => ({ ...c, headerClassName: 'text-left' }))}
data={games.map((game) => {
let transfer = transfers?.find((t) => {
if (!isScopedToTeams(t)) return false;
const idMatch = t.transfer.gameId === game.id;
const metricMatch =
t.transfer.kind.dispatchStrategy?.dispatchMetric ===
game.team.rewardMetric;
const start = t.transfer.kind.startEpoch <= game.epoch;
const end = t.transfer.kind.endEpoch
? t.transfer.kind.endEpoch >= game.epoch
: true;
const rejected = t.transfer.status === TransferStatus.STATUS_REJECTED;
return idMatch && metricMatch && start && end && !rejected;
});
if (!transfer || !isScopedToTeams(transfer)) transfer = undefined;
const asset = transfer?.transfer.asset;
const dailyAmount =
asset && transfer
? addDecimalsFormatNumberQuantum(
transfer.transfer.amount,
asset.decimals,
asset.quantum
)
: '-';
const earnedAmount = asset
? addDecimalsFormatNumberQuantum(
game.team.rewardEarned,
asset.decimals,
asset.quantum
)
: '-';
const totalAmount = asset
? addDecimalsFormatNumberQuantum(
game.team.totalRewardsEarned,
asset.decimals,
asset.quantum
)
: '-';
const assetSymbol = asset ? <RewardAssetCell asset={asset} /> : '-';
return {
id: game.id,
amount: dependable(earnedAmount),
asset: dependable(assetSymbol),
daily: dependable(dailyAmount),
endtime: <EndTimeCell epoch={game.epoch} />,
epoch: game.epoch,
participatingMembers: game.numberOfParticipants,
participatingTeams: game.entities.length,
rank: game.team.rank,
total: totalAmount,
// type: DispatchMetricLabels[game.team.rewardMetric as DispatchMetric],
type: dependable(
<GameTypeCell transfer={transfer} allMarkets={allMarkets} />
),
};
})}
noCollapse={false}
/>
);
};
@@ -253,3 +410,126 @@ const ToggleButton = ({
/>
);
};
const EndTimeCell = ({ epoch }: { epoch?: number }) => {
const { data, loading } = useEpochInfoQuery({
variables: {
epochId: epoch ? epoch.toString() : undefined,
},
fetchPolicy: 'cache-and-network',
});
if (loading) return <Loader size="small" />;
if (data) {
return format(
new Date(data.epoch.timestamps.expiry),
'yyyy/MM/dd hh:mm:ss'
);
}
return null;
};
const RewardAssetCell = ({ asset }: { asset: Asset }) => {
const open = useAssetDetailsDialogStore((state) => state.open);
const ref = useRef<HTMLButtonElement>(null);
return (
<button
ref={ref}
onClick={(e) => {
e.preventDefault();
open(asset.id, ref.current);
}}
className="border-b border-dashed border-vega-clight-200 dark:border-vega-cdark-200 text-left text-nowrap whitespace-nowrap"
>
{asset.symbol}
</button>
);
};
const GameTypeCell = ({
transfer,
allMarkets,
}: {
transfer?: EnrichedTransfer;
allMarkets?: MarketMap;
}) => {
const [open, setOpen] = useState(false);
const ref = useRef<HTMLButtonElement>(null);
if (!transfer) return '-';
return (
<>
<ActiveRewardCardDialog
open={open}
onChange={(isOpen) => setOpen(isOpen)}
trigger={ref.current}
transfer={transfer}
allMarkets={allMarkets}
/>
<button
onClick={(e) => {
e.preventDefault();
setOpen(true);
}}
ref={ref}
className="border-b border-dashed border-vega-clight-200 dark:border-vega-cdark-200 text-left md:truncate md:max-w-[25vw]"
>
<DispatchMetricInfo transferNode={transfer} allMarkets={allMarkets} />
</button>
</>
);
};
const ActiveRewardCardDialog = ({
open,
onChange,
trigger,
transfer,
allMarkets,
}: {
open: boolean;
onChange: (isOpen: boolean) => void;
trigger?: HTMLElement | null;
transfer: EnrichedTransfer;
allMarkets?: MarketMap;
}) => {
const t = useT();
const { data } = useEpochInfoQuery();
return (
<Dialog
open={open}
title={t('Game details')}
onChange={(isOpen) => onChange(isOpen)}
icon={<VegaIcon name={VegaIconNames.INFO} />}
onCloseAutoFocus={(e) => {
/**
* This mimics radix's default behaviour that focuses the dialog's
* trigger after closing itself
*/
if (trigger) {
e.preventDefault();
trigger.focus();
}
}}
>
<div className="py-5 max-w-[454px]">
<ActiveRewardCard
transferNode={transfer}
currentEpoch={Number(data?.epoch.id)}
kind={transfer.transfer.kind as RecurringTransfer}
allMarkets={allMarkets}
/>
</div>
<div className="w-1/4">
<Button
data-testid="close-asset-details-dialog"
fill={true}
size="sm"
onClick={() => onChange(false)}
>
{t('Close')}
</Button>
</div>
</Dialog>
);
};
@@ -3,7 +3,14 @@ import { usePageTitle } from '../../lib/hooks/use-page-title';
import { Box } from '../../components/competitions/box';
import { useT } from '../../lib/use-t';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
import {
Intent,
Loader,
Splash,
TradingAnchorButton,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { RainbowButton } from '../../components/rainbow-button';
import { Link, Navigate, useParams } from 'react-router-dom';
import { Links } from '../../lib/links';
@@ -11,6 +18,7 @@ import { useReferralSetTransaction } from '../../lib/hooks/use-referral-set-tran
import { type FormFields, TeamForm, TransactionType } from './team-form';
import { useTeam } from '../../lib/hooks/use-team';
import { LayoutWithGradient } from '../../components/layouts-inner';
import { useEffect, useState } from 'react';
export const CompetitionsUpdateTeam = () => {
const t = useT();
@@ -29,6 +37,19 @@ export const CompetitionsUpdateTeam = () => {
<LayoutWithGradient>
<div className="mx-auto md:w-2/3 max-w-xl">
<Box className="flex flex-col gap-4">
<Link
to={Links.COMPETITIONS_TEAM(teamId)}
className="text-xs inline-flex items-center gap-1 group"
>
<VegaIcon
name={VegaIconNames.CHEVRON_LEFT}
size={12}
className="text-vega-clight-100 dark:text-vega-cdark-100"
/>{' '}
<span className="group-hover:underline">
{t('Go back to the team profile')}
</span>
</Link>
<h1 className="calt text-2xl lg:text-3xl xl:text-5xl">
{t('Update a team')}
</h1>
@@ -57,7 +78,8 @@ const UpdateTeamFormContainer = ({
pubKey: string;
}) => {
const t = useT();
const { team, loading, error } = useTeam(teamId, pubKey);
const [refetching, setRefetching] = useState<boolean>(false);
const { team, loading, error, refetch } = useTeam(teamId, pubKey);
const { err, status, onSubmit } = useReferralSetTransaction({
onSuccess: () => {
@@ -65,7 +87,15 @@ const UpdateTeamFormContainer = ({
},
});
if (loading) {
// refetch when saved
useEffect(() => {
if (refetch && status === 'confirmed') {
refetch();
setRefetching(true);
}
}, [refetch, status]);
if (loading && !refetching) {
return <Loader size="small" />;
}
if (error) {
@@ -84,6 +114,33 @@ const UpdateTeamFormContainer = ({
return <Navigate to={Links.COMPETITIONS_TEAM(teamId)} />;
}
if (status === 'confirmed') {
return (
<div
className="flex flex-col items-start gap-2"
data-testid="team-creation-success-message"
>
<p className="text-sm">
<VegaIcon
name={VegaIconNames.TICK}
size={18}
className="text-vega-green-500"
/>{' '}
{t('Changes successfully saved to your team.')}
</p>
<TradingAnchorButton
href={Links.COMPETITIONS_TEAM(teamId)}
intent={Intent.Info}
size="small"
data-testid="view-team-button"
>
{t('View team')}
</TradingAnchorButton>
</div>
);
}
const defaultValues: FormFields = {
id: team.teamId,
name: team.name,
@@ -105,7 +105,11 @@ export const JoinButton = ({
// Party is in a team, but not this one
else if (partyTeam && partyTeam.teamId !== team.teamId) {
return (
<Button onClick={() => onJoin('switch')} intent={Intent.Primary}>
<Button
onClick={() => onJoin('switch')}
intent={Intent.Primary}
data-testid="switch-team-button"
>
{t('Switch team')}{' '}
</Button>
);
@@ -215,7 +219,11 @@ const DialogContent = ({
</>
)}
<div className="flex justify-between gap-2">
<Button onClick={joinTeam} intent={Intent.Success}>
<Button
onClick={joinTeam}
intent={Intent.Success}
data-testid="confirm-switch-button"
>
{t('Confirm')}
</Button>
<Button onClick={onCancel} intent={Intent.Danger}>
@@ -46,7 +46,7 @@ const prepareTransaction = (
createReferralSet: {
isTeam: true,
team: {
name: fields.name,
name: fields.name.trim(),
teamUrl: fields.url,
avatarUrl: fields.avatarUrl,
closed: fields.private,
@@ -62,7 +62,7 @@ const prepareTransaction = (
id: fields.id,
isTeam: true,
team: {
name: fields.name,
name: fields.name.trim(),
teamUrl: fields.url,
avatarUrl: fields.avatarUrl,
closed: fields.private,
@@ -115,9 +115,22 @@ export const TeamForm = ({
<form onSubmit={handleSubmit(sendTransaction)}>
<input type="hidden" {...register('id')} />
<TradingFormGroup label={t('Team name')} labelFor="name">
<TradingInput {...register('name', { required: t('Required') })} />
<TradingInput
{...register('name', {
required: t('Required'),
validate: {
notEmpty: (value) => {
if (/^\s*$/.test(value)) {
return t('Team name cannot be empty');
}
return true;
},
},
})}
data-testid="team-name-input"
/>
{errors.name?.message && (
<TradingInputError forInput="name">
<TradingInputError forInput="name" data-testid="team-name-error">
{errors.name.message}
</TradingInputError>
)}
@@ -133,9 +146,10 @@ export const TeamForm = ({
{...register('url', {
pattern: { value: URL_REGEX, message: t('Invalid URL') },
})}
data-testid="team-url-input"
/>
{errors.url?.message && (
<TradingInputError forInput="url">
<TradingInputError forInput="url" data-testid="team-url-error">
{errors.url.message}
</TradingInputError>
)}
@@ -152,9 +166,13 @@ export const TeamForm = ({
message: t('Invalid image URL'),
},
})}
data-testid="avatar-url-input"
/>
{errors.avatarUrl?.message && (
<TradingInputError forInput="avatarUrl">
<TradingInputError
forInput="avatarUrl"
data-testid="avatar-url-error"
>
{errors.avatarUrl.message}
</TradingInputError>
)}
@@ -179,6 +197,7 @@ export const TeamForm = ({
onCheckedChange={(value) => {
field.onChange(value);
}}
data-testid="team-private-checkbox"
/>
);
}}
@@ -207,9 +226,13 @@ export const TeamForm = ({
},
},
})}
data-testid="team-allow-list-textarea"
/>
{errors.allowList?.message && (
<TradingInputError forInput="avatarUrl">
<TradingInputError
forInput="avatarUrl"
data-testid="team-allow-list-error"
>
{errors.allowList.message}
</TradingInputError>
)}
@@ -284,7 +307,12 @@ const SubmitButton = ({
return (
<div className="flex gap-2 items-baseline">
<TradingButton type="submit" intent={Intent.Info} disabled={disabled}>
<TradingButton
type="submit"
intent={Intent.Info}
disabled={disabled}
data-testid="team-form-submit-button"
>
{text}
</TradingButton>
{status === 'confirmed' && confirmed}
@@ -1,16 +1,24 @@
import { useVegaWallet } from '@vegaprotocol/wallet';
import { type Team } from '../../lib/hooks/use-team';
import { type ComponentProps } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { Intent, TradingAnchorButton } from '@vegaprotocol/ui-toolkit';
import { Links } from '../../lib/links';
import { useT } from '../../lib/use-t';
export const UpdateTeamButton = ({ team }: { team: Team }) => {
export const UpdateTeamButton = ({
team,
size = 'medium',
}: {
team: Pick<Team, 'teamId' | 'referrer'>;
size?: ComponentProps<typeof TradingAnchorButton>['size'];
}) => {
const t = useT();
const { pubKey, isReadOnly } = useVegaWallet();
if (pubKey && !isReadOnly && pubKey === team.referrer) {
return (
<TradingAnchorButton
size={size}
data-testid="update-team-button"
href={Links.COMPETITIONS_UPDATE_TEAM(team.teamId)}
intent={Intent.Info}
+6 -13
View File
@@ -1,27 +1,20 @@
import { useEffect } from 'react';
import { titlefy } from '@vegaprotocol/utils';
import { TinyScroll } from '@vegaprotocol/ui-toolkit';
import { ErrorBoundary } from '../../components/error-boundary';
import { FeesContainer } from '../../components/fees-container';
import { useT } from '../../lib/use-t';
import { usePageTitleStore } from '../../stores';
import { usePageTitle } from '../../lib/hooks/use-page-title';
export const Fees = () => {
const t = useT();
const title = t('Fees');
const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
useEffect(() => {
updateTitle(titlefy([title]));
}, [updateTitle, title]);
usePageTitle(title);
return (
<ErrorBoundary feature="fees">
<div className="container p-4 mx-auto">
<h1 className="px-4 pb-4 text-2xl">{title}</h1>
<TinyScroll className="p-4 max-h-full overflow-auto">
<h1 className="md:px-4 pb-4 text-2xl">{title}</h1>
<FeesContainer />
</div>
</TinyScroll>
</ErrorBoundary>
);
};
@@ -19,6 +19,7 @@ import {
useFundingRate,
useMarketTradingMode,
useExternalTwap,
getQuoteName,
} from '@vegaprotocol/markets';
import { MarketState as State } from '@vegaprotocol/types';
import { HeaderStat } from '../../components/header';
@@ -41,6 +42,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
const asset = getAsset(market);
const quoteUnit = getQuoteName(market);
return (
<>
@@ -60,6 +62,8 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
<Last24hVolume
marketId={market.id}
positionDecimalPlaces={market.positionDecimalPlaces}
marketDecimals={market.decimalPlaces}
quoteUnit={quoteUnit}
/>
</HeaderStat>
<HeaderStatMarketTradingMode
@@ -108,7 +112,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
heading={`${t('Funding Rate')} / ${t('Countdown')}`}
testId="market-funding"
>
<div className="flex justify-between gap-2">
<div className="flex gap-2">
<FundingRate marketId={market.id} />
<FundingCountdown marketId={market.id} />
</div>
@@ -3,7 +3,6 @@ import { type Market } from '@vegaprotocol/markets';
// TODO: handle oracle banner
// import { OracleBanner } from '@vegaprotocol/markets';
import { useState } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import classNames from 'classnames';
import {
Popover,
@@ -12,21 +11,21 @@ import {
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { useT } from '../../lib/use-t';
import { MarketBanner } from '../../components/market-banner';
import { ErrorBoundary } from '../../components/error-boundary';
import { type TradingView } from './trade-views';
import { TradingViews } from './trade-views';
interface TradePanelsProps {
market: Market;
pinnedAsset?: PinnedAsset;
}
export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
const [view, setView] = useState<TradingView>('chart');
const viewCfg = TradingViews[view];
const [topView, setTopView] = useState<TradingView>('chart');
const topViewCfg = TradingViews[topView];
const [bottomView, setBottomView] = useState<TradingView>('positions');
const bottomViewCfg = TradingViews[bottomView];
const renderView = () => {
const renderView = (view: TradingView) => {
const Component = TradingViews[view].component;
if (!Component) {
@@ -39,12 +38,13 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
// so watch out for clashes in props
return (
<ErrorBoundary feature={view}>
<Component marketId={market?.id} pinnedAsset={pinnedAsset} />;
<Component marketId={market?.id} pinnedAsset={pinnedAsset} />
</ErrorBoundary>
);
};
const renderMenu = () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const renderMenu = (viewCfg: any) => {
if ('menu' in viewCfg || 'settings' in viewCfg) {
return (
<div className="flex items-center justify-end gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default">
@@ -69,55 +69,87 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
};
return (
<div className="h-full grid grid-rows-[min-content_min-content_1fr_min-content]">
<div>
<MarketBanner market={market} />
</div>
<div>{renderMenu()}</div>
<div className="h-full relative">
<AutoSizer>
{({ width, height }) => (
<div style={{ width, height }} className="overflow-auto">
{renderView()}
</div>
)}
</AutoSizer>
</div>
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
{Object.keys(TradingViews)
// filter to control available views for the current market
// eg only perps should get the funding views
.filter((_key) => {
const key = _key as TradingView;
const perpOnlyViews = ['funding', 'fundingPayments'];
<div className="h-full flex flex-col lg:grid grid-rows-[min-content_min-content_1fr_min-content]">
<div className="flex flex-col w-full overflow-hidden">
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
{[
'chart',
'orderbook',
'trades',
'liquidity',
'fundingPayments',
'funding',
]
// filter to control available views for the current market
// e.g. only perpetuals should get the funding views
.filter((_key) => {
const key = _key as TradingView;
const perpOnlyViews = ['funding', 'fundingPayments'];
if (
market?.tradableInstrument.instrument.product.__typename ===
'Perpetual'
) {
return true;
}
if (perpOnlyViews.includes(key)) {
return false;
}
if (
market?.tradableInstrument.instrument.product.__typename ===
'Perpetual'
) {
return true;
}
})
.map((_key) => {
const key = _key as TradingView;
const isActive = topView === key;
return (
<ViewButton
key={key}
view={key}
isActive={isActive}
onClick={() => {
setTopView(key);
}}
/>
);
})}
</div>
<div className="h-[50vh] lg:h-full relative">
<div>{renderMenu(topViewCfg)}</div>
<div className="overflow-auto h-full">{renderView(topView)}</div>
</div>
</div>
if (perpOnlyViews.includes(key)) {
return false;
}
return true;
})
.map((_key) => {
<div className="flex flex-col w-full grow">
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
{[
'positions',
'activeOrders',
'closedOrders',
'rejectedOrders',
'orders',
'stopOrders',
'collateral',
'fills',
].map((_key) => {
const key = _key as TradingView;
const isActive = view === key;
const isActive = bottomView === key;
return (
<ViewButton
key={key}
view={key}
isActive={isActive}
onClick={() => {
setView(key);
setBottomView(key);
}}
/>
);
})}
</div>
<div className="relative grow">
<div className="flex flex-col">{renderMenu(bottomViewCfg)}</div>
<div className="overflow-auto h-full">{renderView(bottomView)}</div>
</div>
</div>
</div>
);
@@ -138,9 +170,12 @@ const ViewButton = ({
onClick: () => void;
}) => {
const label = useViewLabel(view);
const className = classNames('py-2 px-4 min-w-[100px] capitalize text-sm', {
'bg-vega-clight-500 dark:bg-vega-cdark-500': isActive,
});
const className = classNames(
'py-2 px-4 capitalize text-sm whitespace-nowrap',
{
'bg-vega-clight-500 dark:bg-vega-cdark-500': isActive,
}
);
return (
<button data-testid={view} onClick={onClick} className={className}>
@@ -156,8 +191,8 @@ const useViewLabel = (view: TradingView) => {
chart: t('Chart'),
depth: t('Depth'),
liquidity: t('Liquidity'),
funding: t('Funding'),
fundingPayments: t('Funding Payments'),
funding: t('Funding history'),
fundingPayments: t('Funding payments'),
orderbook: t('Orderbook'),
trades: t('Trades'),
positions: t('Positions'),
@@ -5,7 +5,7 @@ import {
useDataGridEvents,
} from '@vegaprotocol/datagrid';
import type { MarketMaybeWithData } from '@vegaprotocol/markets';
import { useColumnDefs } from './use-column-defs';
import { useMarketsColumnDefs } from './use-column-defs';
import type { DataGridStore } from '../../stores/datagrid-store-slice';
import { type StateCreator, create } from 'zustand';
import { persist } from 'zustand/middleware';
@@ -50,7 +50,7 @@ export const useMarketsStore = create<DataGridSlice>()(
);
export const MarketListTable = (props: Props) => {
const columnDefs = useColumnDefs();
const columnDefs = useMarketsColumnDefs();
const gridStore = useMarketsStore((store) => store.gridStore);
const updateGridStore = useMarketsStore((store) => store.updateGridStore);
@@ -1,5 +1,3 @@
import React, { useEffect } from 'react';
import { titlefy } from '@vegaprotocol/utils';
import {
LocalStoragePersistTabs as Tabs,
Tab,
@@ -7,7 +5,6 @@ import {
} from '@vegaprotocol/ui-toolkit';
import { OpenMarkets } from './open-markets';
import { Proposed } from './proposed';
import { usePageTitleStore } from '../../stores';
import { Closed } from './closed';
import {
DApp,
@@ -17,19 +14,14 @@ import {
import { useT } from '../../lib/use-t';
import { ErrorBoundary } from '../../components/error-boundary';
import { MarketsSettings } from './markets-settings';
import { usePageTitle } from '../../lib/hooks/use-page-title';
export const MarketsPage = () => {
const t = useT();
const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
const governanceLink = useLinks(DApp.Governance);
const externalLink = governanceLink(TOKEN_NEW_MARKET_PROPOSAL);
useEffect(() => {
updateTitle(titlefy([t('Markets')]));
}, [updateTitle, t]);
usePageTitle(t('Markets'));
return (
<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';
import { COL_DEFS, SetFilter } from '@vegaprotocol/datagrid';
import * as Schema from '@vegaprotocol/types';
import { addDecimalsFormatNumber, toBigNum } from '@vegaprotocol/utils';
import {
addDecimalsFormatNumber,
formatNumber,
toBigNum,
} from '@vegaprotocol/utils';
import { ButtonLink, Tooltip } from '@vegaprotocol/ui-toolkit';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import type {
MarketFieldsFragment,
MarketMaybeWithData,
MarketMaybeWithDataAndCandles,
} from '@vegaprotocol/markets';
import { MarketActionsDropdown } from './market-table-actions';
import { calcCandleVolume, getAsset } from '@vegaprotocol/markets';
import {
calcCandleVolume,
calcCandleVolumePrice,
getAsset,
getQuoteName,
} from '@vegaprotocol/markets';
import { MarketCodeCell } from './market-code-cell';
import { useT } from '../../lib/use-t';
const { MarketTradingMode, AuctionTrigger } = Schema;
export const useColumnDefs = () => {
export const useMarketsColumnDefs = () => {
const t = useT();
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
return useMemo<ColDef[]>(
@@ -158,11 +168,25 @@ export const useColumnDefs = () => {
}: ValueFormatterParams<MarketMaybeWithDataAndCandles, 'candles'>) => {
const candles = data?.candles;
const vol = candles ? calcCandleVolume(candles) : '0';
const quoteName = getQuoteName(data as MarketFieldsFragment);
const volPrice =
candles &&
calcCandleVolumePrice(
candles,
data.decimalPlaces,
data.positionDecimalPlaces
);
const volume =
data && vol && vol !== '0'
? addDecimalsFormatNumber(vol, data.positionDecimalPlaces)
: '0.00';
return volume;
const volumePrice =
volPrice && formatNumber(volPrice, data?.decimalPlaces);
return volumePrice
? `${volume} (${volumePrice} ${quoteName})`
: volume;
},
},
{
@@ -2,6 +2,7 @@ import { VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { SidebarButton, ViewType } from '../../components/sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { useT } from '../../lib/use-t';
import { MobileButton } from '../markets/mobile-buttons';
export const PortfolioSidebar = () => {
const t = useT();
@@ -30,3 +31,28 @@ export const PortfolioSidebar = () => {
</>
);
};
export const PortfolioMobileSidebar = () => {
const t = useT();
const currentRouteId = useGetCurrentRouteId();
return (
<div className="grid grid-cols-3 grow md:grow-0 md:flex lg:flex-col items-center gap-2 lg:gap-4 p-1">
<MobileButton
view={ViewType.Deposit}
tooltip={t('Deposit')}
routeId={currentRouteId}
/>
<MobileButton
view={ViewType.Withdraw}
tooltip={t('Withdraw')}
routeId={currentRouteId}
/>
<MobileButton
view={ViewType.Transfer}
tooltip={t('Transfer')}
routeId={currentRouteId}
/>
</div>
);
};
@@ -1,10 +1,8 @@
import { useEffect } from 'react';
import type { ReactNode } from 'react';
import { LayoutPriority } from 'allotment';
import { titlefy } from '@vegaprotocol/utils';
import { useIncompleteWithdrawals } from '@vegaprotocol/withdraws';
import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit';
import { usePageTitleStore } from '../../stores';
import {
AccountsContainer,
AccountsSettings,
@@ -41,6 +39,7 @@ import { WithdrawalsMenu } from '../../components/withdrawals-menu';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { useT } from '../../lib/use-t';
import { ErrorBoundary } from '../../components/error-boundary';
import { usePageTitle } from '../../lib/hooks/use-page-title';
const WithdrawalsIndicator = () => {
const { ready } = useIncompleteWithdrawals();
@@ -69,14 +68,7 @@ const SidebarViewInitializer = () => {
export const Portfolio = () => {
const t = useT();
const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
useEffect(() => {
updateTitle(titlefy([t('Portfolio')]));
}, [updateTitle, t]);
usePageTitle(t('Portfolio'));
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'portfolio' });
const wrapperClasses = 'p-0.5 h-full max-h-full flex flex-col';
@@ -1,9 +0,0 @@
query CurrentEpochInfo {
epoch {
id
timestamps {
start
end
}
}
}
@@ -1,49 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type CurrentEpochInfoQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type CurrentEpochInfoQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string, timestamps: { __typename?: 'EpochTimestamps', start?: any | null, end?: any | null } } };
export const CurrentEpochInfoDocument = gql`
query CurrentEpochInfo {
epoch {
id
timestamps {
start
end
}
}
}
`;
/**
* __useCurrentEpochInfoQuery__
*
* To run a query within a React component, call `useCurrentEpochInfoQuery` and pass it any options that fit your needs.
* When your component renders, `useCurrentEpochInfoQuery` 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 } = useCurrentEpochInfoQuery({
* variables: {
* },
* });
*/
export function useCurrentEpochInfoQuery(baseOptions?: Apollo.QueryHookOptions<CurrentEpochInfoQuery, CurrentEpochInfoQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<CurrentEpochInfoQuery, CurrentEpochInfoQueryVariables>(CurrentEpochInfoDocument, options);
}
export function useCurrentEpochInfoLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<CurrentEpochInfoQuery, CurrentEpochInfoQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<CurrentEpochInfoQuery, CurrentEpochInfoQueryVariables>(CurrentEpochInfoDocument, options);
}
export type CurrentEpochInfoQueryHookResult = ReturnType<typeof useCurrentEpochInfoQuery>;
export type CurrentEpochInfoLazyQueryHookResult = ReturnType<typeof useCurrentEpochInfoLazyQuery>;
export type CurrentEpochInfoQueryResult = Apollo.QueryResult<CurrentEpochInfoQuery, CurrentEpochInfoQueryVariables>;
@@ -1,4 +1,4 @@
import { getNumberFormat } from '@vegaprotocol/utils';
import { formatNumber } from '@vegaprotocol/utils';
import sortBy from 'lodash/sortBy';
import omit from 'lodash/omit';
import { useReferralProgramQuery } from './__generated__/CurrentReferralProgram';
@@ -107,9 +107,7 @@ export const useReferralProgram = () => {
discountFactor: Number(t.referralDiscountFactor),
discount: BigNumber(t.referralDiscountFactor).times(100).toFixed(2) + '%',
minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
volume: getNumberFormat(0).format(
Number(t.minimumRunningNotionalTakerVolume)
),
volume: formatNumber(t.minimumRunningNotionalTakerVolume, 0),
epochs: Number(t.minimumEpochs),
};
});
@@ -11,7 +11,7 @@ import { useEffect } from 'react';
import { useT } from '../../../lib/use-t';
import { matchPath, useLocation, useNavigate } from 'react-router-dom';
import { Routes } from '../../../lib/links';
import { useCurrentEpochInfoQuery } from './__generated__/Epoch';
import { useEpochInfoQuery } from '../../../lib/hooks/__generated__/Epoch';
const REFETCH_INTERVAL = 60 * 60 * 1000; // 1h
const NON_ELIGIBLE_REFERRAL_SET_TOAST_ID = 'non-eligible-referral-set';
@@ -23,7 +23,7 @@ const useNonEligibleReferralSet = () => {
data: epochData,
loading: epochLoading,
refetch: epochRefetch,
} = useCurrentEpochInfoQuery();
} = useEpochInfoQuery();
useEffect(() => {
const interval = setInterval(() => {
@@ -14,9 +14,9 @@ import {
import { useVegaWallet } from '@vegaprotocol/wallet';
import {
addDecimalsFormatNumber,
formatNumber,
getDateFormat,
getDateTimeFormat,
getNumberFormat,
getUserLocale,
removePaginationWrapper,
} from '@vegaprotocol/utils';
@@ -34,9 +34,10 @@ import {
} from './hooks/use-referral';
import { ApplyCodeForm, ApplyCodeFormContainer } from './apply-code-form';
import { useReferralProgram } from './hooks/use-referral-program';
import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
import { useEpochInfoQuery } from '../../lib/hooks/__generated__/Epoch';
import { QUSDTooltip } from './qusd-tooltip';
import { CodeTile, StatTile, Tile } from './tile';
import { areTeamGames, useGames } from '../../lib/hooks/use-games';
export const ReferralStatistics = () => {
const { pubKey } = useVegaWallet();
@@ -94,7 +95,7 @@ export const useStats = ({
program: ReturnType<typeof useReferralProgram>;
}) => {
const { benefitTiers } = program;
const { data: epochData } = useCurrentEpochInfoQuery({
const { data: epochData } = useEpochInfoQuery({
fetchPolicy: 'network-only',
});
const { data: statsData } = useReferralSetStatsQuery({
@@ -323,7 +324,7 @@ export const Statistics = ({
}
description={<QUSDTooltip />}
>
{getNumberFormat(0).format(Number(totalCommissionValue))}
{formatNumber(totalCommissionValue, 0)}
</StatTile>
);
@@ -563,8 +564,8 @@ export const RefereesTable = ({
)
.map((r) => ({
...r,
volume: getNumberFormat(0).format(r.volume),
commission: getNumberFormat(0).format(r.commission),
volume: formatNumber(r.volume, 0),
commission: formatNumber(r.commission, 0),
}))
.reverse()}
/>
@@ -576,7 +577,8 @@ export const RefereesTable = ({
};
const Team = ({ teamId }: { teamId?: string }) => {
const { team, games, members } = useTeam(teamId);
const { team, members } = useTeam(teamId);
const { data: games } = useGames(teamId);
if (!team) return null;
@@ -585,7 +587,10 @@ const Team = ({ teamId }: { teamId?: string }) => {
<TeamAvatar teamId={team.teamId} imgUrl={team.avatarUrl} />
<div className="flex flex-col items-start gap-1 lg:gap-3">
<h1 className="calt text-2xl lg:text-3xl xl:text-5xl">{team.name}</h1>
<TeamStats members={members} games={games} />
<TeamStats
members={members}
games={areTeamGames(games) ? games : undefined}
/>
</div>
</Tile>
);
+5 -11
View File
@@ -1,24 +1,18 @@
import { useEffect } from 'react';
import { titlefy } from '@vegaprotocol/utils';
import { TinyScroll } from '@vegaprotocol/ui-toolkit';
import { useT } from '../../lib/use-t';
import { RewardsContainer } from '../../components/rewards-container';
import { usePageTitleStore } from '../../stores';
import { ErrorBoundary } from '../../components/error-boundary';
import { TinyScroll } from '@vegaprotocol/ui-toolkit';
import { usePageTitle } from '../../lib/hooks/use-page-title';
export const Rewards = () => {
const t = useT();
const title = t('Rewards');
const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
useEffect(() => {
updateTitle(titlefy([title]));
}, [updateTitle, title]);
usePageTitle(title);
return (
<ErrorBoundary feature="rewards">
<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 />
</TinyScroll>
</ErrorBoundary>
@@ -153,5 +153,8 @@ const cacheConfig: InMemoryCacheConfig = {
OrderUpdate: {
keyFields: false,
},
Game: {
keyFields: false,
},
},
};
+20 -3
View File
@@ -5,7 +5,11 @@ export const BORDER_COLOR = 'border-vega-clight-500 dark:border-vega-cdark-500';
export const GRADIENT =
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
export const Box = (props: HTMLAttributes<HTMLDivElement>) => {
export const Box = ({
children,
backgroundImage,
...props
}: HTMLAttributes<HTMLDivElement> & { backgroundImage?: string }) => {
return (
<div
{...props}
@@ -13,9 +17,22 @@ export const Box = (props: HTMLAttributes<HTMLDivElement>) => {
BORDER_COLOR,
GRADIENT,
'border rounded-lg',
'p-6',
'relative p-6 overflow-hidden',
props.className
)}
/>
>
{Boolean(backgroundImage?.length) && (
<div
className={classNames(
'pointer-events-none',
'bg-no-repeat bg-center bg-[length:500px_500px]',
'absolute top-0 left-0 w-full h-full -z-10 opacity-30 blur-lg'
)}
style={{ backgroundImage: `url("${backgroundImage}")` }}
></div>
)}
{children}
</div>
);
};
@@ -1,6 +1,6 @@
import { Link } from 'react-router-dom';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { getNumberFormat } from '@vegaprotocol/utils';
import { formatNumber } from '@vegaprotocol/utils';
import { type useTeams } from '../../lib/hooks/use-teams';
import { useT } from '../../lib/use-t';
import { Table } from '../table';
@@ -15,8 +15,7 @@ export const CompetitionsLeaderboard = ({
}) => {
const t = useT();
const num = (n?: number | string) =>
!n ? '-' : getNumberFormat(0).format(Number(n));
const num = (n?: number | string) => (!n ? '-' : formatNumber(n, 0));
if (!data || data.length === 0) {
return <Splash>{t('Could not find any teams')}</Splash>;
@@ -33,9 +32,9 @@ export const CompetitionsLeaderboard = ({
{ name: 'status', displayName: t('Status') },
{ name: 'volume', displayName: t('Volume') },
]}
data={data.map((td, i) => {
data={data.map((td) => {
// leaderboard place or medal
let rank: number | React.ReactNode = i + 1;
let rank: number | React.ReactNode = td.rank;
if (rank === 1) rank = <Rank variant="gold" />;
if (rank === 2) rank = <Rank variant="silver" />;
if (rank === 3) rank = <Rank variant="bronze" />;
@@ -57,7 +56,10 @@ export const CompetitionsLeaderboard = ({
className="hover:underline"
to={Links.COMPETITIONS_TEAM(td.teamId)}
>
{td.name}
{
// Its possible for a tx to be submitted with an empty space as team name
td.name.trim() !== '' ? td.name : t('[empty]')
}
</Link>
),
earned: num(td.totalQuantumRewards),
@@ -1,15 +1,17 @@
import { type TransferNode } from '@vegaprotocol/types';
import { ActiveRewardCard } from '../rewards-container/active-rewards';
import { useT } from '../../lib/use-t';
import { type EnrichedTransfer } from '../../lib/hooks/use-game-cards';
import { useMarketsMapProvider } from '@vegaprotocol/markets';
export const GamesContainer = ({
data,
currentEpoch,
}: {
data: TransferNode[];
data: EnrichedTransfer[];
currentEpoch: number;
}) => {
const t = useT();
const { data: markets } = useMarketsMapProvider();
if (!data || data.length === 0) {
return (
@@ -36,6 +38,7 @@ export const GamesContainer = ({
transferNode={game}
currentEpoch={currentEpoch}
kind={transfer.kind}
allMarkets={markets || undefined}
/>
);
})}
@@ -1,9 +1,11 @@
import { isValidUrl } from '@vegaprotocol/utils';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
const NUM_AVATARS = 20;
const AVATAR_PATHNAME_PATTERN = '/team-avatars/{id}.png';
const getFallbackAvatar = (teamId: string) => {
export const getFallbackAvatar = (teamId: string) => {
const avatarId = ((parseInt(teamId, 16) % NUM_AVATARS) + 1)
.toString()
.padStart(2, '0'); // between 01 - 20
@@ -11,6 +13,26 @@ const getFallbackAvatar = (teamId: string) => {
return AVATAR_PATHNAME_PATTERN.replace('{id}', avatarId);
};
const useAvatar = (teamId: string, url: string) => {
const fallback = getFallbackAvatar(teamId);
const [avatar, setAvatar] = useState<string>(fallback);
useEffect(() => {
if (!isValidUrl(url)) return;
fetch(url, { cache: 'force-cache' })
.then((response) => {
if (response.ok) {
setAvatar(url);
}
})
.catch(() => {
/** noop */
});
});
return avatar;
};
export const TeamAvatar = ({
teamId,
imgUrl,
@@ -22,7 +44,7 @@ export const TeamAvatar = ({
alt?: string;
size?: 'large' | 'small';
}) => {
const img = imgUrl && imgUrl.length > 0 ? imgUrl : getFallbackAvatar(teamId);
const img = useAvatar(teamId, imgUrl);
return (
// eslint-disable-next-line @next/next/no-img-element
<img
@@ -0,0 +1,155 @@
import { 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';
import { type TeamGame } from '../../lib/hooks/use-games';
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 });
};
@@ -11,10 +11,11 @@ import { formatNumberRounded } from '@vegaprotocol/utils';
import {
type TeamStats as ITeamStats,
type Member,
type TeamGame,
} from '../../lib/hooks/use-team';
import { useT } from '../../lib/use-t';
import { DispatchMetricLabels, type DispatchMetric } from '@vegaprotocol/types';
import classNames from 'classnames';
import { type TeamGame } from '../../lib/hooks/use-games';
export const TeamStats = ({
stats,
@@ -102,7 +103,13 @@ const LatestResults = ({ games }: { games: TeamGame[] }) => {
);
};
const FavoriteGame = ({ games }: { games: TeamGame[] }) => {
export const FavoriteGame = ({
games,
noLabel = false,
}: {
games: TeamGame[];
noLabel?: boolean;
}) => {
const t = useT();
const rewardMetrics = games.map(
@@ -128,7 +135,13 @@ const FavoriteGame = ({ games }: { games: TeamGame[] }) => {
return (
<dl className="flex flex-col gap-1">
<dt className="text-muted text-sm">{t('Favorite game')}</dt>
<dt
className={classNames('text-muted text-sm', {
hidden: noLabel,
})}
>
{t('Favorite game')}
</dt>
<dd>
<Pill className="inline-flex items-center gap-1 bg-transparent text-sm">
<VegaIcon
@@ -142,7 +155,7 @@ const FavoriteGame = ({ games }: { games: TeamGame[] }) => {
);
};
const StatSection = ({ children }: { children: ReactNode }) => {
export const StatSection = ({ children }: { children: ReactNode }) => {
return (
<section className="flex flex-col lg:flex-row gap-4 lg:gap-8">
{children}
@@ -150,11 +163,11 @@ const StatSection = ({ children }: { children: ReactNode }) => {
);
};
const StatSectionSeparator = () => {
export const StatSectionSeparator = () => {
return <div className="hidden md:block border-r border-default" />;
};
const StatList = ({ children }: { children: ReactNode }) => {
export const StatList = ({ children }: { children: ReactNode }) => {
return (
<dl className="grid grid-cols-2 md:flex gap-4 md:gap-6 lg:gap-8 whitespace-nowrap">
{children}
@@ -162,19 +175,21 @@ const StatList = ({ children }: { children: ReactNode }) => {
);
};
const Stat = ({
export const Stat = ({
value,
label,
tooltip,
valueTestId,
className,
}: {
value: ReactNode;
label: ReactNode;
tooltip?: string;
valueTestId?: string;
className?: classNames.Argument;
}) => {
return (
<div>
<div className={classNames(className)}>
<dd className="text-3xl lg:text-4xl" data-testid={valueTestId}>
{value}
</dd>
@@ -3,7 +3,6 @@ import { Outlet } from 'react-router-dom';
import { Sidebar, SidebarContent, useSidebar } from '../sidebar';
import classNames from 'classnames';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
export const LayoutWithSidebar = ({
header,
sidebar,
@@ -17,7 +16,7 @@ export const LayoutWithSidebar = ({
const sidebarOpen = sidebarView !== null;
const gridClasses = classNames(
'h-full relative z-0 grid',
'grid-rows-[min-content_1fr_40px]',
'grid-rows-[min-content_1fr_50px]',
'lg:grid-rows-[min-content_1fr]',
'lg:grid-cols-[1fr_280px_40px]',
'xxxl:grid-cols-[1fr_320px_40px]'
@@ -27,10 +26,13 @@ export const LayoutWithSidebar = ({
<div className={gridClasses}>
<div className="col-span-full">{header}</div>
<main
className={classNames('col-start-1 col-end-1 overflow-y-auto', {
'lg:col-end-3': !sidebarOpen,
'hidden lg:block lg:col-end-2': sidebarOpen,
})}
className={classNames(
'col-start-1 col-end-1 overflow-hidden lg:overflow-y-auto grow lg:grow-0',
{
'lg:col-end-3': !sidebarOpen,
'hidden lg:block lg:col-end-2': sidebarOpen,
}
)}
>
<Outlet />
</main>
@@ -1 +1,2 @@
export * from './market-header';
export * from './mobile-market-header';
@@ -0,0 +1,146 @@
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 grid grid-cols-2 h-10 bg-vega-clight-700 dark:bg-vega-cdark-700">
<FullScreenPopover
open={openMarket}
onOpenChange={(x) => {
setOpenMarket(x);
}}
trigger={
<button
data-testid="popover-trigger"
className="min-w-0 flex gap-1 items-center"
>
<h1 className="whitespace-nowrap overflow-hidden text-ellipsis items-center">
<span className="">
{data
? data.tradableInstrument.instrument.code
: t('Select market')}
</span>
</h1>
<VegaIcon
name={VegaIconNames.CHEVRON_DOWN}
size={16}
className={classNames(
'origin-center transition-transform ease-in-out duration-300 flex',
{
'rotate-180': openMarket,
}
)}
/>
</button>
}
>
<MarketSelector
currentMarketId={marketId}
onSelect={() => setOpenMarket(false)}
/>
</FullScreenPopover>
<FullScreenPopover
open={openPrice}
onOpenChange={(x) => {
setOpenPrice(x);
}}
trigger={
<button
data-testid="popover-trigger"
className="min-w-0 flex gap-2 items-center justify-end"
>
{data && (
<>
<span className="min-w-0 flex flex-col items-end gap-0">
<span className="text-sm">
<MarketMarkPrice
marketId={data.id}
decimalPlaces={data.decimalPlaces}
/>
</span>
<span className="text-xs">
<Last24hPriceChange
marketId={data.id}
decimalPlaces={data.decimalPlaces}
fallback={<span />} // dont render anything so price is vertically centered
/>
</span>
</span>
<VegaIcon
name={VegaIconNames.CHEVRON_DOWN}
size={16}
className={classNames(
'min-w-0 transition-transform ease-in-out duration-300',
{
'rotate-180': openPrice,
}
)}
/>
</>
)}
</button>
}
>
{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>
);
};
interface PopoverProps extends PopoverPrimitive.PopoverProps {
trigger: React.ReactNode | string;
}
const FullScreenPopover = ({
trigger,
children,
open,
onOpenChange,
}: PopoverProps) => {
return (
<PopoverPrimitive.Root open={open} onOpenChange={onOpenChange}>
<PopoverPrimitive.Trigger asChild={true}>
{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 './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 Theme = 'system' | 'yellow';
export const Navbar = ({
children,
theme = 'system',
}: {
children?: ReactNode;
theme?: Theme;
}) => {
export const Navbar = ({ theme = 'system' }: { theme?: Theme }) => {
const i18n = useI18n();
const t = useT();
// menu state for small screens
@@ -75,8 +69,6 @@ export const Navbar = ({
>
<VLogo className="w-4" />
</NavLink>
{/* Left section */}
<div className="flex items-center lg:hidden">{children}</div>
{/* Used to show header in nav on mobile */}
<div className="hidden lg:block">
<NavbarMenu onClick={() => setMenu(null)} />
@@ -73,6 +73,7 @@ query ActiveRewards(
reference
status
timestamp
gameId
kind {
... on RecurringTransfer {
startEpoch
@@ -18,7 +18,7 @@ export type ActiveRewardsQueryVariables = Types.Exact<{
}>;
export type ActiveRewardsQuery = { __typename?: 'Query', transfersConnection?: { __typename?: 'TransferConnection', edges?: Array<{ __typename?: 'TransferEdge', node: { __typename?: 'TransferNode', transfer: { __typename?: 'Transfer', amount: string, id: string, from: string, fromAccountType: Types.AccountType, to: string, toAccountType: Types.AccountType, reference?: string | null, status: Types.TransferStatus, timestamp: any, reason?: string | null, asset?: { __typename?: 'Asset', id: string, symbol: string, decimals: number, name: string, quantum: string, status: Types.AssetStatus } | null, kind: { __typename?: 'OneOffGovernanceTransfer' } | { __typename?: 'OneOffTransfer' } | { __typename?: 'RecurringGovernanceTransfer' } | { __typename?: 'RecurringTransfer', startEpoch: number, endEpoch?: number | null, dispatchStrategy?: { __typename?: 'DispatchStrategy', dispatchMetric: Types.DispatchMetric, dispatchMetricAssetId: string, marketIdsInScope?: Array<string> | null, entityScope: Types.EntityScope, individualScope?: Types.IndividualScope | null, teamScope?: Array<string | null> | null, nTopPerformers?: string | null, stakingRequirement: string, notionalTimeWeightedAveragePositionRequirement: string, windowLength: number, lockPeriod: number, distributionStrategy: Types.DistributionStrategy, rankTable?: Array<{ __typename?: 'RankTable', startRank: number, shareRatio: number } | null> | null } | null } }, fees?: Array<{ __typename?: 'TransferFee', transferId: string, amount: string, epoch: number } | null> | null } } | null> | null } | null };
export type ActiveRewardsQuery = { __typename?: 'Query', transfersConnection?: { __typename?: 'TransferConnection', edges?: Array<{ __typename?: 'TransferEdge', node: { __typename?: 'TransferNode', transfer: { __typename?: 'Transfer', amount: string, id: string, from: string, fromAccountType: Types.AccountType, to: string, toAccountType: Types.AccountType, reference?: string | null, status: Types.TransferStatus, timestamp: any, gameId?: string | null, reason?: string | null, asset?: { __typename?: 'Asset', id: string, symbol: string, decimals: number, name: string, quantum: string, status: Types.AssetStatus } | null, kind: { __typename?: 'OneOffGovernanceTransfer' } | { __typename?: 'OneOffTransfer' } | { __typename?: 'RecurringGovernanceTransfer' } | { __typename?: 'RecurringTransfer', startEpoch: number, endEpoch?: number | null, dispatchStrategy?: { __typename?: 'DispatchStrategy', dispatchMetric: Types.DispatchMetric, dispatchMetricAssetId: string, marketIdsInScope?: Array<string> | null, entityScope: Types.EntityScope, individualScope?: Types.IndividualScope | null, teamScope?: Array<string | null> | null, nTopPerformers?: string | null, stakingRequirement: string, notionalTimeWeightedAveragePositionRequirement: string, windowLength: number, lockPeriod: number, distributionStrategy: Types.DistributionStrategy, rankTable?: Array<{ __typename?: 'RankTable', startRank: number, shareRatio: number } | null> | null } | null } }, fees?: Array<{ __typename?: 'TransferFee', transferId: string, amount: string, epoch: number } | null> | null } } | null> | null } | null };
export type RewardsHistoryQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
@@ -144,6 +144,7 @@ export const ActiveRewardsDocument = gql`
reference
status
timestamp
gameId
kind {
... on RecurringTransfer {
startEpoch
@@ -152,7 +152,11 @@ export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
if (!enrichedTransfers || !enrichedTransfers.length) return null;
return (
<Card title={t('Active rewards')} className="lg:col-span-full">
<Card
title={t('Active rewards')}
className="lg:col-span-full"
data-testid="active-rewards-card"
>
{enrichedTransfers.length > 1 && (
<TradingInput
onChange={(e) =>
@@ -247,12 +251,7 @@ const StatusIndicator = ({
);
};
export const ActiveRewardCard = ({
transferNode,
currentEpoch,
kind,
allMarkets,
}: {
type ActiveRewardCardProps = {
transferNode: TransferNode & {
asset?: AssetFieldsFragment | null;
markets?: (MarketFieldsFragment | null)[];
@@ -260,7 +259,13 @@ export const ActiveRewardCard = ({
currentEpoch: number;
kind: RecurringTransfer;
allMarkets?: Record<string, MarketFieldsFragment | null>;
}) => {
};
export const ActiveRewardCard = ({
transferNode,
currentEpoch,
kind,
allMarkets,
}: ActiveRewardCardProps) => {
const t = useT();
const { transfer } = transferNode;
@@ -312,49 +317,30 @@ export const ActiveRewardCard = ({
MarketState.STATE_CLOSED,
].includes(m.state)
);
if (marketSettled) {
return null;
}
const assetInSettledMarket =
const assetInActiveMarket =
allMarkets &&
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
return (
m?.state &&
[
MarketState.STATE_TRADING_TERMINATED,
MarketState.STATE_SETTLED,
MarketState.STATE_CANCELLED,
MarketState.STATE_CLOSED,
].includes(m.state)
);
return m?.state && MarketState.STATE_ACTIVE === m.state;
}
return false;
});
// Gray out the cards that are related to suspended markets
const suspended = transferNode.markets?.some(
const marketSuspended = transferNode.markets?.some(
(m) =>
m?.state === MarketState.STATE_SUSPENDED ||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
);
const assetInSuspendedMarket =
allMarkets &&
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
return (
m?.state === MarketState.STATE_SUSPENDED ||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
);
}
return false;
});
// Gray out the cards that are related to suspended markets
// Or settlement assets in markets that are not active and eligible for rewards
const { gradientClassName, mainClassName } =
suspended || assetInSuspendedMarket || assetInSettledMarket
marketSuspended || !assetInActiveMarket
? {
gradientClassName: 'from-vega-cdark-500 to-vega-clight-400',
mainClassName: 'from-vega-cdark-400 dark:from-vega-cdark-600 to-20%',
@@ -371,6 +357,7 @@ export const ActiveRewardCard = ({
'rounded-lg',
gradientClassName
)}
data-testid="active-rewards-card"
>
<div
className={classNames(
@@ -382,7 +369,7 @@ export const ActiveRewardCard = ({
<div className="flex flex-col gap-2 items-center text-center">
<EntityIcon transfer={transfer} />
{entityScope && (
<span className="text-muted text-xs">
<span className="text-muted text-xs" data-testid="entity-scope">
{EntityScopeLabelMapping[entityScope] || t('Unspecified')}
</span>
)}
@@ -390,7 +377,7 @@ export const ActiveRewardCard = ({
<div className="flex flex-col gap-2 items-center text-center">
<h3 className="flex flex-col gap-1 text-2xl shrink-1 text-center">
<span className="font-glitch">
<span className="font-glitch" data-testid="reward-value">
{addDecimalsFormatNumber(
transferNode.transfer.amount,
transferNode.transfer.asset?.decimals || 0,
@@ -411,7 +398,7 @@ export const ActiveRewardCard = ({
)}
underline={true}
>
<span className="text-xs">
<span className="text-xs" data-testid="distribution-strategy">
{
DistributionStrategyMapping[
dispatchStrategy.distributionStrategy
@@ -429,7 +416,10 @@ export const ActiveRewardCard = ({
'Number of epochs after distribution to delay vesting of rewards by'
)}
/>
<span className="text-muted text-xs whitespace-nowrap">
<span
className="text-muted text-xs whitespace-nowrap"
data-testid="locked-for"
>
{t('numberEpochs', '{{count}} epochs', {
count: kind.dispatchStrategy?.lockPeriod,
})}
@@ -438,15 +428,15 @@ export const ActiveRewardCard = ({
</div>
<span className="border-[0.5px] border-gray-700" />
<span>
<span data-testid="dispatch-metric-info">
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]} {' '}
<Tooltip
underline={suspended}
underline={marketSuspended}
description={
(suspended || assetInSuspendedMarket) &&
(marketSuspended || !assetInActiveMarket) &&
(specificMarkets
? t('Eligible market(s) currently suspended')
: assetInSuspendedMarket
: !assetInActiveMarket
? t('Currently no markets eligible for reward')
: '')
}
@@ -458,8 +448,8 @@ export const ActiveRewardCard = ({
<div className="flex items-center gap-8 flex-wrap">
{kind.endEpoch && (
<span className="flex flex-col">
<span className="text-muted text-xs">{t('Ends in')}</span>
<span>
<span className="text-muted text-xs">{t('Ends in')} </span>
<span data-testid="ends-in">
{t('numberEpochs', '{{count}} epochs', {
count: kind.endEpoch - currentEpoch,
})}
@@ -470,7 +460,7 @@ export const ActiveRewardCard = ({
{
<span className="flex flex-col">
<span className="text-muted text-xs">{t('Assessed over')}</span>
<span>
<span data-testid="assessed-over">
{t('numberEpochs', '{{count}} epochs', {
count: dispatchStrategy.windowLength,
})}
@@ -479,7 +469,7 @@ export const ActiveRewardCard = ({
}
</div>
{dispatchStrategy?.dispatchMetric && (
<span className="text-muted text-sm h-[2rem]">
<span className="text-muted text-sm h-[3rem]">
{t(DispatchMetricDescription[dispatchStrategy?.dispatchMetric])}
</span>
)}
@@ -496,6 +486,62 @@ export const ActiveRewardCard = ({
);
};
export const DispatchMetricInfo = ({
transferNode,
allMarkets,
}: {
transferNode: ActiveRewardCardProps['transferNode'];
allMarkets?: ActiveRewardCardProps['allMarkets'];
}) => {
const dispatchStrategy =
transferNode.transfer.kind.__typename === 'RecurringTransfer'
? transferNode.transfer.kind.dispatchStrategy
: null;
const dispatchAsset = transferNode.transfer.asset;
const marketIdsInScope = dispatchStrategy?.marketIdsInScope;
const firstMarketData = transferNode.markets?.[0];
const specificMarkets = useMemo(() => {
if (
!firstMarketData ||
!marketIdsInScope ||
marketIdsInScope.length === 0
) {
return null;
}
if (marketIdsInScope.length > 1) {
const marketNames =
allMarkets &&
marketIdsInScope
.map((id) => allMarkets[id]?.tradableInstrument?.instrument?.name)
.join(', ');
return (
<Tooltip description={marketNames}>
<span>Specific markets</span>
</Tooltip>
);
}
const name = firstMarketData?.tradableInstrument?.instrument?.name;
if (name) {
return <span>{name}</span>;
}
return null;
}, [firstMarketData, marketIdsInScope, allMarkets]);
if (!dispatchStrategy) return null;
return (
<span data-testid="dispatch-metric-info">
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]} {' '}
<span>{specificMarkets || dispatchAsset?.name}</span>
</span>
);
};
const RewardRequirements = ({
dispatchStrategy,
assetDecimalPlaces = 0,
@@ -513,7 +559,7 @@ const RewardRequirements = ({
entity: EntityScopeLabelMapping[dispatchStrategy.entityScope],
})}
</dt>
<dd className="flex items-center gap-1">
<dd className="flex items-center gap-1" data-testid="scope">
<RewardEntityScope dispatchStrategy={dispatchStrategy} />
</dd>
</div>
@@ -522,7 +568,10 @@ const RewardRequirements = ({
<dt className="flex items-center gap-1 text-muted">
{t('Staked VEGA')}
</dt>
<dd className="flex items-center gap-1">
<dd
className="flex items-center gap-1"
data-testid="staking-requirement"
>
{addDecimalsFormatNumber(
dispatchStrategy?.stakingRequirement || 0,
assetDecimalPlaces
@@ -534,7 +583,7 @@ const RewardRequirements = ({
<dt className="flex items-center gap-1 text-muted">
{t('Average position')}
</dt>
<dd className="flex items-center gap-1">
<dd className="flex items-center gap-1" data-testid="average-position">
{addDecimalsFormatNumber(
dispatchStrategy?.notionalTimeWeightedAveragePositionRequirement ||
0,
@@ -81,6 +81,7 @@ export const Settings = () => {
intent={Intent.Primary}
onClick={() => {
localStorage.clear();
sessionStorage.clear();
window.location.reload();
}}
>
+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 { useT } from '../../lib/use-t';
import { ErrorBoundary } from '../error-boundary';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
export enum ViewType {
Order = 'Order',
@@ -26,9 +27,10 @@ export enum ViewType {
Transfer = 'Transfer',
Settings = 'Settings',
ViewAs = 'ViewAs',
Close = 'Close',
}
type SidebarView =
export type BarView =
| {
type: ViewType.Deposit;
assetId?: string;
@@ -49,6 +51,9 @@ type SidebarView =
}
| {
type: ViewType.Settings;
}
| {
type: ViewType.Close;
};
export const Sidebar = ({ options }: { options?: ReactNode }) => {
@@ -57,26 +62,52 @@ export const Sidebar = ({ options }: { options?: ReactNode }) => {
const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1';
const setViewAsDialogOpen = useViewAsDialog((state) => state.setOpen);
const { pubKeys } = useVegaWallet();
const { isMobile } = useScreenDimensions();
const { getView } = useSidebar((store) => ({
setViews: store.setViews,
getView: store.getView,
}));
const currView = getView(currentRouteId);
return (
<div className="flex h-full p-1 lg:flex-col gap-2" data-testid="sidebar">
{options && <nav className={navClasses}>{options}</nav>}
<nav className={classNames(navClasses, 'ml-auto lg:mt-auto lg:ml-0')}>
<SidebarButton
view={ViewType.ViewAs}
onClick={() => {
setViewAsDialogOpen(true);
}}
icon={VegaIconNames.EYE}
tooltip={t('View as party')}
disabled={Boolean(pubKeys)}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Settings}
icon={VegaIconNames.COG}
tooltip={t('Settings')}
routeId={currentRouteId}
/>
<div className="flex h-full lg:flex-col gap-1" data-testid="sidebar">
{options && (
<nav className={classNames(navClasses, 'flex grow')}>{options}</nav>
)}
<nav
className={classNames(
navClasses,
'ml-auto lg:mt-auto lg:ml-0 shrink-0'
)}
>
{!isMobile ? (
<>
<SidebarButton
view={ViewType.ViewAs}
onClick={() => {
setViewAsDialogOpen(true);
}}
icon={VegaIconNames.EYE}
tooltip={t('View as party')}
disabled={Boolean(pubKeys)}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Settings}
icon={VegaIconNames.COG}
tooltip={t('Settings')}
routeId={currentRouteId}
/>
</>
) : (
currView && (
<SidebarButton
view={ViewType.Close}
icon={VegaIconNames.ARROW_LEFT}
tooltip={t('Back')}
routeId={currentRouteId}
/>
)
)}
<NodeHealthContainer />
</nav>
</div>
@@ -103,7 +134,7 @@ export const SidebarButton = ({
getView: store.getView,
}));
const currView = getView(routeId);
const onSelect = (view: SidebarView['type']) => {
const onSelect = (view: BarView['type']) => {
if (view === currView?.type) {
setViews(null, routeId);
} else {
@@ -133,7 +164,7 @@ export const SidebarButton = ({
<button
className={buttonClasses}
data-testid={view}
onClick={onClick || (() => onSelect(view as SidebarView['type']))}
onClick={onClick || (() => onSelect(view as BarView['type']))}
disabled={disabled}
>
<VegaIcon name={icon} size={20} />
@@ -180,6 +211,10 @@ export const SidebarContent = () => {
}
}
if (view.type === ViewType.Close) {
return <CloseSidebar />;
}
if (view.type === ViewType.Info) {
if (params.marketId) {
return (
@@ -267,9 +302,9 @@ const CloseSidebar = () => {
};
export const useSidebar = create<{
views: { [key: string]: SidebarView | null };
setViews: (view: SidebarView | null, routeId: string) => void;
getView: (routeId: string) => SidebarView | null | undefined;
views: { [key: string]: BarView | null };
setViews: (view: BarView | null, routeId: string) => void;
getView: (routeId: string) => BarView | null | undefined;
}>()((set, get) => ({
views: {},
setViews: (x, routeId) =>
+3
View File
@@ -93,6 +93,9 @@ export const Table = forwardRef<
key={i}
className={classNames(dataEntry['className'] as string, {
'max-md:flex flex-col w-full': !noCollapse,
// collapsed (mobile) row divider
'first:border-t-0 max-md:border-t border-vega-clight-500 dark:border-vega-cdark-500 first:mt-0 mt-1':
!noCollapse,
})}
onClick={() => {
if (onRowClick) {
+1 -1
View File
@@ -1,3 +1,3 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
VEGA_VERSION=v0.74.0-preview.8
VEGA_VERSION=v0.74.1
LOCAL_SERVER=false
+1 -1
View File
@@ -1,3 +1,3 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:develop
VEGA_VERSION=v0.74.0-preview.6
VEGA_VERSION=v0.74.1
LOCAL_SERVER=false
+1 -1
View File
@@ -1,3 +1,3 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:main
VEGA_VERSION=v0.73.10
VEGA_VERSION=v0.73.13
LOCAL_SERVER=false
+27 -2
View File
@@ -111,6 +111,7 @@ def init_vega(request=None):
f"Container {container.id} started",
extra={"worker_id": os.environ.get("PYTEST_XDIST_WORKER")},
)
vega.container = container
yield vega
except APIError as e:
logger.info(f"Container creation failed.")
@@ -177,10 +178,34 @@ def init_page(vega: VegaServiceNull, browser: Browser, request: pytest.FixtureRe
@pytest.fixture
def vega(request):
with init_vega(request) as vega:
yield vega
with init_vega(request) as vega_instance:
request.addfinalizer(lambda: cleanup_container(vega_instance))
yield vega_instance
def cleanup_container(vega_instance):
try:
# Attempt to stop the container if it's still running
if vega_instance.container.status == 'running':
print(f"Stopping container {vega_instance.container.id}")
vega_instance.container.stop()
else:
print(f"Container {vega_instance.container.id} is not running.")
except docker.errors.NotFound:
print(f"Container {vega_instance.container.id} not found, may have been stopped and removed.")
except Exception as e:
print(f"Error during cleanup: {str(e)}")
try:
# Attempt to remove the container
vega_instance.container.remove()
print(f"Container {vega_instance.container.id} removed.")
except docker.errors.NotFound:
print(f"Container {vega_instance.container.id} not found, may have been removed.")
except Exception as e:
print(f"Error during container removal: {str(e)}")
@pytest.fixture
def page(vega, browser, request):
with init_page(vega, browser, request) as page_instance:
+1 -1
View File
@@ -1161,7 +1161,7 @@ profile = ["pytest-profiling", "snakeviz"]
type = "git"
url = "https://github.com/vegaprotocol/vega-market-sim.git/"
reference = "HEAD"
resolved_reference = "026976549c21e59f6f9c48f06ab15a210c5a5bf3"
resolved_reference = "a8afded34874a01cfd1bb771052aa12a062960b9"
[[package]]
name = "websocket-client"
@@ -2,7 +2,7 @@ import pytest
from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull
from datetime import datetime, timedelta
from conftest import init_vega
from conftest import init_vega, cleanup_container
from fixtures.market import setup_continuous_market
from actions.utils import wait_for_toast_confirmation
@@ -17,8 +17,10 @@ expire = "expire"
@pytest.fixture(scope="module")
def vega(request):
with init_vega(request) as vega:
yield vega
with init_vega(request) as vega_instance:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module")
@@ -9,6 +9,16 @@ leverage_input = "#leverage-input"
tab_positions = "tab-positions"
margin_row = '[col-id="margin"]'
current_margin = "deal-ticket-fee-current-margin"
available_collateral = "deal-ticket-fee-available-collateral"
additional_margin_required = "deal-ticket-fee-additional-margin-required"
liquidation_estimate = "deal-ticket-fee-liquidation-estimate"
dialog_content = "dialog-content"
cross_margin = "cross-margin"
isolated_margin = "isolated-margin"
confirm_cross_margin_mode = "confirm-cross-margin-mode"
confirm_isolated_margin_mode = "confirm-isolated-margin-mode"
def create_position(vega: VegaServiceNull, market_id):
submit_order(vega, "Key 1", market_id, "SIDE_SELL", 100, 110)
@@ -29,10 +39,9 @@ def test_switch_cross_isolated_margin(
expect(page.get_by_test_id(tooltip_content).nth(0)).to_have_text(
"Liquidation: 582.81328Margin: 874.21992General account: 998,084.95183"
)
page.get_by_role("button", name="Isolated 10x").click()
page.locator(leverage_input).clear()
page.locator(leverage_input).type("1")
page.get_by_role("button", name="Confirm").click()
page.get_by_test_id(isolated_margin).click()
page.locator(leverage_input).fill("1")
page.get_by_test_id(confirm_isolated_margin_mode).click()
wait_for_toast_confirmation(page)
next_epoch(vega=vega)
expect(page.get_by_test_id("toast-content")).to_have_text(
@@ -45,9 +54,52 @@ def test_switch_cross_isolated_margin(
expect(page.get_by_test_id(tooltip_content).nth(0)).to_have_text(
"Liquidation: 583.62409Margin: 11,109.99996Order: 11,000.00"
)
page.get_by_role("button", name="Cross").click()
page.get_by_role("button", name="Confirm").click()
page.get_by_test_id(cross_margin).click()
page.get_by_test_id(confirm_cross_margin_mode).click()
wait_for_toast_confirmation(page)
next_epoch(vega=vega)
expect(page.locator(margin_row).nth(1)).to_have_text(
"22,109.99996Cross1.0x")
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_check_cross_isolated_margin_info(
continuous_market, vega: VegaServiceNull, page: Page):
create_position(vega, continuous_market)
page.goto(f"/#/markets/{continuous_market}")
expect(page.get_by_test_id(current_margin)).to_have_text(
"Current margin874.21992 tDAI")
expect(page.get_by_test_id(available_collateral)
).to_have_text("Available collateral998,084.95183 tDAI")
expect(page.get_by_test_id(additional_margin_required)
).to_have_text("Additional margin required0.00 tDAI")
expect(page.get_by_test_id(liquidation_estimate)
).to_have_text("Liquidation estimate- BTC")
page.get_by_test_id(isolated_margin).click()
page.locator(leverage_input).fill("6")
expect(page.get_by_test_id(dialog_content).get_by_test_id("notification")).to_have_text(
"You have an existing position and and open order on this market.Changing the margin mode and leverage will move 2,703.11341 tDAI from your general account to fund the position.")
expect(page.get_by_test_id(dialog_content).get_by_test_id(current_margin)).to_have_text(
"Current margin874.21992 tDAI")
expect(page.get_by_test_id(dialog_content).get_by_test_id(available_collateral)
).to_have_text("Available collateral998,084.95183 tDAI")
expect(page.get_by_test_id(dialog_content).get_by_test_id(additional_margin_required)
).to_have_text("Additional margin required2,703.11341 tDAI")
expect(page.get_by_test_id(dialog_content).get_by_test_id(liquidation_estimate)
).to_have_text("Liquidation estimate95.23553 BTC")
page.get_by_test_id(confirm_isolated_margin_mode).click()
wait_for_toast_confirmation(page)
next_epoch(vega=vega)
expect(page.get_by_test_id("toast-content")).to_have_text(
"ConfirmedYour transaction has been confirmedView in block explorerUpdate margin modeBTC:DAI_2023Isolated margin mode, leverage: 6.0x")
page.get_by_test_id(cross_margin).click()
expect(page.get_by_test_id(dialog_content).get_by_test_id(
"notification")).not_to_be_visible()
expect(page.get_by_test_id(dialog_content).get_by_test_id(current_margin)).to_have_text(
"Current margin4,223.33332 tDAI")
expect(page.get_by_test_id(dialog_content).get_by_test_id(available_collateral)
).to_have_text("Available collateral995,381.83843 tDAI")
expect(page.get_by_test_id(dialog_content).get_by_test_id(additional_margin_required)
).to_have_text("Additional margin required0.00 tDAI")
expect(page.get_by_test_id(dialog_content).get_by_test_id(liquidation_estimate)
).to_have_text("Liquidation estimate0.00 BTC")
@@ -3,7 +3,7 @@ from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull
from actions.vega import submit_order
from datetime import datetime, timedelta
from conftest import init_vega
from conftest import init_vega, cleanup_container
from fixtures.market import setup_continuous_market
stop_order_btn = "order-type-Stop"
@@ -259,9 +259,10 @@ def test_submit_stop_limit_order_cancel(
class TestStopOcoValidation:
@pytest.fixture(scope="class")
def vega(self, request):
with init_vega(request) as vega:
yield vega
def vega(request):
with init_vega(request) as vega_instance:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="class")
def continuous_market(self, vega):
@@ -2,7 +2,7 @@ import pytest
from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull
from actions.utils import change_keys
from conftest import init_vega
from conftest import init_vega, cleanup_container
from fixtures.market import setup_continuous_market
order_size = "order-size"
@@ -14,8 +14,10 @@ deal_ticket_deposit_dialog_button = "deal-ticket-deposit-dialog-button"
@pytest.fixture(scope="module")
def vega(request):
with init_vega(request) as vega:
yield vega
with init_vega(request) as vega_instance:
request.addfinalizer(lambda: cleanup_container(
vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module")
@@ -23,6 +25,7 @@ def continuous_market(vega):
return setup_continuous_market(vega)
@pytest.mark.skip("tbd - issue only on the sim, should work in vega 0.74.0")
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_should_display_info_and_button_for_deposit(continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}")
@@ -33,7 +36,7 @@ def test_should_display_info_and_button_for_deposit(continuous_market, page: Pag
"You may not have enough margin available to open this position.")
page.get_by_test_id(deal_ticket_warning_margin).hover()
expect(page.get_by_test_id("tooltip-content").nth(0)).to_have_text(
"1,661,896.6317 tDAI is currently required.You have only 1,000,000.00.Deposit tDAI")
"1,661,888.12901 tDAI is currently required.You have only 999,991.49731.Deposit tDAI")
page.get_by_test_id(deal_ticket_deposit_dialog_button).nth(0).click()
expect(page.get_by_test_id("sidebar-content")
).to_contain_text("DepositFrom")
+11 -6
View File
@@ -4,7 +4,7 @@ from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull
from actions.vega import submit_order
from wallet_config import MM_WALLET
from conftest import init_vega, init_page, auth_setup
from conftest import init_vega, init_page, auth_setup, cleanup_container
from actions.utils import next_epoch, change_keys, forward_time
from fixtures.market import market_exists, setup_continuous_market
@@ -82,31 +82,36 @@ def market_ids():
@pytest.fixture(scope="module")
def vega_volume_discount_tier_1(request):
with init_vega(request) as vega_volume_discount_tier_1:
yield vega_volume_discount_tier_1
request.addfinalizer(lambda: cleanup_container(vega_volume_discount_tier_1)) # Register the cleanup function
yield vega_volume_discount_tier_1
@pytest.fixture(scope="module")
def vega_volume_discount_tier_2(request):
with init_vega(request) as vega_volume_discount_tier_2:
yield vega_volume_discount_tier_2
request.addfinalizer(lambda: cleanup_container(vega_volume_discount_tier_2)) # Register the cleanup function
yield vega_volume_discount_tier_2
@pytest.fixture(scope="module")
def vega_referral_discount_tier_1(request):
with init_vega(request) as vega_referral_discount_tier_1:
yield vega_referral_discount_tier_1
request.addfinalizer(lambda: cleanup_container(vega_referral_discount_tier_1)) # Register the cleanup function
yield vega_referral_discount_tier_1
@pytest.fixture(scope="module")
def vega_referral_discount_tier_2(request):
with init_vega(request) as vega_referral_discount_tier_2:
yield vega_referral_discount_tier_2
request.addfinalizer(lambda: cleanup_container(vega_referral_discount_tier_2)) # Register the cleanup function
yield vega_referral_discount_tier_2
@pytest.fixture(scope="module")
def vega_referral_and_volume_discount(request):
with init_vega(request) as vega_referral_and_volume_discount:
yield vega_referral_and_volume_discount
request.addfinalizer(lambda: cleanup_container(vega_referral_and_volume_discount)) # Register the cleanup function
yield vega_referral_and_volume_discount
@pytest.fixture
@@ -3,7 +3,7 @@ from playwright.sync_api import expect, Page
import json
from vega_sim.null_service import VegaServiceNull
from fixtures.market import setup_simple_market
from conftest import init_vega
from conftest import init_vega, cleanup_container
from actions.vega import submit_order
from wallet_config import MM_WALLET, TERMINATE_WALLET, wallets
import logging
@@ -12,9 +12,10 @@ logger = logging.getLogger()
@pytest.fixture(scope="class")
def vega():
with init_vega() as vega:
yield vega
def vega(request):
with init_vega(request) as vega_instance:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="class")
@@ -2,8 +2,6 @@ import pytest
from playwright.sync_api import expect, Page
from vega_sim.null_service import VegaServiceNull
from actions.vega import submit_order
from conftest import init_vega
from fixtures.market import setup_continuous_market
from wallet_config import MM_WALLET2
def hover_and_assert_tooltip(page: Page, element_text):
@@ -11,39 +9,30 @@ def hover_and_assert_tooltip(page: Page, element_text):
element.hover()
expect(page.get_by_role("tooltip")).to_be_visible()
class TestIcebergOrdersValidations:
@pytest.fixture(scope="class")
def vega(self, request):
with init_vega(request) as vega:
yield vega
@pytest.fixture(scope="class")
def continuous_market(self, vega):
return setup_continuous_market(vega)
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_iceberg_submit(continuous_market, vega: VegaServiceNull, page: Page):
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id("iceberg").click()
page.get_by_test_id("order-peak-size").type("2")
page.get_by_test_id("order-minimum-size").type("1")
page.get_by_test_id("order-size").type("3")
page.get_by_test_id("order-price").type("107")
page.get_by_test_id("place-order").click()
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_iceberg_submit(self, continuous_market, vega: VegaServiceNull, page: Page):
page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id("iceberg").click()
page.get_by_test_id("order-peak-size").type("2")
page.get_by_test_id("order-minimum-size").type("1")
page.get_by_test_id("order-size").type("3")
page.get_by_test_id("order-price").type("107")
page.get_by_test_id("place-order").click()
expect(page.get_by_test_id("toast-content")).to_have_text(
"Awaiting confirmationPlease wait for your transaction to be confirmedView in block explorer"
)
expect(page.get_by_test_id("toast-content")).to_have_text(
"Awaiting confirmationPlease wait for your transaction to be confirmedView in block explorer"
)
vega.wait_fn(1)
vega.wait_for_total_catchup()
expect(page.get_by_test_id("toast-content")).to_have_text(
"Order filledYour transaction has been confirmedView in block explorerSubmit order - filledBTC:DAI_2023+3 @ 107.00 tDAI"
)
page.get_by_test_id("All").click()
expect(
(page.get_by_role("row").locator('[col-id="type"]')).nth(1)
).to_have_text("Limit (Iceberg)")
vega.wait_fn(1)
vega.wait_for_total_catchup()
expect(page.get_by_test_id("toast-content")).to_have_text(
"Order filledYour transaction has been confirmedView in block explorerSubmit order - filledBTC:DAI_2023+3 @ 107.00 tDAI"
)
page.get_by_test_id("All").click()
expect(
(page.get_by_role("row").locator('[col-id="type"]')).nth(1)
).to_have_text("Limit (Iceberg)")
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_iceberg_open_order(continuous_market, vega: VegaServiceNull, page: Page):
@@ -1,15 +1,16 @@
import pytest
from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull
from conftest import init_vega
from conftest import init_vega, cleanup_container
from fixtures.market import setup_continuous_market
from actions.utils import next_epoch, truncate_middle, change_keys
@pytest.fixture(scope="module")
def vega(request):
with init_vega(request) as vega:
yield vega
with init_vega(request) as vega_instance:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module")
@@ -4,14 +4,15 @@ import vega_sim.api.governance as governance
from vega_sim.null_service import VegaServiceNull
from playwright.sync_api import Page, expect
from fixtures.market import setup_continuous_market
from conftest import init_vega
from conftest import init_vega, cleanup_container
from actions.utils import next_epoch
@pytest.fixture(scope="class")
def vega():
with init_vega() as vega:
yield vega
def vega(request):
with init_vega(request) as vega_instance:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="class")
@@ -3,15 +3,16 @@ import pytest
from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull
from fixtures.market import setup_continuous_market
from conftest import init_page, init_vega, risk_accepted_setup
from conftest import init_page, init_vega, risk_accepted_setup, cleanup_container
market_title_test_id = "accordion-title"
@pytest.fixture(scope="module")
def vega():
with init_vega() as vega:
yield vega
def vega(request):
with init_vega(request) as vega_instance:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module")
@@ -3,7 +3,7 @@ import vega_sim.api.governance as governance
import re
from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull
from conftest import init_vega
from conftest import init_vega, cleanup_container
from fixtures.market import setup_simple_market
from wallet_config import MM_WALLET
@@ -13,8 +13,10 @@ col_market_id = '[col-id="market"] [data-testid="stack-cell-primary"]'
@pytest.fixture(scope="module")
def vega(request):
with init_vega(request) as vega:
yield vega
with init_vega(request) as vega_instance:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module")
@@ -3,7 +3,7 @@ from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull
from actions.vega import submit_order
from fixtures.market import setup_simple_market
from conftest import init_vega
from conftest import init_vega, cleanup_container
from actions.utils import wait_for_toast_confirmation, change_keys
from wallet_config import MM_WALLET, MM_WALLET2
@@ -15,8 +15,10 @@ COL_ID_FEE = ".ag-center-cols-container [col-id='fee'] .ag-cell-value"
@pytest.fixture(scope="module")
def vega(request):
with init_vega(request) as vega:
yield vega
with init_vega(request) as vega_instance:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module")
@@ -35,7 +35,7 @@ def test_market_lifecycle(proposed_market, vega: VegaServiceNull, page: Page):
# 6002-MDET-003
expect(page.get_by_test_id("market-price")).to_have_text("Mark Price0.00")
# 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)-")
# 6002-MDET-005
expect(page.get_by_test_id("market-volume")).to_have_text("Volume (24h)-")
# 6002-MDET-008
@@ -1,13 +1,14 @@
import pytest
from playwright.sync_api import Page, expect, Locator
from conftest import init_page, init_vega
from conftest import init_page, init_vega, cleanup_container
@pytest.fixture(scope="module")
def vega():
with init_vega() as vega:
yield vega
def vega(request):
with init_vega(request) as vega_instance:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
# we can reuse single page instance in all tests
@@ -2,7 +2,7 @@ import pytest
from playwright.sync_api import Page, expect
from vega_sim.service import PeggedOrder
from vega_sim.null_service import VegaServiceNull
from conftest import auth_setup, init_page, init_vega, risk_accepted_setup
from conftest import auth_setup, init_page, init_vega, risk_accepted_setup, cleanup_container
from fixtures.market import setup_continuous_market, setup_simple_market
from actions.utils import wait_for_toast_confirmation
@@ -11,8 +11,9 @@ order_tab = "tab-orders"
@pytest.fixture(scope="module")
def vega(request):
with init_vega(request) as vega:
yield vega
with init_vega(request) as vega_instance:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module", autouse=True)
@@ -2,15 +2,16 @@ import pytest
from playwright.sync_api import Page, expect
from typing import List
from actions.vega import submit_order, submit_liquidity, submit_multiple_orders
from conftest import init_vega
from conftest import init_vega, cleanup_container
from fixtures.market import setup_simple_market
from wallet_config import MM_WALLET, MM_WALLET2
@pytest.fixture(scope="module")
def vega():
with init_vega() as vega:
yield vega
def vega(request):
with init_vega(request) as vega_instance:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module")
@@ -4,7 +4,7 @@ from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull
from vega_sim.service import MarketStateUpdateType
from datetime import datetime, timedelta
from conftest import init_vega
from conftest import init_vega, cleanup_container
from actions.utils import change_keys
from actions.vega import submit_multiple_orders
from fixtures.market import setup_perps_market
@@ -17,8 +17,9 @@ col_amount = '[col-id="amount"]'
class TestPerpetuals:
@pytest.fixture(scope="class")
def vega(self, request):
with init_vega(request) as vega:
yield vega
with init_vega(request) as vega_instance:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="class")
def perps_market(self, vega: VegaServiceNull):
@@ -1,7 +1,7 @@
import pytest
from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull
from conftest import init_vega
from conftest import init_vega, cleanup_container
from fixtures.market import setup_continuous_market
TOOLTIP_LABEL = "margin-health-tooltip-label"
@@ -11,8 +11,10 @@ COL_ID_USED = ".ag-center-cols-container [col-id='used'] .ag-cell-value"
@pytest.fixture(scope="module")
def vega(request):
with init_vega(request) as vega:
yield vega
with init_vega(request) as vega_instance:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module")
@@ -1,7 +1,7 @@
import pytest
from playwright.sync_api import Page
from vega_sim.null_service import VegaServiceNull
from conftest import init_vega
from conftest import init_vega, cleanup_container
from fixtures.market import setup_continuous_market, setup_simple_market
from actions.utils import change_keys, create_and_faucet_wallet, forward_time, selector_contains_text
from actions.vega import submit_order, submit_liquidity
@@ -14,8 +14,10 @@ BUY_ORDERS = [[1, 106], [1, 107], [1, 108]]
@pytest.fixture(scope="module")
def vega(request):
with init_vega(request) as vega:
yield vega
with init_vega(request) as vega_instance:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module")
@@ -59,7 +59,7 @@ def test_filtered_cards(continuous_market, vega: VegaServiceNull, page: Page):
next_epoch(vega=vega)
page.reload()
expect(page.locator(".from-vega-cdark-400")).to_be_visible(timeout=15000)
expect(page.get_by_test_id("active-rewards-card")).to_be_visible(timeout=15000)
governance.submit_oracle_data(
wallet=vega.wallet,
payload={"trading.terminated": "true"},
@@ -67,4 +67,4 @@ def test_filtered_cards(continuous_market, vega: VegaServiceNull, page: Page):
)
next_epoch(vega=vega)
page.reload()
expect(page.locator(".from-vega-cdark-400")).not_to_be_in_viewport()
expect(page.get_by_test_id("active-rewards-card")).not_to_be_in_viewport()

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