Compare commits

...
Author SHA1 Message Date
Matthew Russell a0aa0065b6 chore: align viewing as banner 2024-02-12 11:07:43 -08:00
Matthew Russell bb6ecd2c72 chore: make fees and rewards page layout more consistent 2024-02-12 10:58:47 -08:00
Matthew Russell de62f09fda chore: usePageTitle hook to reduce imports 2024-02-09 15:51:33 -08:00
Matthew Russell 6d70b28f30 fix: title spacing on fees and rewards pages 2024-02-09 15:47:08 -08:00
m.rayandcandida-d 0d850bd8b9 feat(trading): add LP fee settings (#5773)
Co-authored-by: candida-d <62548908+candida-d@users.noreply.github.com>
2024-02-09 19:44:15 +00:00
Bartłomiej Głownia 44189591fc feat(accounts): get trasfer fee from estimateTransferFee API (#5721) 2024-02-09 12:55:17 +00:00
Art 3ed2ec88d7 chore(explorer): remove voting column from proposal table (#5776) 2024-02-09 10:32:54 +00:00
Bartłomiej Głownia a21feea699 chore(utils): improve formatNumber to keep precision (#5761) 2024-02-09 10:31:15 +00:00
m.rayandMatthew Russell 41fd14dd00 feat(trading): mobile layout and buttons (#5751)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2024-02-09 10:30:24 +00:00
Edd c5a27dc6a2 chore(trading): switch eth provider URL (#5779) 2024-02-09 10:26:08 +00:00
Art 0a3b1cadba fix(trading): keep leaderboard rank when filtering (#5775) 2024-02-09 10:00:28 +01:00
Edd b953de953a feat(explorer): update party profile tx (#5719) 2024-02-08 19:06:38 +00:00
Art 5ddcb613e2 chore(trading): create and update team form traversing (#5764) 2024-02-08 14:07:48 +00:00
m.rayandMatthew Russell 76c07992d3 feat(trading): update mobile layout (#5718)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2024-02-08 13:24:48 +00:00
Ben 46e2965fa2 chore(trading): fix test teardown (#5769) 2024-02-08 12:25:16 +00:00
m.rayandbwallacee 532ad3a4b9 feat(trading): show value of the volume traded in the last 24hrs (#5766)
Co-authored-by: bwallacee <ben@vega.xyz>
2024-02-07 11:07:24 +00:00
Madalina Raicu 3a8b40d7a5 Revert "feat(trading): show total value of the valume traded in last 24hrs"
This reverts commit be38813a33.
2024-02-06 22:12:33 +00:00
Madalina Raicu be38813a33 feat(trading): show total value of the valume traded in last 24hrs 2024-02-06 21:52:32 +00:00
bf70dc33ec chore(trading): back merge main to develop (#5749)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
Co-authored-by: Edd <edd@vega.xyz>
Co-authored-by: Bartłomiej Głownia <bglownia@gmail.com>
Co-authored-by: Art <artur@vegaprotocol.io>
2024-02-06 14:41:48 +00:00
Art d6084e75a0 fix(environment): pick best node (#5752) 2024-02-06 14:40:14 +01:00
Art 75e7cea32a fix(trading): trading view on multiple tabs (#5748) 2024-02-06 12:31:12 +01:00
Bartłomiej Głownia b4e98e285e feat(deal-ticket): update fields in margin details (#5722) 2024-02-06 08:42:11 +01:00
Edd f62e29c67f feat(explorer): batch proposal support (#5711) 2024-02-05 17:35:12 +00:00
94e7ad489f feat(trading): main to develop back merge (#5739)
Co-authored-by: m.ray <16125548+MadalinaRaicu@users.noreply.github.com>
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
Co-authored-by: Edd <edd@vega.xyz>
2024-02-05 16:06:33 +01:00
Art 657942d995 feat(trading): team card (#5724) 2024-02-02 20:07:06 +02:00
Matthew Russell a529b9b031 chore(trading): make team stats aggregation epochs 192 (#5723) 2024-02-02 18:01:28 +00:00
Ben 0f311611ba chore(trading): team e2e tests (#5716) 2024-02-01 19:33:25 +00:00
Art 02bd031bee fix(trading): mark as updated when tx confirmed (#5715) 2024-02-01 13:27:21 +00:00
Art bd97651dd3 fix(trading): game cards are not showing (#5713) 2024-02-01 12:04:36 +00:00
Bartłomiej Głownia 516b3e5b93 feat(deal-ticket): fix margin calculations in DealTicketMarginDetails for isolated margin mode (#5709) 2024-02-01 12:52:45 +01:00
Matthew Russellandbwallacee 42a98b6a35 fix(trading): teams snags (#5707)
Co-authored-by: bwallacee <ben@vega.xyz>
2024-02-01 10:38:57 +00:00
Art 2002731c52 feat(trading): gas fee estimation for withdraw transaction (#5668) 2024-02-01 09:07:13 +01:00
a49139f127 fix(trading): closed markets filter (#5691) (#5697)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
Co-authored-by: Edd <edd@vega.xyz>
2024-01-31 14:34:43 +00:00
Edd e216b23472 feat(explorer): updatemarginmode tx view (#5690) 2024-01-31 14:26:35 +00:00
e52ae97233 feat(trading): competitions (#5621)
Co-authored-by: asiaznik <artur@vegaprotocol.io>
Co-authored-by: Ben <ben@vega.xyz>
2024-01-31 14:21:29 +00:00
1780f6fa7f feat(trading): margin estimate update (#5664)
Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
Co-authored-by: daro-maj <119658839+daro-maj@users.noreply.github.com>
2024-01-31 14:05:30 +00:00
246 changed files with 7893 additions and 2803 deletions
+1 -1
View File
@@ -205,7 +205,7 @@ jobs:
# run tests # run tests
#---------------------------------------------- #----------------------------------------------
- name: Run tests - name: Run tests
run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v --numprocesses 1 --dist loadfile --durations=45 run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v --numprocesses 4 --dist loadfile --durations=45
working-directory: apps/trading/e2e working-directory: apps/trading/e2e
#---------------------------------------------- #----------------------------------------------
# upload traces # upload traces
+5 -5
View File
@@ -4,7 +4,7 @@ The front-end monorepo provides a toolkit for building apps that interact with V
This repository is managed using [Nx](https://nx.dev). This repository is managed using [Nx](https://nx.dev).
# 🔎 Applications in this repo ## 🔎 Applications in this repo
### [Block explorer](./apps/explorer) ### [Block explorer](./apps/explorer)
@@ -30,7 +30,7 @@ Hosting for static content being shared across apps, for example fonts.
The utility dApp for validators wishing to add or remove themselves as a signer of the multisig contract. The utility dApp for validators wishing to add or remove themselves as a signer of the multisig contract.
# 🧱 Libraries in this repo ## 🧱 Libraries in this repo
### [UI toolkit](./libs/ui-toolkit) ### [UI toolkit](./libs/ui-toolkit)
@@ -53,7 +53,7 @@ A utility library for connecting to the Ethereum network and interacting with Ve
Generic react helpers that can be used across multiple applications, along with other utilities. Generic react helpers that can be used across multiple applications, along with other utilities.
# 💻 Develop ## 💻 Develop
### Set up ### Set up
@@ -103,7 +103,7 @@ In CI linting, formatting and also run. These checks can be seen in the [CI work
Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more. Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more.
# 🐋 Hosting a console ## 🐋 Hosting a console
To host a console there are two possible build scenarios for running the frontends: nx performed **outside** or **inside** docker build. For specific build instructions follow [build instructions](#build-instructions). To host a console there are two possible build scenarios for running the frontends: nx performed **outside** or **inside** docker build. For specific build instructions follow [build instructions](#build-instructions).
@@ -226,6 +226,6 @@ Note: The script is only needed if capsule was built for first time or fresh. To
vega wallet service run -n DV --load-tokens --tokens-passphrase-file passphrase --no-version-check --automatic-consent --home ~/.vegacapsule/testnet/wallet vega wallet service run -n DV --load-tokens --tokens-passphrase-file passphrase --no-version-check --automatic-consent --home ~/.vegacapsule/testnet/wallet
``` ```
# 📑 License ## 📑 License
[MIT](./LICENSE) [MIT](./LICENSE)
@@ -24,10 +24,6 @@ context('Proposal page', { tags: '@smoke' }, function () {
cy.get_element_by_col_id('title').should('have.text', proposalTitle); cy.get_element_by_col_id('title').should('have.text', proposalTitle);
cy.get_element_by_col_id('type').should('have.text', 'NewMarket'); cy.get_element_by_col_id('type').should('have.text', 'NewMarket');
cy.get_element_by_col_id('state').should('have.text', 'Enacted'); cy.get_element_by_col_id('state').should('have.text', 'Enacted');
cy.getByTestId('vote-progress').should('be.visible');
cy.getByTestId('vote-progress-bar-for')
.invoke('attr', 'style')
.should('eq', 'width: 100%;');
cy.get('[col-id="cDate"]') cy.get('[col-id="cDate"]')
.invoke('text') .invoke('text')
.should('match', dateTimeRegex); .should('match', dateTimeRegex);
@@ -73,10 +69,6 @@ context('Proposal page', { tags: '@smoke' }, function () {
'have.text', 'have.text',
'Waiting for Node Vote' 'Waiting for Node Vote'
); );
cy.getByTestId('vote-progress').should('be.visible');
cy.getByTestId('vote-progress-bar-against')
.invoke('attr', 'style')
.should('eq', 'width: 100%;');
cy.get('[col-id="cDate"]') cy.get('[col-id="cDate"]')
.invoke('text') .invoke('text')
.should('match', dateTimeRegex); .should('match', dateTimeRegex);
@@ -1,4 +1,4 @@
export type HashProps = { export type HashProps = React.HTMLProps<HTMLSpanElement> & {
text: string; text: string;
truncate?: boolean; truncate?: boolean;
}; };
@@ -2,4 +2,5 @@ export { default as BlockLink } from './block-link/block-link';
export { default as PartyLink } from './party-link/party-link'; export { default as PartyLink } from './party-link/party-link';
export { default as NodeLink } from './node-link/node-link'; export { default as NodeLink } from './node-link/node-link';
export { default as MarketLink } from './market-link/market-link'; export { default as MarketLink } from './market-link/market-link';
export { default as NetworkParameterLink } from './network-parameter-link/network-parameter-link';
export * from './asset-link/asset-link'; export * from './asset-link/asset-link';
@@ -0,0 +1,30 @@
import React from 'react';
import { Routes } from '../../../routes/route-names';
import { Link } from 'react-router-dom';
import type { ComponentProps } from 'react';
import Hash from '../hash';
export type NetworkParameterLinkProps = Partial<ComponentProps<typeof Link>> & {
parameter: string;
};
/**
* Links a given network parameter to the relevant page and anchor on the page
*/
const NetworkParameterLink = ({
parameter,
...props
}: NetworkParameterLinkProps) => {
return (
<Link
className="underline"
{...props}
to={`/${Routes.NETWORK_PARAMETERS}#${parameter}`}
>
<Hash text={parameter} />
</Link>
);
};
export default NetworkParameterLink;
@@ -26,7 +26,7 @@ const ProposalLink = ({ id, text }: ProposalLinkProps) => {
>; >;
const base = ENV.dataSources.governanceUrl; const base = ENV.dataSources.governanceUrl;
const label = proposal?.rationale.title || id; const label = proposal?.rationale?.title || id;
return ( return (
<ExternalLink href={`${base}/proposals/${id}`}> <ExternalLink href={`${base}/proposals/${id}`}>
@@ -1,5 +1,4 @@
import { type ProposalListFieldsFragment } from '@vegaprotocol/proposals'; import { type ProposalListFieldsFragment } from '@vegaprotocol/proposals';
import { VoteProgress } from '@vegaprotocol/proposals';
import { type AgGridReact } from 'ag-grid-react'; import { type AgGridReact } from 'ag-grid-react';
import { ExternalLink } from '@vegaprotocol/ui-toolkit'; import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { AgGrid } from '@vegaprotocol/datagrid'; import { AgGrid } from '@vegaprotocol/datagrid';
@@ -12,12 +11,7 @@ import { type ColDef } from 'ag-grid-community';
import type { RowClickedEvent } from 'ag-grid-community'; import type { RowClickedEvent } from 'ag-grid-community';
import { getDateTimeFormat } from '@vegaprotocol/utils'; import { getDateTimeFormat } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import {
NetworkParams,
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { ProposalStateMapping } from '@vegaprotocol/types'; import { ProposalStateMapping } from '@vegaprotocol/types';
import BigNumber from 'bignumber.js';
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment'; import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
import { BREAKPOINT_MD } from '../../config/breakpoints'; import { BREAKPOINT_MD } from '../../config/breakpoints';
import { JsonViewerDialog } from '../dialogs/json-viewer-dialog'; import { JsonViewerDialog } from '../dialogs/json-viewer-dialog';
@@ -31,15 +25,7 @@ type ProposalsTableProps = {
data: ProposalListFieldsFragment[] | null; data: ProposalListFieldsFragment[] | null;
}; };
export const ProposalsTable = ({ data }: ProposalsTableProps) => { export const ProposalsTable = ({ data }: ProposalsTableProps) => {
const { params } = useNetworkParams([
NetworkParams.governance_proposal_market_requiredMajority,
]);
const tokenLink = useLinks(DApp.Governance); const tokenLink = useLinks(DApp.Governance);
const requiredMajorityPercentage = useMemo(() => {
const requiredMajority =
params?.governance_proposal_market_requiredMajority ?? 1;
return new BigNumber(requiredMajority).times(100);
}, [params?.governance_proposal_market_requiredMajority]);
const gridRef = useRef<AgGridReact>(null); const gridRef = useRef<AgGridReact>(null);
useLayoutEffect(() => { useLayoutEffect(() => {
@@ -90,33 +76,6 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
return value ? ProposalStateMapping[value] : '-'; return value ? ProposalStateMapping[value] : '-';
}, },
}, },
{
colId: 'voting',
maxWidth: 100,
hide: window.innerWidth <= BREAKPOINT_MD,
headerName: t('Voting'),
cellRenderer: ({
data,
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
if (data) {
const yesTokens = new BigNumber(data.votes.yes.totalTokens);
const noTokens = new BigNumber(data.votes.no.totalTokens);
const totalTokensVoted = yesTokens.plus(noTokens);
const yesPercentage = totalTokensVoted.isZero()
? new BigNumber(0)
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
return (
<div className="flex h-full items-center justify-center pt-2 uppercase">
<VoteProgress
threshold={requiredMajorityPercentage}
progress={yesPercentage}
/>
</div>
);
}
return '-';
},
},
{ {
colId: 'cDate', colId: 'cDate',
maxWidth: 150, maxWidth: 150,
@@ -184,7 +143,7 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
}, },
}, },
], ],
[requiredMajorityPercentage, tokenLink] [tokenLink]
); );
return ( return (
<> <>
@@ -5,5 +5,10 @@ query ExplorerProposalStatus($id: ID!) {
state state
rejectionReason rejectionReason
} }
... on BatchProposal {
id
state
rejectionReason
}
} }
} }
@@ -8,7 +8,7 @@ export type ExplorerProposalStatusQueryVariables = Types.Exact<{
}>; }>;
export type ExplorerProposalStatusQuery = { __typename?: 'Query', proposal?: { __typename?: 'BatchProposal' } | { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | null }; export type ExplorerProposalStatusQuery = { __typename?: 'Query', proposal?: { __typename?: 'BatchProposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | null };
export const ExplorerProposalStatusDocument = gql` export const ExplorerProposalStatusDocument = gql`
@@ -19,6 +19,11 @@ export const ExplorerProposalStatusDocument = gql`
state state
rejectionReason rejectionReason
} }
... on BatchProposal {
id
state
rejectionReason
}
} }
} }
`; `;
@@ -0,0 +1,257 @@
import { render, screen } from '@testing-library/react';
import { BatchItem } from './batch-item';
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
import type { components } from '../../../../../types/explorer';
type Item = components['schemas']['vegaBatchProposalTermsChange'];
describe('BatchItem', () => {
it('Renders "Unknown proposal type" by default', () => {
const item = {};
render(<BatchItem item={item} />);
expect(screen.getByText('Unknown proposal type')).toBeInTheDocument();
});
it('Renders "Unknown proposal type" for unknown items', () => {
const item = {
newLochNessMonster: {
location: 'Loch Ness',
},
} as unknown as Item;
render(<BatchItem item={item} />);
expect(screen.getByText('Unknown proposal type')).toBeInTheDocument();
});
it('Renders "New spot market"', () => {
const item = {
newSpotMarket: {},
};
render(<BatchItem item={item} />);
expect(screen.getByText('New spot market')).toBeInTheDocument();
});
it('Renders "Cancel transfer"', () => {
const item = {
cancelTransfer: {
changes: {
transferId: 'transfer123',
},
},
};
render(<BatchItem item={item} />);
expect(screen.getByText('Cancel transfer')).toBeInTheDocument();
expect(screen.getByText('transf')).toBeInTheDocument();
});
it('Renders "Cancel transfer" without an id', () => {
const item = {
cancelTransfer: {
changes: {},
},
};
render(<BatchItem item={item} />);
expect(screen.getByText('Cancel transfer')).toBeInTheDocument();
});
it('Renders "New freeform"', () => {
const item = {
newFreeform: {},
};
render(<BatchItem item={item} />);
expect(screen.getByText('New freeform proposal')).toBeInTheDocument();
});
it('Renders "New market"', () => {
const item = {
newMarket: {},
};
render(<BatchItem item={item} />);
expect(screen.getByText('New market')).toBeInTheDocument();
});
it('Renders "New transfer"', () => {
const item = {
newTransfer: {},
};
render(<BatchItem item={item} />);
expect(screen.getByText('New transfer')).toBeInTheDocument();
});
it('Renders "Update asset" with assetId', () => {
const item = {
updateAsset: {
assetId: 'asset123',
},
};
render(
<MemoryRouter>
<MockedProvider>
<BatchItem item={item} />
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('Update asset')).toBeInTheDocument();
expect(screen.getByText('asset123')).toBeInTheDocument();
});
it('Renders "Update asset" even if assetId is not set', () => {
const item = {
updateAsset: {
assetId: undefined,
},
};
render(<BatchItem item={item} />);
expect(screen.getByText('Update asset')).toBeInTheDocument();
});
it('Renders "Update market state" with marketId', () => {
const item = {
updateMarketState: {
changes: {
marketId: 'market123',
},
},
};
render(
<MemoryRouter>
<MockedProvider>
<BatchItem item={item} />
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('Update market state')).toBeInTheDocument();
expect(screen.getByText('market123')).toBeInTheDocument();
});
it('Renders "Update market state" even if marketId is not set', () => {
const item = {
updateMarketState: {
changes: {
marketId: undefined,
},
},
};
render(
<MemoryRouter>
<MockedProvider>
<BatchItem item={item} />
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('Update market state')).toBeInTheDocument();
});
it('Renders "Update network parameter" with parameter', () => {
const item = {
updateNetworkParameter: {
changes: {
key: 'parameter123',
},
},
};
render(
<MockedProvider>
<MemoryRouter>
<BatchItem item={item} />
</MemoryRouter>
</MockedProvider>
);
expect(screen.getByText('Update network parameter')).toBeInTheDocument();
expect(screen.getByText('parameter123')).toBeInTheDocument();
});
it('Renders "Update network parameter" even if parameter is not set', () => {
const item = {
updateNetworkParameter: {
changes: {
key: undefined,
},
},
};
render(<BatchItem item={item} />);
expect(screen.getByText('Update network parameter')).toBeInTheDocument();
});
it('Renders "Update referral program"', () => {
const item = {
updateReferralProgram: {},
};
render(<BatchItem item={item} />);
expect(screen.getByText('Update referral program')).toBeInTheDocument();
});
it('Renders "Update spot market" with marketId', () => {
const item = {
updateSpotMarket: {
marketId: 'market123',
},
};
render(
<MemoryRouter>
<MockedProvider>
<BatchItem item={item} />
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('Update spot market')).toBeInTheDocument();
expect(screen.getByText('market123')).toBeInTheDocument();
});
it('Renders "Update spot market" even if marketId is not set', () => {
const item = {
updateSpotMarket: {
marketId: undefined,
},
};
render(
<MemoryRouter>
<MockedProvider>
<BatchItem item={item} />
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('Update spot market')).toBeInTheDocument();
});
it('Renders "Update market" with marketId', () => {
const item = {
updateMarket: {
marketId: 'market123',
},
};
render(
<MemoryRouter>
<MockedProvider>
<BatchItem item={item} />
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('Update market')).toBeInTheDocument();
expect(screen.getByText('market123')).toBeInTheDocument();
});
it('Renders "Update market" even if marketId is not set', () => {
const item = {
updateMarket: {
marketId: undefined,
},
};
render(
<MemoryRouter>
<MockedProvider>
<BatchItem item={item} />
</MockedProvider>
</MemoryRouter>
);
expect(screen.getByText('Update market')).toBeInTheDocument();
});
it('Renders "Update volume discount program"', () => {
const item = {
updateVolumeDiscountProgram: {},
};
render(<BatchItem item={item} />);
expect(
screen.getByText('Update volume discount program')
).toBeInTheDocument();
});
});
@@ -0,0 +1,87 @@
import { t } from '@vegaprotocol/i18n';
import { AssetLink, MarketLink, NetworkParameterLink } from '../../../links';
import type { components } from '../../../../../types/explorer';
import Hash from '../../../links/hash';
type Item = components['schemas']['vegaBatchProposalTermsChange'];
export interface BatchItemProps {
item: Item;
}
/**
* Produces a one line summary for an item in a batch proposal. Could
* easily be adapted to summarise individual proposals, but there is no
* place for that yet.
*
* Details (like IDs) should be shown and linked if available, but handled
* if not available. This is adequate as the ProposalSummary component contains
* a JSON viewer for the full proposal.
*/
export const BatchItem = ({ item }: BatchItemProps) => {
if (item.cancelTransfer) {
const transferId = item?.cancelTransfer?.changes?.transferId || false;
return (
<span>
{t('Cancel transfer')}&nbsp;
{transferId && (
<Hash className="ml-1" truncate={true} text={transferId} />
)}
</span>
);
} else if (item.newFreeform) {
return <span>{t('New freeform proposal')}</span>;
} else if (item.newMarket) {
return <span>{t('New market')}</span>;
} else if (item.newSpotMarket) {
return <span>{t('New spot market')}</span>;
} else if (item.newTransfer) {
return <span>{t('New transfer')}</span>;
} else if (item.updateAsset) {
const assetId = item?.updateAsset?.assetId || false;
return (
<span>
{t('Update asset')}
{assetId && <AssetLink className="ml-1" assetId={assetId} />}
</span>
);
} else if (item.updateMarket) {
const marketId = item?.updateMarket?.marketId || false;
return (
<span>
{t('Update market')}{' '}
{marketId && <MarketLink className="ml-1" id={marketId} />}
</span>
);
} else if (item.updateMarketState) {
const marketId = item?.updateMarketState?.changes?.marketId || false;
return (
<span>
{t('Update market state')}
{marketId && <MarketLink className="ml-1" id={marketId} />}
</span>
);
} else if (item.updateNetworkParameter) {
const param = item?.updateNetworkParameter?.changes?.key || false;
return (
<span>
{t('Update network parameter')}
{param && <NetworkParameterLink className="ml-1" parameter={param} />}
</span>
);
} else if (item.updateReferralProgram) {
return <span>{t('Update referral program')}</span>;
} else if (item.updateSpotMarket) {
const marketId = item?.updateSpotMarket?.marketId || '';
return (
<span>
{t('Update spot market')}
<MarketLink className="ml-1" id={marketId} />
</span>
);
} else if (item.updateVolumeDiscountProgram) {
return <span>{t('Update volume discount program')}</span>;
}
return <span>{t('Unknown proposal type')}</span>;
};
@@ -1,6 +1,4 @@
import type { ProposalTerms } from '../tx-proposal';
import { useState } from 'react'; import { useState } from 'react';
import type { components } from '../../../../../types/explorer';
import { JsonViewerDialog } from '../../../dialogs/json-viewer-dialog'; import { JsonViewerDialog } from '../../../dialogs/json-viewer-dialog';
import ProposalLink from '../../../links/proposal-link/proposal-link'; import ProposalLink from '../../../links/proposal-link/proposal-link';
import truncate from 'lodash/truncate'; import truncate from 'lodash/truncate';
@@ -9,7 +7,12 @@ import ReactMarkdown from 'react-markdown';
import { ProposalDate } from './proposal-date'; import { ProposalDate } from './proposal-date';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import type { ProposalTerms } from '../tx-proposal';
import type { components } from '../../../../../types/explorer';
import { BatchItem } from './batch-item';
type Rationale = components['schemas']['vegaProposalRationale']; type Rationale = components['schemas']['vegaProposalRationale'];
type Batch = components['schemas']['v1BatchProposalSubmissionTerms']['changes'];
type ProposalTermsDialog = { type ProposalTermsDialog = {
open: boolean; open: boolean;
@@ -21,6 +24,7 @@ interface ProposalSummaryProps {
id: string; id: string;
rationale?: Rationale; rationale?: Rationale;
terms?: ProposalTerms; terms?: ProposalTerms;
batch?: Batch;
} }
/** /**
@@ -31,6 +35,7 @@ export const ProposalSummary = ({
id, id,
rationale, rationale,
terms, terms,
batch,
}: ProposalSummaryProps) => { }: ProposalSummaryProps) => {
const [dialog, setDialog] = useState<ProposalTermsDialog>({ const [dialog, setDialog] = useState<ProposalTermsDialog>({
open: false, open: false,
@@ -72,6 +77,18 @@ export const ProposalSummary = ({
</ReactMarkdown> </ReactMarkdown>
</div> </div>
)} )}
{batch && (
<section className="pt-2 text-sm leading-tight my-3">
<h2 className="text-lg pb-1">{t('Changes')}</h2>
<ol>
{batch.map((change, index) => (
<li className="ml-4 list-decimal" key={`batch-${index}`}>
<BatchItem item={change} />
</li>
))}
</ol>
</section>
)}
<div className="pt-5"> <div className="pt-5">
<button className="underline max-md:hidden mr-5" onClick={openDialog}> <button className="underline max-md:hidden mr-5" onClick={openDialog}>
{t('View terms')} {t('View terms')}
@@ -0,0 +1,46 @@
import { t } from '@vegaprotocol/i18n';
import { TxDetailsShared } from '../shared/tx-details-shared';
import { TableWithTbody } from '../../../table';
import type { components } from '../../../../../types/explorer';
import type { BlockExplorerTransactionResult } from '../../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../../routes/blocks/tendermint-blocks-response';
import { TableCell, TableRow } from '../../../table';
type Update = components['schemas']['v1UpdatePartyProfile'];
interface TxDetailsUpdatePartyProfileProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* Party profiles can be an alias and arbitrary key/values pairs.
* This component displays the alias, if any, but not the metadata. When there is
* some wider usage, we can decide how to render it. For now, it's available in the
* full TX details.
*/
export const TxDetailsUpdatePartyProfile = ({
txData,
pubKey,
blockData,
}: TxDetailsUpdatePartyProfileProps) => {
if (!txData?.command.updatePartyProfile) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const update: Update = txData.command.updatePartyProfile;
return (
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
{update.alias && (
<TableRow modifier="bordered">
<TableCell>{t('New alias')}</TableCell>
<TableCell>{update.alias}</TableCell>
</TableRow>
)}
</TableWithTbody>
);
};
@@ -0,0 +1,75 @@
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { sharedHeaderProps, TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import type { components } from '../../../../types/explorer';
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
import { ProposalSummary } from './proposal/summary';
import Hash from '../../links/hash';
import { t } from '@vegaprotocol/i18n';
export type Proposal = components['schemas']['v1BatchProposalSubmission'];
export type ProposalTerms = components['schemas']['vegaProposalTerms'];
interface TxBatchProposalProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
*
*/
export const TxBatchProposal = ({
txData,
pubKey,
blockData,
}: TxBatchProposalProps) => {
if (!txData || !txData.command.batchProposalSubmission) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
let deterministicId = '';
const proposal: Proposal = txData.command.batchProposalSubmission;
const sig = txData?.signature?.value;
if (sig) {
deterministicId = txSignatureToDeterministicId(sig);
}
return (
<>
<TableWithTbody className="mb-8" allowWrap={true}>
<TableRow modifier="bordered">
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
<TableCell>{t('Batch proposal')}</TableCell>
</TableRow>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
blockData={blockData}
hideTypeRow={true}
/>
<TableRow modifier="bordered">
<TableCell>{t('Batch size')}</TableCell>
<TableCell>
{proposal.terms?.changes?.length || t('No changes')}
</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Proposal ID')}</TableCell>
<TableCell>
<Hash text={deterministicId} />
</TableCell>
</TableRow>
</TableWithTbody>
{proposal && (
<ProposalSummary
id={deterministicId}
rationale={proposal?.rationale}
terms={proposal.terms}
batch={proposal.terms?.changes}
/>
)}
</>
);
};
@@ -32,6 +32,9 @@ import { TxDetailsCreateReferralSet } from './tx-create-referral-set';
import { TxDetailsApplyReferralCode } from './tx-apply-referral-code'; import { TxDetailsApplyReferralCode } from './tx-apply-referral-code';
import { TxDetailsUpdateReferralSet } from './tx-update-referral-set'; import { TxDetailsUpdateReferralSet } from './tx-update-referral-set';
import { TxDetailsJoinTeam } from './tx-join-team'; import { TxDetailsJoinTeam } from './tx-join-team';
import { TxDetailsUpdateMarginMode } from './tx-update-margin-mode';
import { TxBatchProposal } from './tx-batch-proposal';
import { TxDetailsUpdatePartyProfile } from './proposal/tx-update-party-profile';
interface TxDetailsWrapperProps { interface TxDetailsWrapperProps {
txData: BlockExplorerTransactionResult | undefined; txData: BlockExplorerTransactionResult | undefined;
@@ -133,6 +136,12 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
return TxDetailsApplyReferralCode; return TxDetailsApplyReferralCode;
case 'Join Team': case 'Join Team':
return TxDetailsJoinTeam; return TxDetailsJoinTeam;
case 'Update Margin Mode':
return TxDetailsUpdateMarginMode;
case 'Batch Proposal':
return TxBatchProposal;
case 'Update Party Profile':
return TxDetailsUpdatePartyProfile;
default: default:
return TxDetailsGeneric; return TxDetailsGeneric;
} }
@@ -0,0 +1,60 @@
import { t } from '@vegaprotocol/i18n';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import type { components } from '../../../../types/explorer';
import { MarketLink } from '../../links';
interface TxDetailsUpdateMarginModeProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
type Mode = components['schemas']['UpdateMarginModeMode'];
const MarginModeLabels: Record<Mode, string> = {
MODE_CROSS_MARGIN: t('Cross margin'),
MODE_ISOLATED_MARGIN: t('Isolated margin'),
MODE_UNSPECIFIED: t('Unspecified'),
};
export const TxDetailsUpdateMarginMode = ({
txData,
pubKey,
blockData,
}: TxDetailsUpdateMarginModeProps) => {
if (!txData || !txData.command.updateMarginMode) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const u: components['schemas']['v1UpdateMarginMode'] =
txData.command.updateMarginMode;
return (
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
{u.marketId && (
<TableRow modifier="bordered">
<TableCell>{t('Market ID')}</TableCell>
<TableCell>
<MarketLink id={u.marketId} />
</TableCell>
</TableRow>
)}
{u.mode && (
<TableRow modifier="bordered">
<TableCell>{t('New margin mode')}</TableCell>
<TableCell>{MarginModeLabels[u.mode]}</TableCell>
</TableRow>
)}
{u.marginFactor && (
<TableRow modifier="bordered">
<TableCell>{t('Margin factor')}</TableCell>
<TableCell>{u.marginFactor}</TableCell>
</TableRow>
)}
</TableWithTbody>
);
};
@@ -20,6 +20,7 @@ export type FilterOption =
| 'Amend Order' | 'Amend Order'
| 'Apply Referral Code' | 'Apply Referral Code'
| 'Batch Market Instructions' | 'Batch Market Instructions'
| 'Batch Proposal'
| 'Cancel LiquidityProvision Order' | 'Cancel LiquidityProvision Order'
| 'Cancel Order' | 'Cancel Order'
| 'Cancel Transfer Funds' | 'Cancel Transfer Funds'
@@ -43,7 +44,9 @@ export type FilterOption =
| 'Submit Order' | 'Submit Order'
| 'Transfer Funds' | 'Transfer Funds'
| 'Undelegate' | 'Undelegate'
| 'Update Party Profile'
| 'Update Referral Set' | 'Update Referral Set'
| 'Update Margin Mode'
| 'Validator Heartbeat' | 'Validator Heartbeat'
| 'Vote on Proposal' | 'Vote on Proposal'
| 'Withdraw'; | 'Withdraw';
@@ -59,17 +62,25 @@ export const filterOptions: Record<string, FilterOption[]> = {
'Stop Orders Submission', 'Stop Orders Submission',
'Stop Orders Cancellation', 'Stop Orders Cancellation',
'Submit Order', 'Submit Order',
'Update Margin Mode',
], ],
'Transfers and Withdrawals': [ 'Transfers and Withdrawals': [
'Transfer Funds', 'Transfer Funds',
'Cancel Transfer Funds', 'Cancel Transfer Funds',
'Withdraw', 'Withdraw',
], ],
Governance: ['Delegate', 'Undelegate', 'Vote on Proposal', 'Proposal'], Governance: [
'Batch Proposal',
'Delegate',
'Undelegate',
'Vote on Proposal',
'Proposal',
],
Referrals: [ Referrals: [
'Apply Referral Code', 'Apply Referral Code',
'Create Referral Set', 'Create Referral Set',
'Join Team', 'Join Team',
'Update Party Profile',
'Update Referral Set', 'Update Referral Set',
], ],
'External Data': ['Chain Event', 'Submit Oracle Data'], 'External Data': ['Chain Event', 'Submit Oracle Data'],
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"short_name": "Mainnet Stats", "short_name": "Explorer VEGA",
"name": "Vega Mainnet statistics", "name": "Vega Protocol - Explorer",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"short_name": "Mainnet Stats", "short_name": "Governance VEGA",
"name": "Vega Mainnet statistics", "name": "Vega Protocol - Governance",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Vega Protocol static asseets</title> <title>Vega Protocol static assets</title>
<link rel="stylesheet" href="fonts.css" /> <link rel="stylesheet" href="fonts.css" />
<link rel="icon" type="image/x-icon" href="favicon.ico" /> <link rel="icon" type="image/x-icon" href="favicon.ico" />
</head> </head>
+1
View File
@@ -26,6 +26,7 @@ NX_ICEBERG_ORDERS=true
NX_METAMASK_SNAPS=true NX_METAMASK_SNAPS=true
NX_REFERRALS=true NX_REFERRALS=true
# NX_DISABLE_CLOSE_POSITION=false # NX_DISABLE_CLOSE_POSITION=false
NX_TEAM_COMPETITION=true
NX_TENDERMINT_URL=https://be.vega.community NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
+5
View File
@@ -28,3 +28,8 @@ NX_REFERRALS=true
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/ NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
NX_TEAM_COMPETITION=true
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
+1 -1
View File
@@ -1,4 +1,4 @@
NX_ETHEREUM_PROVIDER_URL=https://eth-mainnet.gateway.pokt.network/v1/lb/af6a2d529a11f8158bc8ca2a NX_ETHEREUM_PROVIDER_URL=https://eth-mainnet.rpc.grove.city/v1/af6a2d529a11f8158bc8ca2a
NX_ETHERSCAN_URL=https://etherscan.io NX_ETHERSCAN_URL=https://etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.sentry.io/6300613 NX_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.sentry.io/6300613
+1
View File
@@ -26,6 +26,7 @@ NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS # NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true NX_METAMASK_SNAPS=true
NX_REFERRALS=true NX_REFERRALS=true
NX_TEAM_COMPETITION=true
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/ NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg= NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
+1
View File
@@ -26,6 +26,7 @@ NX_ISOLATED_MARGIN=true
NX_ICEBERG_ORDERS=true NX_ICEBERG_ORDERS=true
NX_METAMASK_SNAPS=true NX_METAMASK_SNAPS=true
NX_REFERRALS=true NX_REFERRALS=true
NX_TEAM_COMPETITION=true
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
+9 -7
View File
@@ -1,6 +1,12 @@
{ {
"short_name": "Mainnet Stats", "name": "Vega Protocol - Trading",
"name": "Vega Mainnet statistics", "short_name": "Console",
"description": "Vega Protocol - Trading dApp",
"start_url": "/",
"display": "standalone",
"orientation": "portrait",
"theme_color": "#000000",
"background_color": "#ffffff",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",
@@ -12,9 +18,5 @@
"type": "image/png", "type": "image/png",
"sizes": "192x192" "sizes": "192x192"
} }
], ]
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
} }
@@ -0,0 +1,160 @@
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';
import { useReferralSetTransaction } from '../../lib/hooks/use-referral-set-transaction';
import { DApp, TokenStaticLinks, useLinks } from '@vegaprotocol/environment';
import { RainbowButton } from '../../components/rainbow-button';
import { usePageTitle } from '../../lib/hooks/use-page-title';
import { ErrorBoundary } from '../../components/error-boundary';
import { Box } from '../../components/competitions/box';
import { LayoutWithGradient } from '../../components/layouts-inner';
import { Links } from '../../lib/links';
import { TeamForm, TransactionType } from './team-form';
export const CompetitionsCreateTeam = () => {
const [searchParams] = useSearchParams();
const isSolo = Boolean(searchParams.get('solo'));
const t = useT();
usePageTitle(t('Create a team'));
const { isReadOnly, pubKey } = useVegaWallet();
const openWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
return (
<ErrorBoundary feature="create-team">
<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>
{pubKey && !isReadOnly ? (
<CreateTeamFormContainer isSolo={isSolo} />
) : (
<>
<p>
{t(
'Create a team to participate in team based rewards as well as access the discount benefits of the current referral program.'
)}
</p>
<RainbowButton variant="border" onClick={openWalletDialog}>
{t('Connect wallet')}
</RainbowButton>
</>
)}
</Box>
</div>
</LayoutWithGradient>
</ErrorBoundary>
);
};
const CreateTeamFormContainer = ({ isSolo }: { isSolo: boolean }) => {
const t = useT();
const createLink = useLinks(DApp.Governance);
const { err, status, code, isEligible, requiredStake, onSubmit } =
useReferralSetTransaction({
onSuccess: (code) => {
// For some reason team creation takes a long time, too long even to make
// polling viable, so its not feasible to navigate to the team page
// after creation
//
// navigate(Links.COMPETITIONS_TEAM(code));
},
});
if (status === 'confirmed') {
return (
<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 && (
<>
<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>
</>
)}
</div>
);
}
if (!isEligible) {
return (
<div className="flex flex-col gap-4">
{requiredStake !== undefined && (
<p>
{t(
'You need at least {{requiredStake}} VEGA staked to generate a referral code and participate in the referral program.',
{
requiredStake: addDecimalsFormatNumber(
requiredStake.toString(),
18
),
}
)}
</p>
)}
<TradingAnchorButton
href={createLink(TokenStaticLinks.ASSOCIATE)}
intent={Intent.Primary}
target="_blank"
>
{t('Stake some $VEGA now')}
</TradingAnchorButton>
</div>
);
}
return (
<TeamForm
type={TransactionType.CreateReferralSet}
onSubmit={onSubmit}
status={status}
err={err}
isCreatingSoloTeam={isSolo}
/>
);
};
@@ -0,0 +1,157 @@
import { useT } from '../../lib/use-t';
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 { Link, useNavigate } from 'react-router-dom';
import { Links } from '../../lib/links';
import {
CompetitionsAction,
CompetitionsActionsContainer,
} from '../../components/competitions/competitions-cta';
import { GamesContainer } from '../../components/competitions/games-container';
import { CompetitionsLeaderboard } from '../../components/competitions/competitions-leaderboard';
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();
const navigate = useNavigate();
usePageTitle(t('Competitions'));
const { data: epochData } = useCurrentEpochInfoQuery();
const currentEpoch = Number(epochData?.epoch.id);
const { data: gamesData, loading: gamesLoading } = useGames({
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')}>
<p className="text-lg mb-1">
{t(
'Be a team player! Participate in games and work together to rake in as much profit to win.'
)}
</p>
</CompetitionsHeader>
{/** 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());
}}
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>
{gamesLoading ? (
<Loader size="small" />
) : (
<GamesContainer data={gamesData} currentEpoch={currentEpoch} />
)}
{/** The teams ranking */}
<div className="mb-6 flex flex-row items-baseline justify-between">
<h2 className="text-2xl">{t('Leaderboard')}</h2>
<Link to={Links.COMPETITIONS_TEAMS()} className="text-sm underline">
{t('View all teams')}
</Link>
</div>
{teamsLoading ? (
<Loader size="small" />
) : (
<CompetitionsLeaderboard data={take(teamsData, 10)} />
)}
</ErrorBoundary>
);
};
@@ -0,0 +1,255 @@
import { useState, type ButtonHTMLAttributes } 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 classNames from 'classnames';
import { useT } from '../../lib/use-t';
import { Table } from '../../components/table';
import { 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';
import { TeamStats } from '../../components/competitions/team-stats';
import { usePageTitle } from '../../lib/hooks/use-page-title';
import { ErrorBoundary } from '../../components/error-boundary';
import { LayoutWithGradient } from '../../components/layouts-inner';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { JoinTeam } from './join-team';
import { UpdateTeamButton } from './update-team-button';
export const CompetitionsTeam = () => {
const t = useT();
const { teamId } = useParams<{ teamId: string }>();
usePageTitle([t('Competitions'), t('Team')]);
return (
<ErrorBoundary feature="team">
<TeamPageContainer teamId={teamId} />
</ErrorBoundary>
);
};
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);
// only show spinner on first load so when users join teams its smoother
if (!data && loading) {
return (
<Splash>
<Loader />
</Splash>
);
}
if (!team) {
return (
<Splash>
<p>{t('Page not found')}</p>
</Splash>
);
}
return (
<TeamPage
team={team}
partyTeam={partyTeam}
stats={stats}
members={members}
games={games}
refetch={refetch}
/>
);
};
const TeamPage = ({
team,
partyTeam,
stats,
members,
games,
refetch,
}: {
team: TeamType;
partyTeam?: TeamType;
stats?: ITeamStats;
members?: Member[];
games?: TeamGame[];
refetch: () => void;
}) => {
const t = useT();
const [showGames, setShowGames] = useState(true);
return (
<LayoutWithGradient>
<header className="flex gap-3 lg:gap-4 pt-5 lg:pt-10">
<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"
data-testid="team-name"
>
{team.name}
</h1>
<div className="flex gap-2">
<JoinTeam team={team} partyTeam={partyTeam} refetch={refetch} />
<UpdateTeamButton team={team} />
</div>
</div>
</header>
<TeamStats stats={stats} members={members} games={games} />
<section>
<div className="flex gap-4 lg:gap-8 mb-4 border-b border-default">
<ToggleButton
active={showGames}
onClick={() => setShowGames(true)}
data-testid="games-toggle"
>
{t('Games ({{count}})', { count: games ? games.length : 0 })}
</ToggleButton>
<ToggleButton
active={!showGames}
onClick={() => setShowGames(false)}
data-testid="members-toggle"
>
{t('Members ({{count}})', {
count: members ? members.length : 0,
})}
</ToggleButton>
</div>
{showGames ? <Games games={games} /> : <Members members={members} />}
</section>
</LayoutWithGradient>
);
};
const Games = ({ games }: { games?: TeamGame[] }) => {
const t = useT();
if (!games?.length) {
return <p>{t('No games')}</p>;
}
return (
<Table
columns={[
{ name: 'rank', displayName: t('Rank') },
{
name: 'epoch',
displayName: t('Epoch'),
headerClassName: 'hidden md:table-cell',
className: 'hidden md:table-cell',
},
{ name: 'type', displayName: t('Type') },
{ name: 'amount', displayName: t('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}
/>
);
};
const Members = ({ members }: { members?: Member[] }) => {
const t = useT();
if (!members?.length) {
return <p>{t('No members')}</p>;
}
const data = orderBy(
members.map((m) => ({
referee: <RefereeLink pubkey={m.referee} isCreator={m.isCreator} />,
rewards: formatNumber(m.totalQuantumRewards),
volume: formatNumber(m.totalQuantumVolume),
gamesPlayed: formatNumber(m.totalGamesPlayed),
joinedAt: getDateTimeFormat().format(new Date(m.joinedAt)),
joinedAtEpoch: Number(m.joinedAtEpoch),
})),
'joinedAtEpoch',
'desc'
);
return (
<Table
columns={[
{ name: 'referee', displayName: t('Member ID') },
{ name: 'rewards', displayName: t('Rewards earned') },
{ name: 'volume', displayName: t('Total volume') },
{ name: 'gamesPlayed', displayName: t('Games played') },
{
name: 'joinedAt',
displayName: t('Joined at'),
},
{
name: 'joinedAtEpoch',
displayName: t('Joined epoch'),
},
]}
data={data}
noCollapse={true}
/>
);
};
const RefereeLink = ({
pubkey,
isCreator,
}: {
pubkey: string;
isCreator: boolean;
}) => {
const t = useT();
const linkCreator = useLinks(DApp.Explorer);
const link = linkCreator(EXPLORER_PARTIES.replace(':id', pubkey));
return (
<>
<Link to={link} target="_blank" className="underline underline-offset-4">
{truncateMiddle(pubkey)}
</Link>{' '}
<span className="text-muted text-xs">{isCreator ? t('Owner') : ''}</span>
</>
);
};
const ToggleButton = ({
active,
...props
}: ButtonHTMLAttributes<HTMLButtonElement> & { active: boolean }) => {
return (
<button
{...props}
className={classNames('relative top-px uppercase border-b-2 py-4', {
'text-muted border-transparent': !active,
'border-vega-yellow': active,
})}
/>
);
};
@@ -0,0 +1,64 @@
import { ErrorBoundary } from '@sentry/react';
import { CompetitionsHeader } from '../../components/competitions/competitions-header';
import { useRef, useState } from 'react';
import { useT } from '../../lib/use-t';
import { useTeams } from '../../lib/hooks/use-teams';
import { CompetitionsLeaderboard } from '../../components/competitions/competitions-leaderboard';
import {
Input,
Loader,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { usePageTitle } from '../../lib/hooks/use-page-title';
export const CompetitionsTeams = () => {
const t = useT();
usePageTitle([t('Competitions'), t('Teams')]);
const { data: teamsData, loading: teamsLoading } = useTeams();
const inputRef = useRef<HTMLInputElement>(null);
const [filter, setFilter] = useState<string | null | undefined>(undefined);
return (
<ErrorBoundary>
<CompetitionsHeader title={t('Join a team')}>
<p className="text-lg mb-1">{t('Choose a team to get involved')}</p>
</CompetitionsHeader>
<div className="mb-6 flex justify-end">
<div className="w-full md:w-60 h-10 relative">
<span className="absolute z-10 pointer-events-none opacity-90 top-[5px] left-[5px]">
<VegaIcon name={VegaIconNames.SEARCH} size={18} />
</span>
<Input
ref={inputRef}
className="opacity-90 text-right"
placeholder={t('Name')}
onKeyUp={() => {
const value = inputRef.current?.value;
if (value != filter) setFilter(value);
}}
/>
</div>
</div>
<div>
{teamsLoading ? (
<Loader size="small" />
) : (
<CompetitionsLeaderboard
data={teamsData.filter((td) => {
if (filter && filter.length > 0) {
const re = new RegExp(filter, 'i');
return re.test(td.name);
}
return true;
})}
/>
)}
</div>
</ErrorBoundary>
);
};
@@ -0,0 +1,163 @@
import { ErrorBoundary } from '../../components/error-boundary';
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 {
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';
import { useReferralSetTransaction } from '../../lib/hooks/use-referral-set-transaction';
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();
usePageTitle([t('Competitions'), t('Update a team')]);
const { pubKey, isReadOnly } = useVegaWallet();
const openWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
const { teamId } = useParams<{ teamId: string }>();
if (!teamId) {
return <Navigate to={Links.COMPETITIONS()} />;
}
return (
<ErrorBoundary feature="update-team">
<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>
{pubKey && !isReadOnly ? (
<UpdateTeamFormContainer teamId={teamId} pubKey={pubKey} />
) : (
<>
<p>{t('Connect to update the details of your team.')}</p>
<RainbowButton variant="border" onClick={openWalletDialog}>
{t('Connect wallet')}
</RainbowButton>
</>
)}
</Box>
</div>
</LayoutWithGradient>
</ErrorBoundary>
);
};
const UpdateTeamFormContainer = ({
teamId,
pubKey,
}: {
teamId: string;
pubKey: string;
}) => {
const t = useT();
const [refetching, setRefetching] = useState<boolean>(false);
const { team, loading, error, refetch } = useTeam(teamId, pubKey);
const { err, status, onSubmit } = useReferralSetTransaction({
onSuccess: () => {
// NOOP
},
});
// refetch when saved
useEffect(() => {
if (refetch && status === 'confirmed') {
refetch();
setRefetching(true);
}
}, [refetch, status]);
if (loading && !refetching) {
return <Loader size="small" />;
}
if (error) {
return (
<Splash className="gap-1">
<span>{t('Something went wrong.')}</span>
<Link to={Links.COMPETITIONS_TEAM(teamId)} className="underline">
{t("Go back to the team's profile")}
</Link>
</Splash>
);
}
const isMyTeam = team?.referrer === pubKey;
if (!isMyTeam) {
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,
url: team.teamUrl,
avatarUrl: team.avatarUrl,
private: team.closed,
allowList: team.allowList.join(','),
};
return (
<TeamForm
type={TransactionType.UpdateReferralSet}
status={status}
err={err}
isCreatingSoloTeam={team.closed}
onSubmit={onSubmit}
defaultValues={defaultValues}
/>
);
};
@@ -0,0 +1,88 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { JoinButton } from './join-team';
import { type Team } from '../../lib/hooks/use-team';
describe('JoinButton', () => {
const teamA = {
teamId: 'teamA',
name: 'Team A',
referrer: 'referrerA',
} as Team;
const teamB = {
teamId: 'teamB',
name: 'Team B',
referrer: 'referrerrB',
} as Team;
const props = {
pubKey: 'pubkey',
isReadOnly: false,
team: teamA,
partyTeam: teamB,
onJoin: jest.fn(),
};
beforeEach(() => {
props.onJoin.mockClear();
});
it('disables button if not connected', async () => {
render(<JoinButton {...props} pubKey={null} />);
const button = screen.getByRole('button');
expect(button).toBeDisabled();
await userEvent.hover(button);
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toHaveTextContent(/Connect your wallet/);
});
it('disables button if you created the current team', () => {
render(
<JoinButton
{...props}
pubKey={teamA.referrer}
team={teamA}
partyTeam={teamA}
/>
);
const button = screen.getByRole('button', { name: /Owner/ });
expect(button).toBeDisabled();
});
it('disables button if you created a team', async () => {
render(<JoinButton {...props} pubKey={teamB.referrer} />);
const button = screen.getByRole('button', { name: /Switch team/ });
expect(button).toBeDisabled();
await userEvent.hover(button);
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toHaveTextContent(/As a team creator/);
});
it('shows if party is already in team', async () => {
render(<JoinButton {...props} team={teamA} partyTeam={teamA} />);
const button = screen.getByRole('button', { name: /Joined/ });
expect(button).toBeDisabled();
});
it('enables switch team if party is in a different team', async () => {
render(<JoinButton {...props} />);
const button = screen.getByRole('button', { name: /Switch team/ });
expect(button).toBeEnabled();
await userEvent.click(button);
expect(props.onJoin).toHaveBeenCalledWith('switch');
});
it('enables join team if party is not in a team', async () => {
render(<JoinButton {...props} partyTeam={undefined} />);
const button = screen.getByRole('button', { name: /Join team/ });
expect(button).toBeEnabled();
await userEvent.click(button);
expect(props.onJoin).toHaveBeenCalledWith('join');
});
});
@@ -0,0 +1,235 @@
import {
TradingButton as Button,
Dialog,
Intent,
Tooltip,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { useSimpleTransaction, useVegaWallet } from '@vegaprotocol/wallet';
import { useT } from '../../lib/use-t';
import { type Team } from '../../lib/hooks/use-team';
import { useState } from 'react';
type JoinType = 'switch' | 'join';
export const JoinTeam = ({
team,
partyTeam,
refetch,
}: {
team: Team;
partyTeam?: Team;
refetch: () => void;
}) => {
const { pubKey, isReadOnly } = useVegaWallet();
const [confirmDialog, setConfirmDialog] = useState<JoinType>();
return (
<>
<JoinButton
team={team}
partyTeam={partyTeam}
pubKey={pubKey}
isReadOnly={isReadOnly}
onJoin={setConfirmDialog}
/>
<Dialog
open={confirmDialog !== undefined}
onChange={() => setConfirmDialog(undefined)}
>
{confirmDialog !== undefined && (
<DialogContent
type={confirmDialog}
team={team}
partyTeam={partyTeam}
onCancel={() => setConfirmDialog(undefined)}
refetch={refetch}
/>
)}
</Dialog>
</>
);
};
export const JoinButton = ({
pubKey,
isReadOnly,
team,
partyTeam,
onJoin,
}: {
pubKey: string | null;
isReadOnly: boolean;
team: Team;
partyTeam?: Team;
onJoin: (type: JoinType) => void;
}) => {
const t = useT();
if (!pubKey || isReadOnly) {
return (
<Tooltip description={t('Connect your wallet to join the team')}>
<Button intent={Intent.Primary} disabled={true}>
{t('Join team')}{' '}
</Button>
</Tooltip>
);
}
// Party is the creator of a team
else if (partyTeam && partyTeam.referrer === pubKey) {
// Party is the creator of THIS team
if (partyTeam.teamId === team.teamId) {
return (
<Button intent={Intent.None} disabled={true}>
<span className="flex items-center gap-2">
{t('Owner')}{' '}
<span className="text-vega-green-600 dark:text-vega-green">
<VegaIcon name={VegaIconNames.TICK} />
</span>
</span>
</Button>
);
} else {
// Not creator of the team, but still can't switch because
// creators cannot leave their own team
return (
<Tooltip description={t('As a team creator, you cannot switch teams')}>
<Button intent={Intent.Primary} disabled={true}>
{t('Switch team')}{' '}
</Button>
</Tooltip>
);
}
}
// Party is in a team, but not this one
else if (partyTeam && partyTeam.teamId !== team.teamId) {
return (
<Button
onClick={() => onJoin('switch')}
intent={Intent.Primary}
data-testid="switch-team-button"
>
{t('Switch team')}{' '}
</Button>
);
}
// Joined. Current party is already in this team
else if (partyTeam && partyTeam.teamId === team.teamId) {
return (
<Button intent={Intent.None} disabled={true}>
<span className="flex items-center gap-2">
{t('Joined')}{' '}
<span className="text-vega-green-600 dark:text-vega-green">
<VegaIcon name={VegaIconNames.TICK} />
</span>
</span>
</Button>
);
}
return (
<Button onClick={() => onJoin('join')} intent={Intent.Primary}>
{t('Join team')}
</Button>
);
};
const DialogContent = ({
type,
team,
partyTeam,
onCancel,
refetch,
}: {
type: JoinType;
team: Team;
partyTeam?: Team;
onCancel: () => void;
refetch: () => void;
}) => {
const t = useT();
const { send, status, error } = useSimpleTransaction({
onSuccess: refetch,
});
const joinTeam = () => {
send({
joinTeam: {
id: team.teamId,
},
});
};
if (error) {
return (
<p className="text-vega-red break-words first-letter:capitalize">
{error}
</p>
);
}
if (status === 'requested') {
return <p>{t('Confirm in wallet...')}</p>;
}
if (status === 'pending') {
return <p>{t('Confirming transaction...')}</p>;
}
if (status === 'confirmed') {
if (type === 'switch') {
return (
<p>
{t(
'Team switch successful. You will switch team at the end of the epoch.'
)}
</p>
);
}
return <p>{t('Team joined')}</p>;
}
return (
<div className="flex flex-col gap-4">
{type === 'switch' && (
<>
<h2 className="font-alpha text-xl">{t('Switch team')}</h2>
<p>
{t(
"Switching team will move you from '{{fromTeam}}' to '{{toTeam}}' at the end of the epoch. Are you sure?",
{
fromTeam: partyTeam?.name,
toTeam: team.name,
}
)}
</p>
</>
)}
{type === 'join' && (
<>
<h2 className="font-alpha text-xl">{t('Join team')}</h2>
<p>
{t('Are you sure you want to join team: {{team}}', {
team: team.name,
})}
</p>
</>
)}
<div className="flex justify-between gap-2">
<Button
onClick={joinTeam}
intent={Intent.Success}
data-testid="confirm-switch-button"
>
{t('Confirm')}
</Button>
<Button onClick={onCancel} intent={Intent.Danger}>
{t('Cancel')}
</Button>
</div>
</div>
);
};
@@ -0,0 +1,318 @@
import {
TradingFormGroup,
TradingInput,
TradingInputError,
TradingCheckbox,
TextArea,
TradingButton,
Intent,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { URL_REGEX, isValidVegaPublicKey } from '@vegaprotocol/utils';
import { type useReferralSetTransaction } from '../../lib/hooks/use-referral-set-transaction';
import { useT } from '../../lib/use-t';
import { useForm, Controller } from 'react-hook-form';
import type {
CreateReferralSet,
UpdateReferralSet,
Status,
} from '@vegaprotocol/wallet';
import classNames from 'classnames';
import { useLayoutEffect, useState } from 'react';
export type FormFields = {
id: string;
name: string;
url: string;
avatarUrl: string;
private: boolean;
allowList: string;
};
export enum TransactionType {
CreateReferralSet = 'CreateReferralSet',
UpdateReferralSet = 'UpdateReferralSet',
}
const prepareTransaction = (
type: TransactionType,
fields: FormFields
): CreateReferralSet | UpdateReferralSet => {
switch (type) {
case TransactionType.CreateReferralSet:
return {
createReferralSet: {
isTeam: true,
team: {
name: fields.name,
teamUrl: fields.url,
avatarUrl: fields.avatarUrl,
closed: fields.private,
allowList: fields.private
? parseAllowListText(fields.allowList)
: [],
},
},
};
case TransactionType.UpdateReferralSet:
return {
updateReferralSet: {
id: fields.id,
isTeam: true,
team: {
name: fields.name,
teamUrl: fields.url,
avatarUrl: fields.avatarUrl,
closed: fields.private,
allowList: fields.private
? parseAllowListText(fields.allowList)
: [],
},
},
};
}
};
export const TeamForm = ({
type,
status,
err,
isCreatingSoloTeam,
onSubmit,
defaultValues,
}: {
type: TransactionType;
status: ReturnType<typeof useReferralSetTransaction>['status'];
err: ReturnType<typeof useReferralSetTransaction>['err'];
isCreatingSoloTeam: boolean;
onSubmit: ReturnType<typeof useReferralSetTransaction>['onSubmit'];
defaultValues?: FormFields;
}) => {
const t = useT();
const {
register,
handleSubmit,
control,
watch,
formState: { errors },
} = useForm<FormFields>({
defaultValues: {
private: isCreatingSoloTeam,
...defaultValues,
},
});
const isPrivate = watch('private');
const sendTransaction = (fields: FormFields) => {
onSubmit(prepareTransaction(type, fields));
};
return (
<form onSubmit={handleSubmit(sendTransaction)}>
<input type="hidden" {...register('id')} />
<TradingFormGroup label={t('Team name')} labelFor="name">
<TradingInput
{...register('name', { required: t('Required') })}
data-testid="team-name-input"
/>
{errors.name?.message && (
<TradingInputError forInput="name" data-testid="team-name-error">
{errors.name.message}
</TradingInputError>
)}
</TradingFormGroup>
<TradingFormGroup
label={t('URL')}
labelFor="url"
labelDescription={t(
'Provide a link so users can learn more about your team'
)}
>
<TradingInput
{...register('url', {
pattern: { value: URL_REGEX, message: t('Invalid URL') },
})}
data-testid="team-url-input"
/>
{errors.url?.message && (
<TradingInputError forInput="url" data-testid="team-url-error">
{errors.url.message}
</TradingInputError>
)}
</TradingFormGroup>
<TradingFormGroup
label={t('Avatar URL')}
labelFor="avatarUrl"
labelDescription={t('Provide a URL to a hosted image')}
>
<TradingInput
{...register('avatarUrl', {
pattern: {
value: URL_REGEX,
message: t('Invalid image URL'),
},
})}
data-testid="avatar-url-input"
/>
{errors.avatarUrl?.message && (
<TradingInputError
forInput="avatarUrl"
data-testid="avatar-url-error"
>
{errors.avatarUrl.message}
</TradingInputError>
)}
</TradingFormGroup>
{
// allow changing to private/public if editing, but don't show these options if making a solo team
(type === TransactionType.UpdateReferralSet || !isCreatingSoloTeam) && (
<>
<TradingFormGroup
label={t('Make team private')}
labelFor="private"
hideLabel={true}
>
<Controller
name="private"
control={control}
render={({ field }) => {
return (
<TradingCheckbox
label={t('Make team private')}
checked={field.value}
onCheckedChange={(value) => {
field.onChange(value);
}}
data-testid="team-private-checkbox"
/>
);
}}
/>
</TradingFormGroup>
{isPrivate && (
<TradingFormGroup
label={t('Public key allow list')}
labelFor="allowList"
labelDescription={t(
'Use a comma separated list to allow only specific public keys to join the team'
)}
>
<TextArea
{...register('allowList', {
required: t('Required'),
validate: {
allowList: (value) => {
const publicKeys = parseAllowListText(value);
if (
publicKeys.every((pk) => isValidVegaPublicKey(pk))
) {
return true;
}
return t('Invalid public key found in allow list');
},
},
})}
data-testid="team-allow-list-textarea"
/>
{errors.allowList?.message && (
<TradingInputError
forInput="avatarUrl"
data-testid="team-allow-list-error"
>
{errors.allowList.message}
</TradingInputError>
)}
</TradingFormGroup>
)}
</>
)
}
{err && (
<p className="text-danger text-xs mb-4 first-letter:capitalize">
{err}
</p>
)}
<SubmitButton type={type} status={status} />
</form>
);
};
const SubmitButton = ({
type,
status,
}: {
type?: TransactionType;
status: Status;
}) => {
const t = useT();
const disabled = status === 'pending' || status === 'requested';
let text = t('Create');
if (type === TransactionType.UpdateReferralSet) {
text = t('Update');
}
let confirmedText = t('Created');
if (type === TransactionType.UpdateReferralSet) {
confirmedText = t('Updated');
}
if (status === 'requested') {
text = t('Confirm in wallet...');
} else if (status === 'pending') {
text = t('Confirming transaction...');
}
const [showConfirmed, setShowConfirmed] = useState<boolean>(false);
useLayoutEffect(() => {
let to: ReturnType<typeof setTimeout>;
if (status === 'confirmed' && !showConfirmed) {
to = setTimeout(() => {
setShowConfirmed(true);
}, 100);
}
return () => {
clearTimeout(to);
};
}, [showConfirmed, status]);
const confirmed = (
<span
className={classNames('text-sm transition-opacity opacity-0', {
'opacity-100': showConfirmed,
})}
>
<VegaIcon
name={VegaIconNames.TICK}
size={18}
className="text-vega-green-500"
/>{' '}
{confirmedText}
</span>
);
return (
<div className="flex gap-2 items-baseline">
<TradingButton
type="submit"
intent={Intent.Info}
disabled={disabled}
data-testid="team-form-submit-button"
>
{text}
</TradingButton>
{status === 'confirmed' && confirmed}
</div>
);
};
const parseAllowListText = (str: string = '') => {
return str
.split(',')
.map((v) => v.trim())
.filter(Boolean);
};
@@ -0,0 +1,32 @@
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,
size = 'medium',
}: {
team: Pick<Team, 'teamId' | 'referrer'>;
size?: ComponentProps<typeof TradingAnchorButton>['size'];
}) => {
const t = useT();
const { pubKey, isReadOnly } = useVegaWallet();
if (pubKey && !isReadOnly && pubKey === team.referrer) {
return (
<TradingAnchorButton
size={size}
data-testid="update-team-button"
href={Links.COMPETITIONS_UPDATE_TEAM(team.teamId)}
intent={Intent.Info}
>
{t('Update team')}
</TradingAnchorButton>
);
}
return null;
};
+6 -13
View File
@@ -1,27 +1,20 @@
import { useEffect } from 'react'; import { TinyScroll } from '@vegaprotocol/ui-toolkit';
import { titlefy } from '@vegaprotocol/utils';
import { ErrorBoundary } from '../../components/error-boundary'; import { ErrorBoundary } from '../../components/error-boundary';
import { FeesContainer } from '../../components/fees-container'; import { FeesContainer } from '../../components/fees-container';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { usePageTitleStore } from '../../stores'; import { usePageTitle } from '../../lib/hooks/use-page-title';
export const Fees = () => { export const Fees = () => {
const t = useT(); const t = useT();
const title = t('Fees'); const title = t('Fees');
const { updateTitle } = usePageTitleStore((store) => ({ usePageTitle(title);
updateTitle: store.updateTitle,
}));
useEffect(() => {
updateTitle(titlefy([title]));
}, [updateTitle, title]);
return ( return (
<ErrorBoundary feature="fees"> <ErrorBoundary feature="fees">
<div className="container p-4 mx-auto"> <TinyScroll className="p-4 max-h-full overflow-auto">
<h1 className="px-4 pb-4 text-2xl">{title}</h1> <h1 className="md:px-4 pb-4 text-2xl">{title}</h1>
<FeesContainer /> <FeesContainer />
</div> </TinyScroll>
</ErrorBoundary> </ErrorBoundary>
); );
}; };
@@ -19,6 +19,7 @@ import {
useFundingRate, useFundingRate,
useMarketTradingMode, useMarketTradingMode,
useExternalTwap, useExternalTwap,
getQuoteName,
} from '@vegaprotocol/markets'; } from '@vegaprotocol/markets';
import { MarketState as State } from '@vegaprotocol/types'; import { MarketState as State } from '@vegaprotocol/types';
import { HeaderStat } from '../../components/header'; import { HeaderStat } from '../../components/header';
@@ -41,6 +42,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore(); const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
const asset = getAsset(market); const asset = getAsset(market);
const quoteUnit = getQuoteName(market);
return ( return (
<> <>
@@ -54,12 +56,15 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
<Last24hPriceChange <Last24hPriceChange
marketId={market.id} marketId={market.id}
decimalPlaces={market.decimalPlaces} decimalPlaces={market.decimalPlaces}
fallback={<span>-</span>}
/> />
</HeaderStat> </HeaderStat>
<HeaderStat heading={t('Volume (24h)')} testId="market-volume"> <HeaderStat heading={t('Volume (24h)')} testId="market-volume">
<Last24hVolume <Last24hVolume
marketId={market.id} marketId={market.id}
positionDecimalPlaces={market.positionDecimalPlaces} positionDecimalPlaces={market.positionDecimalPlaces}
marketDecimals={market.decimalPlaces}
quoteUnit={quoteUnit}
/> />
</HeaderStat> </HeaderStat>
<HeaderStatMarketTradingMode <HeaderStatMarketTradingMode
@@ -108,7 +113,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
heading={`${t('Funding Rate')} / ${t('Countdown')}`} heading={`${t('Funding Rate')} / ${t('Countdown')}`}
testId="market-funding" testId="market-funding"
> >
<div className="flex justify-between gap-2"> <div className="flex gap-2">
<FundingRate marketId={market.id} /> <FundingRate marketId={market.id} />
<FundingCountdown marketId={market.id} /> <FundingCountdown marketId={market.id} />
</div> </div>
@@ -3,7 +3,6 @@ import { type Market } from '@vegaprotocol/markets';
// TODO: handle oracle banner // TODO: handle oracle banner
// import { OracleBanner } from '@vegaprotocol/markets'; // import { OracleBanner } from '@vegaprotocol/markets';
import { useState } from 'react'; import { useState } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import classNames from 'classnames'; import classNames from 'classnames';
import { import {
Popover, Popover,
@@ -12,21 +11,21 @@ import {
VegaIconNames, VegaIconNames,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { MarketBanner } from '../../components/market-banner';
import { ErrorBoundary } from '../../components/error-boundary'; import { ErrorBoundary } from '../../components/error-boundary';
import { type TradingView } from './trade-views'; import { type TradingView } from './trade-views';
import { TradingViews } from './trade-views'; import { TradingViews } from './trade-views';
interface TradePanelsProps { interface TradePanelsProps {
market: Market; market: Market;
pinnedAsset?: PinnedAsset; pinnedAsset?: PinnedAsset;
} }
export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => { export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
const [view, setView] = useState<TradingView>('chart'); const [topView, setTopView] = useState<TradingView>('chart');
const viewCfg = TradingViews[view]; const topViewCfg = TradingViews[topView];
const [bottomView, setBottomView] = useState<TradingView>('positions');
const bottomViewCfg = TradingViews[bottomView];
const renderView = () => { const renderView = (view: TradingView) => {
const Component = TradingViews[view].component; const Component = TradingViews[view].component;
if (!Component) { if (!Component) {
@@ -39,12 +38,13 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
// so watch out for clashes in props // so watch out for clashes in props
return ( return (
<ErrorBoundary feature={view}> <ErrorBoundary feature={view}>
<Component marketId={market?.id} pinnedAsset={pinnedAsset} />; <Component marketId={market?.id} pinnedAsset={pinnedAsset} />
</ErrorBoundary> </ErrorBoundary>
); );
}; };
const renderMenu = () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any
const renderMenu = (viewCfg: any) => {
if ('menu' in viewCfg || 'settings' in viewCfg) { if ('menu' in viewCfg || 'settings' in viewCfg) {
return ( return (
<div className="flex items-center justify-end gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default"> <div className="flex items-center justify-end gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default">
@@ -69,55 +69,80 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
}; };
return ( return (
<div className="h-full grid grid-rows-[min-content_min-content_1fr_min-content]"> <div className="h-full flex flex-col lg:grid grid-rows-[min-content_min-content_1fr_min-content]">
<div> <div className="flex flex-col w-full overflow-hidden">
<MarketBanner market={market} /> <div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
</div> {['chart', 'orderbook', 'trades', 'liquidity', 'fundingPayments']
<div>{renderMenu()}</div> // filter to control available views for the current market
<div className="h-full relative"> // e.g. only perpetuals should get the funding views
<AutoSizer> .filter((_key) => {
{({ width, height }) => ( const key = _key as TradingView;
<div style={{ width, height }} className="overflow-auto"> const perpOnlyViews = ['funding', 'fundingPayments'];
{renderView()}
</div> if (
)} market?.tradableInstrument.instrument.product.__typename ===
</AutoSizer> 'Perpetual'
</div> ) {
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default"> return true;
{Object.keys(TradingViews) }
// filter to control available views for the current market
// eg only perps should get the funding views if (perpOnlyViews.includes(key)) {
.filter((_key) => { return false;
const key = _key as TradingView; }
const perpOnlyViews = ['funding', 'fundingPayments'];
if (
market?.tradableInstrument.instrument.product.__typename ===
'Perpetual'
) {
return true; return true;
} })
.map((_key) => {
const key = _key as TradingView;
const isActive = topView === key;
return (
<ViewButton
key={key}
view={key}
isActive={isActive}
onClick={() => {
setTopView(key);
}}
/>
);
})}
</div>
<div className="h-[50vh] lg:h-full relative">
<div>{renderMenu(topViewCfg)}</div>
<div className="overflow-auto h-full">{renderView(topView)}</div>
</div>
</div>
if (perpOnlyViews.includes(key)) { <div className="flex flex-col w-full grow">
return false; <div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
} {[
'positions',
return true; 'activeOrders',
}) 'closedOrders',
.map((_key) => { 'rejectedOrders',
'orders',
'stopOrders',
'collateral',
'fills',
].map((_key) => {
const key = _key as TradingView; const key = _key as TradingView;
const isActive = view === key; const isActive = bottomView === key;
return ( return (
<ViewButton <ViewButton
key={key} key={key}
view={key} view={key}
isActive={isActive} isActive={isActive}
onClick={() => { onClick={() => {
setView(key); setBottomView(key);
}} }}
/> />
); );
})} })}
</div>
<div className="relative grow">
<div className="flex flex-col">{renderMenu(bottomViewCfg)}</div>
<div className="overflow-auto h-full">{renderView(bottomView)}</div>
</div>
</div> </div>
</div> </div>
); );
@@ -157,7 +182,7 @@ const useViewLabel = (view: TradingView) => {
depth: t('Depth'), depth: t('Depth'),
liquidity: t('Liquidity'), liquidity: t('Liquidity'),
funding: t('Funding'), funding: t('Funding'),
fundingPayments: t('Funding Payments'), fundingPayments: t('Funding'),
orderbook: t('Orderbook'), orderbook: t('Orderbook'),
trades: t('Trades'), trades: t('Trades'),
positions: t('Positions'), positions: t('Positions'),
@@ -5,7 +5,7 @@ import {
useDataGridEvents, useDataGridEvents,
} from '@vegaprotocol/datagrid'; } from '@vegaprotocol/datagrid';
import type { MarketMaybeWithData } from '@vegaprotocol/markets'; import type { MarketMaybeWithData } from '@vegaprotocol/markets';
import { useColumnDefs } from './use-column-defs'; import { useMarketsColumnDefs } from './use-column-defs';
import type { DataGridStore } from '../../stores/datagrid-store-slice'; import type { DataGridStore } from '../../stores/datagrid-store-slice';
import { type StateCreator, create } from 'zustand'; import { type StateCreator, create } from 'zustand';
import { persist } from 'zustand/middleware'; import { persist } from 'zustand/middleware';
@@ -50,7 +50,7 @@ export const useMarketsStore = create<DataGridSlice>()(
); );
export const MarketListTable = (props: Props) => { export const MarketListTable = (props: Props) => {
const columnDefs = useColumnDefs(); const columnDefs = useMarketsColumnDefs();
const gridStore = useMarketsStore((store) => store.gridStore); const gridStore = useMarketsStore((store) => store.gridStore);
const updateGridStore = useMarketsStore((store) => store.updateGridStore); const updateGridStore = useMarketsStore((store) => store.updateGridStore);
@@ -1,5 +1,3 @@
import React, { useEffect } from 'react';
import { titlefy } from '@vegaprotocol/utils';
import { import {
LocalStoragePersistTabs as Tabs, LocalStoragePersistTabs as Tabs,
Tab, Tab,
@@ -7,7 +5,6 @@ import {
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { OpenMarkets } from './open-markets'; import { OpenMarkets } from './open-markets';
import { Proposed } from './proposed'; import { Proposed } from './proposed';
import { usePageTitleStore } from '../../stores';
import { Closed } from './closed'; import { Closed } from './closed';
import { import {
DApp, DApp,
@@ -17,19 +14,14 @@ import {
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { ErrorBoundary } from '../../components/error-boundary'; import { ErrorBoundary } from '../../components/error-boundary';
import { MarketsSettings } from './markets-settings'; import { MarketsSettings } from './markets-settings';
import { usePageTitle } from '../../lib/hooks/use-page-title';
export const MarketsPage = () => { export const MarketsPage = () => {
const t = useT(); const t = useT();
const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
const governanceLink = useLinks(DApp.Governance); const governanceLink = useLinks(DApp.Governance);
const externalLink = governanceLink(TOKEN_NEW_MARKET_PROPOSAL); const externalLink = governanceLink(TOKEN_NEW_MARKET_PROPOSAL);
useEffect(() => { usePageTitle(t('Markets'));
updateTitle(titlefy([t('Markets')]));
}, [updateTitle, t]);
return ( return (
<div className="h-full pt-0.5 pb-3 px-1.5"> <div className="h-full pt-0.5 pb-3 px-1.5">
@@ -0,0 +1,235 @@
import { Route, Routes } from 'react-router-dom';
import {
Intent,
MobileActionsDropdown,
Tooltip,
TradingButton,
TradingDropdownItem,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { type BarView, ViewType, useSidebar } from '../../components/sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { useT } from '../../lib/use-t';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import { useEffect } from 'react';
import classNames from 'classnames';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
const ViewInitializer = () => {
const currentRouteId = useGetCurrentRouteId();
const { setViews, getView } = useSidebar();
const view = getView(currentRouteId);
const { screenSize } = useScreenDimensions();
const largeScreen = ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize);
useEffect(() => {
if (largeScreen && view === undefined) {
setViews({ type: ViewType.Order }, currentRouteId);
}
}, [setViews, view, currentRouteId, largeScreen]);
return null;
};
export const MarketsMobileSidebar = () => {
const t = useT();
const currentRouteId = useGetCurrentRouteId();
const { pubKeys, isReadOnly } = useVegaWallet();
const openVegaWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
return (
<Routes>
<Route
path=":marketId"
element={
<>
<ViewInitializer />
<div className="grid grid-cols-3 grow md:grow-0 md:flex lg:flex-col items-center gap-2 lg:gap-4 p-1">
{!pubKeys || isReadOnly ? (
<>
<TradingButton
intent={Intent.Primary}
size="medium"
onClick={() => {
openVegaWalletDialog();
}}
>
{t('Connect')}
</TradingButton>
<MobileButton
view={ViewType.Order}
tooltip={t('Trade')}
routeId={currentRouteId}
/>
<MobileBarActionsDropdown currentRouteId={currentRouteId} />
</>
) : (
<>
<MobileButton
view={ViewType.Order}
tooltip={t('Trade')}
routeId={currentRouteId}
/>
<MobileButton
view={ViewType.Deposit}
tooltip={t('Deposit')}
routeId={currentRouteId}
/>
<MobileBarActionsDropdown currentRouteId={currentRouteId} />
</>
)}
</div>
</>
}
/>
</Routes>
);
};
export const MobileButton = ({
view,
tooltip: label,
disabled = false,
onClick,
routeId,
}: {
view?: ViewType;
tooltip: string;
disabled?: boolean;
onClick?: () => void;
routeId: string;
}) => {
const { setViews, getView } = useSidebar((store) => ({
setViews: store.setViews,
getView: store.getView,
}));
const currView = getView(routeId);
const onSelect = (view: BarView['type']) => {
if (view === currView?.type) {
setViews(null, routeId);
} else {
setViews({ type: view }, routeId);
}
};
const buttonClasses = classNames(
'flex items-center p-1 rounded',
'disabled:cursor-not-allowed disabled:text-vega-clight-500 dark:disabled:text-vega-cdark-500',
{
'text-vega-clight-200 dark:text-vega-cdark-200 enabled:hover:bg-vega-clight-500 dark:enabled:hover:bg-vega-cdark-500':
!view || view !== currView?.type,
'bg-vega-yellow enabled:hover:bg-vega-yellow-550 text-black':
view && view === currView?.type,
}
);
return (
<Tooltip description={label} align="center" side="right" sideOffset={10}>
<TradingButton
className={buttonClasses}
data-testid={view}
onClick={onClick || (() => onSelect(view as BarView['type']))}
disabled={disabled}
>
{label}
</TradingButton>
</Tooltip>
);
};
export const MobileDropdownItem = ({
view,
icon,
tooltip,
disabled = false,
onClick,
routeId,
}: {
view?: ViewType;
icon: VegaIconNames;
tooltip: string;
disabled?: boolean;
onClick?: () => void;
routeId: string;
}) => {
const { setViews, getView } = useSidebar((store) => ({
setViews: store.setViews,
getView: store.getView,
}));
const currView = getView(routeId);
const onSelect = (view: BarView['type']) => {
if (view === currView?.type) {
setViews(null, routeId);
} else {
setViews({ type: view }, routeId);
}
};
const buttonClasses = classNames(
'flex items-center p-1 rounded',
'disabled:cursor-not-allowed disabled:text-vega-clight-500 dark:disabled:text-vega-cdark-500',
{
'text-vega-clight-200 dark:text-vega-cdark-200 enabled:hover:bg-vega-clight-500 dark:enabled:hover:bg-vega-cdark-500':
!view || view !== currView?.type,
'bg-vega-yellow enabled:hover:bg-vega-yellow-550 text-black':
view && view === currView?.type,
}
);
return (
<Tooltip description={tooltip} align="center" side="right" sideOffset={10}>
<TradingDropdownItem
className={buttonClasses}
data-testid={view}
onClick={onClick || (() => onSelect(view as BarView['type']))}
disabled={disabled}
>
<VegaIcon name={icon} size={20} />
{tooltip}
</TradingDropdownItem>
</Tooltip>
);
};
export const MobileBarActionsDropdown = ({
currentRouteId,
}: {
currentRouteId: string;
}) => {
const t = useT();
return (
<MobileActionsDropdown>
<MobileDropdownItem
view={ViewType.Deposit}
icon={VegaIconNames.DEPOSIT}
tooltip={t('Deposit')}
routeId={currentRouteId}
/>
<MobileDropdownItem
view={ViewType.Withdraw}
icon={VegaIconNames.WITHDRAW}
tooltip={t('Withdraw')}
routeId={currentRouteId}
/>
<MobileDropdownItem
view={ViewType.Transfer}
icon={VegaIconNames.TRANSFER}
tooltip={t('Transfer')}
routeId={currentRouteId}
/>
<MobileDropdownItem
view={ViewType.Info}
icon={VegaIconNames.BREAKDOWN}
tooltip={t('Market specification')}
routeId={currentRouteId}
/>
<MobileDropdownItem
view={ViewType.Settings}
icon={VegaIconNames.COG}
tooltip={t('Settings')}
routeId={currentRouteId}
/>
</MobileActionsDropdown>
);
};
@@ -7,21 +7,31 @@ import type {
} from '@vegaprotocol/datagrid'; } from '@vegaprotocol/datagrid';
import { COL_DEFS, SetFilter } from '@vegaprotocol/datagrid'; import { COL_DEFS, SetFilter } from '@vegaprotocol/datagrid';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import { addDecimalsFormatNumber, toBigNum } from '@vegaprotocol/utils'; import {
addDecimalsFormatNumber,
formatNumber,
toBigNum,
} from '@vegaprotocol/utils';
import { ButtonLink, Tooltip } from '@vegaprotocol/ui-toolkit'; import { ButtonLink, Tooltip } from '@vegaprotocol/ui-toolkit';
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets'; import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import type { import type {
MarketFieldsFragment,
MarketMaybeWithData, MarketMaybeWithData,
MarketMaybeWithDataAndCandles, MarketMaybeWithDataAndCandles,
} from '@vegaprotocol/markets'; } from '@vegaprotocol/markets';
import { MarketActionsDropdown } from './market-table-actions'; import { MarketActionsDropdown } from './market-table-actions';
import { calcCandleVolume, getAsset } from '@vegaprotocol/markets'; import {
calcCandleVolume,
calcCandleVolumePrice,
getAsset,
getQuoteName,
} from '@vegaprotocol/markets';
import { MarketCodeCell } from './market-code-cell'; import { MarketCodeCell } from './market-code-cell';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
const { MarketTradingMode, AuctionTrigger } = Schema; const { MarketTradingMode, AuctionTrigger } = Schema;
export const useColumnDefs = () => { export const useMarketsColumnDefs = () => {
const t = useT(); const t = useT();
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore(); const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
return useMemo<ColDef[]>( return useMemo<ColDef[]>(
@@ -158,11 +168,25 @@ export const useColumnDefs = () => {
}: ValueFormatterParams<MarketMaybeWithDataAndCandles, 'candles'>) => { }: ValueFormatterParams<MarketMaybeWithDataAndCandles, 'candles'>) => {
const candles = data?.candles; const candles = data?.candles;
const vol = candles ? calcCandleVolume(candles) : '0'; const vol = candles ? calcCandleVolume(candles) : '0';
const quoteName = getQuoteName(data as MarketFieldsFragment);
const volPrice =
candles &&
calcCandleVolumePrice(
candles,
data.decimalPlaces,
data.positionDecimalPlaces
);
const volume = const volume =
data && vol && vol !== '0' data && vol && vol !== '0'
? addDecimalsFormatNumber(vol, data.positionDecimalPlaces) ? addDecimalsFormatNumber(vol, data.positionDecimalPlaces)
: '0.00'; : '0.00';
return volume; const volumePrice =
volPrice && formatNumber(volPrice, data?.decimalPlaces);
return volumePrice
? `${volume} (${volumePrice} ${quoteName})`
: volume;
}, },
}, },
{ {
@@ -2,6 +2,7 @@ import { VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { SidebarButton, ViewType } from '../../components/sidebar'; import { SidebarButton, ViewType } from '../../components/sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id'; import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { MobileButton } from '../markets/mobile-buttons';
export const PortfolioSidebar = () => { export const PortfolioSidebar = () => {
const t = useT(); const t = useT();
@@ -30,3 +31,28 @@ export const PortfolioSidebar = () => {
</> </>
); );
}; };
export const PortfolioMobileSidebar = () => {
const t = useT();
const currentRouteId = useGetCurrentRouteId();
return (
<div className="grid grid-cols-3 grow md:grow-0 md:flex lg:flex-col items-center gap-2 lg:gap-4 p-1">
<MobileButton
view={ViewType.Deposit}
tooltip={t('Deposit')}
routeId={currentRouteId}
/>
<MobileButton
view={ViewType.Withdraw}
tooltip={t('Withdraw')}
routeId={currentRouteId}
/>
<MobileButton
view={ViewType.Transfer}
tooltip={t('Transfer')}
routeId={currentRouteId}
/>
</div>
);
};
@@ -1,10 +1,8 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { LayoutPriority } from 'allotment'; import { LayoutPriority } from 'allotment';
import { titlefy } from '@vegaprotocol/utils';
import { useIncompleteWithdrawals } from '@vegaprotocol/withdraws'; import { useIncompleteWithdrawals } from '@vegaprotocol/withdraws';
import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit'; import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit';
import { usePageTitleStore } from '../../stores';
import { import {
AccountsContainer, AccountsContainer,
AccountsSettings, AccountsSettings,
@@ -41,6 +39,7 @@ import { WithdrawalsMenu } from '../../components/withdrawals-menu';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id'; import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { ErrorBoundary } from '../../components/error-boundary'; import { ErrorBoundary } from '../../components/error-boundary';
import { usePageTitle } from '../../lib/hooks/use-page-title';
const WithdrawalsIndicator = () => { const WithdrawalsIndicator = () => {
const { ready } = useIncompleteWithdrawals(); const { ready } = useIncompleteWithdrawals();
@@ -69,14 +68,7 @@ const SidebarViewInitializer = () => {
export const Portfolio = () => { export const Portfolio = () => {
const t = useT(); const t = useT();
usePageTitle(t('Portfolio'));
const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
useEffect(() => {
updateTitle(titlefy([t('Portfolio')]));
}, [updateTitle, t]);
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'portfolio' }); const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'portfolio' });
const wrapperClasses = 'p-0.5 h-full max-h-full flex flex-col'; const wrapperClasses = 'p-0.5 h-full max-h-full flex flex-col';
@@ -5,17 +5,19 @@ import {
VegaIcon, VegaIcon,
VegaIconNames, VegaIconNames,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import type { FieldValues } from 'react-hook-form';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import classNames from 'classnames'; import classNames from 'classnames';
import { Navigate, useNavigate, useSearchParams } from 'react-router-dom'; import { Navigate, useNavigate, useSearchParams } from 'react-router-dom';
import type { ButtonHTMLAttributes, MouseEventHandler } from 'react'; import type { ButtonHTMLAttributes, MouseEventHandler } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback } from 'react';
import { RainbowButton } from './buttons'; import { RainbowButton } from '../../components/rainbow-button';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet'; import {
useSimpleTransaction,
useVegaWallet,
useVegaWalletDialogStore,
} from '@vegaprotocol/wallet';
import { useIsInReferralSet, useReferral } from './hooks/use-referral'; import { useIsInReferralSet, useReferral } from './hooks/use-referral';
import { Routes } from '../../lib/links'; import { Routes } from '../../lib/links';
import { useTransactionEventSubscription } from '@vegaprotocol/web3';
import { Statistics, useStats } from './referral-statistics'; import { Statistics, useStats } from './referral-statistics';
import { useReferralProgram } from './hooks/use-referral-program'; import { useReferralProgram } from './hooks/use-referral-program';
import { ns, useT } from '../../lib/use-t'; import { ns, useT } from '../../lib/use-t';
@@ -73,6 +75,10 @@ export const ApplyCodeFormContainer = ({
return <ApplyCodeForm onSuccess={onSuccess} />; return <ApplyCodeForm onSuccess={onSuccess} />;
}; };
type FormFields = {
code: string;
};
export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => { export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
const t = useT(); const t = useT();
const program = useReferralProgram(); const program = useReferralProgram();
@@ -81,31 +87,47 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
(store) => store.openVegaWalletDialog (store) => store.openVegaWalletDialog
); );
const [status, setStatus] = useState< const { isReadOnly, pubKey } = useVegaWallet();
'requested' | 'no-funds' | 'successful' | null
>(null);
const txHash = useRef<string | null>(null);
const { isReadOnly, pubKey, sendTx } = useVegaWallet();
const { isEligible, requiredFunds } = useFundsAvailable(); const { isEligible, requiredFunds } = useFundsAvailable();
const currentRouteId = useGetCurrentRouteId(); const currentRouteId = useGetCurrentRouteId();
const setViews = useSidebar((s) => s.setViews); const setViews = useSidebar((s) => s.setViews);
const [params] = useSearchParams();
const { const {
register, register,
handleSubmit, handleSubmit,
formState: { errors }, formState: { errors },
setValue,
setError, setError,
watch, watch,
} = useForm(); } = useForm<FormFields>({
const [params] = useSearchParams(); defaultValues: {
code: params.get('code') || '',
},
});
const codeField = watch('code'); const codeField = watch('code');
const { data: previewData, loading: previewLoading } = useReferral({ const { data: previewData, loading: previewLoading } = useReferral({
code: validateCode(codeField, t) ? codeField : undefined, code: validateCode(codeField, t) ? codeField : undefined,
}); });
const { send, status } = useSimpleTransaction({
onSuccess: () => {
// go to main page when successfully applied
setTimeout(() => {
if (onSuccess) onSuccess();
navigate(Routes.REFERRALS);
}, RELOAD_DELAY);
},
onError: (msg) => {
setError('code', {
type: 'required',
message: msg,
});
},
});
/** /**
* Validates if a connected party can apply a code (min funds span protection) * Validates if a connected party can apply a code (min funds span protection)
*/ */
@@ -135,99 +157,55 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
return true; return true;
}, [codeField, previewData, previewLoading, t]); }, [codeField, previewData, previewLoading, t]);
useEffect(() => { const noFunds = validateFundsAvailable() !== true ? true : false;
const code = params.get('code');
if (code) setValue('code', code);
}, [params, setValue]);
useEffect(() => { const onSubmit = ({ code }: FormFields) => {
const err = validateFundsAvailable();
if (err !== true) {
setStatus('no-funds');
} else {
setStatus(null);
}
}, [isEligible, validateFundsAvailable]);
const onSubmit = ({ code }: FieldValues) => {
if (isReadOnly || !pubKey || !code || code.length === 0) { if (isReadOnly || !pubKey || !code || code.length === 0) {
return; return;
} }
setStatus('requested'); send({
sendTx(pubKey, {
applyReferralCode: { applyReferralCode: {
id: code as string, id: code as string,
}, },
}) });
.then((res) => {
if (!res) {
setError('code', {
type: 'required',
message: t('The transaction could not be sent'),
});
}
if (res) {
txHash.current = res.transactionHash.toLowerCase();
}
})
.catch((err) => {
if (err.message.includes('user rejected')) {
setStatus(null);
} else {
setStatus(null);
setError('code', {
type: 'required',
message:
err instanceof Error
? err.message
: t('Your code has been rejected'),
});
}
});
};
useTransactionEventSubscription({ // sendTx(pubKey, {
variables: { partyId: pubKey || '' }, // applyReferralCode: {
skip: !pubKey, // id: code as string,
fetchPolicy: 'no-cache', // },
onData: ({ data: result }) => // })
result.data?.busEvents?.forEach((event) => { // .then((res) => {
if (event.event.__typename === 'TransactionResult') { // if (!res) {
const hash = event.event.hash.toLowerCase(); // setError('code', {
if (txHash.current && txHash.current === hash) { // type: 'required',
const err = event.event.error; // message: t('The transaction could not be sent'),
const status = event.event.status; // });
if (err) { // }
setStatus(null); // if (res) {
setError('code', { // txHash.current = res.transactionHash.toLowerCase();
type: 'required', // }
message: err, // })
}); // .catch((err) => {
} // if (err.message.includes('user rejected')) {
if (status && !err) { // setStatus(null);
setStatus('successful'); // } else {
} // setStatus(null);
} // setError('code', {
} // type: 'required',
}), // message:
}); // err instanceof Error
// ? err.message
// : t('Your code has been rejected'),
// });
// }
// });
};
const { epochsValue, nextBenefitTierValue } = useStats({ program }); const { epochsValue, nextBenefitTierValue } = useStats({ program });
// go to main page when successfully applied
useEffect(() => {
if (status === 'successful') {
setTimeout(() => {
if (onSuccess) onSuccess();
navigate(Routes.REFERRALS);
}, RELOAD_DELAY);
}
}, [navigate, onSuccess, status]);
// show "code applied" message when successfully applied // show "code applied" message when successfully applied
if (status === 'successful') { if (status === 'confirmed') {
return ( return (
<div className="mx-auto w-1/2"> <div className="mx-auto w-1/2">
<h3 className="calt mb-5 flex flex-row items-center justify-center gap-2 text-center text-xl uppercase"> <h3 className="calt mb-5 flex flex-row items-center justify-center gap-2 text-center text-xl uppercase">
@@ -261,7 +239,7 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
}; };
} }
if (status === 'no-funds') { if (noFunds) {
return { return {
disabled: false, disabled: false,
children: t('Deposit funds'), children: t('Deposit funds'),
@@ -332,7 +310,7 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
</label> </label>
<RainbowButton variant="border" {...getButtonProps()} /> <RainbowButton variant="border" {...getButtonProps()} />
</form> </form>
{status === 'no-funds' ? ( {noFunds ? (
<InputError intent="warning" className="overflow-auto break-words"> <InputError intent="warning" className="overflow-auto break-words">
<span> <span>
<SpamProtectionErr requiredFunds={requiredFunds?.toString()} /> <SpamProtectionErr requiredFunds={requiredFunds?.toString()} />
@@ -4,41 +4,6 @@ import type { ComponentProps, ButtonHTMLAttributes } from 'react';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
type RainbowButtonProps = {
variant?: 'full' | 'border';
};
export const RainbowButton = ({
variant = 'full',
children,
className,
...props
}: RainbowButtonProps & ButtonHTMLAttributes<HTMLButtonElement>) => (
<button
className={classNames(
'bg-rainbow rounded-lg overflow-hidden disabled:opacity-40',
'hover:bg-rainbow-180 hover:animate-spin-rainbow',
{
'px-5 py-3 text-white': variant === 'full',
'p-[0.125rem]': variant === 'border',
}
)}
{...props}
>
<div
className={classNames(
{
'bg-vega-clight-800 dark:bg-vega-cdark-800 text-black dark:text-white px-5 py-3 rounded-[0.35rem] overflow-hidden':
variant === 'border',
},
className
)}
>
{children}
</div>
</button>
);
const RAINBOW_TAB_STYLE = classNames( const RAINBOW_TAB_STYLE = classNames(
'inline-block', 'inline-block',
'bg-vega-clight-500 dark:bg-vega-cdark-500', 'bg-vega-clight-500 dark:bg-vega-cdark-500',
@@ -2,9 +2,6 @@ export const BORDER_COLOR = 'border-vega-clight-500 dark:border-vega-cdark-500';
export const GRADIENT = export const GRADIENT =
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent'; 'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
export const SKY_BACKGROUND =
'bg-[url(/sky-light.png)] dark:bg-[url(/sky-dark.png)] bg-[37%_0px] bg-[length:1440px] bg-no-repeat bg-local';
// TODO: Update the links to use the correct referral related pages // TODO: Update the links to use the correct referral related pages
export const REFERRAL_DOCS_LINK = export const REFERRAL_DOCS_LINK =
'https://docs.vega.xyz/mainnet/concepts/trading-on-vega/discounts-rewards#referral-program'; 'https://docs.vega.xyz/mainnet/concepts/trading-on-vega/discounts-rewards#referral-program';
@@ -1,9 +1,5 @@
import { import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
useVegaWallet, import { RainbowButton } from '../../components/rainbow-button';
useVegaWalletDialogStore,
determineId,
} from '@vegaprotocol/wallet';
import { RainbowButton } from './buttons';
import { useState } from 'react'; import { useState } from 'react';
import { import {
CopyWithTooltip, CopyWithTooltip,
@@ -11,6 +7,7 @@ import {
ExternalLink, ExternalLink,
InputError, InputError,
Intent, Intent,
Tooltip,
TradingAnchorButton, TradingAnchorButton,
TradingButton, TradingButton,
VegaIcon, VegaIcon,
@@ -18,34 +15,28 @@ import {
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { DApp, TokenStaticLinks, useLinks } from '@vegaprotocol/environment'; import { DApp, TokenStaticLinks, useLinks } from '@vegaprotocol/environment';
import { useStakeAvailable } from './hooks/use-stake-available';
import { ABOUT_REFERRAL_DOCS_LINK } from './constants'; import { ABOUT_REFERRAL_DOCS_LINK } from './constants';
import { useIsInReferralSet, useReferral } from './hooks/use-referral'; import { useIsInReferralSet, useReferral } from './hooks/use-referral';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { Navigate } from 'react-router-dom'; import { Link, Navigate, useNavigate } from 'react-router-dom';
import { Routes } from '../../lib/links'; import { Links, Routes } from '../../lib/links';
import { useReferralProgram } from './hooks/use-referral-program'; import { useReferralProgram } from './hooks/use-referral-program';
import { useReferralSetTransaction } from '../../lib/hooks/use-referral-set-transaction';
import { Trans } from 'react-i18next';
export const CreateCodeContainer = () => { export const CreateCodeContainer = () => {
const { pubKey } = useVegaWallet(); const t = useT();
const { pubKey, isReadOnly } = useVegaWallet();
const isInReferralSet = useIsInReferralSet(pubKey); const isInReferralSet = useIsInReferralSet(pubKey);
const openWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
// Navigate to the index page when already in the referral set. // Navigate to the index page when already in the referral set.
if (isInReferralSet) { if (isInReferralSet) {
return <Navigate to={Routes.REFERRALS} />; return <Navigate to={Routes.REFERRALS} />;
} }
return <CreateCodeForm />;
};
export const CreateCodeForm = () => {
const t = useT();
const [dialogOpen, setDialogOpen] = useState(false);
const openWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
const { pubKey, isReadOnly } = useVegaWallet();
return ( return (
<div <div
data-testid="referral-create-code-form" data-testid="referral-create-code-form"
@@ -60,22 +51,82 @@ export const CreateCodeForm = () => {
)} )}
</p> </p>
<div className="w-full flex flex-col"> <div className="w-full flex flex-col gap-4 items-stretch">
<RainbowButton {pubKey ? (
variant="border" <CreateCodeForm />
disabled={isReadOnly} ) : (
onClick={() => { <RainbowButton
if (pubKey) { variant="border"
setDialogOpen(true); disabled={isReadOnly}
} else { onClick={openWalletDialog}
openWalletDialog(); >
} {t('Connect wallet')}
}} </RainbowButton>
> )}
{pubKey ? t('Create a referral code') : t('Connect wallet')}
</RainbowButton>
</div> </div>
</div>
);
};
export const CreateCodeForm = () => {
const t = useT();
const navigate = useNavigate();
const [dialogOpen, setDialogOpen] = useState(false);
const { isReadOnly } = useVegaWallet();
return (
<>
<Tooltip
description={t(
'Create a simple referral code to enjoy the referrer commission outlined in the current referral program'
)}
>
<span>
<RainbowButton
variant="border"
disabled={isReadOnly}
onClick={() => setDialogOpen(true)}
className="w-full"
>
{t('Create a referral code')}
</RainbowButton>
</span>
</Tooltip>
<Tooltip
description={
<Trans
i18nKey={
'Make your referral code a Team to compete in Competitions with your friends, appear in leaderboards on the <0>Competitions Homepage</0>, and earn rewards'
}
components={[
<Link
key="homepage-link"
to={Links.COMPETITIONS()}
className="underline"
>
Compeitionts Homepage
</Link>,
]}
/>
}
>
<span>
<RainbowButton
role="link"
variant="border"
disabled={isReadOnly}
onClick={() => navigate(Links.COMPETITIONS_CREATE_TEAM())}
className="w-full"
>
{t('Create a team')}
</RainbowButton>
</span>
</Tooltip>
<p className="text-xs">
<Link className="underline" to={Links.COMPETITIONS()}>
{t('Go to competitions')}
</Link>
</p>
<Dialog <Dialog
title={t('Create a referral code')} title={t('Create a referral code')}
open={dialogOpen} open={dialogOpen}
@@ -84,7 +135,7 @@ export const CreateCodeForm = () => {
> >
<CreateCodeDialog setDialogOpen={setDialogOpen} /> <CreateCodeDialog setDialogOpen={setDialogOpen} />
</Dialog> </Dialog>
</div> </>
); );
}; };
@@ -95,67 +146,42 @@ const CreateCodeDialog = ({
}) => { }) => {
const t = useT(); const t = useT();
const createLink = useLinks(DApp.Governance); const createLink = useLinks(DApp.Governance);
const { isReadOnly, pubKey, sendTx } = useVegaWallet(); const { pubKey } = useVegaWallet();
const { refetch } = useReferral({ pubKey, role: 'referrer' }); const { refetch } = useReferral({ pubKey, role: 'referrer' });
const [err, setErr] = useState<string | null>(null); const {
const [code, setCode] = useState<string | null>(null); err,
const [status, setStatus] = useState< code,
'idle' | 'loading' | 'success' | 'error' status,
>('idle'); stakeAvailable: currentStakeAvailable,
requiredStake,
const { stakeAvailable: currentStakeAvailable, requiredStake } = onSubmit,
useStakeAvailable(); } = useReferralSetTransaction();
const { details: programDetails } = useReferralProgram(); const { details: programDetails } = useReferralProgram();
const onSubmit = () => {
if (isReadOnly || !pubKey) {
setErr('Not connected');
} else {
setErr(null);
setStatus('loading');
setCode(null);
sendTx(pubKey, {
createReferralSet: {
isTeam: false,
},
})
.then((res) => {
if (!res) {
setErr(`Invalid response: ${JSON.stringify(res)}`);
return;
}
const code = determineId(res.signature);
setCode(code);
setStatus('success');
})
.catch((err) => {
if (err.message.includes('user rejected')) {
setStatus('idle');
return;
}
setStatus('error');
setErr(err.message);
});
}
};
const getButtonProps = () => { const getButtonProps = () => {
if (status === 'idle' || status === 'error') { if (status === 'idle') {
return { return {
children: t('Generate code'), children: t('Generate code'),
onClick: () => onSubmit(), onClick: () => onSubmit({ createReferralSet: { isTeam: false } }),
}; };
} }
if (status === 'loading') { if (status === 'requested') {
return { return {
children: t('Confirm in wallet...'), children: t('Confirm in wallet...'),
disabled: true, disabled: true,
}; };
} }
if (status === 'success') { if (status === 'pending') {
return {
children: t('Waiting for transaction...'),
disabled: true,
};
}
if (status === 'confirmed') {
return { return {
children: t('Close'), children: t('Close'),
intent: Intent.Success, intent: Intent.Success,
@@ -209,7 +235,10 @@ const CreateCodeDialog = ({
if (!programDetails) { if (!programDetails) {
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{(status === 'idle' || status === 'loading' || status === 'error') && ( {(status === 'idle' ||
status === 'requested' ||
status === 'pending' ||
err) && (
<> <>
{ {
<p> <p>
@@ -220,7 +249,7 @@ const CreateCodeDialog = ({
} }
</> </>
)} )}
{status === 'success' && code && ( {status === 'confirmed' && code && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex-1 min-w-0 p-2 text-sm rounded bg-vega-clight-700 dark:bg-vega-cdark-700"> <div className="flex-1 min-w-0 p-2 text-sm rounded bg-vega-clight-700 dark:bg-vega-cdark-700">
<p className="overflow-hidden whitespace-nowrap text-ellipsis"> <p className="overflow-hidden whitespace-nowrap text-ellipsis">
@@ -240,7 +269,7 @@ const CreateCodeDialog = ({
<TradingButton <TradingButton
fill={true} fill={true}
intent={Intent.Primary} intent={Intent.Primary}
onClick={() => onSubmit()} onClick={() => onSubmit({ createReferralSet: { isTeam: false } })}
{...getButtonProps()} {...getButtonProps()}
> >
{t('Yes')} {t('Yes')}
@@ -269,14 +298,17 @@ const CreateCodeDialog = ({
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{(status === 'idle' || status === 'loading' || status === 'error') && ( {(status === 'idle' ||
status === 'requested' ||
status === 'pending' ||
err) && (
<p> <p>
{t( {t(
'Generate a referral code to share with your friends and access the commission benefits of the current program.' 'Generate a referral code to share with your friends and access the commission benefits of the current program.'
)} )}
</p> </p>
)} )}
{status === 'success' && code && ( {status === 'confirmed' && code && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex-1 min-w-0 p-2 text-sm rounded bg-vega-clight-700 dark:bg-vega-cdark-700"> <div className="flex-1 min-w-0 p-2 text-sm rounded bg-vega-clight-700 dark:bg-vega-cdark-700">
<p className="overflow-hidden whitespace-nowrap text-ellipsis"> <p className="overflow-hidden whitespace-nowrap text-ellipsis">
@@ -1,53 +1,10 @@
import { isRouteErrorResponse, useNavigate, useRouteError } from 'react-router'; import { useNavigate } from 'react-router';
import { RainbowButton } from './buttons'; import { RainbowButton } from '../../components/rainbow-button';
import { LayoutWithSky } from '../../components/layouts-inner';
import { AnimatedDudeWithWire } from './graphics/dude'; import { AnimatedDudeWithWire } from './graphics/dude';
import { LayoutWithSky } from './layout';
import { Routes } from '../../lib/links'; import { Routes } from '../../lib/links';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
export const ErrorBoundary = () => {
const t = useT();
const error = useRouteError();
const navigate = useNavigate();
const title = isRouteErrorResponse(error)
? `${error.status} ${error.statusText}`
: t('Something went wrong');
const code = isRouteErrorResponse(error) ? error.status : 0;
const messages: Record<number, string> = {
0: t('An unknown error occurred.'),
404: t("The page you're looking for doesn't exists."),
};
return (
<LayoutWithSky className="pt-32">
<div
aria-hidden
className="absolute top-64 right-[220px] md:right-[340px] max-sm:hidden"
>
<AnimatedDudeWithWire className="animate-spin" />
</div>
<h1 className="text-6xl font-alpha calt mb-10">{title}</h1>
{Object.keys(messages).includes(code.toString()) ? (
<p className="text-lg mb-10">{messages[code]}</p>
) : null}
<p className="text-lg mb-10">
<RainbowButton
onClick={() => navigate('..')}
variant="border"
className="text-xs"
>
{t('Go back and try again')}
</RainbowButton>
</p>
</LayoutWithSky>
);
};
export const NotFound = () => { export const NotFound = () => {
const t = useT(); const t = useT();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -1,4 +1,4 @@
import { getNumberFormat } from '@vegaprotocol/utils'; import { formatNumber } from '@vegaprotocol/utils';
import sortBy from 'lodash/sortBy'; import sortBy from 'lodash/sortBy';
import omit from 'lodash/omit'; import omit from 'lodash/omit';
import { useReferralProgramQuery } from './__generated__/CurrentReferralProgram'; import { useReferralProgramQuery } from './__generated__/CurrentReferralProgram';
@@ -107,9 +107,7 @@ export const useReferralProgram = () => {
discountFactor: Number(t.referralDiscountFactor), discountFactor: Number(t.referralDiscountFactor),
discount: BigNumber(t.referralDiscountFactor).times(100).toFixed(2) + '%', discount: BigNumber(t.referralDiscountFactor).times(100).toFixed(2) + '%',
minimumVolume: Number(t.minimumRunningNotionalTakerVolume), minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
volume: getNumberFormat(0).format( volume: formatNumber(t.minimumRunningNotionalTakerVolume, 0),
Number(t.minimumRunningNotionalTakerVolume)
),
epochs: Number(t.minimumEpochs), epochs: Number(t.minimumEpochs),
}; };
}); });
@@ -8,13 +8,13 @@ import type {
ReferralSetsQueryVariables, ReferralSetsQueryVariables,
} from './__generated__/ReferralSets'; } from './__generated__/ReferralSets';
import { useReferralSetsQuery } from './__generated__/ReferralSets'; import { useReferralSetsQuery } from './__generated__/ReferralSets';
import { useStakeAvailable } from './use-stake-available'; import { useStakeAvailable } from '../../../lib/hooks/use-stake-available';
export const DEFAULT_AGGREGATION_DAYS = 30; export const DEFAULT_AGGREGATION_DAYS = 30;
export type Role = 'referrer' | 'referee'; export type Role = 'referrer' | 'referee';
type UseReferralArgs = ( type UseReferralArgs = (
| { code: string } | { code: string | undefined }
| { pubKey: string | null; role: Role } | { pubKey: string | null; role: Role }
) & { ) & {
aggregationEpochs?: number; aggregationEpochs?: number;
@@ -1,6 +1,9 @@
import { MockedProvider, type MockedResponse } from '@apollo/react-testing'; import { MockedProvider, type MockedResponse } from '@apollo/react-testing';
import { render, waitFor } from '@testing-library/react'; import { render, screen, waitFor } from '@testing-library/react';
import { type VegaWalletContextShape } from '@vegaprotocol/wallet'; import {
VegaWalletContext,
type VegaWalletContextShape,
} from '@vegaprotocol/wallet';
import { ReferralStatistics } from './referral-statistics'; import { ReferralStatistics } from './referral-statistics';
import { import {
ReferralProgramDocument, ReferralProgramDocument,
@@ -15,7 +18,7 @@ import {
StakeAvailableDocument, StakeAvailableDocument,
type StakeAvailableQueryVariables, type StakeAvailableQueryVariables,
type StakeAvailableQuery, type StakeAvailableQuery,
} from './hooks/__generated__/StakeAvailable'; } from '../../lib/hooks/__generated__/StakeAvailable';
import { import {
RefereesDocument, RefereesDocument,
type RefereesQueryVariables, type RefereesQueryVariables,
@@ -296,122 +299,99 @@ const refereesMock30: MockedResponse<RefereesQuery, RefereesQueryVariables> = {
}, },
}; };
jest.mock('@vegaprotocol/wallet', () => {
return {
...jest.requireActual('@vegaprotocol/wallet'),
useVegaWallet: () => {
const ctx: Partial<VegaWalletContextShape> = {
pubKey: MOCK_PUBKEY,
};
return ctx;
},
};
});
describe('ReferralStatistics', () => { describe('ReferralStatistics', () => {
it('displays apply code when no data has been found for given pubkey', () => { const renderComponent = (mocks: MockedResponse[]) => {
const { queryByTestId } = render( const walletContext = {
pubKey: MOCK_PUBKEY,
isReadOnly: false,
sendTx: jest.fn(),
} as unknown as VegaWalletContextShape;
return render(
<MemoryRouter> <MemoryRouter>
<MockedProvider mocks={[]} showWarnings={false}> <VegaWalletContext.Provider value={walletContext}>
<ReferralStatistics /> <MockedProvider mocks={mocks} showWarnings={false}>
</MockedProvider> <ReferralStatistics />
</MockedProvider>
</VegaWalletContext.Provider>
</MemoryRouter> </MemoryRouter>
); );
};
expect(queryByTestId('referral-apply-code-form')).toBeInTheDocument(); it('displays apply code when no data has been found for given pubkey', () => {
renderComponent([]);
expect(
screen.queryByTestId('referral-apply-code-form')
).toBeInTheDocument();
}); });
it('displays referrer stats when given pubkey is a referrer', async () => { it('displays referrer stats when given pubkey is a referrer', async () => {
const { queryByTestId } = render( renderComponent([
<MemoryRouter> programMock,
<MockedProvider referralSetAsReferrerMock,
mocks={[ noReferralSetAsRefereeMock,
programMock, stakeAvailableMock,
referralSetAsReferrerMock, refereesMock,
noReferralSetAsRefereeMock, refereesMock30,
stakeAvailableMock, ]);
refereesMock,
refereesMock30,
]}
showWarnings={false}
>
<ReferralStatistics />
</MockedProvider>
</MemoryRouter>
);
await waitFor(() => { await waitFor(() => {
expect( expect(
queryByTestId('referral-create-code-form') screen.queryByTestId('referral-create-code-form')
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
expect(queryByTestId('referral-statistics')).toBeInTheDocument(); expect(screen.queryByTestId('referral-statistics')).toBeInTheDocument();
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual( expect(screen.queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referrer' 'referrer'
); );
// gets commision from 30 epochs query // gets commision from 30 epochs query
expect(queryByTestId('total-commission-value')).toHaveTextContent( expect(screen.queryByTestId('total-commission-value')).toHaveTextContent(
'12,340' '12,340'
); );
}); });
}); });
it('displays referee stats when given pubkey is a referee', async () => { it('displays referee stats when given pubkey is a referee', async () => {
const { queryByTestId } = render( renderComponent([
<MemoryRouter> programMock,
<MockedProvider noReferralSetAsReferrerMock,
mocks={[ referralSetAsRefereeMock,
programMock, stakeAvailableMock,
noReferralSetAsReferrerMock, refereesMock,
referralSetAsRefereeMock, ]);
stakeAvailableMock,
refereesMock,
]}
showWarnings={false}
>
<ReferralStatistics />
</MockedProvider>
</MemoryRouter>
);
await waitFor(() => { await waitFor(() => {
expect( expect(
queryByTestId('referral-create-code-form') screen.queryByTestId('referral-create-code-form')
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
expect(queryByTestId('referral-statistics')).toBeInTheDocument(); expect(screen.queryByTestId('referral-statistics')).toBeInTheDocument();
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual( expect(screen.queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referee' 'referee'
); );
}); });
}); });
it('displays eligibility warning when the set is no longer valid due to the referrers stake', async () => { it('displays eligibility warning when the set is no longer valid due to the referrers stake', async () => {
const { queryByTestId } = render( renderComponent([
<MemoryRouter> programMock,
<MockedProvider noReferralSetAsReferrerMock,
mocks={[ referralSetAsRefereeMock,
programMock, nonEligibleStakeAvailableMock,
noReferralSetAsReferrerMock, refereesMock,
referralSetAsRefereeMock, ]);
nonEligibleStakeAvailableMock,
refereesMock,
]}
showWarnings={false}
>
<ReferralStatistics />
</MockedProvider>
</MemoryRouter>
);
await waitFor(() => { await waitFor(() => {
expect( expect(
queryByTestId('referral-create-code-form') screen.queryByTestId('referral-create-code-form')
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
expect(queryByTestId('referral-statistics')).toBeInTheDocument(); expect(screen.queryByTestId('referral-statistics')).toBeInTheDocument();
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual( expect(screen.queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referee' 'referee'
); );
expect(queryByTestId('referral-eligibility-warning')).toBeInTheDocument(); expect(
expect(queryByTestId('referral-apply-code-form')).toBeInTheDocument(); screen.queryByTestId('referral-eligibility-warning')
).toBeInTheDocument();
expect(
screen.queryByTestId('referral-apply-code-form')
).toBeInTheDocument();
}); });
}); });
}); });
@@ -1,40 +1,42 @@
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
import BigNumber from 'bignumber.js';
import minBy from 'lodash/minBy'; import minBy from 'lodash/minBy';
import { CodeTile, StatTile } from './tile'; import sortBy from 'lodash/sortBy';
import compact from 'lodash/compact';
import { Trans } from 'react-i18next';
import classNames from 'classnames';
import { import {
VegaIcon, VegaIcon,
VegaIconNames, VegaIconNames,
truncateMiddle, truncateMiddle,
TextChildrenTooltip as Tooltip, TextChildrenTooltip as Tooltip,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet } from '@vegaprotocol/wallet'; import { useVegaWallet } from '@vegaprotocol/wallet';
import {
addDecimalsFormatNumber,
formatNumber,
getDateFormat,
getDateTimeFormat,
getUserLocale,
removePaginationWrapper,
} from '@vegaprotocol/utils';
import { useReferralSetStatsQuery } from './hooks/__generated__/ReferralSetStats';
import { useStakeAvailable } from '../../lib/hooks/use-stake-available';
import { useT, ns } from '../../lib/use-t';
import { useTeam } from '../../lib/hooks/use-team';
import { TeamAvatar } from '../../components/competitions/team-avatar';
import { TeamStats } from '../../components/competitions/team-stats';
import { Table } from '../../components/table';
import { import {
DEFAULT_AGGREGATION_DAYS, DEFAULT_AGGREGATION_DAYS,
useReferral, useReferral,
useUpdateReferees, useUpdateReferees,
} from './hooks/use-referral'; } from './hooks/use-referral';
import classNames from 'classnames';
import { Table } from '../../components/table';
import {
addDecimalsFormatNumber,
getDateFormat,
getDateTimeFormat,
getNumberFormat,
getUserLocale,
removePaginationWrapper,
} from '@vegaprotocol/utils';
import { useReferralSetStatsQuery } from './hooks/__generated__/ReferralSetStats';
import compact from 'lodash/compact';
import { useReferralProgram } from './hooks/use-referral-program';
import { useStakeAvailable } from './hooks/use-stake-available';
import sortBy from 'lodash/sortBy';
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
import BigNumber from 'bignumber.js';
import { useT, ns } from '../../lib/use-t';
import { Trans } from 'react-i18next';
import { ApplyCodeForm, ApplyCodeFormContainer } from './apply-code-form'; import { ApplyCodeForm, ApplyCodeFormContainer } from './apply-code-form';
import { useReferralProgram } from './hooks/use-referral-program';
import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
import { QUSDTooltip } from './qusd-tooltip'; import { QUSDTooltip } from './qusd-tooltip';
import { CodeTile, StatTile, Tile } from './tile';
export const ReferralStatistics = () => { export const ReferralStatistics = () => {
const { pubKey } = useVegaWallet(); const { pubKey } = useVegaWallet();
@@ -192,10 +194,7 @@ export const Statistics = ({
nextBenefitTierEpochsValue, nextBenefitTierEpochsValue,
} = useStats({ data, program }); } = useStats({ data, program });
const isApplyCodePreview = useMemo( const isApplyCodePreview = data.referee === null;
() => data.referee === null,
[data.referee]
);
const { benefitTiers } = useReferralProgram(); const { benefitTiers } = useReferralProgram();
@@ -324,27 +323,10 @@ export const Statistics = ({
} }
description={<QUSDTooltip />} description={<QUSDTooltip />}
> >
{getNumberFormat(0).format(Number(totalCommissionValue))} {formatNumber(totalCommissionValue, 0)}
</StatTile> </StatTile>
); );
const referrerTiles = (
<>
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
{baseCommissionTile}
{stakingMultiplierTile}
{finalCommissionTile}
</div>
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
{codeTile}
{referrerVolumeTile}
{numberOfTradersTile}
{totalCommissionTile}
</div>
</>
);
const currentBenefitTierTile = ( const currentBenefitTierTile = (
<StatTile <StatTile
title={t('Current tier')} title={t('Current tier')}
@@ -416,8 +398,41 @@ export const Statistics = ({
</StatTile> </StatTile>
); );
const eligibilityWarningOverlay = as === 'referee' && !isEligible && (
<div
data-testid="referral-eligibility-warning"
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center w-1/2 lg:w-1/3"
>
<h2 className="text-2xl mb-2">{t('Referral code no longer valid')}</h2>
<p>
{t(
'Your referral code is no longer valid as the referrer no longer meets the minimum requirements. Apply a new code to continue receiving discounts.'
)}
</p>
</div>
);
const referrerTiles = (
<>
<Team teamId={data.code} />
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
{baseCommissionTile}
{stakingMultiplierTile}
{finalCommissionTile}
</div>
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
{codeTile}
{referrerVolumeTile}
{numberOfTradersTile}
{totalCommissionTile}
</div>
</>
);
const refereeTiles = ( const refereeTiles = (
<> <>
<Team teamId={data.code} />
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3"> <div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
{currentBenefitTierTile} {currentBenefitTierTile}
{runningVolumeTile} {runningVolumeTile}
@@ -432,20 +447,6 @@ export const Statistics = ({
</> </>
); );
const eligibilityWarning = as === 'referee' && !isEligible && (
<div
data-testid="referral-eligibility-warning"
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center w-1/2 lg:w-1/3"
>
<h2 className="text-2xl mb-2">{t('Referral code no longer valid')}</h2>
<p>
{t(
'Your referral code is no longer valid as the referrer no longer meets the minimum requirements. Apply a new code to continue receiving discounts.'
)}
</p>
</div>
);
return ( return (
<div <div
data-testid="referral-statistics" data-testid="referral-statistics"
@@ -460,8 +461,7 @@ export const Statistics = ({
{as === 'referrer' && referrerTiles} {as === 'referrer' && referrerTiles}
{as === 'referee' && refereeTiles} {as === 'referee' && refereeTiles}
</div> </div>
{eligibilityWarningOverlay}
{eligibilityWarning}
</div> </div>
); );
}; };
@@ -563,8 +563,8 @@ export const RefereesTable = ({
) )
.map((r) => ({ .map((r) => ({
...r, ...r,
volume: getNumberFormat(0).format(r.volume), volume: formatNumber(r.volume, 0),
commission: getNumberFormat(0).format(r.commission), commission: formatNumber(r.commission, 0),
})) }))
.reverse()} .reverse()}
/> />
@@ -574,3 +574,19 @@ export const RefereesTable = ({
</> </>
); );
}; };
const Team = ({ teamId }: { teamId?: string }) => {
const { team, games, members } = useTeam(teamId);
if (!team) return null;
return (
<Tile className="flex gap-3 lg:gap-4">
<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} />
</div>
</Tile>
);
};
@@ -13,11 +13,9 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
import { useReferral } from './hooks/use-referral'; import { useReferral } from './hooks/use-referral';
import { REFERRAL_DOCS_LINK } from './constants'; import { REFERRAL_DOCS_LINK } from './constants';
import classNames from 'classnames'; import classNames from 'classnames';
import { usePageTitleStore } from '../../stores';
import { useEffect } from 'react';
import { titlefy } from '@vegaprotocol/utils';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { ErrorBoundary } from '../../components/error-boundary'; import { ErrorBoundary } from '../../components/error-boundary';
import { usePageTitle } from '../../lib/hooks/use-page-title';
const Nav = () => { const Nav = () => {
const t = useT(); const t = useT();
@@ -57,13 +55,7 @@ export const Referrals = () => {
const loading = refereeLoading || referrerLoading; const loading = refereeLoading || referrerLoading;
const showNav = !loading && !error && !referrer && !referee; const showNav = !loading && !error && !referrer && !referee;
const { updateTitle } = usePageTitleStore((store) => ({ usePageTitle(t('Referrals'));
updateTitle: store.updateTitle,
}));
useEffect(() => {
updateTitle(titlefy([t('Referrals')]));
}, [updateTitle, t]);
return ( return (
<ErrorBoundary feature="referrals"> <ErrorBoundary feature="referrals">
+5 -11
View File
@@ -1,24 +1,18 @@
import { useEffect } from 'react'; import { TinyScroll } from '@vegaprotocol/ui-toolkit';
import { titlefy } from '@vegaprotocol/utils';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { RewardsContainer } from '../../components/rewards-container'; import { RewardsContainer } from '../../components/rewards-container';
import { usePageTitleStore } from '../../stores';
import { ErrorBoundary } from '../../components/error-boundary'; import { ErrorBoundary } from '../../components/error-boundary';
import { TinyScroll } from '@vegaprotocol/ui-toolkit'; import { usePageTitle } from '../../lib/hooks/use-page-title';
export const Rewards = () => { export const Rewards = () => {
const t = useT(); const t = useT();
const title = t('Rewards'); const title = t('Rewards');
const { updateTitle } = usePageTitleStore((store) => ({ usePageTitle(title);
updateTitle: store.updateTitle,
}));
useEffect(() => {
updateTitle(titlefy([title]));
}, [updateTitle, title]);
return ( return (
<ErrorBoundary feature="rewards"> <ErrorBoundary feature="rewards">
<TinyScroll className="p-4 max-h-full overflow-auto"> <TinyScroll className="p-4 max-h-full overflow-auto">
<h1 className="px-4 pb-4 text-2xl">{title}</h1> <h1 className="md:px-4 pb-4 text-2xl">{title}</h1>
<RewardsContainer /> <RewardsContainer />
</TinyScroll> </TinyScroll>
</ErrorBoundary> </ErrorBoundary>
-1
View File
@@ -1 +0,0 @@
export { Teams } from './teams';
@@ -1,7 +0,0 @@
export const Teams = () => {
return (
<div>
<h1>Teams</h1>
</div>
);
};
@@ -0,0 +1,38 @@
import classNames from 'classnames';
import { type HTMLAttributes } from 'react';
export const BORDER_COLOR = 'border-vega-clight-500 dark:border-vega-cdark-500';
export const GRADIENT =
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
export const Box = ({
children,
backgroundImage,
...props
}: HTMLAttributes<HTMLDivElement> & { backgroundImage?: string }) => {
return (
<div
{...props}
className={classNames(
BORDER_COLOR,
GRADIENT,
'border rounded-lg',
'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>
);
};
@@ -0,0 +1,41 @@
import { Box } from './box';
import { type ComponentProps, type ReactElement, type ReactNode } from 'react';
import { DudeBadge } from './graphics/dude-badge';
export const CompetitionsActionsContainer = ({
children,
}: {
children:
| ReactElement<typeof CompetitionsAction>
| Iterable<ReactElement<typeof CompetitionsAction>>;
}) => (
<div
className="grid grid-cols-1 md:grid-cols-3 grid-rows-4'
gap-6 mb-12"
>
{children}
</div>
);
export const CompetitionsAction = ({
variant,
title,
description,
actionElement,
}: {
variant: ComponentProps<typeof DudeBadge>['variant'];
title: string;
description?: string;
actionElement: ReactNode;
}) => {
return (
<Box className="grid md:grid-rows-[subgrid] gap-6 row-span-4 text-center">
<div className="flex justify-center">
<DudeBadge variant={variant} />
</div>
<h2 className="text-2xl">{title}</h2>
{description && <p className="text-muted">{description}</p>}
<div className="flex justify-center">{actionElement}</div>
</Box>
);
};
@@ -0,0 +1,27 @@
import { AnimatedDudeWithWire } from '../../client-pages/referrals/graphics/dude';
import { type ReactNode } from 'react';
export const CompetitionsHeader = ({
title,
children,
}: {
title: string;
children?: ReactNode;
}) => {
return (
<div className="relative mb-4 lg:mb-20">
<div
aria-hidden
className="absolute top-20 right-[220px] md:right-[240px] max-sm:hidden"
>
<AnimatedDudeWithWire />
</div>
<div className="pt-6 lg:pt-20 sm:w-1/2">
<h1 className="text-3xl lg:text-6xl leading-[1em] font-alpha calt mb-2 lg:mb-10">
{title}
</h1>
{children}
</div>
</div>
);
};
@@ -0,0 +1,70 @@
import { Link } from 'react-router-dom';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '@vegaprotocol/utils';
import { type useTeams } from '../../lib/hooks/use-teams';
import { useT } from '../../lib/use-t';
import { Table } from '../table';
import { Rank } from './graphics/rank';
import { Links } from '../../lib/links';
import { TeamAvatar } from './team-avatar';
export const CompetitionsLeaderboard = ({
data,
}: {
data: ReturnType<typeof useTeams>['data'];
}) => {
const t = useT();
const num = (n?: number | string) => (!n ? '-' : formatNumber(n, 0));
if (!data || data.length === 0) {
return <Splash>{t('Could not find any teams')}</Splash>;
}
return (
<Table
columns={[
{ name: 'rank', displayName: '#' },
{ name: 'avatar', displayName: '' },
{ name: 'team', displayName: t('Team') },
{ name: 'earned', displayName: t('Rewards earned') },
{ name: 'games', displayName: t('Total games') },
{ name: 'status', displayName: t('Status') },
{ name: 'volume', displayName: t('Volume') },
]}
data={data.map((td) => {
// leaderboard place or medal
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" />;
const avatar = (
<TeamAvatar
teamId={td.teamId}
imgUrl={td.avatarUrl}
alt={td.name}
size="small"
/>
);
return {
rank,
avatar,
team: (
<Link
className="hover:underline"
to={Links.COMPETITIONS_TEAM(td.teamId)}
>
{td.name}
</Link>
),
earned: num(td.totalQuantumRewards),
games: num(td.totalGamesPlayed),
status: td.closed ? t('Closed') : t('Open'),
volume: num(td.totalQuantumVolume),
};
})}
/>
);
};
@@ -0,0 +1,76 @@
import { type TransferNode } from '@vegaprotocol/types';
import {
ActiveRewardCard,
isActiveReward,
} from '../rewards-container/active-rewards';
import { useT } from '../../lib/use-t';
import { useAssetsMapProvider } from '@vegaprotocol/assets';
import { useMarketsMapProvider } from '@vegaprotocol/markets';
export const GamesContainer = ({
data,
currentEpoch,
}: {
data: TransferNode[];
currentEpoch: number;
}) => {
const t = useT();
// Re-load markets and assets in the games container to ensure that the
// the cards are updated (not grayed out) when the user navigates to the games page
const { data: assets } = useAssetsMapProvider();
const { data: markets } = useMarketsMapProvider();
const enrichedTransfers = data
.filter((node) => isActiveReward(node, currentEpoch))
.map((node) => {
if (node.transfer.kind.__typename !== 'RecurringTransfer') {
return node;
}
const asset =
assets &&
assets[
node.transfer.kind.dispatchStrategy?.dispatchMetricAssetId || ''
];
const marketsInScope =
node.transfer.kind.dispatchStrategy?.marketIdsInScope?.map(
(id) => markets && markets[id]
);
return { ...node, asset, markets: marketsInScope };
});
if (!enrichedTransfers || !enrichedTransfers.length) return null;
if (!enrichedTransfers || enrichedTransfers.length === 0) {
return (
<p className="mb-6 text-muted">
{t('There are currently no games available.')}
</p>
);
}
return (
<div className="mb-12 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{enrichedTransfers.map((game, i) => {
// TODO: Remove `kind` prop from ActiveRewardCard
const { transfer } = game;
if (
transfer.kind.__typename !== 'RecurringTransfer' ||
!transfer.kind.dispatchStrategy?.dispatchMetric
) {
return null;
}
return (
<ActiveRewardCard
key={i}
transferNode={game}
currentEpoch={currentEpoch}
kind={transfer.kind}
/>
);
})}
</div>
);
};
@@ -0,0 +1,40 @@
import classNames from 'classnames';
import { DudeWithFlag } from './dude-with-flag';
/**
* Pre-defined badge gradients
*/
export const BADGE_GRADIENT_VARIANT_A =
'bg-gradient-to-r from-vega-blue-500 via-vega-purple-500 to-vega-pink-500';
export const BADGE_GRADIENT_VARIANT_B =
'bg-gradient-to-r from-vega-purple-500 via-vega-green-500 to-vega-blue-500';
export const BADGE_GRADIENT_VARIANT_C =
'bg-gradient-to-r from-vega-blue-500 via-vega-purple-500 to-vega-green-500';
/** Badge */
export const DudeBadge = ({
variant,
className,
}: {
variant: 'A' | 'B' | 'C' | undefined;
className?: classNames.Argument;
}) => {
return (
<div
className={classNames(
'w-24 h-24 rounded-full bg-black relative',
'rotate-12',
{
[BADGE_GRADIENT_VARIANT_A]: variant === 'A',
[BADGE_GRADIENT_VARIANT_B]: variant === 'B',
[BADGE_GRADIENT_VARIANT_C]: variant === 'C',
},
className
)}
>
<DudeWithFlag className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 -rotate-12" />
</div>
);
};
@@ -0,0 +1,50 @@
import { theme } from '@vegaprotocol/tailwindcss-config';
type DudeWithFlagProps = {
flagColor?: string;
withStar?: boolean;
className?: string;
};
const DEFAULT_FLAG_COLOR = theme.colors.vega.green[500];
export const DudeWithFlag = ({
flagColor = DEFAULT_FLAG_COLOR,
withStar = true,
className,
}: DudeWithFlagProps) => {
return (
<svg
width="49"
height="43"
viewBox="0 0 49 43"
fill="none"
className={className}
>
{withStar && (
<>
<path d="M3.99992 0H2V1.99993H3.99992V0Z" fill="white" />
<path
d="M2 1.99993L0 1.99981V3.99974H1.99992L2 1.99993Z"
fill="white"
/>
<path
d="M3.99995 3.99992L1.99992 3.99974L2 5.99988H3.99995V3.99992Z"
fill="white"
/>
<path
d="M5.99997 1.99981L3.99992 1.99993L3.99995 3.99992L5.99997 3.99974V1.99981Z"
fill="white"
/>
</>
)}
<path
d="M32 4H11V33H15V43H20V33H23V43H28V33H32V4ZM20 17H15V12H20V17ZM28 17H23V12H28V17Z"
fill="white"
/>
<path d="M41 25L32 25L32 20L41 20L41 25Z" fill="white" />
<path d="M36 29V4H35V29" fill="white" />
<path d="M36 13H49L44.55 8.5L49 4H36V13Z" fill={flagColor} />
</svg>
);
};
@@ -0,0 +1,110 @@
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import classNames from 'classnames';
export const Rank = ({
variant,
className,
}: {
variant?: 'gold' | 'silver' | 'bronze';
className?: classNames.Argument;
}) => {
const { theme } = useThemeSwitcher();
return (
<div
title={classNames({
'1': variant === 'gold',
'2': variant === 'silver',
'3': variant === 'bronze',
})}
className={classNames(
{
'text-yellow-300': variant === 'gold',
'text-vega-clight-500': variant === 'silver',
'text-vega-orange-500': variant === 'bronze',
'text-black dark:text-white': variant === undefined,
},
className
)}
>
<svg width="18" height="30" viewBox="0 0 18 30" fill="none">
<defs>
<linearGradient x1="0" y1="0" x2="100%" y2="100%" id="medal">
<stop offset="33%" stopColor="transparent" />
<stop offset="100%" stopColor="black" stopOpacity="50%" />
</linearGradient>
<clipPath id="shape">
<path d="M2 2H4V4H2V2Z" />
<path d="M2 2H4V4H2V2Z" />
<path d="M2 2H6V4H2V2Z" />
<path d="M2 2H6V4H2V2Z" />
<path d="M0 4H4V6H0V4Z" />
<path d="M0 4H4V6H0V4Z" />
<path d="M4 0H14V2H4V0Z" />
<path d="M4 0H14V2H4V0Z" />
<path d="M0 14V4H2V14H0Z" />
<path d="M0 14V4H2V14H0Z" />
<path d="M2 30L2 18H4L4 30H2Z" />
<path d="M2 30L2 18H4L4 30H2Z" />
<path d="M14 30L14 18H16L16 30H14Z" />
<path d="M14 30L14 18H16L16 30H14Z" />
<path d="M16 14L16 4H18V14H16Z" />
<path d="M16 14L16 4H18V14H16Z" />
<path d="M2 6V2H4V6H2Z" />
<path d="M2 6V2H4V6H2Z" />
<path d="M16 2V6H14V2H16Z" />
<path d="M16 2V6H14V2H16Z" />
<path d="M12 2H16L16 4H12V2Z" />
<path d="M12 2H16L16 4H12V2Z" />
<path d="M14 4H18V6H14V4Z" />
<path d="M14 4H18V6H14V4Z" />
<path d="M16 16H12V14H16V16Z" />
<path d="M16 16H12V14H16V16Z" />
<path d="M14 18H4L4 16L14 16L14 18Z" />
<path d="M14 18H4L4 16L14 16L14 18Z" />
<path d="M16 12V16H14V12H16Z" />
<path d="M16 12V16H14V12H16Z" />
<path d="M6 16H2V14H6V16Z" />
<path d="M6 16H2V14H6V16Z" />
<path d="M6 28H4L4 26H6V28Z" />
<path d="M6 28H4L4 26H6V28Z" />
<path d="M8 26H6L6 24H8V26Z" />
<path d="M8 26H6L6 24H8V26Z" />
<path d="M10 24H8V22H10V24Z" />
<path d="M10 24H8V22H10V24Z" />
<path d="M12 26H10L10 24H12V26Z" />
<path d="M12 26H10L10 24H12V26Z" />
<path d="M14 28H12L12 26H14V28Z" />
<path d="M14 28H12L12 26H14V28Z" />
<path d="M4 14H0L2.04189e-07 12H4V14Z" />
<path d="M4 14H0L2.04189e-07 12H4V14Z" />
<path d="M6 4H12V14H6V4Z" />
<path d="M6 4H12V14H6V4Z" />
<path d="M4 6H14V12H4V6Z" />
<path d="M4 6H14V12H4V6Z" />
</clipPath>
</defs>
<rect
rx="0"
ry="0"
width="18"
height="30"
fill="currentColor"
clipPath="url(#shape)"
/>
<rect
rx="0"
ry="0"
width="18"
height="30"
fill="url(#medal)"
clipPath="url(#shape)"
style={{ mixBlendMode: theme === 'dark' ? 'darken' : 'overlay' }}
/>
<g style={{ mixBlendMode: 'overlay' }}>
<path d="M10.5 6H8.5V8H10.5V6Z" fill="white" />
<path d="M12.5 8H10.5V10H12.5V8Z" fill="white" />
</g>
</svg>
</div>
);
};
@@ -0,0 +1,63 @@
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';
export const getFallbackAvatar = (teamId: string) => {
const avatarId = ((parseInt(teamId, 16) % NUM_AVATARS) + 1)
.toString()
.padStart(2, '0'); // between 01 - 20
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,
alt,
size = 'large',
}: {
teamId: string;
imgUrl: string;
alt?: string;
size?: 'large' | 'small';
}) => {
const img = useAvatar(teamId, imgUrl);
return (
// eslint-disable-next-line @next/next/no-img-element
<img
src={img}
alt={alt || 'Team avatar'}
className={classNames(
'rounded-full bg-vega-clight-700 dark:bg-vega-cdark-700 shrink-0',
{
'w-20 h-20 lg:w-[112px] lg:h-[112px]': size === 'large',
'w-10 h-10': size === 'small',
}
)}
referrerPolicy="no-referrer"
/>
);
};
@@ -0,0 +1,154 @@
import { type TeamGame, type TeamStats } from '../../lib/hooks/use-team';
import { type TeamsFieldsFragment } from '../../lib/hooks/__generated__/Teams';
import { TeamAvatar, getFallbackAvatar } from './team-avatar';
import { FavoriteGame, Stat } from './team-stats';
import { useT } from '../../lib/use-t';
import { formatNumberRounded } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
import { Box } from './box';
import { Intent, Tooltip, TradingAnchorButton } from '@vegaprotocol/ui-toolkit';
import { Links } from '../../lib/links';
import orderBy from 'lodash/orderBy';
import { take } from 'lodash';
import { DispatchMetricLabels } from '@vegaprotocol/types';
import classNames from 'classnames';
import { UpdateTeamButton } from '../../client-pages/competitions/update-team-button';
export const TeamCard = ({
rank,
team,
stats,
games,
}: {
rank: number;
team: TeamsFieldsFragment;
stats?: TeamStats;
games?: TeamGame[];
}) => {
const t = useT();
const lastGames = take(
orderBy(
games?.map((g) => ({
rank: g.team.rank,
metric: g.team.rewardMetric,
epoch: g.epoch,
})),
(i) => i.epoch,
'desc'
),
5
);
return (
<div
className={classNames(
'gap-6 grid grid-cols-1 grid-rows-1',
'md:grid-cols-3'
)}
>
{/** Card */}
<Box
backgroundImage={team.avatarUrl || getFallbackAvatar(team.teamId)}
className="flex flex-col items-center gap-3 min-w-[80px] lg:min-w-[112px]"
>
<TeamAvatar teamId={team.teamId} imgUrl={team.avatarUrl} />
<h1 className="calt lg:text-2xl" data-testid="team-name">
{team.name}
</h1>
{games && <FavoriteGame games={games} noLabel />}
<TradingAnchorButton
size="extra-small"
intent={Intent.Primary}
href={Links.COMPETITIONS_TEAM(team.teamId)}
>
{t('Profile')}
</TradingAnchorButton>
<UpdateTeamButton team={team} size="extra-small" />
</Box>
{/** Tiles */}
<Box className="w-full md:col-span-2">
<div
className={classNames(
'grid gap-3 w-full mb-4',
'md:grid-cols-3 md:grid-rows-2',
'grid-cols-2 grid-rows-3'
)}
>
<Stat
className="flex flex-col-reverse"
value={rank}
label={t('Rank')}
valueTestId="team-rank"
/>
<Stat
className="flex flex-col-reverse"
value={team.totalMembers || 0}
label={t('Members')}
valueTestId="members-count-stat"
/>
<Stat
className="flex flex-col-reverse"
value={stats?.totalGamesPlayed || 0}
label={t('Total games')}
valueTestId="total-games-stat"
/>
<Stat
className="flex flex-col-reverse"
value={
stats?.totalQuantumVolume
? formatNumberRounded(
new BigNumber(stats.totalQuantumVolume || 0),
'1e3'
)
: 0
}
label={t('Total volume')}
valueTestId="total-volume-stat"
/>
<Stat
className="flex flex-col-reverse"
value={
stats?.totalQuantumRewards
? formatNumberRounded(
new BigNumber(stats.totalQuantumRewards || 0),
'1e3'
)
: 0
}
label={t('Rewards paid out')}
valueTestId="rewards-paid-stat"
/>
</div>
<dl className="w-full pt-4 border-t border-vega-clight-700 dark:border-vega-cdark-700">
<dt className="mb-1 text-sm text-muted">
{t('Last {{games}} games result', {
replace: { games: lastGames.length || '' },
})}
</dt>
<dd className="flex flex-row flex-wrap gap-2">
{lastGames.length === 0 && t('None available')}
{lastGames.map((game, i) => (
<Tooltip key={i} description={DispatchMetricLabels[game.metric]}>
<button className="cursor-help text-sm bg-vega-clight-700 dark:bg-vega-cdark-700 px-2 py-1 rounded-full">
<RankLabel rank={game.rank} />
</button>
</Tooltip>
))}
</dd>
</dl>
</Box>
</div>
);
};
/**
* Sets the english ordinal for given rank only if the current language is set
* to english.
*/
const RankLabel = ({ rank }: { rank: number }) => {
const t = useT();
return t('place', { count: rank, ordinal: true });
};
@@ -0,0 +1,210 @@
import { type ReactNode } from 'react';
import BigNumber from 'bignumber.js';
import countBy from 'lodash/countBy';
import {
Pill,
VegaIcon,
VegaIconNames,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
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';
export const TeamStats = ({
stats,
members,
games,
}: {
stats?: ITeamStats;
members?: Member[];
games?: TeamGame[];
}) => {
const t = useT();
return (
<>
<StatSection>
<StatList>
<Stat
value={members ? members.length : 0}
label={t('Members')}
valueTestId="members-count-stat"
/>
<Stat
value={stats ? stats.totalGamesPlayed : 0}
label={t('Total games')}
tooltip={t('Total number of games this team has participated in')}
valueTestId="total-games-stat"
/>
<StatSectionSeparator />
<Stat
value={
stats
? formatNumberRounded(
new BigNumber(stats.totalQuantumVolume || 0),
'1e3'
)
: 0
}
label={t('Total volume')}
valueTestId="total-volume-stat"
/>
<Stat
value={
stats
? formatNumberRounded(
new BigNumber(stats.totalQuantumRewards || 0),
'1e3'
)
: 0
}
label={t('Rewards paid out')}
tooltip={'Total amount of rewards paid out to this team in qUSD'}
valueTestId="rewards-paid-stat"
/>
</StatList>
</StatSection>
{games && games.length ? (
<StatSection>
<FavoriteGame games={games} />
<StatSectionSeparator />
<LatestResults games={games} />
</StatSection>
) : null}
</>
);
};
const LatestResults = ({ games }: { games: TeamGame[] }) => {
const t = useT();
const latestGames = games.slice(0, 5);
return (
<dl className="flex flex-col gap-1">
<dt className="text-muted text-sm">
{t('gameCount', { count: latestGames.length })}
</dt>
<dd className="flex gap-1">
{latestGames.map((game) => {
return (
<Pill key={game.id} className="text-sm">
{t('place', { count: game.team.rank, ordinal: true })}
</Pill>
);
})}
</dd>
</dl>
);
};
export const FavoriteGame = ({
games,
noLabel = false,
}: {
games: TeamGame[];
noLabel?: boolean;
}) => {
const t = useT();
const rewardMetrics = games.map(
(game) => game.team.rewardMetric as DispatchMetric
);
const count = countBy(rewardMetrics);
let favoriteMetric = '';
let mostOccurances = 0;
for (const key in count) {
if (count[key] > mostOccurances) {
favoriteMetric = key;
mostOccurances = count[key];
}
}
if (!favoriteMetric) return null;
// rewardMetric is a string, should be typed as DispatchMetric
const favoriteMetricLabel =
DispatchMetricLabels[favoriteMetric as DispatchMetric];
return (
<dl className="flex flex-col gap-1">
<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
name={VegaIconNames.STAR}
className="text-vega-yellow-400 relative top-[-1px]"
/>{' '}
{favoriteMetricLabel}
</Pill>
</dd>
</dl>
);
};
export const StatSection = ({ children }: { children: ReactNode }) => {
return (
<section className="flex flex-col lg:flex-row gap-4 lg:gap-8">
{children}
</section>
);
};
export const StatSectionSeparator = () => {
return <div className="hidden md:block border-r border-default" />;
};
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}
</dl>
);
};
export const Stat = ({
value,
label,
tooltip,
valueTestId,
className,
}: {
value: ReactNode;
label: ReactNode;
tooltip?: string;
valueTestId?: string;
className?: classNames.Argument;
}) => {
return (
<div className={classNames(className)}>
<dd className="text-3xl lg:text-4xl" data-testid={valueTestId}>
{value}
</dd>
<dt className="text-sm text-muted">
{tooltip ? (
<Tooltip description={tooltip} underline={false}>
<span className="flex items-center gap-2">
{label}
<VegaIcon name={VegaIconNames.INFO} size={12} />
</span>
</Tooltip>
) : (
label
)}
</dt>
</div>
);
};
@@ -0,0 +1,2 @@
export { LayoutWithSky } from './layout-with-sky';
export { LayoutWithGradient } from './layout-with-gradient';
@@ -0,0 +1,14 @@
import { type ReactNode } from 'react';
export const LayoutWithGradient = ({ children }: { children: ReactNode }) => {
return (
<div className="relative h-full pt-5 overflow-y-auto">
<div className="absolute top-0 left-0 w-full h-[40%] -z-10 bg-[40%_0px] bg-cover bg-no-repeat bg-local bg-[url(/cover.png)]">
<div className="absolute top-o left-0 w-full h-full bg-gradient-to-t from-white dark:from-vega-cdark-900 to-transparent from-20% to-60%" />
</div>
<div className="flex flex-col gap-4 lg:gap-6 container p-4 mx-auto">
{children}
</div>
</div>
);
};
@@ -1,10 +1,12 @@
import classNames from 'classnames'; import classNames from 'classnames';
import type { HTMLAttributes } from 'react'; import type { HTMLAttributes } from 'react';
import { SKY_BACKGROUND } from './constants';
import { Outlet } from 'react-router-dom'; import { Outlet } from 'react-router-dom';
import { TinyScroll } from '@vegaprotocol/ui-toolkit'; import { TinyScroll } from '@vegaprotocol/ui-toolkit';
export const Layout = ({ export const SKY_BACKGROUND =
'bg-[url(/sky-light.png)] dark:bg-[url(/sky-dark.png)] bg-[37%_0px] bg-[length:1440px] bg-no-repeat bg-local';
const Layout = ({
className, className,
children, children,
...props ...props
+2 -1
View File
@@ -1 +1,2 @@
export * from './layout-with-sidebar'; export { LayoutWithSidebar } from './layout-with-sidebar';
export { LayoutCentered } from './layout-centered';
@@ -3,7 +3,6 @@ import { Outlet } from 'react-router-dom';
import { Sidebar, SidebarContent, useSidebar } from '../sidebar'; import { Sidebar, SidebarContent, useSidebar } from '../sidebar';
import classNames from 'classnames'; import classNames from 'classnames';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id'; import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
export const LayoutWithSidebar = ({ export const LayoutWithSidebar = ({
header, header,
sidebar, sidebar,
@@ -17,7 +16,7 @@ export const LayoutWithSidebar = ({
const sidebarOpen = sidebarView !== null; const sidebarOpen = sidebarView !== null;
const gridClasses = classNames( const gridClasses = classNames(
'h-full relative z-0 grid', 'h-full relative z-0 grid',
'grid-rows-[min-content_1fr_40px]', 'grid-rows-[min-content_1fr_50px]',
'lg:grid-rows-[min-content_1fr]', 'lg:grid-rows-[min-content_1fr]',
'lg:grid-cols-[1fr_280px_40px]', 'lg:grid-cols-[1fr_280px_40px]',
'xxxl:grid-cols-[1fr_320px_40px]' 'xxxl:grid-cols-[1fr_320px_40px]'
@@ -27,10 +26,13 @@ export const LayoutWithSidebar = ({
<div className={gridClasses}> <div className={gridClasses}>
<div className="col-span-full">{header}</div> <div className="col-span-full">{header}</div>
<main <main
className={classNames('col-start-1 col-end-1 overflow-y-auto', { className={classNames(
'lg:col-end-3': !sidebarOpen, 'col-start-1 col-end-1 overflow-hidden lg:overflow-y-auto grow lg:grow-0',
'hidden lg:block lg:col-end-2': sidebarOpen, {
})} 'lg:col-end-3': !sidebarOpen,
'hidden lg:block lg:col-end-2': sidebarOpen,
}
)}
> >
<Outlet /> <Outlet />
</main> </main>
@@ -1 +1,2 @@
export * from './market-header'; export * from './market-header';
export * from './mobile-market-header';
@@ -0,0 +1,133 @@
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { MarketSelector } from '../market-selector';
import {
Last24hPriceChange,
useMarket,
useMarketList,
} from '@vegaprotocol/markets';
import { useParams } from 'react-router-dom';
import * as PopoverPrimitive from '@radix-ui/react-popover';
import { useState } from 'react';
import { useT } from '../../lib/use-t';
import classNames from 'classnames';
import { MarketHeaderStats } from '../../client-pages/market/market-header-stats';
import { MarketMarkPrice } from '../market-mark-price';
/**
* This is only rendered for the mobile navigation
*/
export const MobileMarketHeader = () => {
const t = useT();
const { marketId } = useParams();
const { data } = useMarket(marketId);
const [openMarket, setOpenMarket] = useState(false);
const [openPrice, setOpenPrice] = useState(false);
// Ensure that markets are kept cached so opening the list
// shows all markets instantly
useMarketList();
if (!marketId) return null;
return (
<div className="pl-3 pr-2 flex justify-between gap-2 h-10 bg-vega-clight-700 dark:bg-vega-cdark-700">
<FullScreenPopover
open={openMarket}
onOpenChange={(x) => {
setOpenMarket(x);
}}
trigger={
<h1 className="flex gap-1 sm:gap-2 md:gap-4 items-center text-base leading-3 md:text-lg whitespace-nowrap">
{data
? data.tradableInstrument.instrument.code
: t('Select market')}
<span
className={classNames(
'transition-transform ease-in-out duration-300 flex',
{
'rotate-180': openMarket,
}
)}
>
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={16} />
</span>
</h1>
}
>
<MarketSelector
currentMarketId={marketId}
onSelect={() => setOpenMarket(false)}
/>
</FullScreenPopover>
<FullScreenPopover
open={openPrice}
onOpenChange={(x) => {
setOpenPrice(x);
}}
trigger={
<span className="flex gap-2 items-end md:text-md whitespace-nowrap leading-3">
{data && (
<>
<span className="text-xs">
<Last24hPriceChange
marketId={data.id}
decimalPlaces={data.decimalPlaces}
/>
</span>
<span className="flex items-center gap-1">
<MarketMarkPrice
marketId={data.id}
decimalPlaces={data.decimalPlaces}
/>
<VegaIcon
name={VegaIconNames.CHEVRON_DOWN}
size={16}
className={classNames(
'transition-transform ease-in-out duration-300',
{
'rotate-180': openPrice,
}
)}
/>
</span>
</>
)}
</span>
}
>
{data && (
<div className="px-3 py-6 text-sm grid grid-cols-2 items-center gap-x-4 gap-y-6">
<MarketHeaderStats market={data} />
</div>
)}
</FullScreenPopover>
</div>
);
};
export interface PopoverProps extends PopoverPrimitive.PopoverProps {
trigger: React.ReactNode | string;
}
export const FullScreenPopover = ({
trigger,
children,
open,
onOpenChange,
}: PopoverProps) => {
return (
<PopoverPrimitive.Root open={open} onOpenChange={onOpenChange}>
<PopoverPrimitive.Trigger data-testid="popover-trigger">
{trigger}
</PopoverPrimitive.Trigger>
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-testid="popover-content"
className="w-screen bg-vega-clight-800 dark:bg-vega-cdark-800 border-y border-default"
sideOffset={0}
>
{children}
</PopoverPrimitive.Content>
</PopoverPrimitive.Portal>
</PopoverPrimitive.Root>
);
};
-1
View File
@@ -1,2 +1 @@
export * from './navbar'; export * from './navbar';
export * from './nav-header';
@@ -1,81 +0,0 @@
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { MarketSelector } from '../market-selector';
import { useMarket, useMarketList } from '@vegaprotocol/markets';
import { useParams } from 'react-router-dom';
import * as PopoverPrimitive from '@radix-ui/react-popover';
import { useState } from 'react';
import { useT } from '../../lib/use-t';
import classNames from 'classnames';
/**
* This is only rendered for the mobile navigation
*/
export const NavHeader = () => {
const t = useT();
const { marketId } = useParams();
const { data } = useMarket(marketId);
const [open, setOpen] = useState(false);
// Ensure that markets are kept cached so opening the list
// shows all markets instantly
useMarketList();
if (!marketId) return null;
return (
<FullScreenPopover
open={open}
onOpenChange={(x) => {
setOpen(x);
}}
trigger={
<h1 className="flex gap-1 sm:gap-2 md:gap-4 items-center text-default text-lg whitespace-nowrap xl:pr-4 xl:border-r border-default">
{data ? data.tradableInstrument.instrument.code : t('Select market')}
<span
className={classNames(
'transition-transform ease-in-out duration-300',
{
'rotate-180': open,
}
)}
>
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={20} />
</span>
</h1>
}
>
<MarketSelector
currentMarketId={marketId}
onSelect={() => setOpen(false)}
/>
</FullScreenPopover>
);
};
export interface PopoverProps extends PopoverPrimitive.PopoverProps {
trigger: React.ReactNode | string;
}
export const FullScreenPopover = ({
trigger,
children,
open,
onOpenChange,
}: PopoverProps) => {
return (
<PopoverPrimitive.Root open={open} onOpenChange={onOpenChange}>
<PopoverPrimitive.Trigger data-testid="popover-trigger">
{trigger}
</PopoverPrimitive.Trigger>
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-testid="popover-content"
className="w-screen bg-vega-clight-800 dark:bg-vega-cdark-800 text-default border border-default"
sideOffset={5}
>
{children}
</PopoverPrimitive.Content>
</PopoverPrimitive.Portal>
</PopoverPrimitive.Root>
);
};
+8 -9
View File
@@ -34,13 +34,7 @@ import { supportedLngs } from '../../lib/i18n';
type MenuState = 'wallet' | 'nav' | null; type MenuState = 'wallet' | 'nav' | null;
type Theme = 'system' | 'yellow'; type Theme = 'system' | 'yellow';
export const Navbar = ({ export const Navbar = ({ theme = 'system' }: { theme?: Theme }) => {
children,
theme = 'system',
}: {
children?: ReactNode;
theme?: Theme;
}) => {
const i18n = useI18n(); const i18n = useI18n();
const t = useT(); const t = useT();
// menu state for small screens // menu state for small screens
@@ -75,8 +69,6 @@ export const Navbar = ({
> >
<VLogo className="w-4" /> <VLogo className="w-4" />
</NavLink> </NavLink>
{/* Left section */}
<div className="flex items-center lg:hidden">{children}</div>
{/* Used to show header in nav on mobile */} {/* Used to show header in nav on mobile */}
<div className="hidden lg:block"> <div className="hidden lg:block">
<NavbarMenu onClick={() => setMenu(null)} /> <NavbarMenu onClick={() => setMenu(null)} />
@@ -204,6 +196,13 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
{t('Portfolio')} {t('Portfolio')}
</NavbarLink> </NavbarLink>
</NavbarItem> </NavbarItem>
{featureFlags.TEAM_COMPETITION && (
<NavbarItem>
<NavbarLink to={Links.COMPETITIONS()} onClick={onClick}>
{t('Competitions')}
</NavbarLink>
</NavbarItem>
)}
{featureFlags.REFERRALS && ( {featureFlags.REFERRALS && (
<NavbarItem> <NavbarItem>
<NavbarLink end={false} to={Links.REFERRALS()} onClick={onClick}> <NavbarLink end={false} to={Links.REFERRALS()} onClick={onClick}>
@@ -0,0 +1 @@
export { RainbowButton } from './rainbow-button';
@@ -0,0 +1,35 @@
import classNames from 'classnames';
import { type ButtonHTMLAttributes } from 'react';
type RainbowButtonProps = {
variant?: 'full' | 'border';
};
export const RainbowButton = ({
variant = 'full',
children,
className,
...props
}: RainbowButtonProps & ButtonHTMLAttributes<HTMLButtonElement>) => (
<button
className={classNames(
'bg-rainbow rounded-lg overflow-hidden disabled:opacity-40',
'hover:bg-rainbow-180 hover:animate-spin-rainbow',
{
'px-5 py-3 text-white': variant === 'full',
'p-[0.125rem]': variant === 'border',
},
className
)}
{...props}
>
<div
className={classNames({
'bg-vega-clight-800 dark:bg-vega-cdark-800 text-black dark:text-white px-5 py-3 rounded-[0.35rem] overflow-hidden':
variant === 'border',
})}
>
{children}
</div>
</button>
);
@@ -105,7 +105,7 @@ describe('ActiveRewards', () => {
expect( expect(
screen.getByText(/Liquidity provision fees received/i) screen.getByText(/Liquidity provision fees received/i)
).toBeInTheDocument(); ).toBeInTheDocument();
expect(screen.getByText('Entity scope')).toBeInTheDocument(); expect(screen.getByText('Individual scope')).toBeInTheDocument();
expect(screen.getByText('Average position')).toBeInTheDocument(); expect(screen.getByText('Average position')).toBeInTheDocument();
expect(screen.getByText('Ends in')).toBeInTheDocument(); expect(screen.getByText('Ends in')).toBeInTheDocument();
expect(screen.getByText('115431 epochs')).toBeInTheDocument(); expect(screen.getByText('115431 epochs')).toBeInTheDocument();
@@ -12,6 +12,7 @@ import {
VegaIconNames, VegaIconNames,
TradingInput, TradingInput,
TinyScroll, TinyScroll,
truncateMiddle,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { IconNames } from '@blueprintjs/icons'; import { IconNames } from '@blueprintjs/icons';
import { import {
@@ -30,6 +31,9 @@ import {
DispatchMetricLabels, DispatchMetricLabels,
EntityScopeLabelMapping, EntityScopeLabelMapping,
MarketState, MarketState,
type DispatchStrategy,
IndividualScopeMapping,
IndividualScopeDescriptionMapping,
} from '@vegaprotocol/types'; } from '@vegaprotocol/types';
import { Card } from '../card/card'; import { Card } from '../card/card';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
@@ -148,7 +152,11 @@ export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
if (!enrichedTransfers || !enrichedTransfers.length) return null; if (!enrichedTransfers || !enrichedTransfers.length) return null;
return ( return (
<Card title={t('Active rewards')} className="lg:col-span-full"> <Card
title={t('Active rewards')}
className="lg:col-span-full"
data-testid="active-rewards-card"
>
{enrichedTransfers.length > 1 && ( {enrichedTransfers.length > 1 && (
<TradingInput <TradingInput
onChange={(e) => onChange={(e) =>
@@ -309,49 +317,29 @@ export const ActiveRewardCard = ({
].includes(m.state) ].includes(m.state)
); );
const assetInSettledMarket =
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 false;
});
if (marketSettled) { if (marketSettled) {
return null; return null;
} }
// Gray out the cards that are related to suspended markets const assetInActiveMarket =
const suspended = transferNode.markets?.some( allMarkets &&
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
return m?.state && MarketState.STATE_ACTIVE === m.state;
}
return false;
});
const marketSuspended = transferNode.markets?.some(
(m) => (m) =>
m?.state === MarketState.STATE_SUSPENDED || m?.state === MarketState.STATE_SUSPENDED ||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
); );
const assetInSuspendedMarket =
allMarkets &&
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
return (
m?.state === MarketState.STATE_SUSPENDED ||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
);
}
return false;
});
// Gray out the cards that are related to suspended markets // Gray out the cards that are related to suspended markets
// Or settlement assets in markets that are not active and eligible for rewards
const { gradientClassName, mainClassName } = const { gradientClassName, mainClassName } =
suspended || assetInSuspendedMarket || assetInSettledMarket marketSuspended || !assetInActiveMarket
? { ? {
gradientClassName: 'from-vega-cdark-500 to-vega-clight-400', gradientClassName: 'from-vega-cdark-500 to-vega-clight-400',
mainClassName: 'from-vega-cdark-400 dark:from-vega-cdark-600 to-20%', mainClassName: 'from-vega-cdark-400 dark:from-vega-cdark-600 to-20%',
@@ -359,6 +347,7 @@ export const ActiveRewardCard = ({
: getGradientClasses(dispatchStrategy.dispatchMetric); : getGradientClasses(dispatchStrategy.dispatchMetric);
const entityScope = dispatchStrategy.entityScope; const entityScope = dispatchStrategy.entityScope;
return ( return (
<div> <div>
<div <div
@@ -367,6 +356,7 @@ export const ActiveRewardCard = ({
'rounded-lg', 'rounded-lg',
gradientClassName gradientClassName
)} )}
data-testid="active-rewards-card"
> >
<div <div
className={classNames( className={classNames(
@@ -378,7 +368,7 @@ export const ActiveRewardCard = ({
<div className="flex flex-col gap-2 items-center text-center"> <div className="flex flex-col gap-2 items-center text-center">
<EntityIcon transfer={transfer} /> <EntityIcon transfer={transfer} />
{entityScope && ( {entityScope && (
<span className="text-muted text-xs"> <span className="text-muted text-xs" data-testid="entity-scope">
{EntityScopeLabelMapping[entityScope] || t('Unspecified')} {EntityScopeLabelMapping[entityScope] || t('Unspecified')}
</span> </span>
)} )}
@@ -386,7 +376,7 @@ export const ActiveRewardCard = ({
<div className="flex flex-col gap-2 items-center text-center"> <div className="flex flex-col gap-2 items-center text-center">
<h3 className="flex flex-col gap-1 text-2xl shrink-1 text-center"> <h3 className="flex flex-col gap-1 text-2xl shrink-1 text-center">
<span className="font-glitch"> <span className="font-glitch" data-testid="reward-value">
{addDecimalsFormatNumber( {addDecimalsFormatNumber(
transferNode.transfer.amount, transferNode.transfer.amount,
transferNode.transfer.asset?.decimals || 0, transferNode.transfer.asset?.decimals || 0,
@@ -407,7 +397,7 @@ export const ActiveRewardCard = ({
)} )}
underline={true} underline={true}
> >
<span className="text-xs"> <span className="text-xs" data-testid="distribution-strategy">
{ {
DistributionStrategyMapping[ DistributionStrategyMapping[
dispatchStrategy.distributionStrategy dispatchStrategy.distributionStrategy
@@ -425,7 +415,10 @@ export const ActiveRewardCard = ({
'Number of epochs after distribution to delay vesting of rewards by' 'Number of epochs after distribution to delay vesting of rewards by'
)} )}
/> />
<span className="text-muted text-xs whitespace-nowrap"> <span
className="text-muted text-xs whitespace-nowrap"
data-testid="locked-for"
>
{t('numberEpochs', '{{count}} epochs', { {t('numberEpochs', '{{count}} epochs', {
count: kind.dispatchStrategy?.lockPeriod, count: kind.dispatchStrategy?.lockPeriod,
})} })}
@@ -434,15 +427,15 @@ export const ActiveRewardCard = ({
</div> </div>
<span className="border-[0.5px] border-gray-700" /> <span className="border-[0.5px] border-gray-700" />
<span> <span data-testid="dispatch-metric-info">
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]} {' '} {DispatchMetricLabels[dispatchStrategy.dispatchMetric]} {' '}
<Tooltip <Tooltip
underline={suspended} underline={marketSuspended}
description={ description={
(suspended || assetInSuspendedMarket) && (marketSuspended || !assetInActiveMarket) &&
(specificMarkets (specificMarkets
? t('Eligible market(s) currently suspended') ? t('Eligible market(s) currently suspended')
: assetInSuspendedMarket : !assetInActiveMarket
? t('Currently no markets eligible for reward') ? t('Currently no markets eligible for reward')
: '') : '')
} }
@@ -454,8 +447,8 @@ export const ActiveRewardCard = ({
<div className="flex items-center gap-8 flex-wrap"> <div className="flex items-center gap-8 flex-wrap">
{kind.endEpoch && ( {kind.endEpoch && (
<span className="flex flex-col"> <span className="flex flex-col">
<span className="text-muted text-xs">{t('Ends in')}</span> <span className="text-muted text-xs">{t('Ends in')} </span>
<span> <span data-testid="ends-in">
{t('numberEpochs', '{{count}} epochs', { {t('numberEpochs', '{{count}} epochs', {
count: kind.endEpoch - currentEpoch, count: kind.endEpoch - currentEpoch,
})} })}
@@ -466,7 +459,7 @@ export const ActiveRewardCard = ({
{ {
<span className="flex flex-col"> <span className="flex flex-col">
<span className="text-muted text-xs">{t('Assessed over')}</span> <span className="text-muted text-xs">{t('Assessed over')}</span>
<span> <span data-testid="assessed-over">
{t('numberEpochs', '{{count}} epochs', { {t('numberEpochs', '{{count}} epochs', {
count: dispatchStrategy.windowLength, count: dispatchStrategy.windowLength,
})} })}
@@ -474,83 +467,130 @@ export const ActiveRewardCard = ({
</span> </span>
} }
</div> </div>
{dispatchStrategy?.dispatchMetric && ( {dispatchStrategy?.dispatchMetric && (
<span className="text-muted text-sm h-[2rem]"> <span className="text-muted text-sm h-[3rem]">
{t(DispatchMetricDescription[dispatchStrategy?.dispatchMetric])} {t(DispatchMetricDescription[dispatchStrategy?.dispatchMetric])}
</span> </span>
)} )}
<span className="border-[0.5px] border-gray-700" /> <span className="border-[0.5px] border-gray-700" />
{kind.dispatchStrategy && (
<div className="flex justify-between flex-wrap items-center gap-3 text-xs"> <RewardRequirements
<span className="flex flex-col gap-1"> dispatchStrategy={kind.dispatchStrategy}
<span className="flex items-center gap-1 text-muted"> assetDecimalPlaces={transfer.asset?.decimals}
{t('Entity scope')}{' '} />
</span> )}
<span className="flex items-center gap-1">
{kind.dispatchStrategy?.teamScope && (
<Tooltip
description={
<span>{kind.dispatchStrategy?.teamScope}</span>
}
>
<span className="flex items-center p-1 rounded-full border border-gray-600">
{<VegaIcon name={VegaIconNames.TEAM} size={16} />}
</span>
</Tooltip>
)}
{kind.dispatchStrategy?.individualScope && (
<Tooltip
description={
<span>{kind.dispatchStrategy?.individualScope}</span>
}
>
<span className="flex items-center p-1 rounded-full border border-gray-600">
{<VegaIcon name={VegaIconNames.MAN} size={16} />}
</span>
</Tooltip>
)}
{/* Shows transfer status */}
{/* <StatusIndicator
status={transfer.status}
reason={transfer.reason}
/> */}
</span>
</span>
<span className="flex flex-col gap-1">
<span className="flex items-center gap-1 text-muted">
{t('Staked VEGA')}{' '}
</span>
<span className="flex items-center gap-1">
{addDecimalsFormatNumber(
kind.dispatchStrategy?.stakingRequirement || 0,
transfer.asset?.decimals || 0
)}
</span>
</span>
<span className="flex flex-col gap-1">
<span className="flex items-center gap-1 text-muted">
{t('Average position')}{' '}
</span>
<span className="flex items-center gap-1">
{addDecimalsFormatNumber(
kind.dispatchStrategy
?.notionalTimeWeightedAveragePositionRequirement || 0,
transfer.asset?.decimals || 0
)}
</span>
</span>
</div>
</div> </div>
</div> </div>
</div> </div>
); );
}; };
const RewardRequirements = ({
dispatchStrategy,
assetDecimalPlaces = 0,
}: {
dispatchStrategy: DispatchStrategy;
assetDecimalPlaces: number | undefined;
}) => {
const t = useT();
return (
<dl className="flex justify-between flex-wrap items-center gap-3 text-xs">
<div className="flex flex-col gap-1">
<dt className="flex items-center gap-1 text-muted">
{t('{{entity}} scope', {
entity: EntityScopeLabelMapping[dispatchStrategy.entityScope],
})}
</dt>
<dd className="flex items-center gap-1" data-testid="scope">
<RewardEntityScope dispatchStrategy={dispatchStrategy} />
</dd>
</div>
<div className="flex flex-col gap-1">
<dt className="flex items-center gap-1 text-muted">
{t('Staked VEGA')}
</dt>
<dd
className="flex items-center gap-1"
data-testid="staking-requirement"
>
{addDecimalsFormatNumber(
dispatchStrategy?.stakingRequirement || 0,
assetDecimalPlaces
)}
</dd>
</div>
<div className="flex flex-col gap-1">
<dt className="flex items-center gap-1 text-muted">
{t('Average position')}
</dt>
<dd className="flex items-center gap-1" data-testid="average-position">
{addDecimalsFormatNumber(
dispatchStrategy?.notionalTimeWeightedAveragePositionRequirement ||
0,
assetDecimalPlaces
)}
</dd>
</div>
</dl>
);
};
const RewardEntityScope = ({
dispatchStrategy,
}: {
dispatchStrategy: DispatchStrategy;
}) => {
const t = useT();
if (dispatchStrategy.entityScope === EntityScope.ENTITY_SCOPE_TEAMS) {
return (
<Tooltip
description={
dispatchStrategy.teamScope?.length ? (
<div className="text-xs">
<p className="mb-1">{t('Eligible teams')}</p>
<ul>
{dispatchStrategy.teamScope.map((teamId) => {
if (!teamId) return null;
return <li key={teamId}>{truncateMiddle(teamId)}</li>;
})}
</ul>
</div>
) : (
t('All teams are eligible')
)
}
>
<span>
{dispatchStrategy.teamScope?.length
? t('Some teams')
: t('All teams')}
</span>
</Tooltip>
);
}
if (
dispatchStrategy.entityScope === EntityScope.ENTITY_SCOPE_INDIVIDUALS &&
dispatchStrategy.individualScope
) {
return (
<Tooltip
description={
IndividualScopeDescriptionMapping[dispatchStrategy.individualScope]
}
>
<span>{IndividualScopeMapping[dispatchStrategy.individualScope]}</span>
</Tooltip>
);
}
return null;
};
const getGradientClasses = (d: DispatchMetric | undefined) => { const getGradientClasses = (d: DispatchMetric | undefined) => {
switch (d) { switch (d) {
case DispatchMetric.DISPATCH_METRIC_AVERAGE_POSITION: case DispatchMetric.DISPATCH_METRIC_AVERAGE_POSITION:
@@ -81,6 +81,7 @@ export const Settings = () => {
intent={Intent.Primary} intent={Intent.Primary}
onClick={() => { onClick={() => {
localStorage.clear(); localStorage.clear();
sessionStorage.clear();
window.location.reload(); window.location.reload();
}} }}
> >
+60 -25
View File
@@ -17,6 +17,7 @@ import { useVegaWallet, useViewAsDialog } from '@vegaprotocol/wallet';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id'; import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { ErrorBoundary } from '../error-boundary'; import { ErrorBoundary } from '../error-boundary';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
export enum ViewType { export enum ViewType {
Order = 'Order', Order = 'Order',
@@ -26,9 +27,10 @@ export enum ViewType {
Transfer = 'Transfer', Transfer = 'Transfer',
Settings = 'Settings', Settings = 'Settings',
ViewAs = 'ViewAs', ViewAs = 'ViewAs',
Close = 'Close',
} }
type SidebarView = export type BarView =
| { | {
type: ViewType.Deposit; type: ViewType.Deposit;
assetId?: string; assetId?: string;
@@ -49,6 +51,9 @@ type SidebarView =
} }
| { | {
type: ViewType.Settings; type: ViewType.Settings;
}
| {
type: ViewType.Close;
}; };
export const Sidebar = ({ options }: { options?: ReactNode }) => { export const Sidebar = ({ options }: { options?: ReactNode }) => {
@@ -57,26 +62,52 @@ export const Sidebar = ({ options }: { options?: ReactNode }) => {
const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1'; const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1';
const setViewAsDialogOpen = useViewAsDialog((state) => state.setOpen); const setViewAsDialogOpen = useViewAsDialog((state) => state.setOpen);
const { pubKeys } = useVegaWallet(); const { pubKeys } = useVegaWallet();
const { isMobile } = useScreenDimensions();
const { getView } = useSidebar((store) => ({
setViews: store.setViews,
getView: store.getView,
}));
const currView = getView(currentRouteId);
return ( return (
<div className="flex h-full p-1 lg:flex-col gap-2" data-testid="sidebar"> <div className="flex h-full lg:flex-col gap-1" data-testid="sidebar">
{options && <nav className={navClasses}>{options}</nav>} {options && (
<nav className={classNames(navClasses, 'ml-auto lg:mt-auto lg:ml-0')}> <nav className={classNames(navClasses, 'flex grow')}>{options}</nav>
<SidebarButton )}
view={ViewType.ViewAs} <nav
onClick={() => { className={classNames(
setViewAsDialogOpen(true); navClasses,
}} 'ml-auto lg:mt-auto lg:ml-0 shrink-0'
icon={VegaIconNames.EYE} )}
tooltip={t('View as party')} >
disabled={Boolean(pubKeys)} {!isMobile ? (
routeId={currentRouteId} <>
/> <SidebarButton
<SidebarButton view={ViewType.ViewAs}
view={ViewType.Settings} onClick={() => {
icon={VegaIconNames.COG} setViewAsDialogOpen(true);
tooltip={t('Settings')} }}
routeId={currentRouteId} icon={VegaIconNames.EYE}
/> tooltip={t('View as party')}
disabled={Boolean(pubKeys)}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Settings}
icon={VegaIconNames.COG}
tooltip={t('Settings')}
routeId={currentRouteId}
/>
</>
) : (
currView && (
<SidebarButton
view={ViewType.Close}
icon={VegaIconNames.ARROW_LEFT}
tooltip={t('Back')}
routeId={currentRouteId}
/>
)
)}
<NodeHealthContainer /> <NodeHealthContainer />
</nav> </nav>
</div> </div>
@@ -103,7 +134,7 @@ export const SidebarButton = ({
getView: store.getView, getView: store.getView,
})); }));
const currView = getView(routeId); const currView = getView(routeId);
const onSelect = (view: SidebarView['type']) => { const onSelect = (view: BarView['type']) => {
if (view === currView?.type) { if (view === currView?.type) {
setViews(null, routeId); setViews(null, routeId);
} else { } else {
@@ -133,7 +164,7 @@ export const SidebarButton = ({
<button <button
className={buttonClasses} className={buttonClasses}
data-testid={view} data-testid={view}
onClick={onClick || (() => onSelect(view as SidebarView['type']))} onClick={onClick || (() => onSelect(view as BarView['type']))}
disabled={disabled} disabled={disabled}
> >
<VegaIcon name={icon} size={20} /> <VegaIcon name={icon} size={20} />
@@ -180,6 +211,10 @@ export const SidebarContent = () => {
} }
} }
if (view.type === ViewType.Close) {
return <CloseSidebar />;
}
if (view.type === ViewType.Info) { if (view.type === ViewType.Info) {
if (params.marketId) { if (params.marketId) {
return ( return (
@@ -267,9 +302,9 @@ const CloseSidebar = () => {
}; };
export const useSidebar = create<{ export const useSidebar = create<{
views: { [key: string]: SidebarView | null }; views: { [key: string]: BarView | null };
setViews: (view: SidebarView | null, routeId: string) => void; setViews: (view: BarView | null, routeId: string) => void;
getView: (routeId: string) => SidebarView | null | undefined; getView: (routeId: string) => BarView | null | undefined;
}>()((set, get) => ({ }>()((set, get) => ({
views: {}, views: {},
setViews: (x, routeId) => setViews: (x, routeId) =>
+22 -6
View File
@@ -11,14 +11,21 @@ type TableColumnDefinition = {
name: string; name: string;
tooltip?: string; tooltip?: string;
className?: string; className?: string;
headerClassName?: string;
testId?: string; testId?: string;
}; };
type DataEntry = {
[key: TableColumnDefinition['name']]: ReactNode;
className?: string;
};
type TableProps = { type TableProps = {
columns: TableColumnDefinition[]; columns: TableColumnDefinition[];
data: Record<TableColumnDefinition['name'] | 'className', React.ReactNode>[]; data: DataEntry[];
noHeader?: boolean; noHeader?: boolean;
noCollapse?: boolean; noCollapse?: boolean;
onRowClick?: (index: number) => void;
}; };
const INNER_BORDER_STYLE = `border-b ${BORDER_COLOR}`; const INNER_BORDER_STYLE = `border-b ${BORDER_COLOR}`;
@@ -34,6 +41,7 @@ export const Table = forwardRef<
noHeader = false, noHeader = false,
noCollapse = false, noCollapse = false,
className, className,
onRowClick,
...props ...props
}, },
ref ref
@@ -41,13 +49,14 @@ export const Table = forwardRef<
const header = ( const header = (
<thead className={classNames({ 'max-md:hidden': !noCollapse })}> <thead className={classNames({ 'max-md:hidden': !noCollapse })}>
<tr> <tr>
{columns.map(({ displayName, name, tooltip }) => ( {columns.map(({ displayName, name, tooltip, headerClassName }) => (
<th <th
key={name} key={name}
col-id={name} col-id={name}
className={classNames( className={classNames(
'px-5 py-3 text-xs text-vega-clight-100 dark:text-vega-cdark-100 font-normal', 'px-5 py-3 text-xs text-vega-clight-100 dark:text-vega-cdark-100 font-normal',
INNER_BORDER_STYLE INNER_BORDER_STYLE,
headerClassName
)} )}
> >
<span className="flex flex-row items-center gap-2"> <span className="flex flex-row items-center gap-2">
@@ -79,12 +88,17 @@ export const Table = forwardRef<
> >
{!noHeader && header} {!noHeader && header}
<tbody> <tbody>
{data.map((d, i) => ( {data.map((dataEntry, i) => (
<tr <tr
key={i} key={i}
className={classNames(d['className'] as string, { className={classNames(dataEntry['className'] as string, {
'max-md:flex flex-col w-full': !noCollapse, 'max-md:flex flex-col w-full': !noCollapse,
})} })}
onClick={() => {
if (onRowClick) {
onRowClick(i);
}
}}
> >
{columns.map(({ name, displayName, className, testId }, j) => ( {columns.map(({ name, displayName, className, testId }, j) => (
<td <td
@@ -114,7 +128,9 @@ export const Table = forwardRef<
{displayName} {displayName}
</span> </span>
)} )}
<span data-testid={`${testId || name}-${i}`}>{d[name]}</span> <span data-testid={`${testId || name}-${i}`}>
{dataEntry[name]}
</span>
</td> </td>
))} ))}
</tr> </tr>
+1 -1
View File
@@ -1,3 +1,3 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
VEGA_VERSION=v0.74.0-preview.7 VEGA_VERSION=v0.74.0-preview.10
LOCAL_SERVER=false LOCAL_SERVER=false
+27 -2
View File
@@ -111,6 +111,7 @@ def init_vega(request=None):
f"Container {container.id} started", f"Container {container.id} started",
extra={"worker_id": os.environ.get("PYTEST_XDIST_WORKER")}, extra={"worker_id": os.environ.get("PYTEST_XDIST_WORKER")},
) )
vega.container = container
yield vega yield vega
except APIError as e: except APIError as e:
logger.info(f"Container creation failed.") logger.info(f"Container creation failed.")
@@ -177,10 +178,34 @@ def init_page(vega: VegaServiceNull, browser: Browser, request: pytest.FixtureRe
@pytest.fixture @pytest.fixture
def vega(request): def vega(request):
with init_vega(request) as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance))
yield vega_instance
def cleanup_container(vega_instance):
try:
# Attempt to stop the container if it's still running
if vega_instance.container.status == 'running':
print(f"Stopping container {vega_instance.container.id}")
vega_instance.container.stop()
else:
print(f"Container {vega_instance.container.id} is not running.")
except docker.errors.NotFound:
print(f"Container {vega_instance.container.id} not found, may have been stopped and removed.")
except Exception as e:
print(f"Error during cleanup: {str(e)}")
try:
# Attempt to remove the container
vega_instance.container.remove()
print(f"Container {vega_instance.container.id} removed.")
except docker.errors.NotFound:
print(f"Container {vega_instance.container.id} not found, may have been removed.")
except Exception as e:
print(f"Error during container removal: {str(e)}")
@pytest.fixture @pytest.fixture
def page(vega, browser, request): def page(vega, browser, request):
with init_page(vega, browser, request) as page_instance: with init_page(vega, browser, request) as page_instance:
@@ -2,7 +2,7 @@ import pytest
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from datetime import datetime, timedelta from datetime import datetime, timedelta
from conftest import init_vega from conftest import init_vega, cleanup_container
from fixtures.market import setup_continuous_market from fixtures.market import setup_continuous_market
from actions.utils import wait_for_toast_confirmation from actions.utils import wait_for_toast_confirmation
@@ -17,8 +17,10 @@ expire = "expire"
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega(request):
with init_vega(request) as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@@ -38,7 +38,7 @@ def test_switch_cross_isolated_margin(
expect(page.get_by_test_id("toast-content")).to_have_text( 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: 1.0x") "ConfirmedYour transaction has been confirmedView in block explorerUpdate margin modeBTC:DAI_2023Isolated margin mode, leverage: 1.0x")
expect(page.locator(margin_row).nth(1) expect(page.locator(margin_row).nth(1)
).to_have_text("11,109.99996Isolated1.0x") ).to_have_text("22,109.99996Isolated1.0x")
# tbd - tooltip is not visible without this wait # tbd - tooltip is not visible without this wait
page.wait_for_timeout(1000) page.wait_for_timeout(1000)
page.get_by_test_id(tab_positions).get_by_text("Isolated").hover() page.get_by_test_id(tab_positions).get_by_text("Isolated").hover()
@@ -3,7 +3,7 @@ from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from actions.vega import submit_order from actions.vega import submit_order
from datetime import datetime, timedelta from datetime import datetime, timedelta
from conftest import init_vega from conftest import init_vega, cleanup_container
from fixtures.market import setup_continuous_market from fixtures.market import setup_continuous_market
stop_order_btn = "order-type-Stop" stop_order_btn = "order-type-Stop"
@@ -259,9 +259,10 @@ def test_submit_stop_limit_order_cancel(
class TestStopOcoValidation: class TestStopOcoValidation:
@pytest.fixture(scope="class") @pytest.fixture(scope="class")
def vega(self, request): def vega(request):
with init_vega(request) as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="class") @pytest.fixture(scope="class")
def continuous_market(self, vega): def continuous_market(self, vega):
@@ -2,7 +2,7 @@ import pytest
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from actions.utils import change_keys from actions.utils import change_keys
from conftest import init_vega from conftest import init_vega, cleanup_container
from fixtures.market import setup_continuous_market from fixtures.market import setup_continuous_market
order_size = "order-size" order_size = "order-size"
@@ -14,8 +14,9 @@ deal_ticket_deposit_dialog_button = "deal-ticket-deposit-dialog-button"
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega(request):
with init_vega(request) as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@@ -23,7 +24,6 @@ def continuous_market(vega):
return setup_continuous_market(vega) return setup_continuous_market(vega)
@pytest.mark.skip("marked id issue #5681")
@pytest.mark.usefixtures("auth", "risk_accepted") @pytest.mark.usefixtures("auth", "risk_accepted")
def test_should_display_info_and_button_for_deposit(continuous_market, page: Page): def test_should_display_info_and_button_for_deposit(continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}") page.goto(f"/#/markets/{continuous_market}")
@@ -34,7 +34,7 @@ def test_should_display_info_and_button_for_deposit(continuous_market, page: Pag
"You may not have enough margin available to open this position.") "You may not have enough margin available to open this position.")
page.get_by_test_id(deal_ticket_warning_margin).hover() page.get_by_test_id(deal_ticket_warning_margin).hover()
expect(page.get_by_test_id("tooltip-content").nth(0)).to_have_text( expect(page.get_by_test_id("tooltip-content").nth(0)).to_have_text(
"1,661,896.6317 tDAI is currently required.You have only 1,000,000.00.Deposit tDAI") "1,661,888.12901 tDAI is currently required.You have only 999,991.49731.Deposit tDAI")
page.get_by_test_id(deal_ticket_deposit_dialog_button).nth(0).click() page.get_by_test_id(deal_ticket_deposit_dialog_button).nth(0).click()
expect(page.get_by_test_id("sidebar-content") expect(page.get_by_test_id("sidebar-content")
).to_contain_text("DepositFrom") ).to_contain_text("DepositFrom")
+11 -6
View File
@@ -4,7 +4,7 @@ from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from actions.vega import submit_order from actions.vega import submit_order
from wallet_config import MM_WALLET from wallet_config import MM_WALLET
from conftest import init_vega, init_page, auth_setup from conftest import init_vega, init_page, auth_setup, cleanup_container
from actions.utils import next_epoch, change_keys, forward_time from actions.utils import next_epoch, change_keys, forward_time
from fixtures.market import market_exists, setup_continuous_market from fixtures.market import market_exists, setup_continuous_market
@@ -82,31 +82,36 @@ def market_ids():
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega_volume_discount_tier_1(request): def vega_volume_discount_tier_1(request):
with init_vega(request) as vega_volume_discount_tier_1: with init_vega(request) as vega_volume_discount_tier_1:
yield vega_volume_discount_tier_1 request.addfinalizer(lambda: cleanup_container(vega_volume_discount_tier_1)) # Register the cleanup function
yield vega_volume_discount_tier_1
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega_volume_discount_tier_2(request): def vega_volume_discount_tier_2(request):
with init_vega(request) as vega_volume_discount_tier_2: with init_vega(request) as vega_volume_discount_tier_2:
yield vega_volume_discount_tier_2 request.addfinalizer(lambda: cleanup_container(vega_volume_discount_tier_2)) # Register the cleanup function
yield vega_volume_discount_tier_2
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega_referral_discount_tier_1(request): def vega_referral_discount_tier_1(request):
with init_vega(request) as vega_referral_discount_tier_1: with init_vega(request) as vega_referral_discount_tier_1:
yield vega_referral_discount_tier_1 request.addfinalizer(lambda: cleanup_container(vega_referral_discount_tier_1)) # Register the cleanup function
yield vega_referral_discount_tier_1
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega_referral_discount_tier_2(request): def vega_referral_discount_tier_2(request):
with init_vega(request) as vega_referral_discount_tier_2: with init_vega(request) as vega_referral_discount_tier_2:
yield vega_referral_discount_tier_2 request.addfinalizer(lambda: cleanup_container(vega_referral_discount_tier_2)) # Register the cleanup function
yield vega_referral_discount_tier_2
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega_referral_and_volume_discount(request): def vega_referral_and_volume_discount(request):
with init_vega(request) as vega_referral_and_volume_discount: with init_vega(request) as vega_referral_and_volume_discount:
yield vega_referral_and_volume_discount request.addfinalizer(lambda: cleanup_container(vega_referral_and_volume_discount)) # Register the cleanup function
yield vega_referral_and_volume_discount
@pytest.fixture @pytest.fixture
@@ -3,7 +3,7 @@ from playwright.sync_api import expect, Page
import json import json
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from fixtures.market import setup_simple_market from fixtures.market import setup_simple_market
from conftest import init_vega from conftest import init_vega, cleanup_container
from actions.vega import submit_order from actions.vega import submit_order
from wallet_config import MM_WALLET, TERMINATE_WALLET, wallets from wallet_config import MM_WALLET, TERMINATE_WALLET, wallets
import logging import logging
@@ -12,9 +12,10 @@ logger = logging.getLogger()
@pytest.fixture(scope="class") @pytest.fixture(scope="class")
def vega(): def vega(request):
with init_vega() as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="class") @pytest.fixture(scope="class")
@@ -2,8 +2,6 @@ import pytest
from playwright.sync_api import expect, Page from playwright.sync_api import expect, Page
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from actions.vega import submit_order from actions.vega import submit_order
from conftest import init_vega
from fixtures.market import setup_continuous_market
from wallet_config import MM_WALLET2 from wallet_config import MM_WALLET2
def hover_and_assert_tooltip(page: Page, element_text): def hover_and_assert_tooltip(page: Page, element_text):
@@ -11,39 +9,30 @@ def hover_and_assert_tooltip(page: Page, element_text):
element.hover() element.hover()
expect(page.get_by_role("tooltip")).to_be_visible() expect(page.get_by_role("tooltip")).to_be_visible()
class TestIcebergOrdersValidations:
@pytest.fixture(scope="class")
def vega(self, request):
with init_vega(request) as vega:
yield vega
@pytest.fixture(scope="class") @pytest.mark.usefixtures("auth", "risk_accepted")
def continuous_market(self, vega): def test_iceberg_submit(continuous_market, vega: VegaServiceNull, page: Page):
return setup_continuous_market(vega) page.goto(f"/#/markets/{continuous_market}")
page.get_by_test_id("iceberg").click()
page.get_by_test_id("order-peak-size").type("2")
page.get_by_test_id("order-minimum-size").type("1")
page.get_by_test_id("order-size").type("3")
page.get_by_test_id("order-price").type("107")
page.get_by_test_id("place-order").click()
@pytest.mark.usefixtures("auth", "risk_accepted") expect(page.get_by_test_id("toast-content")).to_have_text(
def test_iceberg_submit(self, continuous_market, vega: VegaServiceNull, page: Page): "Awaiting confirmationPlease wait for your transaction to be confirmedView in block explorer"
page.goto(f"/#/markets/{continuous_market}") )
page.get_by_test_id("iceberg").click()
page.get_by_test_id("order-peak-size").type("2")
page.get_by_test_id("order-minimum-size").type("1")
page.get_by_test_id("order-size").type("3")
page.get_by_test_id("order-price").type("107")
page.get_by_test_id("place-order").click()
expect(page.get_by_test_id("toast-content")).to_have_text( vega.wait_fn(1)
"Awaiting confirmationPlease wait for your transaction to be confirmedView in block explorer" vega.wait_for_total_catchup()
) expect(page.get_by_test_id("toast-content")).to_have_text(
"Order filledYour transaction has been confirmedView in block explorerSubmit order - filledBTC:DAI_2023+3 @ 107.00 tDAI"
vega.wait_fn(1) )
vega.wait_for_total_catchup() page.get_by_test_id("All").click()
expect(page.get_by_test_id("toast-content")).to_have_text( expect(
"Order filledYour transaction has been confirmedView in block explorerSubmit order - filledBTC:DAI_2023+3 @ 107.00 tDAI" (page.get_by_role("row").locator('[col-id="type"]')).nth(1)
) ).to_have_text("Limit (Iceberg)")
page.get_by_test_id("All").click()
expect(
(page.get_by_role("row").locator('[col-id="type"]')).nth(1)
).to_have_text("Limit (Iceberg)")
@pytest.mark.usefixtures("auth", "risk_accepted") @pytest.mark.usefixtures("auth", "risk_accepted")
def test_iceberg_open_order(continuous_market, vega: VegaServiceNull, page: Page): def test_iceberg_open_order(continuous_market, vega: VegaServiceNull, page: Page):
@@ -1,15 +1,16 @@
import pytest import pytest
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from conftest import init_vega from conftest import init_vega, cleanup_container
from fixtures.market import setup_continuous_market from fixtures.market import setup_continuous_market
from actions.utils import next_epoch, truncate_middle, change_keys from actions.utils import next_epoch, truncate_middle, change_keys
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega(request):
with init_vega(request) as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@@ -4,14 +4,15 @@ import vega_sim.api.governance as governance
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from fixtures.market import setup_continuous_market from fixtures.market import setup_continuous_market
from conftest import init_vega from conftest import init_vega, cleanup_container
from actions.utils import next_epoch from actions.utils import next_epoch
@pytest.fixture(scope="class") @pytest.fixture(scope="class")
def vega(): def vega(request):
with init_vega() as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="class") @pytest.fixture(scope="class")
@@ -3,15 +3,16 @@ import pytest
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from fixtures.market import setup_continuous_market from fixtures.market import setup_continuous_market
from conftest import init_page, init_vega, risk_accepted_setup from conftest import init_page, init_vega, risk_accepted_setup, cleanup_container
market_title_test_id = "accordion-title" market_title_test_id = "accordion-title"
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(): def vega(request):
with init_vega() as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@@ -3,7 +3,7 @@ import vega_sim.api.governance as governance
import re import re
from playwright.sync_api import Page, expect from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from conftest import init_vega from conftest import init_vega, cleanup_container
from fixtures.market import setup_simple_market from fixtures.market import setup_simple_market
from wallet_config import MM_WALLET from wallet_config import MM_WALLET
@@ -13,8 +13,10 @@ col_market_id = '[col-id="market"] [data-testid="stack-cell-primary"]'
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega(request):
with init_vega(request) as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
@@ -3,7 +3,7 @@ from playwright.sync_api import Page, expect
from vega_sim.null_service import VegaServiceNull from vega_sim.null_service import VegaServiceNull
from actions.vega import submit_order from actions.vega import submit_order
from fixtures.market import setup_simple_market from fixtures.market import setup_simple_market
from conftest import init_vega from conftest import init_vega, cleanup_container
from actions.utils import wait_for_toast_confirmation, change_keys from actions.utils import wait_for_toast_confirmation, change_keys
from wallet_config import MM_WALLET, MM_WALLET2 from wallet_config import MM_WALLET, MM_WALLET2
@@ -15,8 +15,10 @@ COL_ID_FEE = ".ag-center-cols-container [col-id='fee'] .ag-cell-value"
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def vega(request): def vega(request):
with init_vega(request) as vega: with init_vega(request) as vega_instance:
yield vega request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
yield vega_instance
@pytest.fixture(scope="module") @pytest.fixture(scope="module")

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