Compare commits

..
463 changed files with 5294 additions and 14854 deletions
-1
View File
@@ -1,4 +1,3 @@
* text eol=lf
*.png binary
*.ico binary
*.woff2 binary
+3 -46
View File
@@ -19,7 +19,7 @@ jobs:
create-docker-image:
name: Create docker image for console-test
runs-on: ubuntu-22.04
timeout-minutes: 90
timeout-minutes: 20
steps:
#----------------------------------------------
# check-out frontend-monorepo
@@ -138,7 +138,7 @@ jobs:
name: run-tests
runs-on: 8-cores
needs: [create-docker-image, console-test-branch]
timeout-minutes: 90
timeout-minutes: 20
steps:
#----------------------------------------------
# load docker image
@@ -205,7 +205,7 @@ jobs:
# run tests
#----------------------------------------------
- name: Run tests
run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v -s --numprocesses 2 --dist loadfile --durations=90
run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v -s --numprocesses 4 --dist loadfile --durations=15
working-directory: apps/trading/e2e
#----------------------------------------------
# upload traces
@@ -227,46 +227,3 @@ jobs:
name: worker-logs
path: ./logs/
retention-days: 15
#----------------------------------------------
# ----- upload market-sim logs -----
#----------------------------------------------
- name: Prepare and Zip market-sim-logs
if: always()
run: |
parent_dir="/tmp/market-sim-logs"
echo "Creating parent directory at $parent_dir"
mkdir -p "$parent_dir"
echo "Waiting for vega-sim-* folders to be created..."
sleep 10 # Waits 10 seconds to ensure all folders are created
echo "Before searching for vega-sim-* folders in /tmp..."
folders=$(find /tmp -mindepth 1 -type d -name 'vega-sim-*' -print) || echo "Find command failed with exit code $?"
echo "After searching for vega-sim-* folders in /tmp..."
if [ -z "$folders" ]; then
echo "No vega-sim-* folders found."
exit 0
fi
echo "Moving vega-sim-* folders to $parent_dir"
echo "$folders" | xargs -I {} mv {} "$parent_dir/"
echo "Checking if $parent_dir is not empty..."
if [ "$(ls -A $parent_dir)" ]; then
echo "Zipping the parent directory..."
zip -r market-sim-logs.zip "$parent_dir" && echo "Zip file created successfully."
else
echo "$parent_dir is empty. No zip file created."
exit 0
fi
shell: /usr/bin/bash -e {0}
- name: Upload market-sim-logs
uses: actions/upload-artifact@v3
if: always()
with:
name: market-sim-logs
path: market-sim-logs.zip
retention-days: 15
-3
View File
@@ -1,8 +1,5 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# Auto-format all files
yarn nx format:write
# Lint all staged files
yarn lint-staged
+3 -3
View File
@@ -1,8 +1,8 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# Lint all staged files - this brings more value as pre-commit
# yarn nx format:check
# Lint all staged files
yarn nx format:check
# Test all projects with changes
# yarn nx affected -t test --exclude trading
yarn nx affected -t test --exclude trading
@@ -44,7 +44,7 @@ context('Proposal page', { tags: '@smoke' }, function () {
cy.getByTestId('icon-cross').click();
});
it('Proposal page displayed on mobile', function () {
it.skip('Proposal page displayed on mobile', function () {
const proposalTitle = 'Add Lorem Ipsum market';
cy.common_switch_to_mobile_and_click_toggle();
@@ -55,7 +55,7 @@ context('Proposal page', { tags: '@smoke' }, function () {
});
});
it.skip('Able to view new asset proposal', function () {
it('Able to view new asset proposal', function () {
const proposalTitle = 'Test new asset proposal';
const newAssetProposalBody = getNewAssetTxBody();
cy.VegaWalletSubmitProposal(newAssetProposalBody);
@@ -24,7 +24,7 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
const navigate = useNavigate();
const ref = useRef<AgGridReact>(null);
const showColumnsOnDesktop = () => {
ref.current?.api.setColumnsVisible(
ref.current?.columnApi.setColumnsVisible(
['id', 'type', 'status'],
window.innerWidth > BREAKPOINT_MD
);
@@ -60,7 +60,7 @@ const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
}
return (
<span>
<span className="whitespace-nowrap">
{useName && <Icon size={4} name="cube" className="mr-2" />}
<Link
className="underline font-mono"
@@ -1,11 +1,9 @@
query ExplorerProposal($id: ID!) {
proposal(id: $id) {
... on Proposal {
id
rationale {
title
description
}
id
rationale {
title
description
}
}
}
@@ -8,18 +8,16 @@ export type ExplorerProposalQueryVariables = Types.Exact<{
}>;
export type ExplorerProposalQuery = { __typename?: 'Query', proposal?: { __typename?: 'BatchProposal' } | { __typename?: 'Proposal', id?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string } } | null };
export type ExplorerProposalQuery = { __typename?: 'Query', proposal?: { __typename?: 'Proposal', id?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string } } | null };
export const ExplorerProposalDocument = gql`
query ExplorerProposal($id: ID!) {
proposal(id: $id) {
... on Proposal {
id
rationale {
title
description
}
id
rationale {
title
description
}
}
}
@@ -1,11 +1,7 @@
import {
useExplorerProposalQuery,
type ExplorerProposalQuery,
} from './__generated__/Proposal';
import { useExplorerProposalQuery } from './__generated__/Proposal';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { ENV } from '../../../config/env';
import Hash from '../hash';
export type ProposalLinkProps = {
id: string;
text?: string;
@@ -20,13 +16,8 @@ const ProposalLink = ({ id, text }: ProposalLinkProps) => {
variables: { id },
});
const proposal = data?.proposal as Extract<
ExplorerProposalQuery['proposal'],
{ __typename?: 'Proposal' }
>;
const base = ENV.dataSources.governanceUrl;
const label = proposal?.rationale.title || id;
const label = data?.proposal?.rationale.title || id;
return (
<ExternalLink href={`${base}/proposals/${id}`}>
@@ -29,7 +29,7 @@ export const MarketsTable = ({ data }: MarketsTableProps) => {
const gridRef = useRef<AgGridReact>(null);
useLayoutEffect(() => {
const showColumnsOnDesktop = () => {
gridRef.current?.api.setColumnsVisible(
gridRef.current?.columnApi.setColumnsVisible(
['id', 'state', 'asset'],
window.innerWidth > BREAKPOINT_MD
);
@@ -60,7 +60,7 @@ const DeterministicOrderDetails = ({
const o = data.orderByID;
return (
<div className={wrapperClasses}>
<div className="mb-0">
<div className="mb-12 lg:mb-0">
<div className="relative block px-3 py-6 md:px-6 lg:-mr-7">
<h2 className="text-3xl font-bold mb-4 display-5">
<abbr title={tifFull[o.timeInForce]} className="bb-dotted mr-2">
@@ -89,9 +89,9 @@ const DeterministicOrderDetails = ({
<span>{t('Reference')}</span>: {o.reference}
</p>
) : null}
<div className="grid grid-cols-2 md:grid-cols-5 gap-x-6 mt-4">
<div className="mb-6 md:mb-0">
<h2 className="text-2xl font-bold text-dark mb-0 md:mb-4">
<div className="grid md:grid-cols-5 gap-x-6 mt-4">
<div className="mb-12 md:mb-0">
<h2 className="text-2xl font-bold text-dark mb-4">
{t('Status')}
</h2>
<h5 className="text-lg font-medium text-gray-500 mb-0 capitalize">
@@ -99,17 +99,15 @@ const DeterministicOrderDetails = ({
</h5>
</div>
<div className="mb-6 md:mb-0">
<h2 className="text-2xl font-bold text-dark mb-0 md:mb-4">
{t('Size')}
</h2>
<div className="mb-12 md:mb-0">
<h2 className="text-2xl font-bold text-dark mb-4">{t('Size')}</h2>
<h5 className="text-lg font-medium text-gray-500 mb-0">
<SizeInMarket size={o.size} marketId={o.market.id} />
</h5>
</div>
<div className="mb-6 md:mb-0">
<h2 className="text-2xl font-bold text-dark mb-0 md:mb-4">
<div className="">
<h2 className="text-2xl font-bold text-dark mb-4">
{t('Version')}
</h2>
<h5 className="text-lg font-medium text-gray-500 mb-0">
@@ -117,8 +115,8 @@ const DeterministicOrderDetails = ({
</h5>
</div>
{o.type ? (
<div className="mb-6 md:mb-0">
<h2 className="text-2xl font-bold text-dark mb-0 md:mb-4">
<div className="">
<h2 className="text-2xl font-bold text-dark mb-4">
{t('Type')}
</h2>
<h5 className="text-lg font-medium text-gray-500 mb-0">
@@ -44,11 +44,11 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
const gridRef = useRef<AgGridReact>(null);
useLayoutEffect(() => {
const showColumnsOnDesktop = () => {
gridRef.current?.api.setColumnsVisible(
gridRef.current?.columnApi.setColumnsVisible(
['voting', 'cDate', 'eDate', 'type'],
window.innerWidth > BREAKPOINT_MD
);
gridRef.current?.api.setColumnWidth(
gridRef.current?.columnApi.setColumnWidth(
'actions',
window.innerWidth > BREAKPOINT_MD ? 221 : 80
);
@@ -30,12 +30,12 @@ export const Signature = ({ signature }: SignatureProps) => {
return (
<div className="inline-flex border rounded signature-component relative pr-[20px]">
<div
<span
className="bg-gray-100 px-2.5 py-0.5 text-xs text-gray-500 select-none cursor-default"
title={`${signature.algo}`}
title={`Version ${signature.version}`}
>
<span>v{signature.version}</span>
</div>
{signature.algo}
</span>
<div
className={
isOpen
@@ -1,9 +1,7 @@
query ExplorerProposalStatus($id: ID!) {
proposal(id: $id) {
... on Proposal {
id
state
rejectionReason
}
id
state
rejectionReason
}
}
@@ -8,17 +8,15 @@ 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?: 'Proposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | null };
export const ExplorerProposalStatusDocument = gql`
query ExplorerProposalStatus($id: ID!) {
proposal(id: $id) {
... on Proposal {
id
state
rejectionReason
}
id
state
rejectionReason
}
}
`;
@@ -14,18 +14,16 @@ export function format(date: string | undefined, def: string) {
return new Date().toLocaleDateString() || def;
}
type Proposal = Extract<
ExplorerProposalStatusQuery['proposal'],
{ __typename?: 'Proposal' }
>;
export function getDate(proposal: Proposal | undefined, terms: Terms): string {
export function getDate(
data: ExplorerProposalStatusQuery | undefined,
terms: Terms
): string {
const DEFAULT = t('Unknown');
if (!proposal?.state) {
if (!data?.proposal?.state) {
return DEFAULT;
}
switch (proposal.state) {
switch (data.proposal.state) {
case 'STATE_DECLINED':
return `${t('Rejected on')}: ${format(terms.closingTimestamp, DEFAULT)}`;
case 'STATE_ENACTED':
@@ -64,11 +62,9 @@ export const ProposalDate = ({ terms, id }: ProposalDateProps) => {
},
});
const proposal = data?.proposal as Proposal;
return (
<Lozenge className="font-sans text-xs float-right">
{getDate(proposal, terms)}
{getDate(data, terms)}
</Lozenge>
);
};
@@ -2,8 +2,17 @@ import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import type { IconProps } from '@vegaprotocol/ui-toolkit';
import { useExplorerProposalStatusQuery } from './__generated__/Proposal';
import type { ExplorerProposalStatusQuery } from './__generated__/Proposal';
import type * as Apollo from '@apollo/client';
import type * as Types from '@vegaprotocol/types';
import { t } from '@vegaprotocol/i18n';
type ProposalQueryResult = Apollo.QueryResult<
ExplorerProposalStatusQuery,
Types.Exact<{
id: string;
}>
>;
interface ProposalStatusIconProps {
id: string;
}
@@ -20,38 +29,29 @@ type IconAndLabel = {
* @param data a data result from useExplorerProposalStatusQuery
* @returns Icon name
*/
export function useIconAndLabelForStatus(id: string): IconAndLabel {
const { data, loading, error } = useExplorerProposalStatusQuery({
variables: {
id,
},
});
const proposal = data?.proposal as Extract<
ExplorerProposalStatusQuery['proposal'],
{ __typename?: 'Proposal' }
>;
export function getIconAndLabelForStatus(
res: ProposalQueryResult
): IconAndLabel {
const DEFAULT: IconAndLabel = {
icon: 'error',
label: t('Proposal state unknown'),
};
if (loading) {
if (res.loading) {
return {
icon: 'more',
label: t('Loading data'),
};
}
if (!data?.proposal || error) {
if (!res?.data?.proposal || res.error) {
return {
icon: 'error',
label: error?.message || DEFAULT.label,
label: res.error?.message || DEFAULT.label,
};
}
switch (proposal.state) {
switch (res.data.proposal.state) {
case 'STATE_DECLINED':
return {
icon: 'stop',
@@ -99,7 +99,13 @@ export function useIconAndLabelForStatus(id: string): IconAndLabel {
/**
*/
export const ProposalStatusIcon = ({ id }: ProposalStatusIconProps) => {
const { icon, label } = useIconAndLabelForStatus(id);
const { icon, label } = getIconAndLabelForStatus(
useExplorerProposalStatusQuery({
variables: {
id,
},
})
);
return (
<div className="float-left mr-3">
@@ -54,7 +54,7 @@ const Block = () => {
</Button>
</Link>
</div>
{blockData && 'result' in blockData && (
{blockData && (
<>
<TableWithTbody className="mb-8">
<TableRow modifier="bordered">
+1 -1
View File
@@ -73,7 +73,7 @@ export const Layout = () => {
<ProtocolUpgradeInProgressNotification />
</div>
<div className={fixedWidthClasses}>
<main className="md:p-4">
<main className="p-4">
{!isHome && <BreadcrumbsContainer className="mb-4" />}
<Outlet />
</main>
@@ -196,7 +196,6 @@ export function createNewMarketProposalTxBody(): ProposalSubmissionBody {
timeWindow: '3600',
scalingFactor: 10,
},
// FIXME: workaround because of https://github.com/vegaprotocol/vega/issues/10343
triggeringRatio: '0.7',
auctionExtension: '1',
},
@@ -335,7 +334,6 @@ export function createSuccessorMarketProposalTxBody(
timeWindow: '3600',
scalingFactor: 10,
},
// FIXME: workaround because of https://github.com/vegaprotocol/vega/issues/10343
triggeringRatio: '0.7',
auctionExtension: '1',
},
+1 -1
View File
@@ -32,7 +32,7 @@ CYPRESS_FAIRGROUND=false
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
NX_METAMASK_SNAPS=false
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
+1 -1
View File
@@ -23,7 +23,7 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
NX_METAMASK_SNAPS=false
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
+1 -1
View File
@@ -22,7 +22,7 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.mainnet-mirror.vega.rocks/websocket
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
NX_METAMASK_SNAPS=false
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
+1 -1
View File
@@ -21,7 +21,7 @@ NX_TENDERMINT_WEBSOCKET_URL=wss://be.validators-testnet.vega.
# Cosmic elevator flags
NX_SUCCESSOR_MARKETS=true
NX_METAMASK_SNAPS=true
NX_METAMASK_SNAPS=false
NX_PRODUCT_PERPETUALS=true
NX_UPDATE_MARKET_STATE=true
NX_REFERRALS=true
+2 -2
View File
@@ -31,7 +31,7 @@ import {
orderByUpgradeBlockHeight,
} from '../proposals/components/proposals-list/proposals-list';
import { BigNumber } from '../../lib/bignumber';
import { type Proposal } from '../proposals/types';
import type { ProposalQuery } from '../proposals/proposal/__generated__/Proposal';
const nodesToShow = 6;
@@ -39,7 +39,7 @@ const HomeProposals = ({
proposals,
protocolUpgradeProposals,
}: {
proposals: Proposal[];
proposals: ProposalQuery['proposal'][];
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
}) => {
const { t } = useTranslation();
@@ -1,11 +1,16 @@
import { useTranslation } from 'react-i18next';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { ProposalState } from '@vegaprotocol/types';
import { ProposalInfoLabel } from '../proposal-info-label';
import { type ReactNode } from 'react';
import { type ProposalInfoLabelVariant } from '../proposal-info-label';
import { type Proposal } from '../../types';
import type { ReactNode } from 'react';
import type { ProposalInfoLabelVariant } from '../proposal-info-label';
export const CurrentProposalState = ({ proposal }: { proposal: Proposal }) => {
export const CurrentProposalState = ({
proposal,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}) => {
const { t } = useTranslation();
let proposalStatus: ReactNode;
let variant = 'tertiary' as ProposalInfoLabelVariant;
@@ -0,0 +1,272 @@
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import { render, screen } from '@testing-library/react';
import { ProposalRejectionReason, ProposalState } from '@vegaprotocol/types';
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { generateProposal } from '../../test-helpers/generate-proposals';
import { CurrentProposalStatus } from './current-proposal-status';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
const networkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
request: {
query: NetworkParamsDocument,
},
result: {
data: {
networkParametersConnection: {
edges: [
{
node: {
__typename: 'NetworkParameter',
key: 'governance.proposal.updateNetParam.requiredMajority',
value: '0.00000001',
},
},
{
node: {
__typename: 'NetworkParameter',
key: 'governance.proposal.updateNetParam.requiredParticipation',
value: '0.000000001',
},
},
],
},
},
},
};
const renderComponent = ({
proposal,
}: {
proposal: ProposalQuery['proposal'];
}) => {
render(
<AppStateProvider>
<MockedProvider mocks={[networkParamsQueryMock]}>
<CurrentProposalStatus proposal={proposal} />
</MockedProvider>
</AppStateProvider>
);
};
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(60 * 60 * 1000);
});
afterEach(() => {
jest.useRealTimers();
});
it('Proposal open - renders will fail state if the proposal will fail', async () => {
const failedProposal = generateProposal({
votes: {
__typename: 'ProposalVotes',
yes: {
__typename: 'ProposalVoteSide',
totalNumber: '0',
totalTokens: '0',
totalEquityLikeShareWeight: '0',
},
no: {
__typename: 'ProposalVoteSide',
totalNumber: '0',
totalTokens: '0',
totalEquityLikeShareWeight: '0',
},
},
});
renderComponent({ proposal: failedProposal });
expect(await screen.findByText('Currently expected to')).toBeInTheDocument();
expect(await screen.findByText('fail.')).toBeInTheDocument();
});
it('Proposal open - renders will pass state if the proposal will pass', async () => {
const proposal = generateProposal();
renderComponent({ proposal });
expect(await screen.findByText('Currently expected to')).toBeInTheDocument();
expect(await screen.findByText('pass.')).toBeInTheDocument();
});
it('Proposal enacted - renders vote passed and time since enactment', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_ENACTED,
terms: {
enactmentDatetime: new Date(0).toISOString(),
},
});
renderComponent({ proposal });
expect(await screen.findByText('Vote passed.')).toBeInTheDocument();
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
});
it('Proposal passed - renders vote passed and time since vote closed', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
closingDatetime: new Date(0).toISOString(),
},
});
renderComponent({ proposal });
expect(await screen.findByText('Vote passed.')).toBeInTheDocument();
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
});
it('Proposal waiting for node vote - will pass - renders if the vote will pass and status', async () => {
const failedProposal = generateProposal({
state: ProposalState.STATE_WAITING_FOR_NODE_VOTE,
votes: {
__typename: 'ProposalVotes',
yes: {
__typename: 'ProposalVoteSide',
totalNumber: '0',
totalTokens: '0',
totalEquityLikeShareWeight: '0',
},
no: {
__typename: 'ProposalVoteSide',
totalNumber: '0',
totalTokens: '0',
totalEquityLikeShareWeight: '0',
},
},
});
renderComponent({ proposal: failedProposal });
expect(
await screen.findByText('Waiting for nodes to validate asset.')
).toBeInTheDocument();
expect(await screen.findByText('Currently expected to')).toBeInTheDocument();
expect(await screen.findByText('fail.')).toBeInTheDocument();
});
it('Proposal waiting for node vote - will fail - renders if the vote will pass and status', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_WAITING_FOR_NODE_VOTE,
});
renderComponent({ proposal });
expect(
await screen.findByText('Waiting for nodes to validate asset.')
).toBeInTheDocument();
expect(await screen.findByText('Currently expected to')).toBeInTheDocument();
expect(await screen.findByText('pass.')).toBeInTheDocument();
});
it('Proposal failed - renders vote failed reason and vote closed ago', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_FAILED,
errorDetails: 'foo',
terms: {
closingDatetime: new Date(0).toISOString(),
},
});
renderComponent({ proposal });
expect(
await screen.findByText('Vote closed. Failed due to:')
).toBeInTheDocument();
expect(await screen.findByText('foo')).toBeInTheDocument();
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
});
it('Proposal failed - renders rejection reason there are no error details', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_FAILED,
rejectionReason: ProposalRejectionReason.PROPOSAL_ERROR_CLOSE_TIME_TOO_LATE,
terms: {
closingDatetime: new Date(0).toISOString(),
},
});
renderComponent({ proposal });
expect(
await screen.findByText('Vote closed. Failed due to:')
).toBeInTheDocument();
expect(
await screen.findByText('PROPOSAL_ERROR_CLOSE_TIME_TOO_LATE')
).toBeInTheDocument();
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
});
it('Proposal failed - renders unknown reason if there are no error details or rejection reason', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_FAILED,
terms: {
closingDatetime: new Date(0).toISOString(),
},
});
renderComponent({ proposal });
expect(
await screen.findByText('Vote closed. Failed due to:')
).toBeInTheDocument();
expect(await screen.findByText('unknown reason')).toBeInTheDocument();
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
});
it('Proposal failed - renders participation not met if participation is not met', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_FAILED,
terms: {
closingDatetime: new Date(0).toISOString(),
},
votes: {
__typename: 'ProposalVotes',
yes: {
__typename: 'ProposalVoteSide',
totalNumber: '0',
totalTokens: '0',
totalEquityLikeShareWeight: '0',
},
no: {
__typename: 'ProposalVoteSide',
totalNumber: '0',
totalTokens: '0',
totalEquityLikeShareWeight: '0',
},
},
});
renderComponent({ proposal });
expect(
await screen.findByText('Vote closed. Failed due to:')
).toBeInTheDocument();
expect(await screen.findByText('Participation not met')).toBeInTheDocument();
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
});
it('Proposal failed - renders majority not met if majority is not met', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_FAILED,
terms: {
closingDatetime: new Date(0).toISOString(),
},
votes: {
__typename: 'ProposalVotes',
yes: {
__typename: 'ProposalVoteSide',
totalNumber: '0',
totalTokens: '0',
totalEquityLikeShareWeight: '0',
},
no: {
__typename: 'ProposalVoteSide',
totalNumber: '1',
totalTokens: '25242474195500835440000',
totalEquityLikeShareWeight: '0',
},
},
});
renderComponent({ proposal });
expect(
await screen.findByText('Vote closed. Failed due to:')
).toBeInTheDocument();
expect(await screen.findByText('Majority not met')).toBeInTheDocument();
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
});
@@ -0,0 +1,143 @@
import type { ReactNode } from 'react';
import { formatDistanceToNow } from 'date-fns';
import { useTranslation } from 'react-i18next';
import { ProposalState } from '@vegaprotocol/types';
import { useVoteInformation } from '../../hooks';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
export const StatusPass = ({ children }: { children: ReactNode }) => (
<span className="text-vega-green">{children}</span>
);
export const StatusFail = ({ children }: { children: ReactNode }) => (
<span className="text-danger">{children}</span>
);
const WillPass = ({
willPass,
children,
}: {
willPass: boolean;
children?: ReactNode;
}) => {
const { t } = useTranslation();
if (willPass) {
return (
<>
{children}
<StatusPass>{t('pass')}.</StatusPass>
<span className="ml-2">{t('finalOutcomeMayDiffer')}</span>
</>
);
} else {
return (
<>
{children}
<StatusFail>{t('fail')}.</StatusFail>
<span className="ml-2">{t('finalOutcomeMayDiffer')}</span>
</>
);
}
};
export const CurrentProposalStatus = ({
proposal,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}) => {
const { willPassByTokenVote, majorityMet, participationMet } =
useVoteInformation({
proposal,
});
const { t } = useTranslation();
const daysClosedAgo = formatDistanceToNow(
new Date(proposal?.terms.closingDatetime),
{ addSuffix: true }
);
const daysEnactedAgo =
proposal?.terms.enactmentDatetime &&
formatDistanceToNow(new Date(proposal.terms.enactmentDatetime), {
addSuffix: true,
});
if (proposal?.state === ProposalState.STATE_OPEN) {
return (
<WillPass willPass={willPassByTokenVote}>{t('currentlySetTo')}</WillPass>
);
}
if (
proposal?.state === ProposalState.STATE_FAILED ||
proposal?.state === ProposalState.STATE_DECLINED ||
proposal?.state === ProposalState.STATE_REJECTED
) {
if (!participationMet) {
return (
<>
<span>{t('voteFailedReason')}</span>
<StatusFail>{t('participationNotMet')}</StatusFail>
<span>&nbsp;{daysClosedAgo}</span>
</>
);
}
if (!majorityMet) {
return (
<>
<span>{t('voteFailedReason')}</span>
<StatusFail>{t('majorityNotMet')}</StatusFail>
<span>&nbsp;{daysClosedAgo}</span>
</>
);
}
return (
<>
<span>{t('voteFailedReason')}</span>
<StatusFail>
{proposal?.errorDetails ||
proposal?.rejectionReason ||
t('unknownReason')}
</StatusFail>
<span>&nbsp;{daysClosedAgo}</span>
</>
);
}
if (
proposal?.state === ProposalState.STATE_ENACTED ||
proposal?.state === ProposalState.STATE_PASSED
) {
return (
<>
<span>{t('votePassed')}</span>
<StatusPass>
&nbsp;
{proposal?.state === ProposalState.STATE_ENACTED
? t('Enacted')
: t('Passed')}
</StatusPass>
<span>
&nbsp;
{proposal?.state === ProposalState.STATE_ENACTED
? daysEnactedAgo
: daysClosedAgo}
</span>
</>
);
}
if (proposal?.state === ProposalState.STATE_WAITING_FOR_NODE_VOTE) {
return (
<WillPass willPass={willPassByTokenVote}>
<span>{t('WaitingForNodeVote')}</span>{' '}
<span>{t('currentlySetTo')}</span>
</WillPass>
);
}
return null;
};
@@ -0,0 +1 @@
export { CurrentProposalStatus } from './current-proposal-status';
@@ -6,10 +6,11 @@ import {
KeyValueTableRow,
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import { type Proposal } from '../../types';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
interface ProposalChangeTableProps {
proposal: Proposal;
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}
export const ProposalChangeTable = ({ proposal }: ProposalChangeTableProps) => {
@@ -23,8 +23,8 @@ import { useFeatureFlags } from '@vegaprotocol/environment';
import { BrowserRouter } from 'react-router-dom';
import { VoteState } from '../vote-details/use-user-vote';
import { useNewTransferProposalDetails } from '@vegaprotocol/proposals';
import { type MockedResponse } from '@apollo/client/testing';
import { type Proposal } from '../../types';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MockedResponse } from '@apollo/client/testing';
jest.mock('@vegaprotocol/proposals', () => ({
...jest.requireActual('@vegaprotocol/proposals'),
@@ -36,7 +36,7 @@ jest.mock('@vegaprotocol/proposals', () => ({
}));
const renderComponent = (
proposal: Proposal,
proposal: ProposalQuery['proposal'],
isListItem = true,
mocks: MockedResponse[] = [],
voteState?: VoteState
@@ -64,7 +64,6 @@ describe('Proposal header', () => {
it('Renders New market proposal', () => {
useFeatureFlags.setState({ flags: { SUCCESSOR_MARKETS: true } });
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
rationale: {
title: 'New some market',
@@ -103,7 +102,6 @@ describe('Proposal header', () => {
it('Renders Update market proposal', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
rationale: {
title: 'New market id',
@@ -132,7 +130,6 @@ describe('Proposal header', () => {
it('Renders New asset proposal - ERC20', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
rationale: {
title: 'New asset: Fake currency',
@@ -162,7 +159,6 @@ describe('Proposal header', () => {
it('Renders New asset proposal - BuiltInAsset', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
terms: {
change: {
@@ -188,7 +184,6 @@ describe('Proposal header', () => {
it('Renders Update network', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
rationale: {
title: 'Network parameter',
@@ -218,7 +213,6 @@ describe('Proposal header', () => {
it('Renders Freeform proposal - short rationale', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
id: 'short',
rationale: {
@@ -240,7 +234,6 @@ describe('Proposal header', () => {
it('Renders Freeform proposal - long rationale (105 chars) - listing', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
id: 'long',
rationale: {
@@ -266,7 +259,6 @@ describe('Proposal header', () => {
// Remove once proposals have rationale and re-enable above tests
it('Renders Freeform proposal - id for title', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
id: 'freeform id',
rationale: {
@@ -288,7 +280,6 @@ describe('Proposal header', () => {
it('Renders asset change proposal header', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
terms: {
change: {
@@ -306,7 +297,6 @@ describe('Proposal header', () => {
it("Renders unknown proposal if it's a different proposal type", () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
terms: {
change: {
@@ -323,7 +313,6 @@ describe('Proposal header', () => {
it('Renders proposal state: Enacted', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
state: ProposalState.STATE_ENACTED,
terms: {
@@ -336,7 +325,6 @@ describe('Proposal header', () => {
it('Renders proposal state: Passed', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
@@ -350,7 +338,6 @@ describe('Proposal header', () => {
it('Renders proposal state: Waiting for node vote', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
state: ProposalState.STATE_WAITING_FOR_NODE_VOTE,
terms: {
@@ -365,7 +352,6 @@ describe('Proposal header', () => {
it('Renders proposal state: Open', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
state: ProposalState.STATE_OPEN,
votes: {
@@ -2,7 +2,8 @@ import { useTranslation } from 'react-i18next';
import { Lozenge, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { shorten } from '@vegaprotocol/utils';
import { Heading, SubHeading } from '../../../../components/heading';
import { type ReactNode } from 'react';
import type { ReactNode } from 'react';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { truncateMiddle } from '../../../../lib/truncate-middle';
import { CurrentProposalState } from '../current-proposal-state';
import { ProposalInfoLabel } from '../proposal-info-label';
@@ -14,17 +15,16 @@ import {
import { useFeatureFlags } from '@vegaprotocol/environment';
import Routes from '../../../routes';
import { Link } from 'react-router-dom';
import { type VoteState } from '../vote-details/use-user-vote';
import type { VoteState } from '../vote-details/use-user-vote';
import { VoteBreakdown } from '../vote-breakdown';
import { GovernanceTransferKindMapping } from '@vegaprotocol/types';
import { type Proposal } from '../../types';
export const ProposalHeader = ({
proposal,
isListItem = true,
voteState,
}: {
proposal: Proposal;
proposal: ProposalQuery['proposal'];
isListItem?: boolean;
voteState?: VoteState | null;
}) => {
@@ -40,7 +40,7 @@ export const ProposalHeader = ({
const titleContent = shorten(title ?? '', 100);
const getAsset = (proposal: Proposal) => {
const getAsset = (proposal: ProposalQuery['proposal']) => {
const terms = proposal?.terms;
if (
terms?.change.__typename === 'NewMarket' &&
@@ -1,4 +1,5 @@
import { useTranslation } from 'react-i18next';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import {
KeyValueTable,
KeyValueTableRow,
@@ -13,10 +14,9 @@ import {
} from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
import { useAppState } from '../../../../contexts/app-state/app-state-context';
import { type Proposal } from '../../types';
interface ProposalReferralProgramDetailsProps {
proposal: Proposal | null;
proposal: ProposalQuery['proposal'];
}
export const formatEndOfProgramTimestamp = (value: string) => {
@@ -1,4 +1,6 @@
import { useTranslation } from 'react-i18next';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import { useCancelTransferProposalDetails } from '@vegaprotocol/proposals';
import {
KeyValueTable,
@@ -6,12 +8,11 @@ import {
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { type Proposal } from '../../types';
export const ProposalCancelTransferDetails = ({
proposal,
}: {
proposal: Proposal;
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}) => {
const { t } = useTranslation();
const details = useCancelTransferProposalDetails(proposal?.id);
@@ -1,4 +1,6 @@
import { useState } from 'react';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import { SubHeading } from '../../../../components/heading';
import { useTranslation } from 'react-i18next';
@@ -19,12 +21,11 @@ import {
addDecimalsFormatNumberQuantum,
formatDateWithLocalTimezone,
} from '@vegaprotocol/utils';
import { type Proposal } from '../../types';
export const ProposalTransferDetails = ({
proposal,
}: {
proposal: Proposal;
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}) => {
const { t } = useTranslation();
const [show, setShow] = useState(false);
@@ -1,4 +1,5 @@
import { useTranslation } from 'react-i18next';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import {
KeyValueTable,
KeyValueTableRow,
@@ -11,7 +12,6 @@ import {
} from '../proposal-referral-program-details';
import { formatNumberPercentage } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
import { type Proposal } from '../../types';
// These types are not generated as it's not known how dynamic these are
type VestingBenefitTier = {
@@ -43,7 +43,7 @@ export const formatVolumeDiscountFactor = (value: string) => {
};
interface ProposalReferralProgramDetailsProps {
proposal: Proposal | null;
proposal: ProposalQuery['proposal'];
}
/**
@@ -5,13 +5,13 @@ import {
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import { Row } from '@vegaprotocol/markets';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { useState } from 'react';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import { SubHeading } from '../../../../components/heading';
import { type Proposal } from '../../types';
interface ProposalUpdateMarketStateProps {
proposal: Proposal | null;
proposal: ProposalQuery['proposal'];
}
export const ProposalUpdateMarketState = ({
@@ -1,4 +1,5 @@
import { useTranslation } from 'react-i18next';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import {
KeyValueTable,
KeyValueTableRow,
@@ -11,10 +12,9 @@ import {
} from '../proposal-referral-program-details';
import { formatNumberPercentage } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
import { type Proposal } from '../../types';
interface ProposalReferralProgramDetailsProps {
proposal: Proposal | null;
proposal: ProposalQuery['proposal'];
}
export const formatVolumeDiscountFactor = (value: string) => {
@@ -1,13 +1,13 @@
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
import { VegaWalletProvider } from '@vegaprotocol/wallet';
import { type VegaWalletConfig } from '@vegaprotocol/wallet';
import type { VegaWalletConfig } from '@vegaprotocol/wallet';
import { render, screen } from '@testing-library/react';
import { generateProposal } from '../../test-helpers/generate-proposals';
import { Proposal } from './proposal';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { ProposalState } from '@vegaprotocol/types';
import { mockNetworkParams } from '../../test-helpers/mocks';
import { type Proposal as IProposal } from '../../types';
jest.mock('@vegaprotocol/network-parameters', () => ({
...jest.requireActual('@vegaprotocol/network-parameters'),
@@ -50,14 +50,14 @@ const vegaWalletConfig: VegaWalletConfig = {
},
};
const renderComponent = (proposal: IProposal) => {
const renderComponent = (proposal: ProposalQuery['proposal']) => {
render(
<MemoryRouter>
<MockedProvider>
<VegaWalletProvider config={vegaWalletConfig}>
<Proposal
restData={{}}
proposal={proposal}
proposal={proposal as ProposalQuery['proposal']}
networkParams={mockNetworkParams}
/>
</VegaWalletProvider>
@@ -12,13 +12,14 @@ import { UserVote } from '../vote-details';
import { ListAsset } from '../list-asset';
import Routes from '../../../routes';
import { ProposalMarketData } from '../proposal-market-data';
import { type MarketInfo } from '@vegaprotocol/markets';
import { type AssetQuery } from '@vegaprotocol/assets';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MarketInfo } from '@vegaprotocol/markets';
import type { AssetQuery } from '@vegaprotocol/assets';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import { ProposalState } from '@vegaprotocol/types';
import { ProposalMarketChanges } from '../proposal-market-changes';
import { ProposalUpdateMarketState } from '../proposal-update-market-state';
import { type NetworkParamsResult } from '@vegaprotocol/network-parameters';
import type { NetworkParamsResult } from '@vegaprotocol/network-parameters';
import { useVoteSubmit } from '@vegaprotocol/proposals';
import { useUserVote } from '../vote-details/use-user-vote';
import {
@@ -27,10 +28,9 @@ import {
} from '../proposal-transfer';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers';
import { type Proposal as IProposal } from '../../types';
export interface ProposalProps {
proposal: IProposal;
proposal: ProposalQuery['proposal'];
networkParams: Partial<NetworkParamsResult>;
marketData?: MarketInfo | null;
parentMarketData?: MarketInfo | null;
@@ -1,7 +1,7 @@
import { BrowserRouter as Router } from 'react-router-dom';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { type MockedResponse } from '@apollo/client/testing';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import { render, screen } from '@testing-library/react';
import { format } from 'date-fns';
@@ -18,10 +18,10 @@ import {
lastWeek,
nextWeek,
} from '../../test-helpers/mocks';
import { type Proposal } from '../../types';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
const renderComponent = (
proposal: Proposal,
proposal: ProposalQuery['proposal'],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mocks: MockedResponse<any>[] = [networkParamsQueryMock]
) =>
@@ -1,20 +1,21 @@
import { type ReactNode } from 'react';
import { Link } from 'react-router-dom';
import { Button } from '@vegaprotocol/ui-toolkit';
import { differenceInHours, format, formatDistanceToNowStrict } from 'date-fns';
import { useTranslation } from 'react-i18next';
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
import type { ReactNode } from 'react';
import {
ProposalRejectionReasonMapping,
ProposalState,
} from '@vegaprotocol/types';
import Routes from '../../../routes';
import { type Proposal } from '../../types';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
export const ProposalsListItemDetails = ({
proposal,
}: {
proposal: Proposal;
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}) => {
const { t } = useTranslation();
const state = proposal?.state;
@@ -2,10 +2,10 @@ import { RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { ProposalHeader } from '../proposal-detail-header/proposal-header';
import { ProposalsListItemDetails } from './proposals-list-item-details';
import { useUserVote } from '../vote-details/use-user-vote';
import { type Proposal } from '../../types';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
interface ProposalsListItemProps {
proposal?: Proposal | null;
proposal?: ProposalQuery['proposal'] | null;
}
export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => {
@@ -17,8 +17,8 @@ import {
lastMonth,
nextMonth,
} from '../../test-helpers/mocks';
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { type Proposal } from '../../types';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
const openProposalClosesNextMonth = generateProposal({
id: 'proposal1',
@@ -63,7 +63,7 @@ const closedProtocolUpgradeProposal = generateProtocolUpgradeProposal({
});
const renderComponent = (
proposals: Proposal,
proposals: ProposalQuery['proposal'][],
protocolUpgradeProposals?: ProtocolUpgradeProposalFieldsFragment[]
) => (
<Router>
@@ -10,20 +10,20 @@ import Routes from '../../../routes';
import { Button, Toggle } from '@vegaprotocol/ui-toolkit';
import { Link } from 'react-router-dom';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { ExternalLinks } from '@vegaprotocol/environment';
import { type ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { type Proposal } from '../../types';
interface ProposalsListProps {
proposals: Proposal[];
proposals: Array<ProposalQuery['proposal']>;
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
lastBlockHeight?: string;
}
interface SortedProposalsProps {
open: Proposal[];
closed: Proposal[];
open: ProposalQuery['proposal'][];
closed: ProposalQuery['proposal'][];
}
interface SortedProtocolUpgradeProposalsProps {
@@ -31,7 +31,7 @@ interface SortedProtocolUpgradeProposalsProps {
closed: ProtocolUpgradeProposalFieldsFragment[];
}
export const orderByDate = (arr: Proposal[]) =>
export const orderByDate = (arr: ProposalQuery['proposal'][]) =>
orderBy(
arr,
[
@@ -91,10 +91,14 @@ export const ProposalsList = ({
);
return {
open:
initialSorting.open.length > 0 ? orderByDate(initialSorting.open) : [],
initialSorting.open.length > 0
? orderByDate(initialSorting.open as ProposalQuery['proposal'][])
: [],
closed:
initialSorting.closed.length > 0
? orderByDate(initialSorting.closed).reverse()
? orderByDate(
initialSorting.closed as ProposalQuery['proposal'][]
).reverse()
: [],
};
}, [proposals]);
@@ -121,7 +125,9 @@ export const ProposalsList = ({
};
}, [protocolUpgradeProposals, lastBlockHeight]);
const filterPredicate = (p: ProposalFieldsFragment | Proposal) =>
const filterPredicate = (
p: ProposalFieldsFragment | ProposalQuery['proposal']
) =>
p?.id?.includes(filterString) ||
p?.party?.id?.toString().includes(filterString);
@@ -12,7 +12,7 @@ import {
nextWeek,
lastMonth,
} from '../../test-helpers/mocks';
import { type Proposal } from '../../types';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
const rejectedProposalClosesNextWeek = generateProposal({
id: 'rejected1',
@@ -35,7 +35,7 @@ const rejectedProposalClosedLastMonth = generateProposal({
},
});
const renderComponent = (proposals: Proposal[]) => (
const renderComponent = (proposals: ProposalQuery['proposal'][]) => (
<Router>
<MockedProvider mocks={[networkParamsQueryMock]}>
<AppStateProvider>
@@ -3,17 +3,17 @@ import { useTranslation } from 'react-i18next';
import { Heading } from '../../../../components/heading';
import { ProposalsListItem } from '../proposals-list-item';
import { ProposalsListFilter } from '../proposals-list-filter';
import { type Proposal } from '../../types';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
interface ProposalsListProps {
proposals: Proposal[];
proposals: ProposalQuery['proposal'][];
}
export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
const { t } = useTranslation();
const [filterString, setFilterString] = useState('');
const filterPredicate = (p: Proposal) =>
const filterPredicate = (p: ProposalQuery['proposal']) =>
p?.id?.includes(filterString) ||
p?.party?.id?.toString().includes(filterString);
@@ -9,7 +9,8 @@ import {
nextWeek,
} from '../../test-helpers/mocks';
import { CompactVotes, VoteBreakdown } from './vote-breakdown';
import { type MockedResponse } from '@apollo/client/testing';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MockedResponse } from '@apollo/client/testing';
import {
generateNoVotes,
generateProposal,
@@ -17,8 +18,7 @@ import {
} from '../../test-helpers/generate-proposals';
import { ProposalState } from '@vegaprotocol/types';
import { BigNumber } from '../../../../lib/bignumber';
import { type AppState } from '../../../../contexts/app-state/app-state-context';
import { type Proposal } from '../../types';
import type { AppState } from '../../../../contexts/app-state/app-state-context';
const mockTotalSupply = new BigNumber(100);
// Note - giving a fixedTokenValue of 1 means a ratio of 1:1 votes to tokens, making sums easier :)
@@ -41,7 +41,7 @@ jest.mock('../../../../contexts/app-state/app-state-context', () => ({
}));
const renderComponent = (
proposal: Proposal,
proposal: ProposalQuery['proposal'],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mocks: MockedResponse<any>[] = [networkParamsQueryMock]
) =>
@@ -281,8 +281,8 @@ describe('VoteBreakdown', () => {
});
it('Progress bar displays status - LP majority', () => {
const yesVotesLP = 0.8;
const noVotesLP = 0.2;
const yesVotesLP = 800;
const noVotesLP = 200;
const expectedProgress = (yesVotesLP / (yesVotesLP + noVotesLP)) * 100; // 80%
renderComponent(
@@ -1,4 +1,3 @@
import { type ReactNode } from 'react';
import classNames from 'classnames';
import BigNumber from 'bignumber.js';
import { useTranslation } from 'react-i18next';
@@ -6,8 +5,10 @@ import { useVoteInformation } from '../../hooks';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '@vegaprotocol/utils';
import { ProposalState } from '@vegaprotocol/types';
import type { ReactNode } from 'react';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { CompactNumber } from '@vegaprotocol/react-helpers';
import { type Proposal } from '../../types';
export const CompactVotes = ({ number }: { number: BigNumber }) => (
<CompactNumber
@@ -19,7 +20,7 @@ export const CompactVotes = ({ number }: { number: BigNumber }) => (
);
interface VoteBreakdownProps {
proposal: Proposal;
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}
interface VoteProgressProps {
@@ -104,6 +105,8 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
yesLPPercentage,
yesTokens,
noTokens,
yesEquityLikeShareWeight,
noEquityLikeShareWeight,
totalEquityLikeShareWeight,
requiredMajorityPercentage,
requiredMajorityLPPercentage,
@@ -132,7 +135,6 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
.multipliedBy(100),
new BigNumber(100)
);
const willPass = willPassByTokenVote || willPassByLPVote;
const updateMarketVotePassMethod = willPassByTokenVote
? t('byTokenVote')
@@ -200,24 +202,50 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
<div className="flex items-center gap-1">
<span>{t('liquidityProviderVotesFor')}:</span>
<Tooltip
description={
<span>{yesLPPercentage.toFixed(defaultDP)}%</span>
}
description={formatNumber(
yesEquityLikeShareWeight,
defaultDP
)}
>
<button>{yesLPPercentage.toFixed(1)}%</button>
<button>
<CompactVotes number={yesEquityLikeShareWeight} />
</button>
</Tooltip>
<span>
(
<Tooltip
description={
<span>{yesLPPercentage.toFixed(defaultDP)}%</span>
}
>
<button>{yesLPPercentage.toFixed(0)}%</button>
</Tooltip>
)
</span>
</div>
<div className="flex items-center gap-1">
<span>{t('liquidityProviderVotesAgainst')}:</span>
<Tooltip
description={formatNumber(
noEquityLikeShareWeight,
defaultDP
)}
>
<button>
<CompactVotes number={noEquityLikeShareWeight} />
</button>
</Tooltip>
<span>
(
<Tooltip
description={
<span>{noLPPercentage.toFixed(defaultDP)}%</span>
}
>
<button>{noLPPercentage.toFixed(1)}%</button>
<button>{noLPPercentage.toFixed(0)}%</button>
</Tooltip>
)
</span>
</div>
</div>
@@ -254,8 +282,13 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
defaultDP
)}
>
<span>{totalEquityLikeShareWeight.toFixed(1)}%</span>
<button>
<CompactVotes number={totalEquityLikeShareWeight} />
</button>
</Tooltip>
<span>
({totalEquityLikeShareWeight.toFixed(defaultDP)}%)
</span>
</div>
</div>
</section>
@@ -5,13 +5,14 @@ import { ProposalState } from '@vegaprotocol/types';
import { ConnectToVega } from '../../../../components/connect-to-vega';
import { VoteButtonsContainer } from './vote-buttons';
import { SubHeading } from '../../../../components/heading';
import { type VoteValue } from '@vegaprotocol/types';
import { type DialogProps, type VegaTxState } from '@vegaprotocol/proposals';
import { type VoteState } from './use-user-vote';
import { type Proposal } from '../../types';
import type { VoteValue } from '@vegaprotocol/types';
import type { DialogProps, VegaTxState } from '@vegaprotocol/proposals';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { VoteState } from './use-user-vote';
interface UserVoteProps {
proposal: Proposal;
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
minVoterBalance: string | null | undefined;
spamProtectionMinTokens: string | null | undefined;
transaction: VegaTxState | null;
@@ -3,12 +3,13 @@ import {
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { BigNumber } from '../../../lib/bignumber';
import { type Proposal } from '../types';
import type { ProposalFieldsFragment } from '../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../proposal/__generated__/Proposal';
export const useProposalNetworkParams = ({
proposal,
}: {
proposal: Proposal;
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}) => {
const { params } = useNetworkParams([
NetworkParams.governance_proposal_updateMarket_requiredMajority,
@@ -54,8 +54,8 @@ describe('use-vote-information', () => {
it('returns all required vote information', () => {
const yesVotes = 40;
const noVotes = 60;
const yesEquityLikeShareWeight = '0.30';
const noEquityLikeShareWeight = '0.70';
const yesEquityLikeShareWeight = '30';
const noEquityLikeShareWeight = '70';
// Note - giving a fixedTokenValue of 1 means a ratio of 1:1 votes to tokens, making sums easier :)
const fixedTokenValue = 1000000000000000000;
@@ -195,10 +195,10 @@ describe('use-vote-information', () => {
});
it('correctly shows whether an update market proposal will pass by token or LP vote - both failing', () => {
const yesVotes = 0.2;
const noVotes = 0.7;
const yesEquityLikeShareWeight = '0.30';
const noEquityLikeShareWeight = '0.60';
const yesVotes = 20;
const noVotes = 70;
const yesEquityLikeShareWeight = '30';
const noEquityLikeShareWeight = '60';
const fixedTokenValue = 1000000000000000000;
const proposal = generateProposal({
@@ -2,10 +2,15 @@ import { useMemo } from 'react';
import { useAppState } from '../../../contexts/app-state/app-state-context';
import { BigNumber } from '../../../lib/bignumber';
import { useProposalNetworkParams } from './use-proposal-network-params';
import type { ProposalFieldsFragment } from '../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../proposal/__generated__/Proposal';
import { addDecimal } from '@vegaprotocol/utils';
import { type Proposal } from '../types';
export const useVoteInformation = ({ proposal }: { proposal: Proposal }) => {
export const useVoteInformation = ({
proposal,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}) => {
const {
appState: { totalSupply, decimals },
} = useAppState();
@@ -56,7 +61,7 @@ export const useVoteInformation = ({ proposal }: { proposal: Proposal }) => {
const noEquityLikeShareWeight = !proposal?.votes.no
.totalEquityLikeShareWeight
? new BigNumber(0)
: new BigNumber(proposal.votes.no.totalEquityLikeShareWeight).times(100);
: new BigNumber(proposal.votes.no.totalEquityLikeShareWeight);
const yesTokens = new BigNumber(
addDecimal(proposal?.votes.yes.totalTokens ?? 0, decimals)
@@ -65,7 +70,7 @@ export const useVoteInformation = ({ proposal }: { proposal: Proposal }) => {
const yesEquityLikeShareWeight = !proposal?.votes.yes
.totalEquityLikeShareWeight
? new BigNumber(0)
: new BigNumber(proposal.votes.yes.totalEquityLikeShareWeight).times(100);
: new BigNumber(proposal.votes.yes.totalEquityLikeShareWeight);
const totalTokensVoted = yesTokens.plus(noTokens);
@@ -76,7 +81,12 @@ export const useVoteInformation = ({ proposal }: { proposal: Proposal }) => {
const yesPercentage = totalTokensVoted.isZero()
? new BigNumber(0)
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
const yesLPPercentage = yesEquityLikeShareWeight;
const yesLPPercentage = totalEquityLikeShareWeight.isZero()
? new BigNumber(0)
: yesEquityLikeShareWeight
.multipliedBy(100)
.dividedBy(totalEquityLikeShareWeight);
const noPercentage = totalTokensVoted.isZero()
? new BigNumber(0)
@@ -93,7 +103,9 @@ export const useVoteInformation = ({ proposal }: { proposal: Proposal }) => {
);
const participationLPMet = requiredParticipationLP
? totalEquityLikeShareWeight.isGreaterThan(requiredParticipationLP)
? totalEquityLikeShareWeight.isGreaterThan(
totalSupply.multipliedBy(requiredParticipationLP)
)
: false;
const majorityMet = yesPercentage.isGreaterThanOrEqualTo(
@@ -108,7 +120,9 @@ export const useVoteInformation = ({ proposal }: { proposal: Proposal }) => {
.multipliedBy(100)
.dividedBy(totalSupply);
const totalLPTokensPercentage = totalEquityLikeShareWeight;
const totalLPTokensPercentage = totalEquityLikeShareWeight
.multipliedBy(100)
.dividedBy(totalSupply);
const willPassByTokenVote =
participationMet &&
@@ -86,65 +86,143 @@ query Proposal(
$includeUpdateReferralProgram: Boolean!
) {
proposal(id: $proposalId) {
... on Proposal {
id
rationale {
title
description
}
reference
state
datetime
rejectionReason
party {
id
rationale {
title
description
}
reference
state
datetime
rejectionReason
party {
id
}
errorDetails
...NewMarketProductField @include(if: $includeNewMarketProductField)
...UpdateMarketState @include(if: $includeUpdateMarketState)
...UpdateReferralProgram @include(if: $includeUpdateReferralProgram)
...UpdateVolumeDiscountProgram
terms {
closingDatetime
enactmentDatetime
change {
... on NewMarket {
decimalPlaces
metadata
riskParameters {
... on LogNormalRiskModel {
riskAversionParameter
tau
params {
mu
r
sigma
}
}
... on SimpleRiskModel {
params {
factorLong
factorShort
}
}
errorDetails
...NewMarketProductField @include(if: $includeNewMarketProductField)
...UpdateMarketState @include(if: $includeUpdateMarketState)
...UpdateReferralProgram @include(if: $includeUpdateReferralProgram)
...UpdateVolumeDiscountProgram
terms {
closingDatetime
enactmentDatetime
change {
... on NewMarket {
decimalPlaces
metadata
riskParameters {
... on LogNormalRiskModel {
riskAversionParameter
tau
params {
mu
r
sigma
}
}
... on SimpleRiskModel {
params {
factorLong
factorShort
}
}
}
instrument {
name
code
product {
... on FutureProduct {
settlementAsset {
id
name
symbol
decimals
quantum
}
quoteName
dataSourceSpecBinding {
settlementDataProperty
tradingTerminationProperty
}
dataSourceSpecForSettlementData {
sourceType {
... on DataSourceDefinitionInternal {
sourceType {
... on DataSourceSpecConfigurationTime {
conditions {
operator
value
}
}
}
}
... on DataSourceDefinitionExternal {
sourceType {
... on DataSourceSpecConfiguration {
signers {
signer {
... on PubKey {
key
}
... on ETHAddress {
address
}
}
}
filters {
key {
name
type
}
conditions {
operator
value
}
}
}
}
}
}
}
}
... on PerpetualProduct {
settlementAsset {
id
name
symbol
decimals
quantum
}
quoteName
}
}
}
priceMonitoringParameters {
triggers {
horizonSecs
probability
auctionExtensionSecs
}
}
liquidityMonitoringParameters {
triggeringRatio
targetStakeParameters {
timeWindow
scalingFactor
}
}
positionDecimalPlaces
linearSlippageFactor
quadraticSlippageFactor
}
... on UpdateMarket {
marketId
updateMarketConfiguration {
instrument {
name
code
product {
... on FutureProduct {
settlementAsset {
id
name
symbol
decimals
quantum
}
... on UpdateFutureProduct {
quoteName
dataSourceSpecBinding {
settlementDataProperty
tradingTerminationProperty
}
dataSourceSpecForSettlementData {
sourceType {
... on DataSourceDefinitionInternal {
@@ -185,19 +263,101 @@ query Proposal(
}
}
}
}
... on PerpetualProduct {
settlementAsset {
id
name
symbol
decimals
quantum
# dataSourceSpecForTradingTermination {
# sourceType {
# ... on DataSourceDefinitionInternal {
# sourceType {
# ... on DataSourceSpecConfigurationTime {
# conditions {
# operator
# value
# }
# }
# }
# }
# ... on DataSourceDefinitionExternal {
# sourceType {
# ... on DataSourceSpecConfiguration {
# signers {
# signer {
# ... on PubKey {
# key
# }
# ... on ETHAddress {
# address
# }
# }
# }
# filters {
# key {
# name
# type
# }
# conditions {
# operator
# value
# }
# }
# }
# }
# }
# }
# }
dataSourceSpecBinding {
settlementDataProperty
tradingTerminationProperty
}
}
... on UpdatePerpetualProduct {
quoteName
dataSourceSpecForSettlementData {
sourceType {
... on DataSourceDefinitionInternal {
sourceType {
... on DataSourceSpecConfigurationTime {
conditions {
operator
value
}
}
}
}
... on DataSourceDefinitionExternal {
sourceType {
... on DataSourceSpecConfiguration {
signers {
signer {
... on PubKey {
key
}
... on ETHAddress {
address
}
}
}
filters {
key {
name
type
}
conditions {
operator
value
}
}
}
}
}
}
}
dataSourceSpecBinding {
settlementDataProperty
settlementScheduleProperty
}
}
}
}
metadata
priceMonitoringParameters {
triggers {
horizonSecs
@@ -206,237 +366,77 @@ query Proposal(
}
}
liquidityMonitoringParameters {
triggeringRatio
targetStakeParameters {
timeWindow
scalingFactor
}
}
positionDecimalPlaces
linearSlippageFactor
quadraticSlippageFactor
}
... on UpdateMarket {
marketId
updateMarketConfiguration {
instrument {
code
product {
... on UpdateFutureProduct {
quoteName
dataSourceSpecForSettlementData {
sourceType {
... on DataSourceDefinitionInternal {
sourceType {
... on DataSourceSpecConfigurationTime {
conditions {
operator
value
}
}
}
}
... on DataSourceDefinitionExternal {
sourceType {
... on DataSourceSpecConfiguration {
signers {
signer {
... on PubKey {
key
}
... on ETHAddress {
address
}
}
}
filters {
key {
name
type
}
conditions {
operator
value
}
}
}
}
}
}
}
# dataSourceSpecForTradingTermination {
# sourceType {
# ... on DataSourceDefinitionInternal {
# sourceType {
# ... on DataSourceSpecConfigurationTime {
# conditions {
# operator
# value
# }
# }
# }
# }
# ... on DataSourceDefinitionExternal {
# sourceType {
# ... on DataSourceSpecConfiguration {
# signers {
# signer {
# ... on PubKey {
# key
# }
# ... on ETHAddress {
# address
# }
# }
# }
# filters {
# key {
# name
# type
# }
# conditions {
# operator
# value
# }
# }
# }
# }
# }
# }
# }
dataSourceSpecBinding {
settlementDataProperty
tradingTerminationProperty
}
}
... on UpdatePerpetualProduct {
quoteName
dataSourceSpecForSettlementData {
sourceType {
... on DataSourceDefinitionInternal {
sourceType {
... on DataSourceSpecConfigurationTime {
conditions {
operator
value
}
}
}
}
... on DataSourceDefinitionExternal {
sourceType {
... on DataSourceSpecConfiguration {
signers {
signer {
... on PubKey {
key
}
... on ETHAddress {
address
}
}
}
filters {
key {
name
type
}
conditions {
operator
value
}
}
}
}
}
}
}
dataSourceSpecBinding {
settlementDataProperty
settlementScheduleProperty
}
}
riskParameters {
... on UpdateMarketSimpleRiskModel {
simple {
factorLong
factorShort
}
}
metadata
priceMonitoringParameters {
triggers {
horizonSecs
probability
auctionExtensionSecs
}
}
liquidityMonitoringParameters {
targetStakeParameters {
timeWindow
scalingFactor
}
}
riskParameters {
... on UpdateMarketSimpleRiskModel {
simple {
factorLong
factorShort
}
}
... on UpdateMarketLogNormalRiskModel {
logNormal {
riskAversionParameter
tau
params {
r
sigma
mu
}
... on UpdateMarketLogNormalRiskModel {
logNormal {
riskAversionParameter
tau
params {
r
sigma
mu
}
}
}
}
}
... on NewAsset {
name
symbol
decimals
quantum
source {
... on BuiltinAsset {
maxFaucetAmountMint
}
... on ERC20 {
contractAddress
lifetimeLimit
withdrawThreshold
}
}
... on NewAsset {
name
symbol
decimals
quantum
source {
... on BuiltinAsset {
maxFaucetAmountMint
}
... on ERC20 {
contractAddress
lifetimeLimit
withdrawThreshold
}
}
... on UpdateNetworkParameter {
networkParameter {
key
value
}
}
... on UpdateNetworkParameter {
networkParameter {
key
value
}
... on UpdateAsset {
quantum
assetId
source {
... on UpdateERC20 {
lifetimeLimit
withdrawThreshold
}
}
... on UpdateAsset {
quantum
assetId
source {
... on UpdateERC20 {
lifetimeLimit
withdrawThreshold
}
}
}
}
votes {
yes {
totalTokens
totalNumber
totalEquityLikeShareWeight
}
no {
totalTokens
totalNumber
totalEquityLikeShareWeight
}
}
votes {
yes {
totalTokens
totalNumber
totalEquityLikeShareWeight
}
no {
totalTokens
totalNumber
totalEquityLikeShareWeight
}
}
}
File diff suppressed because one or more lines are too long
@@ -17,7 +17,6 @@ import {
import { useParentMarketIdQuery } from '@vegaprotocol/markets';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { useSuccessorMarketProposalDetails } from '@vegaprotocol/proposals';
import { type Proposal as IProposal } from '../types';
export const ProposalContainer = () => {
const featureFlags = useFeatureFlags((state) => state.flags);
@@ -68,8 +67,6 @@ export const ProposalContainer = () => {
skip: !params.proposalId,
});
const proposal = data?.proposal as IProposal;
const successor = useSuccessorMarketProposalDetails(params.proposalId);
const isSuccessor = !!successor?.parentMarketId || !!successor.code;
@@ -82,12 +79,12 @@ export const ProposalContainer = () => {
},
} = useFetch(
`${ENV.rest}governance?proposalId=${
proposal?.terms.change.__typename === 'UpdateMarket' &&
proposal.terms.change.marketId
data?.proposal?.terms.change.__typename === 'UpdateMarket' &&
data?.proposal.terms.change.marketId
}`,
undefined,
true,
proposal?.terms.change.__typename !== 'UpdateMarket'
data?.proposal?.terms.change.__typename !== 'UpdateMarket'
);
const {
@@ -100,7 +97,7 @@ export const ProposalContainer = () => {
`${ENV.rest}governances?proposalState=STATE_ENACTED&proposalType=TYPE_UPDATE_MARKET`,
undefined,
true,
proposal?.terms.change.__typename !== 'UpdateMarket'
data?.proposal?.terms.change.__typename !== 'UpdateMarket'
);
const {
@@ -111,8 +108,8 @@ export const ProposalContainer = () => {
dataProvider: marketInfoProvider,
skipUpdates: true,
variables: {
marketId: proposal?.id || '',
skip: !proposal?.id,
marketId: data?.proposal?.id || '',
skip: !data?.proposal?.id,
},
});
@@ -151,22 +148,23 @@ export const ProposalContainer = () => {
fetchPolicy: 'network-only',
variables: {
assetId:
(proposal?.terms.change.__typename === 'NewAsset' && proposal?.id) ||
(proposal?.terms.change.__typename === 'UpdateAsset' &&
proposal.terms.change.assetId) ||
(data?.proposal?.terms.change.__typename === 'NewAsset' &&
data?.proposal?.id) ||
(data?.proposal?.terms.change.__typename === 'UpdateAsset' &&
data.proposal.terms.change.assetId) ||
'',
},
skip: !['NewAsset', 'UpdateAsset'].includes(
proposal?.terms?.change?.__typename || ''
data?.proposal?.terms?.change?.__typename || ''
),
});
useEffect(() => {
if (
previouslyEnactedMarketProposalsRestData &&
proposal?.terms.change.__typename === 'UpdateMarket'
data?.proposal?.terms.change.__typename === 'UpdateMarket'
) {
const change = proposal?.terms?.change as { marketId: string };
const change = data?.proposal?.terms?.change as { marketId: string };
const filteredProposals =
// @ts-ignore rest data is not typed
@@ -190,8 +188,8 @@ export const ProposalContainer = () => {
}, [
previouslyEnactedMarketProposalsRestData,
params.proposalId,
proposal?.terms.change.__typename,
proposal?.terms.change,
data?.proposal?.terms.change.__typename,
data?.proposal?.terms.change,
]);
useEffect(() => {
@@ -244,7 +242,7 @@ export const ProposalContainer = () => {
>
{data?.proposal ? (
<Proposal
proposal={proposal}
proposal={data.proposal}
networkParams={networkParams}
restData={restData}
marketData={marketData}
@@ -95,7 +95,7 @@ export const ProtocolUpgradeProposalContainer = () => {
time={
pending && time ? (
convertToCountdownString(time, '0:00:00:00')
) : blockInfo && 'result' in blockInfo && blockInfo?.result ? (
) : blockInfo?.result ? (
<span title={blockInfo.result.block.header.time}>
{formatDateWithLocalTimezone(
new Date(blockInfo.result.block.header.time)
@@ -8,7 +8,6 @@ import mergeWith from 'lodash/mergeWith';
import { type PartialDeep } from 'type-fest';
import { type ProposalQuery } from '../proposal/__generated__/Proposal';
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { type Proposal } from '../types';
export function generateProtocolUpgradeProposal(
override: PartialDeep<ProtocolUpgradeProposalFieldsFragment> = {}
@@ -44,8 +43,8 @@ export function generateProtocolUpgradeProposal(
}
export function generateProposal(
override: PartialDeep<Proposal> = {}
): Proposal {
override: PartialDeep<ProposalQuery['proposal']> = {}
): ProposalQuery['proposal'] {
const defaultProposal: ProposalQuery['proposal'] = {
__typename: 'Proposal',
id: faker.datatype.uuid(),
@@ -93,16 +92,15 @@ export function generateProposal(
},
};
return mergeWith<Proposal, PartialDeep<Proposal>>(
defaultProposal,
override,
(objValue, srcValue) => {
if (!isArray(objValue)) {
return;
}
return srcValue;
return mergeWith<
ProposalQuery['proposal'],
PartialDeep<ProposalQuery['proposal']>
>(defaultProposal, override, (objValue, srcValue) => {
if (!isArray(objValue)) {
return;
}
);
return srcValue;
});
}
type Vote = Pick<Schema.Vote, '__typename' | 'value' | 'party' | 'datetime'>;
@@ -118,8 +116,7 @@ export const generateYesVotes = (
fixedTokenValue?: number,
totalEquityLikeShareWeight?: string
): Votes => {
const votes = [];
for (let i = 0; i < numberOfVotes; i++) {
const votes = Array.from(Array(numberOfVotes)).map(() => {
const vote: Vote = {
__typename: 'Vote',
value: Schema.VoteValue.VALUE_YES,
@@ -155,9 +152,8 @@ export const generateYesVotes = (
datetime: faker.date.past().toISOString(),
};
votes.push(vote);
}
return vote;
});
return {
__typename: 'ProposalVoteSide',
totalNumber: votes.length.toString(),
@@ -176,8 +172,7 @@ export const generateNoVotes = (
fixedTokenValue?: number,
totalEquityLikeShareWeight?: string
): Votes => {
const votes = [];
for (let i = 0; i < numberOfVotes; i++) {
const votes = Array.from(Array(numberOfVotes)).map(() => {
const vote: Vote = {
__typename: 'Vote',
value: Schema.VoteValue.VALUE_NO,
@@ -212,9 +207,8 @@ export const generateNoVotes = (
},
datetime: faker.date.past().toISOString(),
};
votes.push(vote);
}
return vote;
});
return {
__typename: 'ProposalVoteSide',
totalNumber: votes.length.toString(),
@@ -1,11 +0,0 @@
import type { ProposalQuery } from './proposal/__generated__/Proposal';
/**
* The default Proposal type needs extracting from the ProposalNode union type
* as lots of fields on the original type don't exist on BatchProposal. Eventually
* we will support BatchProposal but for now we don't
*/
export type Proposal = Extract<
ProposalQuery['proposal'],
{ __typename?: 'Proposal' }
>;
-1
View File
@@ -25,4 +25,3 @@ NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_REFERRALS=true
NX_TEAM_COMPETITION=true
+3 -5
View File
@@ -22,12 +22,10 @@ NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_REFERRALS=true
# NX_DISABLE_CLOSE_POSITION=false
NX_REFERRALS=false
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
NX_DISABLE_CLOSE_POSITION=true
+1 -4
View File
@@ -24,7 +24,4 @@ NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_REFERRALS=true
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
NX_REFERRALS=true
+1 -3
View File
@@ -23,11 +23,9 @@ NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_REFERRALS=true
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
-2
View File
@@ -54,5 +54,3 @@ To run the UI automation tests with a mocked API, run:
```bash
yarn nx run trading-e2e:e2e
```
To run tests with market sim please read [the readme](e2e/README.md).
@@ -1,332 +0,0 @@
import { useSearchParams } from 'react-router-dom';
import { Controller, useForm } from 'react-hook-form';
import {
Intent,
TextArea,
TradingAnchorButton,
TradingButton,
TradingCheckbox,
TradingFormGroup,
TradingInput,
TradingInputError,
} from '@vegaprotocol/ui-toolkit';
import {
useVegaWallet,
type CreateReferralSet,
type Status,
useVegaWalletDialogStore,
} from '@vegaprotocol/wallet';
import {
addDecimalsFormatNumber,
isValidVegaPublicKey,
URL_REGEX,
} from '@vegaprotocol/utils';
import { useT } from '../../lib/use-t';
import { useCreateReferralSet } from '../../lib/hooks/use-create-referral-set';
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 { Links } from '../../lib/links';
interface FormFields {
name: string;
url: string;
avatarUrl: string;
private: boolean;
allowList: string;
}
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">
<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="lg:gap-6 container p-4 mx-auto">
<div className="mx-auto md:w-2/3 max-w-xl">
<Box className="flex flex-col gap-4">
<h1 className="calt text-2xl lg:text-3xl xl:text-5xl">
{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>
</div>
</div>
</ErrorBoundary>
);
};
const CreateTeamFormContainer = ({ isSolo }: { isSolo: boolean }) => {
const t = useT();
const createLink = useLinks(DApp.Governance);
const { err, status, code, isEligible, requiredStake, onSubmit } =
useCreateReferralSet({
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">
<p className="text-sm">{t('Team creation transaction successful')}</p>
{code && (
<>
<p className="text-sm">
Your team ID is:{' '}
<span className="font-mono break-all">{code}</span>
</p>
<TradingAnchorButton
href={Links.COMPETITIONS_TEAM(code)}
intent={Intent.Info}
size="small"
>
{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 (
<CreateTeamForm
onSubmit={onSubmit}
status={status}
err={err}
isSolo={isSolo}
/>
);
};
const CreateTeamForm = ({
status,
err,
isSolo,
onSubmit,
}: {
status: ReturnType<typeof useCreateReferralSet>['status'];
err: ReturnType<typeof useCreateReferralSet>['err'];
isSolo: boolean;
onSubmit: (tx: CreateReferralSet) => void;
}) => {
const t = useT();
const {
register,
handleSubmit,
control,
watch,
formState: { errors },
} = useForm<FormFields>({
defaultValues: {
private: isSolo,
},
});
const isPrivate = watch('private');
const createTeam = (fields: FormFields) => {
onSubmit({
createReferralSet: {
isTeam: true,
team: {
name: fields.name,
teamUrl: fields.url,
avatarUrl: fields.avatarUrl,
closed: fields.private,
allowList: fields.private ? parseAllowListText(fields.allowList) : [],
},
},
});
};
return (
<form onSubmit={handleSubmit(createTeam)}>
<TradingFormGroup label={t('Team name')} labelFor="name">
<TradingInput {...register('name', { required: t('Required') })} />
{errors.name?.message && (
<TradingInputError forInput="name">
{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') },
})}
/>
{errors.url?.message && (
<TradingInputError forInput="url">
{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'),
},
})}
/>
{errors.avatarUrl?.message && (
<TradingInputError forInput="avatarUrl">
{errors.avatarUrl.message}
</TradingInputError>
)}
</TradingFormGroup>
<TradingFormGroup
label={t('Make team private')}
labelFor="private"
hideLabel={true}
>
<Controller
name="private"
control={control}
render={({ field }) => {
return (
<TradingCheckbox
label={t('Make team private')}
checked={field.value}
onCheckedChange={(value) => {
field.onChange(value);
}}
disabled={isSolo}
/>
);
}}
/>
</TradingFormGroup>
{isPrivate && (
<TradingFormGroup
label={t('Public key allow list')}
labelFor="allowList"
labelDescription={t(
'Use a comma separated list to allow only specific public keys to join the team'
)}
>
<TextArea
{...register('allowList', {
required: t('Required'),
disabled: isSolo,
validate: {
allowList: (value) => {
const publicKeys = parseAllowListText(value);
if (publicKeys.every((pk) => isValidVegaPublicKey(pk))) {
return true;
}
return t('Invalid public key found in allow list');
},
},
})}
/>
{errors.allowList?.message && (
<TradingInputError forInput="avatarUrl">
{errors.allowList.message}
</TradingInputError>
)}
</TradingFormGroup>
)}
{err && <p className="text-danger text-xs mb-4 capitalize">{err}</p>}
<SubmitButton status={status} />
</form>
);
};
const SubmitButton = ({ status }: { status: Status }) => {
const t = useT();
const disabled = status === 'pending' || status === 'requested';
let text = t('Create');
if (status === 'requested') {
text = t('Confirm in wallet...');
} else if (status === 'pending') {
text = t('Confirming transaction...');
}
return (
<TradingButton type="submit" intent={Intent.Info} disabled={disabled}>
{text}
</TradingButton>
);
};
const parseAllowListText = (str: string) => {
return str
.split(',')
.map((v) => v.trim())
.filter(Boolean);
};
@@ -1,133 +0,0 @@
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 './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 './hooks/use-teams';
import take from 'lodash/take';
import { usePageTitle } from '../../lib/hooks/use-page-title';
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({
sortByField: ['totalQuantumRewards'],
order: 'desc',
});
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>
{/** Get started */}
<h2 className="text-2xl mb-6">{t('Get started')}</h2>
<CompetitionsActionsContainer>
<CompetitionsAction
variant="A"
title={t('Create a team')}
description={t(
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
)}
actionElement={
<TradingButton
intent={Intent.Primary}
onClick={(e) => {
e.preventDefault();
navigate(Links.COMPETITIONS_CREATE_TEAM());
}}
>
{t('Create a public team')}
</TradingButton>
}
/>
<CompetitionsAction
variant="B"
title={t('Solo team / lone wolf')}
description={t(
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
)}
actionElement={
<TradingButton
intent={Intent.Primary}
onClick={(e) => {
e.preventDefault();
navigate(Links.COMPETITIONS_CREATE_TEAM_SOLO());
}}
>
{t('Create a private team')}
</TradingButton>
}
/>
<CompetitionsAction
variant="C"
title={t('Join a team')}
description={t(
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
)}
actionElement={
<TradingButton
intent={Intent.Primary}
onClick={(e) => {
e.preventDefault();
navigate(Links.COMPETITIONS_TEAMS());
}}
>
{t('Choose a team')}
</TradingButton>
}
/>
</CompetitionsActionsContainer>
{/** 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>
);
};
@@ -1,392 +0,0 @@
import { useState, type ReactNode, type ButtonHTMLAttributes } from 'react';
import { Link, useParams } from 'react-router-dom';
import orderBy from 'lodash/orderBy';
import countBy from 'lodash/countBy';
import {
Pill,
VegaIcon,
VegaIconNames,
Tooltip,
Splash,
truncateMiddle,
} from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import { useT } from '../../lib/use-t';
import { Table } from '../../components/table';
import { formatNumberRounded, getDateTimeFormat } from '@vegaprotocol/utils';
import {
useTeam,
type Team as TeamType,
type TeamStats,
type Member,
type TeamGame,
} from './hooks/use-team';
import { DApp, EXPLORER_PARTIES, useLinks } from '@vegaprotocol/environment';
import BigNumber from 'bignumber.js';
import { TeamAvatar } from '../../components/competitions/team-avatar';
import { usePageTitle } from '../../lib/hooks/use-page-title';
import { ErrorBoundary } from '../../components/error-boundary';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { JoinTeam } from './join-team';
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 { team, partyTeam, stats, members, games, loading, refetch } = useTeam(
teamId,
pubKey || undefined
);
if (loading) {
return (
<Splash>
<p>Loading...</p>
</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?: TeamStats;
members?: Member[];
games?: TeamGame[];
refetch: () => void;
}) => {
const t = useT();
const [showGames, setShowGames] = useState(true);
return (
<div className="relative h-full overflow-y-auto pt-5">
<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">
<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">
{team.name}
</h1>
<JoinTeam team={team} partyTeam={partyTeam} refetch={refetch} />
</div>
</header>
<StatSection>
<StatList>
<Stat value={members ? members.length : 0} label={t('Members')} />
<Stat
value={stats ? stats.totalGamesPlayed : 0}
label={t('Total games')}
tooltip={t('Total number of games this team has participated in')}
/>
<StatSectionSeparator />
<Stat
value={
stats
? formatNumberRounded(
new BigNumber(stats.totalQuantumVolume),
'1e3'
)
: 0
}
label={t('Total volume')}
/>
<Stat
value={
stats
? formatNumberRounded(
new BigNumber(stats.totalQuantumRewards),
'1e3'
)
: 0
}
label={t('Rewards paid')}
tooltip={'Total amount of rewards paid out to this team in qUSD'}
/>
</StatList>
</StatSection>
{games && games.length ? (
<StatSection>
<FavoriteGame games={games} />
<StatSectionSeparator />
<LatestResults games={games} />
</StatSection>
) : null}
<section>
<div className="flex gap-4 lg:gap-8 mb-4 border-b border-default">
<ToggleButton active={showGames} onClick={() => setShowGames(true)}>
{t('Games ({{count}})', { count: games ? games.length : 0 })}
</ToggleButton>
<ToggleButton
active={!showGames}
onClick={() => setShowGames(false)}
>
{t('Members ({{count}})', {
count: members ? members.length : 0,
})}
</ToggleButton>
</div>
{showGames ? <Games games={games} /> : <Members members={members} />}
</section>
</div>
</div>
);
};
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:block',
className: 'hidden md:block',
},
{ name: 'type', displayName: t('Type') },
{ name: 'amount', displayName: t('Amount earned') },
{
name: 'teams',
displayName: t('No. of participating teams'),
headerClassName: 'hidden md:block',
className: 'hidden md:block',
},
{ name: 'status', displayName: t('Status') },
]}
data={games.map((game) => ({
rank: game.team.rank,
epoch: game.epoch,
type: game.team.rewardMetric,
amount: game.team.totalRewardsEarned,
teams: 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: <RefereeCell pubkey={m.referee} />,
joinedAt: getDateTimeFormat().format(new Date(m.joinedAt)),
joinedAtEpoch: Number(m.joinedAtEpoch),
explorerLink: <RefereeLink pubkey={m.referee} />,
})),
'joinedAtEpoch',
'desc'
);
return (
<Table
columns={[
{ name: 'referee', displayName: t('Referee') },
{
name: 'joinedAt',
displayName: t('Joined at'),
},
{
name: 'joinedAtEpoch',
displayName: t('Joined epoch'),
headerClassName: 'text-right',
className: 'text-right',
},
{
name: 'explorerLink',
displayName: '',
headerClassName: 'hidden md:block',
className: 'hidden md:block text-right',
},
]}
data={data}
noCollapse={true}
/>
);
};
const RefereeCell = ({ pubkey }: { pubkey: string }) => {
return <span title={pubkey}>{truncateMiddle(pubkey)}</span>;
};
const RefereeLink = ({ pubkey }: { pubkey: string }) => {
const t = useT();
const linkCreator = useLinks(DApp.Explorer);
const link = linkCreator(EXPLORER_PARTIES.replace(':id', pubkey));
return (
<Link to={link} className="underline underline-offset-4">
{t('View on explorer')}
</Link>
);
};
const LatestResults = ({ games }: { games: TeamGame[] }) => {
const t = useT();
const latestGames = games.slice(0, 5);
return (
<dl>
<dt className="text-muted text-sm">
{t('Last {{count}} game results', { 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>
);
};
const FavoriteGame = ({ games }: { games: TeamGame[] }) => {
const t = useT();
const rewardMetrics = games.map((game) => game.team.rewardMetric);
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;
return (
<dl>
<dt className="text-muted text-sm">{t('Favorite game')}</dt>
<dd>
<Pill className="flex-inline items-center gap-2 bg-transparent text-sm">
<VegaIcon
name={VegaIconNames.STAR}
className="text-vega-yellow-400"
/>{' '}
{favoriteMetric}
</Pill>
</dd>
</dl>
);
};
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,
})}
/>
);
};
const StatSection = ({ children }: { children: ReactNode }) => {
return (
<section className="flex flex-col lg:flex-row gap-2 lg:gap-8">
{children}
</section>
);
};
const StatSectionSeparator = () => {
return <div className="hidden md:block border-r border-default" />;
};
const StatList = ({ children }: { children: ReactNode }) => {
return (
<dl className="grid grid-cols-[min-content_min-content] md:flex gap-4 md:gap-6 lg:gap-8 whitespace-nowrap">
{children}
</dl>
);
};
const Stat = ({
value,
label,
tooltip,
}: {
value: ReactNode;
label: ReactNode;
tooltip?: string;
}) => {
return (
<div>
<dd className="text-3xl lg:text-4xl">{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>
);
};
@@ -1,236 +0,0 @@
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 './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({
sortByField: ['totalQuantumRewards'],
order: 'desc',
});
const inputRef = useRef<HTMLInputElement>(null);
// const teamsData = [
// {
// referrer: '12345678909876543212345678765432345676543234567',
// avatarUrl: '',
// teamUrl: 'https://vega.xyz',
// closed: true,
// teamId:
// '8d81a2ba54c21cbb891e46a2a8debac508b33ee83e2d7c3b8a5bcd420753515e',
// name: 'cat lovers 2000',
// totalQuantumRewards: '1234567890',
// totalGamesPlayed: 12,
// totalQuantumVolume: '1234567890',
// gamesPlayed: [],
// createdAt: 123,
// createdAtEpoch: 123,
// },
// {
// referrer: '12345678909876543212345678765432345676543234567',
// avatarUrl: '',
// teamUrl: 'https://vega.xyz',
// closed: true,
// teamId:
// 'afe0f77322c9bc9ebe36faa8b0d846617018b86db78f33cadbe09dc15c95f408',
// name: 'dog lovers 2000',
// totalQuantumRewards: '1234567890',
// totalGamesPlayed: 12,
// totalQuantumVolume: '1234567890',
// gamesPlayed: [],
// createdAt: 123,
// createdAtEpoch: 123,
// },
// {
// referrer: '12345678909876543212345678765432345676543234567',
// avatarUrl: '',
// teamUrl: 'https://vega.xyz',
// closed: true,
// teamId:
// '580e52a0ef7f706898c6e0a3a34a6f870ef2a49e191408a3e1ed2c0a92e6e588',
// name: 'we like vega',
// totalQuantumRewards: '1234567890',
// totalGamesPlayed: 12,
// totalQuantumVolume: '1234567890',
// gamesPlayed: [],
// createdAt: 123,
// createdAtEpoch: 123,
// },
// {
// referrer: '12345678909876543212345678765432345676543234567',
// avatarUrl: '',
// teamUrl: 'https://vega.xyz',
// closed: true,
// teamId:
// '38c49f91670f536ce5ab6609899780cfb6bb9dcfc6c79842cbc348325e10c10f',
// name: 'pure gold',
// totalQuantumRewards: '1234567890',
// totalGamesPlayed: 12,
// totalQuantumVolume: '1234567890',
// gamesPlayed: [],
// createdAt: 123,
// createdAtEpoch: 123,
// },
// {
// referrer: '12345678909876543212345678765432345676543234567',
// avatarUrl: '',
// teamUrl: 'https://vega.xyz',
// closed: true,
// teamId:
// 'dfaaace4de83cab2443ebd7f75dc0d9a22a4f4a38f757b3adb5269aa930e0bdc',
// name: 'diamond hands',
// totalQuantumRewards: '1234567890',
// totalGamesPlayed: 12,
// totalQuantumVolume: '1234567890',
// gamesPlayed: [],
// createdAt: 123,
// createdAtEpoch: 123,
// },
// {
// referrer: '12345678909876543212345678765432345676543234567',
// avatarUrl: '',
// teamUrl: 'https://vega.xyz',
// closed: true,
// teamId:
// '1627a2ea10b07ed9cf08bfc7818bd76716493f5e6a5b5ffe4ed856ef34602a14',
// name: 'to the moon',
// totalQuantumRewards: '1234567890',
// totalGamesPlayed: 12,
// totalQuantumVolume: '1234567890',
// gamesPlayed: [],
// createdAt: 123,
// createdAtEpoch: 123,
// },
// {
// referrer: '12345678909876543212345678765432345676543234567',
// avatarUrl: '',
// teamUrl: 'https://vega.xyz',
// closed: true,
// teamId:
// 'd21f7b04985e5b5f510a53ee61c4d77c100374eafeffd754b5296d386b7454da',
// name: 'beyond cats',
// totalQuantumRewards: '1234567890',
// totalGamesPlayed: 12,
// totalQuantumVolume: '1234567890',
// gamesPlayed: [],
// createdAt: 123,
// createdAtEpoch: 123,
// },
// {
// referrer: '12345678909876543212345678765432345676543234567',
// avatarUrl: '',
// teamUrl: 'https://vega.xyz',
// closed: true,
// teamId:
// 'a40339f4cf31f4496e4d85ac7f56d5bbc9e257a337070fa361205c1f2fdaa0eb',
// name: 'cat lovers 2000',
// totalQuantumRewards: '1234567890',
// totalGamesPlayed: 12,
// totalQuantumVolume: '1234567890',
// gamesPlayed: [],
// createdAt: 123,
// createdAtEpoch: 123,
// },
// {
// referrer: '12345678909876543212345678765432345676543234567',
// avatarUrl: '',
// teamUrl: 'https://vega.xyz',
// closed: true,
// teamId:
// 'af059e7508a302cce9523edcbde52bf7f2e876f34854c6274a83aec4045b9657',
// name: 'cat lovers 2000',
// totalQuantumRewards: '1234567890',
// totalGamesPlayed: 12,
// totalQuantumVolume: '1234567890',
// gamesPlayed: [],
// createdAt: 123,
// createdAtEpoch: 123,
// },
// {
// referrer: '12345678909876543212345678765432345676543234567',
// avatarUrl: '',
// teamUrl: 'https://vega.xyz',
// closed: true,
// teamId:
// '3f06f0e0ec863e686212d6b100f03122c9997f890d304f15f55215ac32334e6f',
// name: 'cat lovers 2000',
// totalQuantumRewards: '1234567890',
// totalGamesPlayed: 12,
// totalQuantumVolume: '1234567890',
// gamesPlayed: [],
// createdAt: 123,
// createdAtEpoch: 123,
// },
// {
// referrer: '12345678909876543212345678765432345676543234567',
// avatarUrl: '',
// teamUrl: 'https://vega.xyz',
// closed: true,
// teamId:
// '0fdc948a548ccd29001c2411629065bc5e248b6535b59b4b089f07a577074bf9',
// name: 'cat lovers 2000',
// totalQuantumRewards: '1234567890',
// totalGamesPlayed: 12,
// totalQuantumVolume: '1234567890',
// gamesPlayed: [],
// createdAt: 123,
// createdAtEpoch: 123,
// },
// ];
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-40 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>
);
};
@@ -1,89 +0,0 @@
fragment TeamFields on Team {
teamId
referrer
name
teamUrl
avatarUrl
createdAt
createdAtEpoch
closed
}
fragment TeamStatsFields on TeamStatistics {
teamId
totalQuantumVolume
totalQuantumRewards
totalGamesPlayed
quantumRewards {
epoch
total_quantum_rewards
}
gamesPlayed
}
fragment TeamRefereeFields on TeamReferee {
teamId
referee
joinedAt
joinedAtEpoch
}
fragment TeamEntity on TeamGameEntity {
rank
volume
rewardMetric
rewardEarned
totalRewardsEarned
team {
teamId
}
}
fragment TeamGameFields on Game {
id
epoch
numberOfParticipants
entities {
... on TeamGameEntity {
...TeamEntity
}
}
}
query Team($teamId: ID!, $partyId: ID) {
teams(teamId: $teamId) {
edges {
node {
...TeamFields
}
}
}
partyTeams: teams(partyId: $partyId) {
edges {
node {
...TeamFields
}
}
}
teamsStatistics(teamId: $teamId) {
edges {
node {
...TeamStatsFields
}
}
}
teamReferees(teamId: $teamId) {
edges {
node {
...TeamRefereeFields
}
}
}
games(entityScope: ENTITY_SCOPE_TEAMS) {
edges {
node {
...TeamGameFields
}
}
}
}
@@ -1,16 +0,0 @@
query Teams($teamId: ID, $partyId: ID) {
teams(teamId: $teamId, partyId: $partyId) {
edges {
node {
teamId
referrer
name
teamUrl
avatarUrl
createdAt
createdAtEpoch
closed
}
}
}
}
@@ -1,13 +0,0 @@
query TeamsStatistics($teamId: ID, $aggregationEpochs: Int) {
teamsStatistics(teamId: $teamId, aggregationEpochs: $aggregationEpochs) {
edges {
node {
teamId
totalQuantumVolume
totalQuantumRewards
totalGamesPlayed
gamesPlayed
}
}
}
}
@@ -1,151 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type TeamFieldsFragment = { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean };
export type TeamStatsFieldsFragment = { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string>, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, total_quantum_rewards: string }> };
export type TeamRefereeFieldsFragment = { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number };
export type TeamEntityFragment = { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: string, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } };
export type TeamGameFieldsFragment = { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: string, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } }> };
export type TeamQueryVariables = Types.Exact<{
teamId: Types.Scalars['ID'];
partyId?: Types.InputMaybe<Types.Scalars['ID']>;
}>;
export type TeamQuery = { __typename?: 'Query', teams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean } }> } | null, partyTeams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean } }> } | null, teamsStatistics?: { __typename?: 'TeamsStatisticsConnection', edges: Array<{ __typename?: 'TeamStatisticsEdge', node: { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string>, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, total_quantum_rewards: string }> } }> } | null, teamReferees?: { __typename?: 'TeamRefereeConnection', edges: Array<{ __typename?: 'TeamRefereeEdge', node: { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number } }> } | null, games: { __typename?: 'GamesConnection', edges?: Array<{ __typename?: 'GameEdge', node: { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: string, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } }> } } | null> | null } };
export const TeamFieldsFragmentDoc = gql`
fragment TeamFields on Team {
teamId
referrer
name
teamUrl
avatarUrl
createdAt
createdAtEpoch
closed
}
`;
export const TeamStatsFieldsFragmentDoc = gql`
fragment TeamStatsFields on TeamStatistics {
teamId
totalQuantumVolume
totalQuantumRewards
totalGamesPlayed
quantumRewards {
epoch
total_quantum_rewards
}
gamesPlayed
}
`;
export const TeamRefereeFieldsFragmentDoc = gql`
fragment TeamRefereeFields on TeamReferee {
teamId
referee
joinedAt
joinedAtEpoch
}
`;
export const TeamEntityFragmentDoc = gql`
fragment TeamEntity on TeamGameEntity {
rank
volume
rewardMetric
rewardEarned
totalRewardsEarned
team {
teamId
}
}
`;
export const TeamGameFieldsFragmentDoc = gql`
fragment TeamGameFields on Game {
id
epoch
numberOfParticipants
entities {
... on TeamGameEntity {
...TeamEntity
}
}
}
${TeamEntityFragmentDoc}`;
export const TeamDocument = gql`
query Team($teamId: ID!, $partyId: ID) {
teams(teamId: $teamId) {
edges {
node {
...TeamFields
}
}
}
partyTeams: teams(partyId: $partyId) {
edges {
node {
...TeamFields
}
}
}
teamsStatistics(teamId: $teamId) {
edges {
node {
...TeamStatsFields
}
}
}
teamReferees(teamId: $teamId) {
edges {
node {
...TeamRefereeFields
}
}
}
games(entityScope: ENTITY_SCOPE_TEAMS) {
edges {
node {
...TeamGameFields
}
}
}
}
${TeamFieldsFragmentDoc}
${TeamStatsFieldsFragmentDoc}
${TeamRefereeFieldsFragmentDoc}
${TeamGameFieldsFragmentDoc}`;
/**
* __useTeamQuery__
*
* To run a query within a React component, call `useTeamQuery` and pass it any options that fit your needs.
* When your component renders, `useTeamQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useTeamQuery({
* variables: {
* teamId: // value for 'teamId'
* partyId: // value for 'partyId'
* },
* });
*/
export function useTeamQuery(baseOptions: Apollo.QueryHookOptions<TeamQuery, TeamQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<TeamQuery, TeamQueryVariables>(TeamDocument, options);
}
export function useTeamLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<TeamQuery, TeamQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<TeamQuery, TeamQueryVariables>(TeamDocument, options);
}
export type TeamQueryHookResult = ReturnType<typeof useTeamQuery>;
export type TeamLazyQueryHookResult = ReturnType<typeof useTeamLazyQuery>;
export type TeamQueryResult = Apollo.QueryResult<TeamQuery, TeamQueryVariables>;
@@ -1,61 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type TeamsQueryVariables = Types.Exact<{
teamId?: Types.InputMaybe<Types.Scalars['ID']>;
partyId?: Types.InputMaybe<Types.Scalars['ID']>;
}>;
export type TeamsQuery = { __typename?: 'Query', teams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean } }> } | null };
export const TeamsDocument = gql`
query Teams($teamId: ID, $partyId: ID) {
teams(teamId: $teamId, partyId: $partyId) {
edges {
node {
teamId
referrer
name
teamUrl
avatarUrl
createdAt
createdAtEpoch
closed
}
}
}
}
`;
/**
* __useTeamsQuery__
*
* To run a query within a React component, call `useTeamsQuery` and pass it any options that fit your needs.
* When your component renders, `useTeamsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useTeamsQuery({
* variables: {
* teamId: // value for 'teamId'
* partyId: // value for 'partyId'
* },
* });
*/
export function useTeamsQuery(baseOptions?: Apollo.QueryHookOptions<TeamsQuery, TeamsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<TeamsQuery, TeamsQueryVariables>(TeamsDocument, options);
}
export function useTeamsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<TeamsQuery, TeamsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<TeamsQuery, TeamsQueryVariables>(TeamsDocument, options);
}
export type TeamsQueryHookResult = ReturnType<typeof useTeamsQuery>;
export type TeamsLazyQueryHookResult = ReturnType<typeof useTeamsLazyQuery>;
export type TeamsQueryResult = Apollo.QueryResult<TeamsQuery, TeamsQueryVariables>;
@@ -1,58 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type TeamsStatisticsQueryVariables = Types.Exact<{
teamId?: Types.InputMaybe<Types.Scalars['ID']>;
aggregationEpochs?: Types.InputMaybe<Types.Scalars['Int']>;
}>;
export type TeamsStatisticsQuery = { __typename?: 'Query', teamsStatistics?: { __typename?: 'TeamsStatisticsConnection', edges: Array<{ __typename?: 'TeamStatisticsEdge', node: { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string> } }> } | null };
export const TeamsStatisticsDocument = gql`
query TeamsStatistics($teamId: ID, $aggregationEpochs: Int) {
teamsStatistics(teamId: $teamId, aggregationEpochs: $aggregationEpochs) {
edges {
node {
teamId
totalQuantumVolume
totalQuantumRewards
totalGamesPlayed
gamesPlayed
}
}
}
}
`;
/**
* __useTeamsStatisticsQuery__
*
* To run a query within a React component, call `useTeamsStatisticsQuery` and pass it any options that fit your needs.
* When your component renders, `useTeamsStatisticsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useTeamsStatisticsQuery({
* variables: {
* teamId: // value for 'teamId'
* aggregationEpochs: // value for 'aggregationEpochs'
* },
* });
*/
export function useTeamsStatisticsQuery(baseOptions?: Apollo.QueryHookOptions<TeamsStatisticsQuery, TeamsStatisticsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<TeamsStatisticsQuery, TeamsStatisticsQueryVariables>(TeamsStatisticsDocument, options);
}
export function useTeamsStatisticsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<TeamsStatisticsQuery, TeamsStatisticsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<TeamsStatisticsQuery, TeamsStatisticsQueryVariables>(TeamsStatisticsDocument, options);
}
export type TeamsStatisticsQueryHookResult = ReturnType<typeof useTeamsStatisticsQuery>;
export type TeamsStatisticsLazyQueryHookResult = ReturnType<typeof useTeamsStatisticsLazyQuery>;
export type TeamsStatisticsQueryResult = Apollo.QueryResult<TeamsStatisticsQuery, TeamsStatisticsQueryVariables>;
@@ -1,38 +0,0 @@
import compact from 'lodash/compact';
import { useActiveRewardsQuery } from '../../../components/rewards-container/__generated__/Rewards';
import { isActiveReward } from '../../../components/rewards-container/active-rewards';
import { EntityScope, type TransferNode } from '@vegaprotocol/types';
const isScopedToTeams = (node: TransferNode) =>
node.transfer.kind.__typename === 'RecurringTransfer' &&
node.transfer.kind.dispatchStrategy?.entityScope ===
EntityScope.ENTITY_SCOPE_TEAMS;
export const useGames = ({
currentEpoch,
onlyActive,
}: {
currentEpoch: number;
onlyActive: boolean;
}) => {
const { data, loading, error } = useActiveRewardsQuery({
variables: {
isReward: true,
},
fetchPolicy: 'cache-and-network',
});
const games = compact(data?.transfersConnection?.edges?.map((n) => n?.node))
.map((n) => n as TransferNode)
.filter((node) => {
const recurring = node.transfer.kind.__typename !== 'RecurringTransfer';
const active = onlyActive ? isActiveReward(node, currentEpoch) : true;
return active && recurring && isScopedToTeams(node);
});
return {
data: games,
loading,
error,
};
};
@@ -1,67 +0,0 @@
import compact from 'lodash/compact';
import orderBy from 'lodash/orderBy';
import {
useTeamQuery,
type TeamFieldsFragment,
type TeamStatsFieldsFragment,
type TeamRefereeFieldsFragment,
type TeamEntityFragment,
} from './__generated__/Team';
export type Team = TeamFieldsFragment;
export type TeamStats = TeamStatsFieldsFragment;
export type Member = TeamRefereeFieldsFragment;
export type TeamEntity = TeamEntityFragment;
export type TeamGame = ReturnType<typeof useTeam>['games'][number];
export const useTeam = (teamId?: string, partyId?: string) => {
const { data, loading, error, refetch } = useTeamQuery({
variables: { teamId: teamId || '', partyId },
skip: !teamId,
fetchPolicy: 'cache-and-network',
});
const teamEdge = data?.teams?.edges.find((e) => e.node.teamId === teamId);
const partyTeam = data?.partyTeams?.edges?.length
? data.partyTeams.edges[0].node
: undefined;
const teamStatsEdge = data?.teamsStatistics?.edges.find(
(e) => e.node.teamId === teamId
);
const members = data?.teamReferees?.edges
.filter((e) => e.node.teamId === teamId)
.map((e) => e.node);
// Find games where the current team participated in
const gamesWithTeam = compact(data?.games.edges).map((edge) => {
const team = edge.node.entities.find((e) => {
if (e.__typename !== 'TeamGameEntity') return false;
if (e.team.teamId !== teamId) return false;
return true;
});
if (!team) return null;
return {
id: edge.node.id,
epoch: edge.node.epoch,
numberOfParticipants: edge.node.numberOfParticipants,
team: team as TeamEntity, // TS can't infer that all the game entities are teams
};
});
const games = orderBy(compact(gamesWithTeam), 'epoch', 'desc');
return {
data,
loading,
error,
refetch,
stats: teamStatsEdge?.node,
team: teamEdge?.node,
members,
games,
partyTeam,
};
};
@@ -1,72 +0,0 @@
import { useMemo } from 'react';
import { type TeamsQuery, useTeamsQuery } from './__generated__/Teams';
import {
type TeamsStatisticsQuery,
useTeamsStatisticsQuery,
} from './__generated__/TeamsStatistics';
import compact from 'lodash/compact';
import sortBy from 'lodash/sortBy';
import { type ArrayElement } from 'type-fest/source/internal';
type SortableField = keyof Omit<
ArrayElement<NonNullable<TeamsQuery['teams']>['edges']>['node'] &
ArrayElement<
NonNullable<TeamsStatisticsQuery['teamsStatistics']>['edges']
>['node'],
'__typename'
>;
type UseTeamsArgs = {
aggregationEpochs?: number;
sortByField?: SortableField[];
order?: 'asc' | 'desc';
};
const DEFAULT_AGGREGATION_EPOCHS = 10;
export const useTeams = ({
aggregationEpochs = DEFAULT_AGGREGATION_EPOCHS,
sortByField = ['createdAtEpoch'],
order = 'asc',
}: UseTeamsArgs) => {
const {
data: teamsData,
loading: teamsLoading,
error: teamsError,
} = useTeamsQuery({
fetchPolicy: 'cache-and-network',
});
const {
data: statsData,
loading: statsLoading,
error: statsError,
} = useTeamsStatisticsQuery({
variables: {
aggregationEpochs,
},
fetchPolicy: 'cache-and-network',
});
const teams = compact(teamsData?.teams?.edges).map((e) => e.node);
const stats = compact(statsData?.teamsStatistics?.edges).map((e) => e.node);
const data = useMemo(() => {
const data = teams.map((t) => ({
...t,
...stats.find((s) => s.teamId === t.teamId),
}));
const sorted = sortBy(data, sortByField);
if (order === 'desc') {
return sorted.reverse();
}
return sorted;
}, [teams, sortByField, order, stats]);
return {
data,
loading: teamsLoading && statsLoading,
error: teamsError || statsError,
};
};
@@ -1,88 +0,0 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { JoinButton } from './join-team';
import { type Team } from './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');
});
});
@@ -1,225 +0,0 @@
import {
TradingButton as Button,
Dialog,
Intent,
Tooltip,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import {
useSimpleTransaction,
useVegaWallet,
type Status,
} from '@vegaprotocol/wallet';
import { useT } from '../../lib/use-t';
import { type Team } from './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 { send, status } = useSimpleTransaction({
onSuccess: refetch,
});
const [confirmDialog, setConfirmDialog] = useState<JoinType>();
const joinTeam = () => {
send({
joinTeam: {
id: team.teamId,
},
});
};
return (
<>
<JoinButton
team={team}
partyTeam={partyTeam}
pubKey={pubKey}
isReadOnly={isReadOnly}
onJoin={setConfirmDialog}
/>
<Dialog
open={confirmDialog !== undefined}
onChange={() => setConfirmDialog(undefined)}
>
{confirmDialog !== undefined && (
<DialogContent
type={confirmDialog}
status={status}
team={team}
partyTeam={partyTeam}
onConfirm={joinTeam}
onCancel={() => setConfirmDialog(undefined)}
/>
)}
</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="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}>
{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,
status,
team,
partyTeam,
onConfirm,
onCancel,
}: {
type: JoinType;
status: Status;
team: Team;
partyTeam?: Team;
onConfirm: () => void;
onCancel: () => void;
}) => {
const t = useT();
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={onConfirm} intent={Intent.Success}>
{t('Confirm')}
</Button>
<Button onClick={onCancel} intent={Intent.Danger}>
{t('Cancel')}
</Button>
</div>
</div>
);
};
@@ -188,11 +188,12 @@ const useNow = () => {
return now;
};
const useEvery = (marketId: string, skip: boolean) => {
const useEvery = (marketId: string) => {
const { data: marketTradingMode } = useMarketTradingMode(marketId);
const { data: marketInfo } = useDataProvider({
dataProvider: marketInfoProvider,
variables: { marketId },
skip,
skip: !marketTradingMode || isMarketInAuction(marketTradingMode),
});
let every: number | undefined = undefined;
const sourceType =
@@ -210,10 +211,8 @@ const useEvery = (marketId: string, skip: boolean) => {
return every;
};
const useStartTime = (marketId: string, skip: boolean) => {
const useStartTime = (marketId: string) => {
const { data: fundingPeriods } = useFundingPeriodsQuery({
pollInterval: 5000,
skip,
variables: {
marketId: marketId,
pagination: { first: 1 },
@@ -247,10 +246,8 @@ const useFormatCountdown = (
export const FundingCountdown = ({ marketId }: { marketId: string }) => {
const now = useNow();
const { data: marketTradingMode } = useMarketTradingMode(marketId);
const skip = !marketTradingMode || isMarketInAuction(marketTradingMode);
const startTime = useStartTime(marketId, skip);
const every = useEvery(marketId, skip);
const startTime = useStartTime(marketId);
const every = useEvery(marketId);
return (
<div data-testid="funding-countdown">
+28 -9
View File
@@ -1,4 +1,4 @@
import { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { addDecimalsFormatNumber, titlefy } from '@vegaprotocol/utils';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import { useThrottledDataProvider } from '@vegaprotocol/data-provider';
@@ -9,6 +9,8 @@ import { TradeGrid } from './trade-grid';
import { TradePanels } from './trade-panels';
import { useNavigate, useParams } from 'react-router-dom';
import { Links } from '../../lib/links';
import { ViewType, useSidebar } from '../../components/sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { useT, ns } from '../../lib/use-t';
import { Trans } from 'react-i18next';
@@ -59,6 +61,9 @@ export const MarketPage = () => {
const t = useT();
const { marketId } = useParams();
const navigate = useNavigate();
const currentRouteId = useGetCurrentRouteId();
const { setViews, getView } = useSidebar();
const view = getView(currentRouteId);
const { screenSize } = useScreenDimensions();
const largeScreen = ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize);
const update = useGlobalStore((store) => store.update);
@@ -67,11 +72,31 @@ export const MarketPage = () => {
const { data, loading } = useMarket(marketId);
useEffect(() => {
if (data?.id && data.id !== lastMarketId) {
if (data?.id && data.id !== lastMarketId && !closed) {
update({ marketId: data.id });
}
}, [update, lastMarketId, data?.id]);
useEffect(() => {
if (largeScreen && view === undefined) {
setViews(
{ type: closed ? ViewType.Info : ViewType.Order },
currentRouteId
);
}
}, [setViews, view, currentRouteId, largeScreen]);
const pinnedAsset = data && getAsset(data);
const tradeView = useMemo(() => {
if (pinnedAsset) {
if (largeScreen) {
return <TradeGrid market={data} pinnedAsset={pinnedAsset} />;
}
return <TradePanels market={data} pinnedAsset={pinnedAsset} />;
}
}, [largeScreen, data, pinnedAsset]);
if (loading) {
return (
<Splash>
@@ -107,8 +132,6 @@ export const MarketPage = () => {
);
}
const pinnedAsset = data && getAsset(data);
return (
<>
<TitleUpdater
@@ -116,11 +139,7 @@ export const MarketPage = () => {
marketName={data?.tradableInstrument.instrument.name}
decimalPlaces={data?.decimalPlaces}
/>
{largeScreen ? (
<TradeGrid market={data} pinnedAsset={pinnedAsset} />
) : (
<TradePanels market={data} pinnedAsset={pinnedAsset} />
)}
{tradeView}
</>
);
};
+45 -52
View File
@@ -1,10 +1,11 @@
import { memo, type ReactNode } from 'react';
import { memo } from 'react';
import type { ReactNode } from 'react';
import { LayoutPriority } from 'allotment';
import classNames from 'classnames';
import AutoSizer from 'react-virtualized-auto-sizer';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { type PinnedAsset } from '@vegaprotocol/accounts';
import { type Market } from '@vegaprotocol/markets';
import type { PinnedAsset } from '@vegaprotocol/accounts';
import { OracleBanner, useMarket } from '@vegaprotocol/markets';
import type { Market } from '@vegaprotocol/markets';
import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit';
import {
ResizableGrid,
@@ -12,19 +13,31 @@ import {
usePaneLayout,
} from '../../components/resizable-grid';
import { TradingViews } from './trade-views';
import {
MarketSuccessorBanner,
MarketSuccessorProposalBanner,
MarketTerminationBanner,
} from '../../components/market-banner';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { useT } from '../../lib/use-t';
import { ErrorBoundary } from '../../components/error-boundary';
import { MarketBanner } from '../../components/market-banner';
interface TradeGridProps {
market: Market;
pinnedAsset?: PinnedAsset | undefined;
market: Market | null;
pinnedAsset?: PinnedAsset;
}
const MainGrid = memo(
({ market, pinnedAsset }: { market: Market; pinnedAsset?: PinnedAsset }) => {
({
marketId,
pinnedAsset,
}: {
marketId: string;
pinnedAsset?: PinnedAsset;
}) => {
const featureFlags = useFeatureFlags((state) => state.flags);
const t = useT();
const { data: market } = useMarket(marketId);
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'top' });
const [sizesMiddle, handleOnMiddleLayoutChange] = usePaneLayout({
id: 'middle-1',
@@ -49,20 +62,20 @@ const MainGrid = memo(
id="chart"
overflowHidden
name={t('Chart')}
menu={<TradingViews.chart.menu />}
menu={<TradingViews.candles.menu />}
>
<ErrorBoundary feature="chart">
<TradingViews.chart.component marketId={market.id} />
<TradingViews.candles.component marketId={marketId} />
</ErrorBoundary>
</Tab>
<Tab id="depth" name={t('Depth')}>
<ErrorBoundary feature="depth">
<TradingViews.depth.component marketId={market.id} />
<TradingViews.depth.component marketId={marketId} />
</ErrorBoundary>
</Tab>
<Tab id="liquidity" name={t('Liquidity')}>
<ErrorBoundary feature="liquidity">
<TradingViews.liquidity.component marketId={market.id} />
<TradingViews.liquidity.component marketId={marketId} />
</ErrorBoundary>
</Tab>
{market &&
@@ -70,21 +83,17 @@ const MainGrid = memo(
'Perpetual' ? (
<Tab id="funding-history" name={t('Funding history')}>
<ErrorBoundary feature="funding-history">
<TradingViews.funding.component marketId={market.id} />
<TradingViews.funding.component marketId={marketId} />
</ErrorBoundary>
</Tab>
) : null}
{market &&
market.tradableInstrument.instrument.product.__typename ===
'Perpetual' ? (
<Tab
id="funding-payments"
name={t('Funding payments')}
settings={<TradingViews.fundingPayments.settings />}
>
<Tab id="funding-payments" name={t('Funding payments')}>
<ErrorBoundary feature="funding-payments">
<TradingViews.fundingPayments.component
marketId={market.id}
marketId={marketId}
/>
</ErrorBoundary>
</Tab>
@@ -100,16 +109,12 @@ const MainGrid = memo(
<Tabs storageKey="console-trade-grid-main-right">
<Tab id="orderbook" name={t('Orderbook')}>
<ErrorBoundary feature="orderbook">
<TradingViews.orderbook.component marketId={market.id} />
<TradingViews.orderbook.component marketId={marketId} />
</ErrorBoundary>
</Tab>
<Tab
id="trades"
name={t('Trades')}
settings={<TradingViews.trades.settings />}
>
<Tab id="trades" name={t('Trades')}>
<ErrorBoundary feature="trades">
<TradingViews.trades.component marketId={market.id} />
<TradingViews.trades.component marketId={marketId} />
</ErrorBoundary>
</Tab>
</Tabs>
@@ -128,7 +133,6 @@ const MainGrid = memo(
id="positions"
name={t('Positions')}
menu={<TradingViews.positions.menu />}
settings={<TradingViews.positions.settings />}
>
<ErrorBoundary feature="positions">
<TradingViews.positions.component />
@@ -138,26 +142,17 @@ const MainGrid = memo(
id="open-orders"
name={t('Open')}
menu={<TradingViews.activeOrders.menu />}
settings={<TradingViews.activeOrders.settings />}
>
<ErrorBoundary feature="activeOrders">
<TradingViews.activeOrders.component />
</ErrorBoundary>
</Tab>
<Tab
id="closed-orders"
name={t('Closed')}
settings={<TradingViews.closedOrders.settings />}
>
<Tab id="closed-orders" name={t('Closed')}>
<ErrorBoundary feature="closedOrders">
<TradingViews.closedOrders.component />
</ErrorBoundary>
</Tab>
<Tab
id="rejected-orders"
name={t('Rejected')}
settings={<TradingViews.rejectedOrders.settings />}
>
<Tab id="rejected-orders" name={t('Rejected')}>
<ErrorBoundary feature="rejectedOrders">
<TradingViews.rejectedOrders.component />
</ErrorBoundary>
@@ -166,35 +161,25 @@ const MainGrid = memo(
id="orders"
name={t('All')}
menu={<TradingViews.orders.menu />}
settings={<TradingViews.orders.settings />}
>
<ErrorBoundary feature="orders">
<TradingViews.orders.component />
</ErrorBoundary>
</Tab>
{featureFlags.STOP_ORDERS ? (
<Tab
id="stop-orders"
name={t('Stop orders')}
settings={<TradingViews.stopOrders.settings />}
>
<Tab id="stop-orders" name={t('Stop orders')}>
<ErrorBoundary feature="stop-orders">
<TradingViews.stopOrders.component />
</ErrorBoundary>
</Tab>
) : null}
<Tab
id="fills"
name={t('Fills')}
settings={<TradingViews.fills.settings />}
>
<Tab id="fills" name={t('Fills')}>
<TradingViews.fills.component />
</Tab>
<Tab
id="accounts"
name={t('Collateral')}
menu={<TradingViews.collateral.menu />}
settings={<TradingViews.collateral.settings />}
>
<ErrorBoundary feature="collateral">
<TradingViews.collateral.component
@@ -212,6 +197,7 @@ const MainGrid = memo(
MainGrid.displayName = 'MainGrid';
export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
const featureFlags = useFeatureFlags((state) => state.flags);
const wrapperClasses = classNames(
'h-full grid',
'grid-rows-[min-content_1fr]'
@@ -220,10 +206,17 @@ export const TradeGrid = ({ market, pinnedAsset }: TradeGridProps) => {
return (
<div className={wrapperClasses}>
<div>
<MarketBanner market={market} />
{featureFlags.SUCCESSOR_MARKETS && (
<>
<MarketSuccessorBanner market={market} />
<MarketSuccessorProposalBanner marketId={market?.id} />
</>
)}
<MarketTerminationBanner market={market} />
<OracleBanner marketId={market?.id || ''} />
</div>
<div className="min-h-0 p-0.5">
<MainGrid market={market} pinnedAsset={pinnedAsset} />
<MainGrid marketId={market?.id || ''} pinnedAsset={pinnedAsset} />
</div>
</div>
);
@@ -1,30 +1,29 @@
import { type PinnedAsset } from '@vegaprotocol/accounts';
import { type Market } from '@vegaprotocol/markets';
// TODO: handle oracle banner
// import { OracleBanner } from '@vegaprotocol/markets';
import { OracleBanner } from '@vegaprotocol/markets';
import { useState } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import classNames from 'classnames';
import {
Popover,
Splash,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { useT } from '../../lib/use-t';
import { MarketBanner } from '../../components/market-banner';
import {
MarketSuccessorBanner,
MarketSuccessorProposalBanner,
MarketTerminationBanner,
} from '../../components/market-banner';
import { ErrorBoundary } from '../../components/error-boundary';
import { type TradingView } from './trade-views';
import { TradingViews } from './trade-views';
import { useFeatureFlags } from '@vegaprotocol/environment';
interface TradePanelsProps {
market: Market;
market: Market | null;
pinnedAsset?: PinnedAsset;
}
export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
const [view, setView] = useState<TradingView>('chart');
const viewCfg = TradingViews[view];
const featureFlags = useFeatureFlags((state) => state.flags);
const [view, setView] = useState<TradingView>('candles');
const renderView = () => {
const Component = TradingViews[view].component;
@@ -45,36 +44,35 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
};
const renderMenu = () => {
if ('menu' in viewCfg || 'settings' in viewCfg) {
const viewCfg = TradingViews[view];
if ('menu' in viewCfg) {
const Menu = viewCfg.menu;
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">
{'menu' in viewCfg ? <viewCfg.menu /> : null}
{'settings' in viewCfg ? (
<Popover
align="end"
trigger={
<span className="ml-1 flex items-center justify-center h-6 w-6">
<VegaIcon name={VegaIconNames.COG} size={16} />
</span>
}
>
<div className="p-4 flex justify-end">
<viewCfg.settings />
</div>
</Popover>
) : null}
<div className="flex gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default">
<Menu />
</div>
);
}
return null;
};
return (
<div className="h-full grid grid-rows-[min-content_min-content_1fr_min-content]">
<div>
<MarketBanner market={market} />
{featureFlags.SUCCESSOR_MARKETS && (
<>
<MarketSuccessorBanner market={market} />
<MarketSuccessorProposalBanner marketId={market?.id} />
</>
)}
<MarketTerminationBanner market={market} />
<OracleBanner marketId={market?.id || ''} />
</div>
<div>{renderMenu()}</div>
<div className="h-full relative">
<div className="h-full">
<AutoSizer>
{({ width, height }) => (
<div style={{ width, height }} className="overflow-auto">
@@ -112,9 +110,7 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
key={key}
view={key}
isActive={isActive}
onClick={() => {
setView(key);
}}
onClick={() => setView(key)}
/>
);
})}
@@ -153,7 +149,7 @@ const useViewLabel = (view: TradingView) => {
const t = useT();
const labels = {
chart: t('Chart'),
candles: t('Candles'),
depth: t('Depth'),
liquidity: t('Liquidity'),
funding: t('Funding'),
@@ -1,46 +1,28 @@
import { DepthChartContainer } from '@vegaprotocol/market-depth';
import {
CandlesChartContainer,
CandlesMenu,
} from '@vegaprotocol/candles-chart';
import { Filter, OpenOrdersMenu } from '@vegaprotocol/orders';
import {
TradesContainer,
TradesSettings,
} from '../../components/trades-container';
import { TradesContainer } from '../../components/trades-container';
import { OrderbookContainer } from '../../components/orderbook-container';
import {
FillsContainer,
FillsSettings,
} from '../../components/fills-container';
import {
PositionsContainer,
PositionsSettings,
} from '../../components/positions-container';
import {
AccountsContainer,
AccountsSettings,
} from '../../components/accounts-container';
import { FillsContainer } from '../../components/fills-container';
import { PositionsContainer } from '../../components/positions-container';
import { AccountsContainer } from '../../components/accounts-container';
import { LiquidityContainer } from '../../components/liquidity-container';
import { FundingContainer } from '../../components/funding-container';
import {
FundingPaymentsContainer,
FundingPaymentsSettings,
} from '../../components/funding-payments-container';
import {
OrdersContainer,
OrdersSettings,
} from '../../components/orders-container';
import {
StopOrdersContainer,
StopOrdersSettings,
} from '../../components/stop-orders-container';
import { FundingPaymentsContainer } from '../../components/funding-payments-container';
import { OrdersContainer } from '../../components/orders-container';
import { StopOrdersContainer } from '../../components/stop-orders-container';
import { AccountsMenu } from '../../components/accounts-menu';
import { PositionsMenu } from '../../components/positions-menu';
import { ChartContainer, ChartMenu } from '../../components/chart-container';
export type TradingView = keyof typeof TradingViews;
export const TradingViews = {
chart: {
component: ChartContainer,
menu: ChartMenu,
candles: {
component: CandlesChartContainer,
menu: CandlesMenu,
},
depth: {
component: DepthChartContainer,
@@ -53,46 +35,37 @@ export const TradingViews = {
},
fundingPayments: {
component: FundingPaymentsContainer,
settings: FundingPaymentsSettings,
},
orderbook: {
component: OrderbookContainer,
},
trades: {
component: TradesContainer,
settings: TradesSettings,
},
positions: {
component: PositionsContainer,
menu: PositionsMenu,
settings: PositionsSettings,
},
activeOrders: {
component: () => <OrdersContainer filter={Filter.Open} />,
menu: OpenOrdersMenu,
settings: () => <OrdersSettings filter={Filter.Open} />,
},
closedOrders: {
component: () => <OrdersContainer filter={Filter.Closed} />,
settings: () => <OrdersSettings filter={Filter.Closed} />,
},
rejectedOrders: {
component: () => <OrdersContainer filter={Filter.Rejected} />,
settings: () => <OrdersSettings filter={Filter.Rejected} />,
},
orders: {
component: OrdersContainer,
menu: OpenOrdersMenu,
settings: OrdersSettings,
},
stopOrders: {
component: StopOrdersContainer,
settings: StopOrdersSettings,
},
collateral: {
component: AccountsContainer,
menu: AccountsMenu,
settings: AccountsSettings,
},
fills: { component: FillsContainer, settings: FillsSettings },
fills: { component: FillsContainer },
} as const;
@@ -314,7 +314,7 @@ describe('Closed', () => {
});
it('display market actions', async () => {
// Use market with a successor Id as the actions dropdown will optionally
// Use market with a succcessor Id as the actions dropdown will optionally
// show a link to the successor market
const marketsWithSuccessorAndParent = [
{
@@ -137,8 +137,6 @@ const ClosedMarketsDataGrid = ({
headerName: t('Market'),
field: 'code',
cellRenderer: 'MarketCodeCell',
width: 150,
resizable: true,
},
{
headerName: t('Status'),
@@ -282,7 +280,6 @@ const ClosedMarketsDataGrid = ({
return (
<AgGrid
rowData={rowData}
defaultColDef={COL_DEFS.default}
columnDefs={colDefs}
getRowId={({ data }) => data.id}
overlayNoRowsTemplate={error ? error.message : t('No markets')}
@@ -17,7 +17,6 @@ const defaultColDef = {
filter: true,
resizable: true,
filterParams: { buttons: ['reset'] },
minWidth: 120,
};
const components = {
@@ -43,7 +42,7 @@ export const createDataGridSlice: StateCreator<DataGridSlice> = (set) => ({
},
});
export const useMarketsStore = create<DataGridSlice>()(
const useMarketsStore = create<DataGridSlice>()(
persist(createDataGridSlice, {
name: 'vega_market_list_store',
})
@@ -16,7 +16,6 @@ import {
} from '@vegaprotocol/environment';
import { useT } from '../../lib/use-t';
import { ErrorBoundary } from '../../components/error-boundary';
import { MarketsSettings } from './markets-settings';
export const MarketsPage = () => {
const t = useT();
@@ -35,11 +34,7 @@ export const MarketsPage = () => {
<div className="h-full pt-0.5 pb-3 px-1.5">
<div className="h-full my-1 border rounded-sm border-default">
<Tabs storageKey="console-markets">
<Tab
id="open-markets"
name={t('Open markets')}
settings={<MarketsSettings />}
>
<Tab id="open-markets" name={t('Open markets')}>
<ErrorBoundary feature="markets-open">
<OpenMarkets />
</ErrorBoundary>
@@ -47,13 +42,11 @@ export const MarketsPage = () => {
<Tab
id="proposed-markets"
name={t('Proposed markets')}
settings={<MarketsSettings />}
menu={
<TradingAnchorButton
size="extra-small"
data-testid="propose-new-market"
href={externalLink}
target="_blank"
>
{t('Propose a new market')}
</TradingAnchorButton>
@@ -63,11 +56,7 @@ export const MarketsPage = () => {
<Proposed />
</ErrorBoundary>
</Tab>
<Tab
id="closed-markets"
name={t('Closed markets')}
settings={<MarketsSettings />}
>
<Tab id="closed-markets" name={t('Closed markets')}>
<ErrorBoundary feature="markets-closed">
<Closed />
</ErrorBoundary>
@@ -1,8 +0,0 @@
import { GridSettings } from '../../components/grid-settings/grid-settings';
import { useMarketsStore } from './market-list-table';
export const MarketsSettings = () => (
<GridSettings
updateGridStore={useMarketsStore((store) => store.updateGridStore)}
/>
);
@@ -4,26 +4,9 @@ import {
SidebarButton,
SidebarDivider,
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';
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 MarketsSidebar = () => {
const t = useT();
@@ -54,7 +37,6 @@ export const MarketsSidebar = () => {
path=":marketId"
element={
<>
<ViewInitializer />
<SidebarDivider />
<SidebarButton
view={ViewType.Order}
@@ -29,7 +29,6 @@ export const useColumnDefs = () => {
{
headerName: t('Market'),
field: 'tradableInstrument.instrument.code',
pinned: true,
cellRenderer: ({
value,
data,
@@ -5,29 +5,14 @@ import { titlefy } from '@vegaprotocol/utils';
import { useIncompleteWithdrawals } from '@vegaprotocol/withdraws';
import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit';
import { usePageTitleStore } from '../../stores';
import {
AccountsContainer,
AccountsSettings,
} from '../../components/accounts-container';
import { AccountsContainer } from '../../components/accounts-container';
import { DepositsContainer } from '../../components/deposits-container';
import {
FillsContainer,
FillsSettings,
} from '../../components/fills-container';
import {
FundingPaymentsContainer,
FundingPaymentsSettings,
} from '../../components/funding-payments-container';
import {
PositionsContainer,
PositionsSettings,
} from '../../components/positions-container';
import { FillsContainer } from '../../components/fills-container';
import { FundingPaymentsContainer } from '../../components/funding-payments-container';
import { PositionsContainer } from '../../components/positions-container';
import { PositionsMenu } from '../../components/positions-menu';
import { WithdrawalsContainer } from '../../components/withdrawals-container';
import {
OrdersContainer,
OrdersSettings,
} from '../../components/orders-container';
import { OrdersContainer } from '../../components/orders-container';
import { LedgerContainer } from '../../components/ledger-container';
import {
ResizableGrid,
@@ -54,21 +39,11 @@ const WithdrawalsIndicator = () => {
);
};
const SidebarViewInitializer = () => {
export const Portfolio = () => {
const t = useT();
const currentRouteId = useGetCurrentRouteId();
const { getView, setViews } = useSidebar();
const view = getView(currentRouteId);
// Make transfer sidebar open by default
useEffect(() => {
if (view === undefined) {
setViews({ type: ViewType.Transfer }, currentRouteId);
}
}, [view, setViews, currentRouteId]);
return null;
};
export const Portfolio = () => {
const t = useT();
const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
@@ -78,11 +53,17 @@ export const Portfolio = () => {
updateTitle(titlefy([t('Portfolio')]));
}, [updateTitle, t]);
// Make transfer sidebar open by default
useEffect(() => {
if (view === undefined) {
setViews({ type: ViewType.Transfer }, currentRouteId);
}
}, [view, setViews, currentRouteId]);
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'portfolio' });
const wrapperClasses = 'p-0.5 h-full max-h-full flex flex-col';
return (
<div className={wrapperClasses}>
<SidebarViewInitializer />
<ResizableGrid vertical onChange={handleOnLayoutChange}>
<ResizableGridPanel minSize={75}>
<PortfolioGridChild>
@@ -91,27 +72,22 @@ export const Portfolio = () => {
id="positions"
name={t('Positions')}
menu={<PositionsMenu />}
settings={<PositionsSettings />}
>
<ErrorBoundary feature="portfolio-positions">
<PositionsContainer allKeys />
</ErrorBoundary>
</Tab>
<Tab id="orders" name={t('Orders')} settings={<OrdersSettings />}>
<Tab id="orders" name={t('Orders')}>
<ErrorBoundary feature="portfolio-orders">
<OrdersContainer />
</ErrorBoundary>
</Tab>
<Tab id="fills" name={t('Fills')} settings={<FillsSettings />}>
<Tab id="fills" name={t('Fills')}>
<ErrorBoundary feature="portfolio-fills">
<FillsContainer />
</ErrorBoundary>
</Tab>
<Tab
id="funding-payments"
name={t('Funding payments')}
settings={<FundingPaymentsSettings />}
>
<Tab id="funding-payments" name={t('Funding payments')}>
<ErrorBoundary feature="portfolio-funding-payments">
<FundingPaymentsContainer />
</ErrorBoundary>
@@ -134,7 +110,6 @@ export const Portfolio = () => {
<Tab
id="collateral"
name={t('Collateral')}
settings={<AccountsSettings />}
menu={<AccountsMenu />}
>
<ErrorBoundary feature="portfolio-accounts">
@@ -5,50 +5,26 @@ import {
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import type { FieldValues } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import classNames from 'classnames';
import { Navigate, useNavigate, useSearchParams } from 'react-router-dom';
import type { ButtonHTMLAttributes, MouseEventHandler } from 'react';
import { useCallback } from 'react';
import { RainbowButton } from '../../components/rainbow-button';
import {
useSimpleTransaction,
useVegaWallet,
useVegaWalletDialogStore,
} from '@vegaprotocol/wallet';
import { useCallback, useEffect, useRef, useState } from 'react';
import { RainbowButton } from './buttons';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { useIsInReferralSet, useReferral } from './hooks/use-referral';
import { Routes } from '../../lib/links';
import { useTransactionEventSubscription } from '@vegaprotocol/web3';
import { Statistics, useStats } from './referral-statistics';
import { useReferralProgram } from './hooks/use-referral-program';
import { ns, useT } from '../../lib/use-t';
import { useT } from '../../lib/use-t';
import { useFundsAvailable } from './hooks/use-funds-available';
import { ViewType, useSidebar } from '../../components/sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { QUSDTooltip } from './qusd-tooltip';
import { Trans } from 'react-i18next';
const RELOAD_DELAY = 3000;
const SPAM_PROTECTION_ERR = 'SPAM_PROTECTION_ERR';
const SpamProtectionErr = ({
requiredFunds,
}: {
requiredFunds?: string | number | bigint;
}) => {
if (!requiredFunds) return null;
// eslint-disable-next-line react/jsx-no-undef
return (
<Trans
defaults="To protect the network from spam, you must have at least {{requiredFunds}} <0>qUSD</0> of any asset on the network to proceed."
values={{
requiredFunds,
}}
components={[<QUSDTooltip key="qusd" />]}
ns={ns}
/>
);
};
const validateCode = (value: string, t: ReturnType<typeof useT>) => {
const number = +`0x${value}`;
if (!value || value.length !== 64) {
@@ -75,10 +51,6 @@ export const ApplyCodeFormContainer = ({
return <ApplyCodeForm onSuccess={onSuccess} />;
};
type FormFields = {
code: string;
};
export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
const t = useT();
const program = useReferralProgram();
@@ -87,57 +59,63 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
(store) => store.openVegaWalletDialog
);
const { isReadOnly, pubKey } = useVegaWallet();
const [status, setStatus] = useState<
'requested' | 'no-funds' | 'successful' | null
>(null);
const txHash = useRef<string | null>(null);
const { isReadOnly, pubKey, sendTx } = useVegaWallet();
const { isEligible, requiredFunds } = useFundsAvailable();
const currentRouteId = useGetCurrentRouteId();
const setViews = useSidebar((s) => s.setViews);
const [params] = useSearchParams();
const {
register,
handleSubmit,
formState: { errors },
setValue,
setError,
watch,
} = useForm<FormFields>({
defaultValues: {
code: params.get('code') || '',
},
});
clearErrors,
} = useForm();
const [params] = useSearchParams();
const codeField = watch('code');
const { data: previewData, loading: previewLoading } = useReferral({
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)
*/
const validateFundsAvailable = useCallback(() => {
if (requiredFunds && !isEligible) {
const err = SPAM_PROTECTION_ERR;
const err = t(
'To protect the network from spam, you must have at least {{requiredFunds}} qUSD of any asset on the network to proceed.',
{
requiredFunds,
}
);
return err;
}
return true;
}, [isEligible, requiredFunds]);
}, [isEligible, requiredFunds, t]);
useEffect(() => {
if (codeField) {
const err = validateFundsAvailable();
if (err !== true) {
setStatus('no-funds');
setError('code', {
type: 'required',
message: err,
});
} else {
setStatus(null);
clearErrors('code');
}
}
}, [clearErrors, codeField, isEligible, setError, validateFundsAvailable]);
/**
* Validates the set a user tries to apply to.
@@ -157,55 +135,90 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
return true;
}, [codeField, previewData, previewLoading, t]);
const noFunds = validateFundsAvailable() !== true ? true : false;
useEffect(() => {
const code = params.get('code');
if (code) setValue('code', code);
}, [params, setValue]);
const onSubmit = ({ code }: FormFields) => {
const onSubmit = ({ code }: FieldValues) => {
if (isReadOnly || !pubKey || !code || code.length === 0) {
return;
}
send({
setStatus('requested');
sendTx(pubKey, {
applyReferralCode: {
id: code as string,
},
});
// sendTx(pubKey, {
// applyReferralCode: {
// 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'),
// });
// }
// });
})
.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({
variables: { partyId: pubKey || '' },
skip: !pubKey,
fetchPolicy: 'no-cache',
onData: ({ data: result }) =>
result.data?.busEvents?.forEach((event) => {
if (event.event.__typename === 'TransactionResult') {
const hash = event.event.hash.toLowerCase();
if (txHash.current && txHash.current === hash) {
const err = event.event.error;
const status = event.event.status;
if (err) {
setStatus(null);
setError('code', {
type: 'required',
message: err,
});
}
if (status && !err) {
setStatus('successful');
}
}
}
}),
});
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
if (status === 'confirmed') {
if (status === 'successful') {
return (
<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">
@@ -239,7 +252,7 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
};
}
if (noFunds) {
if (status === 'no-funds') {
return {
disabled: false,
children: t('Deposit funds'),
@@ -274,7 +287,7 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
<>
<div
data-testid="referral-apply-code-form"
className="bg-vega-clight-800 dark:bg-vega-cdark-800 mx-auto md:w-2/3 max-w-md rounded-lg p-8"
className="bg-vega-clight-800 dark:bg-vega-cdark-800 mx-auto w-2/3 max-w-md rounded-lg p-8"
>
<h3 className="calt mb-4 text-center text-2xl">
{t('Apply a referral code')}
@@ -310,26 +323,10 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
</label>
<RainbowButton variant="border" {...getButtonProps()} />
</form>
{noFunds ? (
<InputError intent="warning" className="overflow-auto break-words">
<span>
<SpamProtectionErr requiredFunds={requiredFunds?.toString()} />
</span>
{errors.code && (
<InputError className="overflow-auto break-words">
{errors.code.message?.toString()}
</InputError>
) : (
errors.code && (
<InputError intent="warning" className="overflow-auto break-words">
{errors.code.message === SPAM_PROTECTION_ERR ? (
<span>
<SpamProtectionErr
requiredFunds={requiredFunds?.toString()}
/>
</span>
) : (
errors.code.message?.toString()
)}
</InputError>
)
)}
</div>
{validateCode(codeField, t) === true && previewLoading && !previewData ? (
@@ -4,6 +4,41 @@ import type { ComponentProps, ButtonHTMLAttributes } from 'react';
import { forwardRef } from 'react';
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(
'inline-block',
'bg-vega-clight-500 dark:bg-vega-cdark-500',
@@ -3,7 +3,7 @@ export const GRADIENT =
'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';
'bg-[url(/sky-light.png)] dark:bg-[url(/sky-dark.png)] bg-[40%_0px] bg-[length:1440px] bg-no-repeat bg-local';
// TODO: Update the links to use the correct referral related pages
export const REFERRAL_DOCS_LINK =
@@ -1,5 +1,9 @@
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { RainbowButton } from '../../components/rainbow-button';
import {
useVegaWallet,
useVegaWalletDialogStore,
determineId,
} from '@vegaprotocol/wallet';
import { RainbowButton } from './buttons';
import { useState } from 'react';
import {
CopyWithTooltip,
@@ -14,13 +18,13 @@ import {
} from '@vegaprotocol/ui-toolkit';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { DApp, TokenStaticLinks, useLinks } from '@vegaprotocol/environment';
import { useStakeAvailable } from './hooks/use-stake-available';
import { ABOUT_REFERRAL_DOCS_LINK } from './constants';
import { useIsInReferralSet, useReferral } from './hooks/use-referral';
import { useT } from '../../lib/use-t';
import { Navigate } from 'react-router-dom';
import { Routes } from '../../lib/links';
import { useReferralProgram } from './hooks/use-referral-program';
import { useCreateReferralSet } from '../../lib/hooks/use-create-referral-set';
export const CreateCodeContainer = () => {
const { pubKey } = useVegaWallet();
@@ -45,7 +49,7 @@ export const CreateCodeForm = () => {
return (
<div
data-testid="referral-create-code-form"
className="md:w-2/3 max-w-md mx-auto bg-vega-clight-800 dark:bg-vega-cdark-800 p-8 rounded-lg"
className="w-2/3 max-w-md mx-auto bg-vega-clight-800 dark:bg-vega-cdark-800 p-8 rounded-lg"
>
<h3 className="mb-4 text-2xl text-center calt">
{t('Create a referral code')}
@@ -91,42 +95,67 @@ const CreateCodeDialog = ({
}) => {
const t = useT();
const createLink = useLinks(DApp.Governance);
const { pubKey } = useVegaWallet();
const { isReadOnly, pubKey, sendTx } = useVegaWallet();
const { refetch } = useReferral({ pubKey, role: 'referrer' });
const {
err,
code,
status,
stakeAvailable: currentStakeAvailable,
requiredStake,
onSubmit,
} = useCreateReferralSet();
const [err, setErr] = useState<string | null>(null);
const [code, setCode] = useState<string | null>(null);
const [status, setStatus] = useState<
'idle' | 'loading' | 'success' | 'error'
>('idle');
const { stakeAvailable: currentStakeAvailable, requiredStake } =
useStakeAvailable();
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 = () => {
if (status === 'idle') {
if (status === 'idle' || status === 'error') {
return {
children: t('Generate code'),
onClick: () => onSubmit({ createReferralSet: { isTeam: false } }),
onClick: () => onSubmit(),
};
}
if (status === 'requested') {
if (status === 'loading') {
return {
children: t('Confirm in wallet...'),
disabled: true,
};
}
if (status === 'pending') {
return {
children: t('Waiting for transaction...'),
disabled: true,
};
}
if (status === 'confirmed') {
if (status === 'success') {
return {
children: t('Close'),
intent: Intent.Success,
@@ -180,10 +209,7 @@ const CreateCodeDialog = ({
if (!programDetails) {
return (
<div className="flex flex-col gap-4">
{(status === 'idle' ||
status === 'requested' ||
status === 'pending' ||
err) && (
{(status === 'idle' || status === 'loading' || status === 'error') && (
<>
{
<p>
@@ -194,7 +220,7 @@ const CreateCodeDialog = ({
}
</>
)}
{status === 'confirmed' && code && (
{status === 'success' && code && (
<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">
<p className="overflow-hidden whitespace-nowrap text-ellipsis">
@@ -214,7 +240,7 @@ const CreateCodeDialog = ({
<TradingButton
fill={true}
intent={Intent.Primary}
onClick={() => onSubmit({ createReferralSet: { isTeam: false } })}
onClick={() => onSubmit()}
{...getButtonProps()}
>
{t('Yes')}
@@ -243,17 +269,14 @@ const CreateCodeDialog = ({
return (
<div className="flex flex-col gap-4">
{(status === 'idle' ||
status === 'requested' ||
status === 'pending' ||
err) && (
{(status === 'idle' || status === 'loading' || status === 'error') && (
<p>
{t(
'Generate a referral code to share with your friends and access the commission benefits of the current program.'
)}
</p>
)}
{status === 'confirmed' && code && (
{status === 'success' && code && (
<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">
<p className="overflow-hidden whitespace-nowrap text-ellipsis">
@@ -1,5 +1,5 @@
import { isRouteErrorResponse, useNavigate, useRouteError } from 'react-router';
import { RainbowButton } from '../../components/rainbow-button';
import { RainbowButton } from './buttons';
import { AnimatedDudeWithWire } from './graphics/dude';
import { LayoutWithSky } from './layout';
import { Routes } from '../../lib/links';

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