Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ede909dacc | ||
|
|
7d3bdd2283 | ||
|
|
83c7e0b98f | ||
|
|
6fc25290ab | ||
|
|
f201a522d5 | ||
|
|
cf042492b4 | ||
|
|
96cbbfc3a0 | ||
|
|
cb80d6c20b | ||
|
|
b74a04ed94 | ||
|
|
fca8c19898 | ||
|
|
cfb7715124 | ||
|
|
1004347b33 | ||
|
|
d14469f2f9 | ||
|
|
da7dc1309b | ||
|
|
e57bf9a207 | ||
|
|
44842169e7 | ||
|
|
d32f27fcb1 | ||
|
|
d05cd6a2ed | ||
|
|
c003e5fa30 | ||
|
|
f62d3289ab | ||
|
|
a3d3d18c5c | ||
|
|
77ca101781 | ||
|
|
d238c37d0c | ||
|
|
79feb485f6 | ||
|
|
6aa5c3b6e3 | ||
|
|
82abc13fda | ||
|
|
933f07cddf | ||
|
|
c8067669c2 | ||
|
|
78f5a9c520 | ||
|
|
462066959d | ||
|
|
f12f5ab961 |
@@ -60,7 +60,7 @@ const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="whitespace-nowrap">
|
||||
<span>
|
||||
{useName && <Icon size={4} name="cube" className="mr-2" />}
|
||||
<Link
|
||||
className="underline font-mono"
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
query ExplorerProposal($id: ID!) {
|
||||
proposal(id: $id) {
|
||||
id
|
||||
rationale {
|
||||
title
|
||||
description
|
||||
... on Proposal {
|
||||
id
|
||||
rationale {
|
||||
title
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-5
@@ -8,16 +8,18 @@ export type ExplorerProposalQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ExplorerProposalQuery = { __typename?: 'Query', proposal?: { __typename?: 'Proposal', id?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string } } | null };
|
||||
export type ExplorerProposalQuery = { __typename?: 'Query', proposal?: { __typename?: 'BatchProposal' } | { __typename?: 'Proposal', id?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string } } | null };
|
||||
|
||||
|
||||
export const ExplorerProposalDocument = gql`
|
||||
query ExplorerProposal($id: ID!) {
|
||||
proposal(id: $id) {
|
||||
id
|
||||
rationale {
|
||||
title
|
||||
description
|
||||
... on Proposal {
|
||||
id
|
||||
rationale {
|
||||
title
|
||||
description
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { useExplorerProposalQuery } from './__generated__/Proposal';
|
||||
import {
|
||||
useExplorerProposalQuery,
|
||||
type ExplorerProposalQuery,
|
||||
} 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;
|
||||
@@ -16,8 +20,13 @@ const ProposalLink = ({ id, text }: ProposalLinkProps) => {
|
||||
variables: { id },
|
||||
});
|
||||
|
||||
const proposal = data?.proposal as Extract<
|
||||
ExplorerProposalQuery['proposal'],
|
||||
{ __typename?: 'Proposal' }
|
||||
>;
|
||||
|
||||
const base = ENV.dataSources.governanceUrl;
|
||||
const label = data?.proposal?.rationale.title || id;
|
||||
const label = proposal?.rationale.title || id;
|
||||
|
||||
return (
|
||||
<ExternalLink href={`${base}/proposals/${id}`}>
|
||||
|
||||
@@ -60,7 +60,7 @@ const DeterministicOrderDetails = ({
|
||||
const o = data.orderByID;
|
||||
return (
|
||||
<div className={wrapperClasses}>
|
||||
<div className="mb-12 lg:mb-0">
|
||||
<div className="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 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">
|
||||
<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">
|
||||
{t('Status')}
|
||||
</h2>
|
||||
<h5 className="text-lg font-medium text-gray-500 mb-0 capitalize">
|
||||
@@ -99,15 +99,17 @@ const DeterministicOrderDetails = ({
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div className="mb-12 md:mb-0">
|
||||
<h2 className="text-2xl font-bold text-dark mb-4">{t('Size')}</h2>
|
||||
<div className="mb-6 md:mb-0">
|
||||
<h2 className="text-2xl font-bold text-dark mb-0 md: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="">
|
||||
<h2 className="text-2xl font-bold text-dark mb-4">
|
||||
<div className="mb-6 md:mb-0">
|
||||
<h2 className="text-2xl font-bold text-dark mb-0 md:mb-4">
|
||||
{t('Version')}
|
||||
</h2>
|
||||
<h5 className="text-lg font-medium text-gray-500 mb-0">
|
||||
@@ -115,8 +117,8 @@ const DeterministicOrderDetails = ({
|
||||
</h5>
|
||||
</div>
|
||||
{o.type ? (
|
||||
<div className="">
|
||||
<h2 className="text-2xl font-bold text-dark mb-4">
|
||||
<div className="mb-6 md:mb-0">
|
||||
<h2 className="text-2xl font-bold text-dark mb-0 md:mb-4">
|
||||
{t('Type')}
|
||||
</h2>
|
||||
<h5 className="text-lg font-medium text-gray-500 mb-0">
|
||||
|
||||
@@ -30,12 +30,12 @@ export const Signature = ({ signature }: SignatureProps) => {
|
||||
|
||||
return (
|
||||
<div className="inline-flex border rounded signature-component relative pr-[20px]">
|
||||
<span
|
||||
<div
|
||||
className="bg-gray-100 px-2.5 py-0.5 text-xs text-gray-500 select-none cursor-default"
|
||||
title={`Version ${signature.version}`}
|
||||
title={`${signature.algo}`}
|
||||
>
|
||||
{signature.algo}
|
||||
</span>
|
||||
<span>v{signature.version}</span>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
isOpen
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
query ExplorerProposalStatus($id: ID!) {
|
||||
proposal(id: $id) {
|
||||
id
|
||||
state
|
||||
rejectionReason
|
||||
... on Proposal {
|
||||
id
|
||||
state
|
||||
rejectionReason
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-4
@@ -8,15 +8,17 @@ export type ExplorerProposalStatusQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ExplorerProposalStatusQuery = { __typename?: 'Query', proposal?: { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | null };
|
||||
export type ExplorerProposalStatusQuery = { __typename?: 'Query', proposal?: { __typename?: 'BatchProposal' } | { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | null };
|
||||
|
||||
|
||||
export const ExplorerProposalStatusDocument = gql`
|
||||
query ExplorerProposalStatus($id: ID!) {
|
||||
proposal(id: $id) {
|
||||
id
|
||||
state
|
||||
rejectionReason
|
||||
... on Proposal {
|
||||
id
|
||||
state
|
||||
rejectionReason
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -14,16 +14,18 @@ export function format(date: string | undefined, def: string) {
|
||||
return new Date().toLocaleDateString() || def;
|
||||
}
|
||||
|
||||
export function getDate(
|
||||
data: ExplorerProposalStatusQuery | undefined,
|
||||
terms: Terms
|
||||
): string {
|
||||
type Proposal = Extract<
|
||||
ExplorerProposalStatusQuery['proposal'],
|
||||
{ __typename?: 'Proposal' }
|
||||
>;
|
||||
|
||||
export function getDate(proposal: Proposal | undefined, terms: Terms): string {
|
||||
const DEFAULT = t('Unknown');
|
||||
if (!data?.proposal?.state) {
|
||||
if (!proposal?.state) {
|
||||
return DEFAULT;
|
||||
}
|
||||
|
||||
switch (data.proposal.state) {
|
||||
switch (proposal.state) {
|
||||
case 'STATE_DECLINED':
|
||||
return `${t('Rejected on')}: ${format(terms.closingTimestamp, DEFAULT)}`;
|
||||
case 'STATE_ENACTED':
|
||||
@@ -62,9 +64,11 @@ export const ProposalDate = ({ terms, id }: ProposalDateProps) => {
|
||||
},
|
||||
});
|
||||
|
||||
const proposal = data?.proposal as Proposal;
|
||||
|
||||
return (
|
||||
<Lozenge className="font-sans text-xs float-right">
|
||||
{getDate(data, terms)}
|
||||
{getDate(proposal, terms)}
|
||||
</Lozenge>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,17 +2,8 @@ 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;
|
||||
}
|
||||
@@ -29,29 +20,38 @@ type IconAndLabel = {
|
||||
* @param data a data result from useExplorerProposalStatusQuery
|
||||
* @returns Icon name
|
||||
*/
|
||||
export function getIconAndLabelForStatus(
|
||||
res: ProposalQueryResult
|
||||
): IconAndLabel {
|
||||
export function useIconAndLabelForStatus(id: string): IconAndLabel {
|
||||
const { data, loading, error } = useExplorerProposalStatusQuery({
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
const proposal = data?.proposal as Extract<
|
||||
ExplorerProposalStatusQuery['proposal'],
|
||||
{ __typename?: 'Proposal' }
|
||||
>;
|
||||
|
||||
const DEFAULT: IconAndLabel = {
|
||||
icon: 'error',
|
||||
label: t('Proposal state unknown'),
|
||||
};
|
||||
|
||||
if (res.loading) {
|
||||
if (loading) {
|
||||
return {
|
||||
icon: 'more',
|
||||
label: t('Loading data'),
|
||||
};
|
||||
}
|
||||
|
||||
if (!res?.data?.proposal || res.error) {
|
||||
if (!data?.proposal || error) {
|
||||
return {
|
||||
icon: 'error',
|
||||
label: res.error?.message || DEFAULT.label,
|
||||
label: error?.message || DEFAULT.label,
|
||||
};
|
||||
}
|
||||
|
||||
switch (res.data.proposal.state) {
|
||||
switch (proposal.state) {
|
||||
case 'STATE_DECLINED':
|
||||
return {
|
||||
icon: 'stop',
|
||||
@@ -99,13 +99,7 @@ export function getIconAndLabelForStatus(
|
||||
/**
|
||||
*/
|
||||
export const ProposalStatusIcon = ({ id }: ProposalStatusIconProps) => {
|
||||
const { icon, label } = getIconAndLabelForStatus(
|
||||
useExplorerProposalStatusQuery({
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
})
|
||||
);
|
||||
const { icon, label } = useIconAndLabelForStatus(id);
|
||||
|
||||
return (
|
||||
<div className="float-left mr-3">
|
||||
|
||||
@@ -73,7 +73,7 @@ export const Layout = () => {
|
||||
<ProtocolUpgradeInProgressNotification />
|
||||
</div>
|
||||
<div className={fixedWidthClasses}>
|
||||
<main className="p-4">
|
||||
<main className="md:p-4">
|
||||
{!isHome && <BreadcrumbsContainer className="mb-4" />}
|
||||
<Outlet />
|
||||
</main>
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
orderByUpgradeBlockHeight,
|
||||
} from '../proposals/components/proposals-list/proposals-list';
|
||||
import { BigNumber } from '../../lib/bignumber';
|
||||
import type { ProposalQuery } from '../proposals/proposal/__generated__/Proposal';
|
||||
import { type Proposal } from '../proposals/types';
|
||||
|
||||
const nodesToShow = 6;
|
||||
|
||||
@@ -39,7 +39,7 @@ const HomeProposals = ({
|
||||
proposals,
|
||||
protocolUpgradeProposals,
|
||||
}: {
|
||||
proposals: ProposalQuery['proposal'][];
|
||||
proposals: Proposal[];
|
||||
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
+4
-9
@@ -1,16 +1,11 @@
|
||||
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 ReactNode } from 'react';
|
||||
import { type ProposalInfoLabelVariant } from '../proposal-info-label';
|
||||
import { type Proposal } from '../../types';
|
||||
|
||||
export const CurrentProposalState = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
}) => {
|
||||
export const CurrentProposalState = ({ proposal }: { proposal: Proposal }) => {
|
||||
const { t } = useTranslation();
|
||||
let proposalStatus: ReactNode;
|
||||
let variant = 'tertiary' as ProposalInfoLabelVariant;
|
||||
|
||||
-272
@@ -1,272 +0,0 @@
|
||||
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();
|
||||
});
|
||||
-143
@@ -1,143 +0,0 @@
|
||||
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> {daysClosedAgo}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!majorityMet) {
|
||||
return (
|
||||
<>
|
||||
<span>{t('voteFailedReason')}</span>
|
||||
<StatusFail>{t('majorityNotMet')}</StatusFail>
|
||||
<span> {daysClosedAgo}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<span>{t('voteFailedReason')}</span>
|
||||
<StatusFail>
|
||||
{proposal?.errorDetails ||
|
||||
proposal?.rejectionReason ||
|
||||
t('unknownReason')}
|
||||
</StatusFail>
|
||||
<span> {daysClosedAgo}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (
|
||||
proposal?.state === ProposalState.STATE_ENACTED ||
|
||||
proposal?.state === ProposalState.STATE_PASSED
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
<span>{t('votePassed')}</span>
|
||||
<StatusPass>
|
||||
|
||||
{proposal?.state === ProposalState.STATE_ENACTED
|
||||
? t('Enacted')
|
||||
: t('Passed')}
|
||||
</StatusPass>
|
||||
<span>
|
||||
|
||||
{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;
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { CurrentProposalStatus } from './current-proposal-status';
|
||||
+2
-3
@@ -6,11 +6,10 @@ import {
|
||||
KeyValueTableRow,
|
||||
RoundedWrapper,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import { type Proposal } from '../../types';
|
||||
|
||||
interface ProposalChangeTableProps {
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
proposal: Proposal;
|
||||
}
|
||||
|
||||
export const ProposalChangeTable = ({ proposal }: ProposalChangeTableProps) => {
|
||||
|
||||
+17
-3
@@ -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 { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { type MockedResponse } from '@apollo/client/testing';
|
||||
import { type Proposal } from '../../types';
|
||||
|
||||
jest.mock('@vegaprotocol/proposals', () => ({
|
||||
...jest.requireActual('@vegaprotocol/proposals'),
|
||||
@@ -36,7 +36,7 @@ jest.mock('@vegaprotocol/proposals', () => ({
|
||||
}));
|
||||
|
||||
const renderComponent = (
|
||||
proposal: ProposalQuery['proposal'],
|
||||
proposal: Proposal,
|
||||
isListItem = true,
|
||||
mocks: MockedResponse[] = [],
|
||||
voteState?: VoteState
|
||||
@@ -64,6 +64,7 @@ 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',
|
||||
@@ -102,6 +103,7 @@ describe('Proposal header', () => {
|
||||
|
||||
it('Renders Update market proposal', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
rationale: {
|
||||
title: 'New market id',
|
||||
@@ -130,6 +132,7 @@ 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',
|
||||
@@ -159,6 +162,7 @@ describe('Proposal header', () => {
|
||||
|
||||
it('Renders New asset proposal - BuiltInAsset', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
@@ -184,6 +188,7 @@ describe('Proposal header', () => {
|
||||
|
||||
it('Renders Update network', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
rationale: {
|
||||
title: 'Network parameter',
|
||||
@@ -213,6 +218,7 @@ describe('Proposal header', () => {
|
||||
|
||||
it('Renders Freeform proposal - short rationale', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
id: 'short',
|
||||
rationale: {
|
||||
@@ -234,6 +240,7 @@ 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: {
|
||||
@@ -259,6 +266,7 @@ 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: {
|
||||
@@ -280,6 +288,7 @@ describe('Proposal header', () => {
|
||||
|
||||
it('Renders asset change proposal header', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
terms: {
|
||||
change: {
|
||||
@@ -297,6 +306,7 @@ 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: {
|
||||
@@ -313,6 +323,7 @@ describe('Proposal header', () => {
|
||||
|
||||
it('Renders proposal state: Enacted', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_ENACTED,
|
||||
terms: {
|
||||
@@ -325,6 +336,7 @@ describe('Proposal header', () => {
|
||||
|
||||
it('Renders proposal state: Passed', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_PASSED,
|
||||
terms: {
|
||||
@@ -338,6 +350,7 @@ 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: {
|
||||
@@ -352,6 +365,7 @@ describe('Proposal header', () => {
|
||||
|
||||
it('Renders proposal state: Open', () => {
|
||||
renderComponent(
|
||||
// @ts-ignore we aren't using batch yet
|
||||
generateProposal({
|
||||
state: ProposalState.STATE_OPEN,
|
||||
votes: {
|
||||
|
||||
+5
-5
@@ -2,8 +2,7 @@ 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 { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import { type ReactNode } from 'react';
|
||||
import { truncateMiddle } from '../../../../lib/truncate-middle';
|
||||
import { CurrentProposalState } from '../current-proposal-state';
|
||||
import { ProposalInfoLabel } from '../proposal-info-label';
|
||||
@@ -15,16 +14,17 @@ 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: ProposalQuery['proposal'];
|
||||
proposal: Proposal;
|
||||
isListItem?: boolean;
|
||||
voteState?: VoteState | null;
|
||||
}) => {
|
||||
@@ -40,7 +40,7 @@ export const ProposalHeader = ({
|
||||
|
||||
const titleContent = shorten(title ?? '', 100);
|
||||
|
||||
const getAsset = (proposal: ProposalQuery['proposal']) => {
|
||||
const getAsset = (proposal: Proposal) => {
|
||||
const terms = proposal?.terms;
|
||||
if (
|
||||
terms?.change.__typename === 'NewMarket' &&
|
||||
|
||||
+2
-2
@@ -1,5 +1,4 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import {
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
@@ -14,9 +13,10 @@ 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: ProposalQuery['proposal'];
|
||||
proposal: Proposal | null;
|
||||
}
|
||||
|
||||
export const formatEndOfProgramTimestamp = (value: string) => {
|
||||
|
||||
+2
-3
@@ -1,6 +1,4 @@
|
||||
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,
|
||||
@@ -8,11 +6,12 @@ import {
|
||||
RoundedWrapper,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { SubHeading } from '../../../../components/heading';
|
||||
import { type Proposal } from '../../types';
|
||||
|
||||
export const ProposalCancelTransferDetails = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
proposal: Proposal;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const details = useCancelTransferProposalDetails(proposal?.id);
|
||||
|
||||
+2
-3
@@ -1,6 +1,4 @@
|
||||
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';
|
||||
@@ -21,11 +19,12 @@ import {
|
||||
addDecimalsFormatNumberQuantum,
|
||||
formatDateWithLocalTimezone,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { type Proposal } from '../../types';
|
||||
|
||||
export const ProposalTransferDetails = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
proposal: Proposal;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
+2
-2
@@ -1,5 +1,4 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import {
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
@@ -12,6 +11,7 @@ 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: ProposalQuery['proposal'];
|
||||
proposal: Proposal | null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+2
-2
@@ -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: ProposalQuery['proposal'];
|
||||
proposal: Proposal | null;
|
||||
}
|
||||
|
||||
export const ProposalUpdateMarketState = ({
|
||||
|
||||
+2
-2
@@ -1,5 +1,4 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import {
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
@@ -12,9 +11,10 @@ import {
|
||||
} from '../proposal-referral-program-details';
|
||||
import { formatNumberPercentage } from '@vegaprotocol/utils';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { type Proposal } from '../../types';
|
||||
|
||||
interface ProposalReferralProgramDetailsProps {
|
||||
proposal: ProposalQuery['proposal'];
|
||||
proposal: Proposal | null;
|
||||
}
|
||||
|
||||
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: ProposalQuery['proposal']) => {
|
||||
const renderComponent = (proposal: IProposal) => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MockedProvider>
|
||||
<VegaWalletProvider config={vegaWalletConfig}>
|
||||
<Proposal
|
||||
restData={{}}
|
||||
proposal={proposal as ProposalQuery['proposal']}
|
||||
proposal={proposal}
|
||||
networkParams={mockNetworkParams}
|
||||
/>
|
||||
</VegaWalletProvider>
|
||||
|
||||
@@ -12,14 +12,13 @@ import { UserVote } from '../vote-details';
|
||||
import { ListAsset } from '../list-asset';
|
||||
import Routes from '../../../routes';
|
||||
import { ProposalMarketData } from '../proposal-market-data';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import type { MarketInfo } from '@vegaprotocol/markets';
|
||||
import type { AssetQuery } from '@vegaprotocol/assets';
|
||||
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 {
|
||||
@@ -28,9 +27,10 @@ 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: ProposalQuery['proposal'];
|
||||
proposal: IProposal;
|
||||
networkParams: Partial<NetworkParamsResult>;
|
||||
marketData?: MarketInfo | null;
|
||||
parentMarketData?: MarketInfo | null;
|
||||
|
||||
+3
-3
@@ -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 { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import { type Proposal } from '../../types';
|
||||
|
||||
const renderComponent = (
|
||||
proposal: ProposalQuery['proposal'],
|
||||
proposal: Proposal,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
mocks: MockedResponse<any>[] = [networkParamsQueryMock]
|
||||
) =>
|
||||
|
||||
+3
-4
@@ -1,21 +1,20 @@
|
||||
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 { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import { type Proposal } from '../../types';
|
||||
|
||||
export const ProposalsListItemDetails = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
proposal: Proposal;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const state = proposal?.state;
|
||||
|
||||
+2
-2
@@ -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 { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import { type Proposal } from '../../types';
|
||||
|
||||
interface ProposalsListItemProps {
|
||||
proposal?: ProposalQuery['proposal'] | null;
|
||||
proposal?: Proposal | null;
|
||||
}
|
||||
|
||||
export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => {
|
||||
|
||||
+3
-3
@@ -17,8 +17,8 @@ import {
|
||||
lastMonth,
|
||||
nextMonth,
|
||||
} from '../../test-helpers/mocks';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
|
||||
import { type Proposal } from '../../types';
|
||||
|
||||
const openProposalClosesNextMonth = generateProposal({
|
||||
id: 'proposal1',
|
||||
@@ -63,7 +63,7 @@ const closedProtocolUpgradeProposal = generateProtocolUpgradeProposal({
|
||||
});
|
||||
|
||||
const renderComponent = (
|
||||
proposals: ProposalQuery['proposal'][],
|
||||
proposals: 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: Array<ProposalQuery['proposal']>;
|
||||
proposals: Proposal[];
|
||||
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
|
||||
lastBlockHeight?: string;
|
||||
}
|
||||
|
||||
interface SortedProposalsProps {
|
||||
open: ProposalQuery['proposal'][];
|
||||
closed: ProposalQuery['proposal'][];
|
||||
open: Proposal[];
|
||||
closed: Proposal[];
|
||||
}
|
||||
|
||||
interface SortedProtocolUpgradeProposalsProps {
|
||||
@@ -31,7 +31,7 @@ interface SortedProtocolUpgradeProposalsProps {
|
||||
closed: ProtocolUpgradeProposalFieldsFragment[];
|
||||
}
|
||||
|
||||
export const orderByDate = (arr: ProposalQuery['proposal'][]) =>
|
||||
export const orderByDate = (arr: Proposal[]) =>
|
||||
orderBy(
|
||||
arr,
|
||||
[
|
||||
@@ -91,14 +91,10 @@ export const ProposalsList = ({
|
||||
);
|
||||
return {
|
||||
open:
|
||||
initialSorting.open.length > 0
|
||||
? orderByDate(initialSorting.open as ProposalQuery['proposal'][])
|
||||
: [],
|
||||
initialSorting.open.length > 0 ? orderByDate(initialSorting.open) : [],
|
||||
closed:
|
||||
initialSorting.closed.length > 0
|
||||
? orderByDate(
|
||||
initialSorting.closed as ProposalQuery['proposal'][]
|
||||
).reverse()
|
||||
? orderByDate(initialSorting.closed).reverse()
|
||||
: [],
|
||||
};
|
||||
}, [proposals]);
|
||||
@@ -125,9 +121,7 @@ export const ProposalsList = ({
|
||||
};
|
||||
}, [protocolUpgradeProposals, lastBlockHeight]);
|
||||
|
||||
const filterPredicate = (
|
||||
p: ProposalFieldsFragment | ProposalQuery['proposal']
|
||||
) =>
|
||||
const filterPredicate = (p: ProposalFieldsFragment | Proposal) =>
|
||||
p?.id?.includes(filterString) ||
|
||||
p?.party?.id?.toString().includes(filterString);
|
||||
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ import {
|
||||
nextWeek,
|
||||
lastMonth,
|
||||
} from '../../test-helpers/mocks';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import { type Proposal } from '../../types';
|
||||
|
||||
const rejectedProposalClosesNextWeek = generateProposal({
|
||||
id: 'rejected1',
|
||||
@@ -35,7 +35,7 @@ const rejectedProposalClosedLastMonth = generateProposal({
|
||||
},
|
||||
});
|
||||
|
||||
const renderComponent = (proposals: ProposalQuery['proposal'][]) => (
|
||||
const renderComponent = (proposals: Proposal[]) => (
|
||||
<Router>
|
||||
<MockedProvider mocks={[networkParamsQueryMock]}>
|
||||
<AppStateProvider>
|
||||
|
||||
+3
-3
@@ -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 { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import { type Proposal } from '../../types';
|
||||
|
||||
interface ProposalsListProps {
|
||||
proposals: ProposalQuery['proposal'][];
|
||||
proposals: Proposal[];
|
||||
}
|
||||
|
||||
export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [filterString, setFilterString] = useState('');
|
||||
|
||||
const filterPredicate = (p: ProposalQuery['proposal']) =>
|
||||
const filterPredicate = (p: Proposal) =>
|
||||
p?.id?.includes(filterString) ||
|
||||
p?.party?.id?.toString().includes(filterString);
|
||||
|
||||
|
||||
+4
-4
@@ -9,8 +9,7 @@ import {
|
||||
nextWeek,
|
||||
} from '../../test-helpers/mocks';
|
||||
import { CompactVotes, VoteBreakdown } from './vote-breakdown';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { type MockedResponse } from '@apollo/client/testing';
|
||||
import {
|
||||
generateNoVotes,
|
||||
generateProposal,
|
||||
@@ -18,7 +17,8 @@ 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 AppState } from '../../../../contexts/app-state/app-state-context';
|
||||
import { type Proposal } from '../../types';
|
||||
|
||||
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: ProposalQuery['proposal'],
|
||||
proposal: Proposal,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
mocks: MockedResponse<any>[] = [networkParamsQueryMock]
|
||||
) =>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { type ReactNode } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -5,10 +6,8 @@ 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
|
||||
@@ -20,7 +19,7 @@ export const CompactVotes = ({ number }: { number: BigNumber }) => (
|
||||
);
|
||||
|
||||
interface VoteBreakdownProps {
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
proposal: Proposal;
|
||||
}
|
||||
|
||||
interface VoteProgressProps {
|
||||
|
||||
@@ -5,14 +5,13 @@ 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, 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';
|
||||
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';
|
||||
|
||||
interface UserVoteProps {
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
proposal: Proposal;
|
||||
minVoterBalance: string | null | undefined;
|
||||
spamProtectionMinTokens: string | null | undefined;
|
||||
transaction: VegaTxState | null;
|
||||
|
||||
@@ -3,13 +3,12 @@ import {
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
import { BigNumber } from '../../../lib/bignumber';
|
||||
import type { ProposalFieldsFragment } from '../proposals/__generated__/Proposals';
|
||||
import type { ProposalQuery } from '../proposal/__generated__/Proposal';
|
||||
import { type Proposal } from '../types';
|
||||
|
||||
export const useProposalNetworkParams = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
proposal: Proposal;
|
||||
}) => {
|
||||
const { params } = useNetworkParams([
|
||||
NetworkParams.governance_proposal_updateMarket_requiredMajority,
|
||||
|
||||
@@ -2,15 +2,10 @@ 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: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
}) => {
|
||||
export const useVoteInformation = ({ proposal }: { proposal: Proposal }) => {
|
||||
const {
|
||||
appState: { totalSupply, decimals },
|
||||
} = useAppState();
|
||||
|
||||
@@ -86,230 +86,65 @@ query Proposal(
|
||||
$includeUpdateReferralProgram: Boolean!
|
||||
) {
|
||||
proposal(id: $proposalId) {
|
||||
id
|
||||
rationale {
|
||||
title
|
||||
description
|
||||
}
|
||||
reference
|
||||
state
|
||||
datetime
|
||||
rejectionReason
|
||||
party {
|
||||
... on Proposal {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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 PerpetualProduct {
|
||||
settlementAsset {
|
||||
id
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
... on SimpleRiskModel {
|
||||
params {
|
||||
factorLong
|
||||
factorShort
|
||||
}
|
||||
quoteName
|
||||
}
|
||||
}
|
||||
}
|
||||
priceMonitoringParameters {
|
||||
triggers {
|
||||
horizonSecs
|
||||
probability
|
||||
auctionExtensionSecs
|
||||
}
|
||||
}
|
||||
liquidityMonitoringParameters {
|
||||
triggeringRatio
|
||||
targetStakeParameters {
|
||||
timeWindow
|
||||
scalingFactor
|
||||
}
|
||||
}
|
||||
positionDecimalPlaces
|
||||
linearSlippageFactor
|
||||
quadraticSlippageFactor
|
||||
}
|
||||
... on UpdateMarket {
|
||||
marketId
|
||||
updateMarketConfiguration {
|
||||
instrument {
|
||||
name
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on FutureProduct {
|
||||
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
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
quoteName
|
||||
dataSourceSpecBinding {
|
||||
settlementDataProperty
|
||||
tradingTerminationProperty
|
||||
}
|
||||
}
|
||||
... on UpdatePerpetualProduct {
|
||||
quoteName
|
||||
dataSourceSpecForSettlementData {
|
||||
sourceType {
|
||||
... on DataSourceDefinitionInternal {
|
||||
@@ -350,14 +185,19 @@ query Proposal(
|
||||
}
|
||||
}
|
||||
}
|
||||
dataSourceSpecBinding {
|
||||
settlementDataProperty
|
||||
settlementScheduleProperty
|
||||
}
|
||||
... on PerpetualProduct {
|
||||
settlementAsset {
|
||||
id
|
||||
name
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
}
|
||||
quoteName
|
||||
}
|
||||
}
|
||||
}
|
||||
metadata
|
||||
priceMonitoringParameters {
|
||||
triggers {
|
||||
horizonSecs
|
||||
@@ -372,71 +212,233 @@ query Proposal(
|
||||
scalingFactor
|
||||
}
|
||||
}
|
||||
riskParameters {
|
||||
... on UpdateMarketSimpleRiskModel {
|
||||
simple {
|
||||
factorLong
|
||||
factorShort
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
... on UpdateMarketLogNormalRiskModel {
|
||||
logNormal {
|
||||
riskAversionParameter
|
||||
tau
|
||||
params {
|
||||
r
|
||||
sigma
|
||||
mu
|
||||
metadata
|
||||
priceMonitoringParameters {
|
||||
triggers {
|
||||
horizonSecs
|
||||
probability
|
||||
auctionExtensionSecs
|
||||
}
|
||||
}
|
||||
liquidityMonitoringParameters {
|
||||
triggeringRatio
|
||||
targetStakeParameters {
|
||||
timeWindow
|
||||
scalingFactor
|
||||
}
|
||||
}
|
||||
riskParameters {
|
||||
... on UpdateMarketSimpleRiskModel {
|
||||
simple {
|
||||
factorLong
|
||||
factorShort
|
||||
}
|
||||
}
|
||||
... 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+228
-226
File diff suppressed because one or more lines are too long
@@ -17,6 +17,7 @@ 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);
|
||||
@@ -67,6 +68,8 @@ export const ProposalContainer = () => {
|
||||
skip: !params.proposalId,
|
||||
});
|
||||
|
||||
const proposal = data?.proposal as IProposal;
|
||||
|
||||
const successor = useSuccessorMarketProposalDetails(params.proposalId);
|
||||
|
||||
const isSuccessor = !!successor?.parentMarketId || !!successor.code;
|
||||
@@ -79,12 +82,12 @@ export const ProposalContainer = () => {
|
||||
},
|
||||
} = useFetch(
|
||||
`${ENV.rest}governance?proposalId=${
|
||||
data?.proposal?.terms.change.__typename === 'UpdateMarket' &&
|
||||
data?.proposal.terms.change.marketId
|
||||
proposal?.terms.change.__typename === 'UpdateMarket' &&
|
||||
proposal.terms.change.marketId
|
||||
}`,
|
||||
undefined,
|
||||
true,
|
||||
data?.proposal?.terms.change.__typename !== 'UpdateMarket'
|
||||
proposal?.terms.change.__typename !== 'UpdateMarket'
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -97,7 +100,7 @@ export const ProposalContainer = () => {
|
||||
`${ENV.rest}governances?proposalState=STATE_ENACTED&proposalType=TYPE_UPDATE_MARKET`,
|
||||
undefined,
|
||||
true,
|
||||
data?.proposal?.terms.change.__typename !== 'UpdateMarket'
|
||||
proposal?.terms.change.__typename !== 'UpdateMarket'
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -108,8 +111,8 @@ export const ProposalContainer = () => {
|
||||
dataProvider: marketInfoProvider,
|
||||
skipUpdates: true,
|
||||
variables: {
|
||||
marketId: data?.proposal?.id || '',
|
||||
skip: !data?.proposal?.id,
|
||||
marketId: proposal?.id || '',
|
||||
skip: !proposal?.id,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -148,23 +151,22 @@ export const ProposalContainer = () => {
|
||||
fetchPolicy: 'network-only',
|
||||
variables: {
|
||||
assetId:
|
||||
(data?.proposal?.terms.change.__typename === 'NewAsset' &&
|
||||
data?.proposal?.id) ||
|
||||
(data?.proposal?.terms.change.__typename === 'UpdateAsset' &&
|
||||
data.proposal.terms.change.assetId) ||
|
||||
(proposal?.terms.change.__typename === 'NewAsset' && proposal?.id) ||
|
||||
(proposal?.terms.change.__typename === 'UpdateAsset' &&
|
||||
proposal.terms.change.assetId) ||
|
||||
'',
|
||||
},
|
||||
skip: !['NewAsset', 'UpdateAsset'].includes(
|
||||
data?.proposal?.terms?.change?.__typename || ''
|
||||
proposal?.terms?.change?.__typename || ''
|
||||
),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
previouslyEnactedMarketProposalsRestData &&
|
||||
data?.proposal?.terms.change.__typename === 'UpdateMarket'
|
||||
proposal?.terms.change.__typename === 'UpdateMarket'
|
||||
) {
|
||||
const change = data?.proposal?.terms?.change as { marketId: string };
|
||||
const change = proposal?.terms?.change as { marketId: string };
|
||||
|
||||
const filteredProposals =
|
||||
// @ts-ignore rest data is not typed
|
||||
@@ -188,8 +190,8 @@ export const ProposalContainer = () => {
|
||||
}, [
|
||||
previouslyEnactedMarketProposalsRestData,
|
||||
params.proposalId,
|
||||
data?.proposal?.terms.change.__typename,
|
||||
data?.proposal?.terms.change,
|
||||
proposal?.terms.change.__typename,
|
||||
proposal?.terms.change,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -242,7 +244,7 @@ export const ProposalContainer = () => {
|
||||
>
|
||||
{data?.proposal ? (
|
||||
<Proposal
|
||||
proposal={data.proposal}
|
||||
proposal={proposal}
|
||||
networkParams={networkParams}
|
||||
restData={restData}
|
||||
marketData={marketData}
|
||||
|
||||
@@ -8,6 +8,7 @@ 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> = {}
|
||||
@@ -43,8 +44,8 @@ export function generateProtocolUpgradeProposal(
|
||||
}
|
||||
|
||||
export function generateProposal(
|
||||
override: PartialDeep<ProposalQuery['proposal']> = {}
|
||||
): ProposalQuery['proposal'] {
|
||||
override: PartialDeep<Proposal> = {}
|
||||
): Proposal {
|
||||
const defaultProposal: ProposalQuery['proposal'] = {
|
||||
__typename: 'Proposal',
|
||||
id: faker.datatype.uuid(),
|
||||
@@ -92,15 +93,16 @@ export function generateProposal(
|
||||
},
|
||||
};
|
||||
|
||||
return mergeWith<
|
||||
ProposalQuery['proposal'],
|
||||
PartialDeep<ProposalQuery['proposal']>
|
||||
>(defaultProposal, override, (objValue, srcValue) => {
|
||||
if (!isArray(objValue)) {
|
||||
return;
|
||||
return mergeWith<Proposal, PartialDeep<Proposal>>(
|
||||
defaultProposal,
|
||||
override,
|
||||
(objValue, srcValue) => {
|
||||
if (!isArray(objValue)) {
|
||||
return;
|
||||
}
|
||||
return srcValue;
|
||||
}
|
||||
return srcValue;
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
type Vote = Pick<Schema.Vote, '__typename' | 'value' | 'party' | 'datetime'>;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
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' }
|
||||
>;
|
||||
@@ -54,3 +54,5 @@ 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).
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { useEffect } from 'react';
|
||||
import { titlefy } from '@vegaprotocol/utils';
|
||||
import { usePageTitleStore } from '../../stores';
|
||||
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';
|
||||
|
||||
export const CompetitionsHome = () => {
|
||||
const t = useT();
|
||||
const navigate = useNavigate();
|
||||
|
||||
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',
|
||||
});
|
||||
|
||||
const { updateTitle } = usePageTitleStore((store) => ({
|
||||
updateTitle: store.updateTitle,
|
||||
}));
|
||||
useEffect(() => {
|
||||
updateTitle(titlefy([t('Competitions')]));
|
||||
}, [updateTitle, t]);
|
||||
|
||||
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());
|
||||
}}
|
||||
>
|
||||
{t('Create a private team')}
|
||||
</TradingButton>
|
||||
}
|
||||
/>
|
||||
<CompetitionsAction
|
||||
variant="C"
|
||||
title={t('Join a team')}
|
||||
description={t(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||
)}
|
||||
actionElement={
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(Links.COMPETITIONS_TEAMS());
|
||||
}}
|
||||
>
|
||||
{t('Choose a team')}
|
||||
</TradingButton>
|
||||
}
|
||||
/>
|
||||
</CompetitionsActionsContainer>
|
||||
|
||||
{/** List of available games */}
|
||||
<h2 className="text-2xl mb-6">{t('Games')}</h2>
|
||||
|
||||
{gamesLoading ? (
|
||||
<Loader size="small" />
|
||||
) : (
|
||||
<GamesContainer data={gamesData} currentEpoch={currentEpoch} />
|
||||
)}
|
||||
|
||||
{/** The teams ranking */}
|
||||
<div className="mb-6 flex flex-row items-baseline justify-between">
|
||||
<h2 className="text-2xl">{t('Leaderboard')}</h2>
|
||||
<Link to={Links.COMPETITIONS_TEAMS()} className="text-sm underline">
|
||||
{t('View all teams')}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{teamsLoading ? (
|
||||
<Loader size="small" />
|
||||
) : (
|
||||
<CompetitionsLeaderboard data={take(teamsData, 10)} />
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,395 @@
|
||||
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 {
|
||||
TradingButton as Button,
|
||||
Intent,
|
||||
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';
|
||||
|
||||
export const CompetitionsTeam = () => {
|
||||
const t = useT();
|
||||
const { teamId } = useParams<{ teamId: string }>();
|
||||
const { team, stats, partyInTeam, members, games } = useTeam(teamId);
|
||||
|
||||
// const team = {
|
||||
// teamId: '12345678909876543212345678765432345676543234567',
|
||||
// referrer: '12345678909876543212345678765432345676543234567',
|
||||
// name: 'The Kittens',
|
||||
// teamUrl: 'http://placekitten.com/g/200/300',
|
||||
// avatarUrl: 'http://placekitten.com/g/200/300',
|
||||
// createdAt: '2024-01-01',
|
||||
// createdAtEpoch: 123,
|
||||
// closed: true,
|
||||
// };
|
||||
|
||||
if (!team) {
|
||||
return (
|
||||
<Splash>
|
||||
<p>{t('Page not found')}</p>
|
||||
</Splash>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TeamPage
|
||||
team={team}
|
||||
stats={stats}
|
||||
partyInTeam={partyInTeam}
|
||||
members={members}
|
||||
games={games}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const TeamPage = ({
|
||||
team,
|
||||
stats,
|
||||
partyInTeam,
|
||||
members,
|
||||
games,
|
||||
}: {
|
||||
team: TeamType;
|
||||
stats?: TeamStats;
|
||||
partyInTeam: boolean;
|
||||
members?: Member[];
|
||||
games?: TeamGame[];
|
||||
}) => {
|
||||
const t = useT();
|
||||
const [showGames, setShowGames] = useState(true);
|
||||
|
||||
return (
|
||||
<div className="relative h-full overflow-y-auto">
|
||||
<div className="absolute top-0 left-0 w-full h-[40%] -z-10 bg-[40%_0px] bg-cover bg-no-repeat bg-local bg-[url(/cover.png)]">
|
||||
<div className="absolute top-o left-0 w-full h-full bg-gradient-to-t from-white dark:from-vega-cdark-900 to-transparent from-20% to-60%" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 lg:gap-6 container p-4 mx-auto">
|
||||
<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>
|
||||
<JoinButton joined={partyInTeam} />
|
||||
</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 JoinButton = ({ joined }: { joined: boolean }) => {
|
||||
const t = useT();
|
||||
|
||||
if (joined) {
|
||||
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 intent={Intent.Primary}>{t('Join this team')}</Button>;
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,241 @@
|
||||
import { ErrorBoundary } from '@sentry/react';
|
||||
import { CompetitionsHeader } from '../../components/competitions/competitions-header';
|
||||
import { usePageTitleStore } from '../../stores';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { titlefy } from '@vegaprotocol/utils';
|
||||
import { useTeams } from './hooks/use-teams';
|
||||
import { CompetitionsLeaderboard } from '../../components/competitions/competitions-leaderboard';
|
||||
import {
|
||||
Input,
|
||||
Loader,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const CompetitionsTeams = () => {
|
||||
const t = useT();
|
||||
const { updateTitle } = usePageTitleStore((store) => ({
|
||||
updateTitle: store.updateTitle,
|
||||
}));
|
||||
useEffect(() => {
|
||||
updateTitle(titlefy([t('Competitions'), t('Teams')]));
|
||||
}, [updateTitle, t]);
|
||||
|
||||
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>x
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
query TeamReferees($teamId: ID!) {
|
||||
teamReferees(teamId: $teamId) {
|
||||
edges {
|
||||
node {
|
||||
teamId
|
||||
referee
|
||||
joinedAt
|
||||
joinedAtEpoch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
query Teams($teamId: ID, $partyId: ID) {
|
||||
teams(teamId: $teamId, partyId: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
teamId
|
||||
referrer
|
||||
name
|
||||
teamUrl
|
||||
avatarUrl
|
||||
createdAt
|
||||
createdAtEpoch
|
||||
closed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
query TeamsStatistics($teamId: ID, $aggregationEpochs: Int) {
|
||||
teamsStatistics(teamId: $teamId, aggregationEpochs: $aggregationEpochs) {
|
||||
edges {
|
||||
node {
|
||||
teamId
|
||||
totalQuantumVolume
|
||||
totalQuantumRewards
|
||||
totalGamesPlayed
|
||||
gamesPlayed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
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>;
|
||||
@@ -0,0 +1,55 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type TeamRefereesQueryVariables = Types.Exact<{
|
||||
teamId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type TeamRefereesQuery = { __typename?: 'Query', teamReferees?: { __typename?: 'TeamRefereeConnection', edges: Array<{ __typename?: 'TeamRefereeEdge', node: { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number } }> } | null };
|
||||
|
||||
|
||||
export const TeamRefereesDocument = gql`
|
||||
query TeamReferees($teamId: ID!) {
|
||||
teamReferees(teamId: $teamId) {
|
||||
edges {
|
||||
node {
|
||||
teamId
|
||||
referee
|
||||
joinedAt
|
||||
joinedAtEpoch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useTeamRefereesQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useTeamRefereesQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useTeamRefereesQuery` 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 } = useTeamRefereesQuery({
|
||||
* variables: {
|
||||
* teamId: // value for 'teamId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useTeamRefereesQuery(baseOptions: Apollo.QueryHookOptions<TeamRefereesQuery, TeamRefereesQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<TeamRefereesQuery, TeamRefereesQueryVariables>(TeamRefereesDocument, options);
|
||||
}
|
||||
export function useTeamRefereesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<TeamRefereesQuery, TeamRefereesQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<TeamRefereesQuery, TeamRefereesQueryVariables>(TeamRefereesDocument, options);
|
||||
}
|
||||
export type TeamRefereesQueryHookResult = ReturnType<typeof useTeamRefereesQuery>;
|
||||
export type TeamRefereesLazyQueryHookResult = ReturnType<typeof useTeamRefereesLazyQuery>;
|
||||
export type TeamRefereesQueryResult = Apollo.QueryResult<TeamRefereesQuery, TeamRefereesQueryVariables>;
|
||||
@@ -0,0 +1,61 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type 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>;
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
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>;
|
||||
@@ -0,0 +1,37 @@
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import compact from 'lodash/compact';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import {
|
||||
useTeamQuery,
|
||||
type TeamFieldsFragment,
|
||||
type TeamStatsFieldsFragment,
|
||||
type TeamRefereeFieldsFragment,
|
||||
type TeamEntityFragment,
|
||||
} from './__generated__/Team';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
|
||||
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) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const { data, loading, error } = useTeamQuery({
|
||||
variables: { teamId: teamId || '', partyId: pubKey },
|
||||
skip: !teamId,
|
||||
});
|
||||
|
||||
const teamEdge = data?.teams?.edges.find((e) => e.node.teamId === teamId);
|
||||
const partyTeamEdge = data?.partyTeams?.edges[0];
|
||||
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,
|
||||
stats: teamStatsEdge?.node,
|
||||
team: teamEdge?.node,
|
||||
members,
|
||||
games,
|
||||
partyInTeam: Boolean(partyTeamEdge),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
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();
|
||||
|
||||
const {
|
||||
data: statsData,
|
||||
loading: statsLoading,
|
||||
error: statsError,
|
||||
} = useTeamsStatisticsQuery({
|
||||
variables: {
|
||||
aggregationEpochs,
|
||||
},
|
||||
});
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { useT } from '../../lib/use-t';
|
||||
import { RewardsContainer } from '../../components/rewards-container';
|
||||
import { usePageTitleStore } from '../../stores';
|
||||
import { ErrorBoundary } from '../../components/error-boundary';
|
||||
import { TinyScroll } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const Rewards = () => {
|
||||
const t = useT();
|
||||
@@ -16,10 +17,10 @@ export const Rewards = () => {
|
||||
}, [updateTitle, title]);
|
||||
return (
|
||||
<ErrorBoundary feature="rewards">
|
||||
<div className="container mx-auto p-4">
|
||||
<TinyScroll className="p-4 max-h-full overflow-auto">
|
||||
<h1 className="px-4 pb-4 text-2xl">{title}</h1>
|
||||
<RewardsContainer />
|
||||
</div>
|
||||
</TinyScroll>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { Teams } from './teams';
|
||||
@@ -1,7 +0,0 @@
|
||||
export const Teams = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Teams</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -12,6 +12,7 @@ import { VegaWalletProvider } from '@vegaprotocol/wallet';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Web3Provider } from './web3-provider';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { DataLoader } from './data-loader';
|
||||
|
||||
export const Bootstrapper = ({ children }: { children: ReactNode }) => {
|
||||
const t = useT();
|
||||
@@ -52,28 +53,38 @@ export const Bootstrapper = ({ children }: { children: ReactNode }) => {
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Web3Provider
|
||||
<DataLoader
|
||||
skeleton={<AppLoader />}
|
||||
failure={
|
||||
<AppFailure title={t('Could not configure web3 provider')} />
|
||||
<AppFailure
|
||||
title={t('Could not load market data or asset data')}
|
||||
error={error}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<VegaWalletProvider
|
||||
config={{
|
||||
network: VEGA_ENV,
|
||||
vegaUrl: VEGA_URL,
|
||||
vegaWalletServiceUrl: VEGA_WALLET_URL,
|
||||
links: {
|
||||
explorer: VEGA_EXPLORER_URL,
|
||||
concepts: DocsLinks.VEGA_WALLET_CONCEPTS_URL,
|
||||
chromeExtensionUrl: CHROME_EXTENSION_URL,
|
||||
mozillaExtensionUrl: MOZILLA_EXTENSION_URL,
|
||||
},
|
||||
}}
|
||||
<Web3Provider
|
||||
skeleton={<AppLoader />}
|
||||
failure={
|
||||
<AppFailure title={t('Could not configure web3 provider')} />
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</VegaWalletProvider>
|
||||
</Web3Provider>
|
||||
<VegaWalletProvider
|
||||
config={{
|
||||
network: VEGA_ENV,
|
||||
vegaUrl: VEGA_URL,
|
||||
vegaWalletServiceUrl: VEGA_WALLET_URL,
|
||||
links: {
|
||||
explorer: VEGA_EXPLORER_URL,
|
||||
concepts: DocsLinks.VEGA_WALLET_CONCEPTS_URL,
|
||||
chromeExtensionUrl: CHROME_EXTENSION_URL,
|
||||
mozillaExtensionUrl: MOZILLA_EXTENSION_URL,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</VegaWalletProvider>
|
||||
</Web3Provider>
|
||||
</DataLoader>
|
||||
</NodeGuard>
|
||||
</NetworkLoader>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useAssetsMapProvider } from '@vegaprotocol/assets';
|
||||
import { useMarketsMapProvider } from '@vegaprotocol/markets';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export const DataLoader = ({
|
||||
children,
|
||||
failure,
|
||||
skeleton,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
failure: ReactNode;
|
||||
skeleton: ReactNode;
|
||||
}) => {
|
||||
// Query all markets and assets to ensure they are cached
|
||||
const { data: markets, error, loading } = useMarketsMapProvider();
|
||||
const {
|
||||
data: assets,
|
||||
error: errorAssets,
|
||||
loading: loadingAssets,
|
||||
} = useAssetsMapProvider();
|
||||
|
||||
if (loading || loadingAssets) {
|
||||
// eslint-disable-next-line
|
||||
return <>{skeleton}</>;
|
||||
}
|
||||
|
||||
if (error || errorAssets || !markets || !assets) {
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{failure}</>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{children}</>;
|
||||
};
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@vegaprotocol/candles-chart';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { useChartSettings, STUDY_SIZE } from './use-chart-settings';
|
||||
import { SUPPORTED_INTERVALS, type SupportedInterval } from './constants';
|
||||
|
||||
/**
|
||||
* Renders either the pennant chart or the tradingview chart
|
||||
@@ -36,7 +37,7 @@ export const ChartContainer = ({ marketId }: { marketId: string }) => {
|
||||
const pennantChart = (
|
||||
<CandlesChartContainer
|
||||
marketId={marketId}
|
||||
interval={toPennantInterval(interval)}
|
||||
interval={toPennantInterval(interval as SupportedInterval)}
|
||||
chartType={chartType}
|
||||
overlays={overlays}
|
||||
studies={studies}
|
||||
@@ -63,7 +64,7 @@ export const ChartContainer = ({ marketId }: { marketId: string }) => {
|
||||
libraryPath={CHARTING_LIBRARY_PATH}
|
||||
libraryHash={CHARTING_LIBRARY_HASH}
|
||||
marketId={marketId}
|
||||
interval={toTradingViewResolution(interval)}
|
||||
interval={toTradingViewResolution(interval as SupportedInterval)}
|
||||
studies={tradingViewStudies}
|
||||
onIntervalChange={(newInterval) => {
|
||||
setInterval(fromTradingViewResolution(newInterval));
|
||||
@@ -83,7 +84,11 @@ export const ChartContainer = ({ marketId }: { marketId: string }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const toTradingViewResolution = (interval: Interval) => {
|
||||
const toTradingViewResolution = (interval: SupportedInterval) => {
|
||||
if (!SUPPORTED_INTERVALS.includes(interval)) {
|
||||
throw new Error(`interval ${interval} is not supported`);
|
||||
}
|
||||
|
||||
const resolution = TRADINGVIEW_INTERVAL_MAP[interval];
|
||||
|
||||
if (!resolution) {
|
||||
@@ -107,7 +112,11 @@ const fromTradingViewResolution = (resolution: string) => {
|
||||
return interval as Interval;
|
||||
};
|
||||
|
||||
const toPennantInterval = (interval: Interval) => {
|
||||
const toPennantInterval = (interval: SupportedInterval) => {
|
||||
if (!SUPPORTED_INTERVALS.includes(interval)) {
|
||||
throw new Error(`interval ${interval} is not supported`);
|
||||
}
|
||||
|
||||
const pennantInterval = PENNANT_INTERVAL_MAP[interval];
|
||||
|
||||
if (!pennantInterval) {
|
||||
|
||||
@@ -18,21 +18,13 @@ import {
|
||||
TradingDropdownTrigger,
|
||||
Icon,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { Interval } from '@vegaprotocol/types';
|
||||
import { type Interval } from '@vegaprotocol/types';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { ALLOWED_TRADINGVIEW_HOSTNAMES } from '@vegaprotocol/trading-view';
|
||||
import { IconNames, type IconName } from '@blueprintjs/icons';
|
||||
import { useChartSettings } from './use-chart-settings';
|
||||
import { useT } from '../../lib/use-t';
|
||||
|
||||
const INTERVALS = [
|
||||
Interval.INTERVAL_I1M,
|
||||
Interval.INTERVAL_I5M,
|
||||
Interval.INTERVAL_I15M,
|
||||
Interval.INTERVAL_I1H,
|
||||
Interval.INTERVAL_I6H,
|
||||
Interval.INTERVAL_I1D,
|
||||
];
|
||||
import { SUPPORTED_INTERVALS } from './constants';
|
||||
|
||||
const chartTypeIcon = new Map<ChartType, IconName>([
|
||||
[ChartType.AREA, IconNames.TIMELINE_AREA_CHART],
|
||||
@@ -93,7 +85,7 @@ export const ChartMenu = () => {
|
||||
setInterval(value as Interval);
|
||||
}}
|
||||
>
|
||||
{INTERVALS.map((timeInterval) => (
|
||||
{SUPPORTED_INTERVALS.map((timeInterval) => (
|
||||
<TradingDropdownRadioItem
|
||||
key={timeInterval}
|
||||
inset
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Interval } from '@vegaprotocol/types';
|
||||
|
||||
export type SupportedInterval = typeof SUPPORTED_INTERVALS[number];
|
||||
|
||||
export const SUPPORTED_INTERVALS = [
|
||||
Interval.INTERVAL_I1M,
|
||||
Interval.INTERVAL_I5M,
|
||||
Interval.INTERVAL_I15M,
|
||||
Interval.INTERVAL_I1H,
|
||||
Interval.INTERVAL_I6H,
|
||||
Interval.INTERVAL_I1D,
|
||||
] as const;
|
||||
@@ -0,0 +1,49 @@
|
||||
import classNames from 'classnames';
|
||||
import { type ComponentProps, type ReactElement, type ReactNode } from 'react';
|
||||
import { DudeBadge } from './graphics/dude-badge';
|
||||
|
||||
export const BORDER_COLOR = 'border-vega-clight-500 dark:border-vega-cdark-500';
|
||||
export const GRADIENT =
|
||||
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
|
||||
|
||||
export const CompetitionsActionsContainer = ({
|
||||
children,
|
||||
}: {
|
||||
children:
|
||||
| ReactElement<typeof CompetitionsAction>
|
||||
| Iterable<ReactElement<typeof CompetitionsAction>>;
|
||||
}) => (
|
||||
<div className="grid grid-rows-3 grid-cols-1 md:grid-rows-1 md:grid-cols-3 gap-6 mb-12">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const CompetitionsAction = ({
|
||||
variant,
|
||||
title,
|
||||
description,
|
||||
actionElement,
|
||||
children,
|
||||
}: {
|
||||
variant: ComponentProps<typeof DudeBadge>['variant'];
|
||||
title: string;
|
||||
description?: string;
|
||||
actionElement: ReactNode;
|
||||
children?: ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
BORDER_COLOR,
|
||||
GRADIENT,
|
||||
'border rounded-lg',
|
||||
'p-6 flex flex-col items-center gap-6 text-center'
|
||||
)}
|
||||
>
|
||||
<DudeBadge variant={variant} />
|
||||
<h2 className="text-2xl">{title}</h2>
|
||||
{description && <p className="text-muted">{description}</p>}
|
||||
{actionElement}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import classNames from 'classnames';
|
||||
import { AnimatedDudeWithWire } from '../../client-pages/referrals/graphics/dude';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
export const CompetitionsHeader = ({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
children?: ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div className={classNames('relative mb-10 lg:mb-20')}>
|
||||
<div className="">
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute top-20 right-[220px] md:right-[240px] max-sm:hidden"
|
||||
>
|
||||
<AnimatedDudeWithWire />
|
||||
</div>
|
||||
<div className="pt-10 lg:pt-20 sm:w-[50%]">
|
||||
<h1 className="text-3xl _text-[6vw] lg:!text-6xl leading-[1em] font-alpha calt mb-10">
|
||||
{title}
|
||||
</h1>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { getNumberFormat } from '@vegaprotocol/utils';
|
||||
import { type useTeams } from '../../client-pages/competitions/hooks/use-teams';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { Table } from '../table';
|
||||
import { Rank } from './graphics/rank';
|
||||
import { Links } from '../../lib/links';
|
||||
import { TeamAvatar } from './team-avatar';
|
||||
|
||||
export const CompetitionsLeaderboard = ({
|
||||
data,
|
||||
}: {
|
||||
data: ReturnType<typeof useTeams>['data'];
|
||||
}) => {
|
||||
const t = useT();
|
||||
|
||||
const num = (n?: number | string) =>
|
||||
!n ? '-' : getNumberFormat(0).format(Number(n));
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return <Splash>{t('Could not find any teams')}</Splash>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={[
|
||||
{ name: 'rank', displayName: '#' },
|
||||
{ name: 'avatar', displayName: '' },
|
||||
{ name: 'team', displayName: t('Team') },
|
||||
{ name: 'earned', displayName: t('Rewards earned') },
|
||||
{ name: 'games', displayName: t('Total games') },
|
||||
{ name: 'members', displayName: t('No. of members') },
|
||||
{ name: 'status', displayName: t('Status') },
|
||||
{ name: 'volume', displayName: t('Volume') },
|
||||
]}
|
||||
data={data.map((td, i) => {
|
||||
// leaderboard place or medal
|
||||
let rank: number | React.ReactNode = i + 1;
|
||||
if (rank === 1) rank = <Rank variant="gold" />;
|
||||
if (rank === 2) rank = <Rank variant="silver" />;
|
||||
if (rank === 3) rank = <Rank variant="bronze" />;
|
||||
|
||||
const avatar = (
|
||||
<TeamAvatar
|
||||
teamId={td.teamId}
|
||||
imgUrl={td.avatarUrl}
|
||||
alt={td.name}
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
|
||||
return {
|
||||
rank,
|
||||
avatar,
|
||||
team: (
|
||||
<Link
|
||||
className="hover:underline"
|
||||
to={Links.COMPETITIONS_TEAM(td.teamId)}
|
||||
>
|
||||
{td.name}
|
||||
</Link>
|
||||
),
|
||||
earned: num(td.totalQuantumRewards),
|
||||
games: num(td.totalGamesPlayed),
|
||||
members: 0,
|
||||
status: td.closed ? t('Closed') : t('Open'),
|
||||
volume: num(td.totalQuantumVolume),
|
||||
};
|
||||
})}
|
||||
></Table>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { type TransferNode } from '@vegaprotocol/types';
|
||||
import { ActiveRewardCard } from '../rewards-container/active-rewards';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const GamesContainer = ({
|
||||
data,
|
||||
currentEpoch,
|
||||
}: {
|
||||
data: TransferNode[];
|
||||
currentEpoch: number;
|
||||
}) => {
|
||||
const t = useT();
|
||||
if (!data || data.length === 0) {
|
||||
return <Splash>{t('There are currently no games available.')}</Splash>;
|
||||
}
|
||||
return (
|
||||
<div className="mb-12 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{data.map((game, i) => {
|
||||
// TODO: Remove `kind` prop from ActiveRewardCard
|
||||
const { transfer } = game;
|
||||
if (
|
||||
transfer.kind.__typename !== 'RecurringTransfer' ||
|
||||
!transfer.kind.dispatchStrategy?.dispatchMetric
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ActiveRewardCard
|
||||
key={i}
|
||||
transferNode={game}
|
||||
currentEpoch={currentEpoch}
|
||||
kind={transfer.kind}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import classNames from 'classnames';
|
||||
import { DudeWithFlag } from './dude-with-flag';
|
||||
|
||||
/**
|
||||
* Pre-defined badge gradients
|
||||
*/
|
||||
|
||||
export const BADGE_GRADIENT_VARIANT_A =
|
||||
'bg-gradient-to-r from-vega-blue-500 via-vega-purple-500 to-vega-pink-500';
|
||||
export const BADGE_GRADIENT_VARIANT_B =
|
||||
'bg-gradient-to-r from-vega-purple-500 via-vega-green-500 to-vega-blue-500';
|
||||
export const BADGE_GRADIENT_VARIANT_C =
|
||||
'bg-gradient-to-r from-vega-blue-500 via-vega-purple-500 to-vega-green-500';
|
||||
|
||||
/** Badge */
|
||||
|
||||
export const DudeBadge = ({
|
||||
variant,
|
||||
className,
|
||||
}: {
|
||||
variant: 'A' | 'B' | 'C' | undefined;
|
||||
className?: classNames.Argument;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'w-24 h-24 rounded-full bg-black relative',
|
||||
'rotate-12',
|
||||
{
|
||||
[BADGE_GRADIENT_VARIANT_A]: variant === 'A',
|
||||
[BADGE_GRADIENT_VARIANT_B]: variant === 'B',
|
||||
[BADGE_GRADIENT_VARIANT_C]: variant === 'C',
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
<DudeWithFlag className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 -rotate-12" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { theme } from '@vegaprotocol/tailwindcss-config';
|
||||
|
||||
type DudeWithFlagProps = {
|
||||
flagColor?: string;
|
||||
withStar?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const DEFAULT_FLAG_COLOR = theme.colors.vega.green[500];
|
||||
|
||||
export const DudeWithFlag = ({
|
||||
flagColor = DEFAULT_FLAG_COLOR,
|
||||
withStar = true,
|
||||
className,
|
||||
}: DudeWithFlagProps) => {
|
||||
return (
|
||||
<svg
|
||||
width="49"
|
||||
height="43"
|
||||
viewBox="0 0 49 43"
|
||||
fill="none"
|
||||
className={className}
|
||||
>
|
||||
{withStar && (
|
||||
<>
|
||||
<path d="M3.99992 0H2V1.99993H3.99992V0Z" fill="white" />
|
||||
<path
|
||||
d="M2 1.99993L0 1.99981V3.99974H1.99992L2 1.99993Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M3.99995 3.99992L1.99992 3.99974L2 5.99988H3.99995V3.99992Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M5.99997 1.99981L3.99992 1.99993L3.99995 3.99992L5.99997 3.99974V1.99981Z"
|
||||
fill="white"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<path
|
||||
d="M32 4H11V33H15V43H20V33H23V43H28V33H32V4ZM20 17H15V12H20V17ZM28 17H23V12H28V17Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path d="M41 25L32 25L32 20L41 20L41 25Z" fill="white" />
|
||||
<path d="M36 29V4H35V29" fill="white" />
|
||||
<path d="M36 13H49L44.55 8.5L49 4H36V13Z" fill={flagColor} />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export const Rank = ({
|
||||
variant,
|
||||
className,
|
||||
}: {
|
||||
variant?: 'gold' | 'silver' | 'bronze';
|
||||
className?: classNames.Argument;
|
||||
}) => {
|
||||
const { theme } = useThemeSwitcher();
|
||||
return (
|
||||
<div
|
||||
title={classNames({
|
||||
'1': variant === 'gold',
|
||||
'2': variant === 'silver',
|
||||
'3': variant === 'bronze',
|
||||
})}
|
||||
className={classNames(
|
||||
{
|
||||
'text-yellow-300': variant === 'gold',
|
||||
'text-vega-clight-500': variant === 'silver',
|
||||
'text-vega-orange-500': variant === 'bronze',
|
||||
'text-black dark:text-white': variant === undefined,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
<svg width="18" height="30" viewBox="0 0 18 30" fill="none">
|
||||
<defs>
|
||||
<linearGradient x1="0" y1="0" x2="100%" y2="100%" id="medal">
|
||||
<stop offset="33%" stop-color="transparent" />
|
||||
<stop offset="100%" stop-color="black" stop-opacity="50%" />
|
||||
</linearGradient>
|
||||
<clipPath id="shape">
|
||||
<path d="M2 2H4V4H2V2Z" />
|
||||
<path d="M2 2H4V4H2V2Z" />
|
||||
<path d="M2 2H6V4H2V2Z" />
|
||||
<path d="M2 2H6V4H2V2Z" />
|
||||
<path d="M0 4H4V6H0V4Z" />
|
||||
<path d="M0 4H4V6H0V4Z" />
|
||||
<path d="M4 0H14V2H4V0Z" />
|
||||
<path d="M4 0H14V2H4V0Z" />
|
||||
<path d="M0 14V4H2V14H0Z" />
|
||||
<path d="M0 14V4H2V14H0Z" />
|
||||
<path d="M2 30L2 18H4L4 30H2Z" />
|
||||
<path d="M2 30L2 18H4L4 30H2Z" />
|
||||
<path d="M14 30L14 18H16L16 30H14Z" />
|
||||
<path d="M14 30L14 18H16L16 30H14Z" />
|
||||
<path d="M16 14L16 4H18V14H16Z" />
|
||||
<path d="M16 14L16 4H18V14H16Z" />
|
||||
<path d="M2 6V2H4V6H2Z" />
|
||||
<path d="M2 6V2H4V6H2Z" />
|
||||
<path d="M16 2V6H14V2H16Z" />
|
||||
<path d="M16 2V6H14V2H16Z" />
|
||||
<path d="M12 2H16L16 4H12V2Z" />
|
||||
<path d="M12 2H16L16 4H12V2Z" />
|
||||
<path d="M14 4H18V6H14V4Z" />
|
||||
<path d="M14 4H18V6H14V4Z" />
|
||||
<path d="M16 16H12V14H16V16Z" />
|
||||
<path d="M16 16H12V14H16V16Z" />
|
||||
<path d="M14 18H4L4 16L14 16L14 18Z" />
|
||||
<path d="M14 18H4L4 16L14 16L14 18Z" />
|
||||
<path d="M16 12V16H14V12H16Z" />
|
||||
<path d="M16 12V16H14V12H16Z" />
|
||||
<path d="M6 16H2V14H6V16Z" />
|
||||
<path d="M6 16H2V14H6V16Z" />
|
||||
<path d="M6 28H4L4 26H6V28Z" />
|
||||
<path d="M6 28H4L4 26H6V28Z" />
|
||||
<path d="M8 26H6L6 24H8V26Z" />
|
||||
<path d="M8 26H6L6 24H8V26Z" />
|
||||
<path d="M10 24H8V22H10V24Z" />
|
||||
<path d="M10 24H8V22H10V24Z" />
|
||||
<path d="M12 26H10L10 24H12V26Z" />
|
||||
<path d="M12 26H10L10 24H12V26Z" />
|
||||
<path d="M14 28H12L12 26H14V28Z" />
|
||||
<path d="M14 28H12L12 26H14V28Z" />
|
||||
<path d="M4 14H0L2.04189e-07 12H4V14Z" />
|
||||
<path d="M4 14H0L2.04189e-07 12H4V14Z" />
|
||||
<path d="M6 4H12V14H6V4Z" />
|
||||
<path d="M6 4H12V14H6V4Z" />
|
||||
<path d="M4 6H14V12H4V6Z" />
|
||||
<path d="M4 6H14V12H4V6Z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="18"
|
||||
height="30"
|
||||
fill="currentColor"
|
||||
clipPath="url(#shape)"
|
||||
/>
|
||||
<rect
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="18"
|
||||
height="30"
|
||||
fill="url(#medal)"
|
||||
clipPath="url(#shape)"
|
||||
style={{ mixBlendMode: theme === 'dark' ? 'darken' : 'overlay' }}
|
||||
/>
|
||||
<g style={{ mixBlendMode: 'overlay' }}>
|
||||
<path d="M10.5 6H8.5V8H10.5V6Z" fill="white" />
|
||||
<path d="M12.5 8H10.5V10H12.5V8Z" fill="white" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
const NUM_AVATARS = 20;
|
||||
const AVATAR_PATHNAME_PATTERN = '/team-avatars/{id}.png';
|
||||
|
||||
const getFallbackAvatar = (teamId: string) => {
|
||||
const avatarId = ((parseInt(teamId, 16) % NUM_AVATARS) + 1)
|
||||
.toString()
|
||||
.padStart(2, '0'); // between 01 - 20
|
||||
|
||||
return AVATAR_PATHNAME_PATTERN.replace('{id}', avatarId);
|
||||
};
|
||||
|
||||
export const TeamAvatar = ({
|
||||
teamId,
|
||||
imgUrl,
|
||||
alt,
|
||||
size = 'large',
|
||||
}: {
|
||||
teamId: string;
|
||||
imgUrl: string;
|
||||
alt?: string;
|
||||
size?: 'large' | 'small';
|
||||
}) => {
|
||||
const img = imgUrl && imgUrl.length > 0 ? imgUrl : getFallbackAvatar(teamId);
|
||||
return (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={img}
|
||||
alt={alt || 'Team avatar'}
|
||||
className={classNames(
|
||||
'rounded-full bg-vega-clight-700 dark:bg-vega-cdark-700 shrink-0',
|
||||
{
|
||||
'w-20 h-20 lg:w-[112px] lg:h-[112px]': size === 'large',
|
||||
'w-10 h-10': size === 'small',
|
||||
}
|
||||
)}
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -38,7 +38,11 @@ export const FeesContainer = () => {
|
||||
const { data: markets, loading: marketsLoading } = useMarketList();
|
||||
|
||||
const { data: programData, loading: programLoading } =
|
||||
useDiscountProgramsQuery({ errorPolicy: 'ignore' });
|
||||
useDiscountProgramsQuery({
|
||||
errorPolicy: 'ignore',
|
||||
fetchPolicy: 'cache-and-network',
|
||||
pollInterval: 15000,
|
||||
});
|
||||
|
||||
const volumeDiscountWindowLength =
|
||||
programData?.currentVolumeDiscountProgram?.windowLength || 1;
|
||||
@@ -49,6 +53,8 @@ export const FeesContainer = () => {
|
||||
partyId: pubKey || '',
|
||||
},
|
||||
skip: !pubKey,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
pollInterval: 15000,
|
||||
});
|
||||
|
||||
const previousEpoch = (Number(feesData?.epoch.id) || 0) - 1;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
|
||||
|
||||
export const useReferralStats = (
|
||||
previousEpoch?: number,
|
||||
referralStats?: NonNullable<
|
||||
|
||||
@@ -97,9 +97,9 @@ const MarketData = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="w-2/5" role="gridcell">
|
||||
<div className="w-2/6" role="gridcell">
|
||||
<h3 className="flex items-baseline">
|
||||
<span className="overflow-hidden text-sm lg:text-base text-ellipsis whitespace-nowrap">
|
||||
<span className="overflow-hidden text-xs md:text-sm lg:text-base text-ellipsis whitespace-nowrap">
|
||||
{market.tradableInstrument.instrument.code}
|
||||
</span>
|
||||
{allProducts && productType && (
|
||||
@@ -113,7 +113,7 @@ const MarketData = ({
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="w-1/5 overflow-hidden text-xs lg:text-sm whitespace-nowrap text-ellipsis"
|
||||
className="w-2/6 overflow-hidden text-xs lg:text-sm whitespace-nowrap text-ellipsis text-right"
|
||||
title={symbol}
|
||||
data-testid="market-selector-price"
|
||||
role="gridcell"
|
||||
@@ -121,14 +121,14 @@ const MarketData = ({
|
||||
{price} {symbol}
|
||||
</div>
|
||||
<div
|
||||
className="w-1/5 overflow-hidden text-xs text-right lg:text-sm whitespace-nowrap text-ellipsis"
|
||||
className="w-2/6 sm:w-1/6 overflow-hidden text-xs lg:text-sm whitespace-nowrap text-ellipsis text-right"
|
||||
title={t('24h vol')}
|
||||
data-testid="market-selector-volume"
|
||||
role="gridcell"
|
||||
>
|
||||
{volume}
|
||||
</div>
|
||||
<div className="flex justify-end w-1/5" role="gridcell">
|
||||
<div className="hidden sm:w-1/6 sm:flex justify-end" role="gridcell">
|
||||
{oneDayCandles && (
|
||||
<Sparkline
|
||||
width={64}
|
||||
|
||||
@@ -64,7 +64,7 @@ export const MarketSelector = ({
|
||||
setFilter((curr) => ({ ...curr, product }));
|
||||
}}
|
||||
/>
|
||||
<div className="text-sm grid grid-cols-[2fr_1fr_1fr] gap-1 ">
|
||||
<div className="text-sm flex sm:grid grid-cols-[2fr_1fr_1fr] gap-1 ">
|
||||
<div className="flex-1">
|
||||
<TradingInput
|
||||
onChange={(e) =>
|
||||
@@ -182,16 +182,16 @@ const MarketList = ({
|
||||
'p-2 mx-2 border-b border-default text-xs text-secondary'
|
||||
)}
|
||||
>
|
||||
<div className="w-2/5" role="columnheader">
|
||||
<div className="w-2/6" role="columnheader">
|
||||
{t('Name')}
|
||||
</div>
|
||||
<div className="w-1/5" role="columnheader">
|
||||
<div className="w-2/6 text-right pr-4" role="columnheader">
|
||||
{t('Price')}
|
||||
</div>
|
||||
<div className="w-1/5 text-right" role="columnheader">
|
||||
<div className="w-2/6 sm:w-1/6 text-right" role="columnheader">
|
||||
{t('24h volume')}
|
||||
</div>
|
||||
<div className="w-1/5" role="columnheader" />
|
||||
<div className="hidden sm:w-1/6" role="columnheader" />
|
||||
</div>
|
||||
<div ref={listRef}>
|
||||
<List
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useParams } from 'react-router-dom';
|
||||
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
||||
import { useState } from 'react';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import classNames from 'classnames';
|
||||
|
||||
/**
|
||||
* This is only rendered for the mobile navigation
|
||||
@@ -30,7 +31,16 @@ export const NavHeader = () => {
|
||||
trigger={
|
||||
<h1 className="flex gap-1 sm:gap-2 md:gap-4 items-center text-default text-lg whitespace-nowrap xl:pr-4 xl:border-r border-default">
|
||||
{data ? data.tradableInstrument.instrument.code : t('Select market')}
|
||||
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={20} />
|
||||
<span
|
||||
className={classNames(
|
||||
'transition-transform ease-in-out duration-300',
|
||||
{
|
||||
'rotate-180': open,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={20} />
|
||||
</span>
|
||||
</h1>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -202,6 +202,13 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
|
||||
{t('Portfolio')}
|
||||
</NavbarLink>
|
||||
</NavbarItem>
|
||||
{featureFlags.TEAM_COMPETITION && (
|
||||
<NavbarItem>
|
||||
<NavbarLink to={Links.COMPETITIONS()} onClick={onClick}>
|
||||
{t('Competitions')}
|
||||
</NavbarLink>
|
||||
</NavbarItem>
|
||||
)}
|
||||
{featureFlags.REFERRALS && (
|
||||
<NavbarItem>
|
||||
<NavbarLink end={false} to={Links.REFERRALS()} onClick={onClick}>
|
||||
|
||||
@@ -2,14 +2,19 @@ query RewardsPage($partyId: ID!) {
|
||||
party(id: $partyId) {
|
||||
id
|
||||
vestingStats {
|
||||
# AKA hoarder reward multiplier
|
||||
rewardBonusMultiplier
|
||||
quantumBalance
|
||||
epochSeq
|
||||
}
|
||||
activityStreak {
|
||||
# vesting multiplier
|
||||
rewardVestingMultiplier
|
||||
# AKA streak multiplier
|
||||
activeFor
|
||||
isActive
|
||||
inactiveFor
|
||||
rewardDistributionMultiplier
|
||||
rewardVestingMultiplier
|
||||
epoch
|
||||
tradedVolume
|
||||
openVolume
|
||||
}
|
||||
vestingBalancesSummary {
|
||||
epoch
|
||||
@@ -36,6 +41,74 @@ query RewardsPage($partyId: ID!) {
|
||||
}
|
||||
}
|
||||
|
||||
query ActiveRewards(
|
||||
$isReward: Boolean
|
||||
$partyId: ID
|
||||
$direction: TransferDirection
|
||||
$pagination: Pagination
|
||||
) {
|
||||
transfersConnection(
|
||||
partyId: $partyId
|
||||
isReward: $isReward
|
||||
direction: $direction
|
||||
pagination: $pagination
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
transfer {
|
||||
amount
|
||||
id
|
||||
from
|
||||
fromAccountType
|
||||
to
|
||||
toAccountType
|
||||
asset {
|
||||
id
|
||||
symbol
|
||||
decimals
|
||||
name
|
||||
quantum
|
||||
status
|
||||
}
|
||||
reference
|
||||
status
|
||||
timestamp
|
||||
kind {
|
||||
... on RecurringTransfer {
|
||||
startEpoch
|
||||
endEpoch
|
||||
dispatchStrategy {
|
||||
dispatchMetric
|
||||
dispatchMetricAssetId
|
||||
marketIdsInScope
|
||||
entityScope
|
||||
individualScope
|
||||
teamScope
|
||||
nTopPerformers
|
||||
stakingRequirement
|
||||
notionalTimeWeightedAveragePositionRequirement
|
||||
windowLength
|
||||
lockPeriod
|
||||
distributionStrategy
|
||||
rankTable {
|
||||
startRank
|
||||
shareRatio
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
reason
|
||||
}
|
||||
fees {
|
||||
transferId
|
||||
amount
|
||||
epoch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query RewardsHistory(
|
||||
$partyId: ID!
|
||||
$epochRewardSummariesPagination: Pagination
|
||||
@@ -92,3 +165,18 @@ query RewardsEpoch {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
query MarketForRewards($marketId: ID!) {
|
||||
market(id: $marketId) {
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
id
|
||||
name
|
||||
code
|
||||
metadata {
|
||||
tags
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+167
-3
@@ -8,7 +8,17 @@ export type RewardsPageQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type RewardsPageQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, vestingStats?: { __typename?: 'PartyVestingStats', rewardBonusMultiplier: string } | null, activityStreak?: { __typename?: 'PartyActivityStreak', rewardVestingMultiplier: string, rewardDistributionMultiplier: string } | null, vestingBalancesSummary: { __typename?: 'PartyVestingBalancesSummary', epoch?: number | null, vestingBalances?: Array<{ __typename?: 'PartyVestingBalance', balance: string, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, quantum: string } }> | null, lockedBalances?: Array<{ __typename?: 'PartyLockedBalance', balance: string, untilEpoch: number, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, quantum: string } }> | null } } | null };
|
||||
export type RewardsPageQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, vestingStats?: { __typename?: 'PartyVestingStats', rewardBonusMultiplier: string, quantumBalance: string, epochSeq: number } | null, activityStreak?: { __typename?: 'PartyActivityStreak', activeFor: number, isActive: boolean, inactiveFor: number, rewardDistributionMultiplier: string, rewardVestingMultiplier: string, epoch: number, tradedVolume: string, openVolume: string } | null, vestingBalancesSummary: { __typename?: 'PartyVestingBalancesSummary', epoch?: number | null, vestingBalances?: Array<{ __typename?: 'PartyVestingBalance', balance: string, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, quantum: string } }> | null, lockedBalances?: Array<{ __typename?: 'PartyLockedBalance', balance: string, untilEpoch: number, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, quantum: string } }> | null } } | null };
|
||||
|
||||
export type ActiveRewardsQueryVariables = Types.Exact<{
|
||||
isReward?: Types.InputMaybe<Types.Scalars['Boolean']>;
|
||||
partyId?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||
direction?: Types.InputMaybe<Types.TransferDirection>;
|
||||
pagination?: Types.InputMaybe<Types.Pagination>;
|
||||
}>;
|
||||
|
||||
|
||||
export type ActiveRewardsQuery = { __typename?: 'Query', transfersConnection?: { __typename?: 'TransferConnection', edges?: Array<{ __typename?: 'TransferEdge', node: { __typename?: 'TransferNode', transfer: { __typename?: 'Transfer', amount: string, id: string, from: string, fromAccountType: Types.AccountType, to: string, toAccountType: Types.AccountType, reference?: string | null, status: Types.TransferStatus, timestamp: any, reason?: string | null, asset?: { __typename?: 'Asset', id: string, symbol: string, decimals: number, name: string, quantum: string, status: Types.AssetStatus } | null, kind: { __typename?: 'OneOffGovernanceTransfer' } | { __typename?: 'OneOffTransfer' } | { __typename?: 'RecurringGovernanceTransfer' } | { __typename?: 'RecurringTransfer', startEpoch: number, endEpoch?: number | null, dispatchStrategy?: { __typename?: 'DispatchStrategy', dispatchMetric: Types.DispatchMetric, dispatchMetricAssetId: string, marketIdsInScope?: Array<string> | null, entityScope: Types.EntityScope, individualScope?: Types.IndividualScope | null, teamScope?: Array<string | null> | null, nTopPerformers?: string | null, stakingRequirement: string, notionalTimeWeightedAveragePositionRequirement: string, windowLength: number, lockPeriod: number, distributionStrategy: Types.DistributionStrategy, rankTable?: Array<{ __typename?: 'RankTable', startRank: number, shareRatio: number } | null> | null } | null } }, fees?: Array<{ __typename?: 'TransferFee', transferId: string, amount: string, epoch: number } | null> | null } } | null> | null } | null };
|
||||
|
||||
export type RewardsHistoryQueryVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
@@ -26,6 +36,13 @@ export type RewardsEpochQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
export type RewardsEpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string } };
|
||||
|
||||
export type MarketForRewardsQueryVariables = Types.Exact<{
|
||||
marketId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type MarketForRewardsQuery = { __typename?: 'Query', market?: { __typename?: 'Market', tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, code: string, metadata: { __typename?: 'InstrumentMetadata', tags?: Array<string> | null } } } } | null };
|
||||
|
||||
|
||||
export const RewardsPageDocument = gql`
|
||||
query RewardsPage($partyId: ID!) {
|
||||
@@ -33,10 +50,18 @@ export const RewardsPageDocument = gql`
|
||||
id
|
||||
vestingStats {
|
||||
rewardBonusMultiplier
|
||||
quantumBalance
|
||||
epochSeq
|
||||
}
|
||||
activityStreak {
|
||||
rewardVestingMultiplier
|
||||
activeFor
|
||||
isActive
|
||||
inactiveFor
|
||||
rewardDistributionMultiplier
|
||||
rewardVestingMultiplier
|
||||
epoch
|
||||
tradedVolume
|
||||
openVolume
|
||||
}
|
||||
vestingBalancesSummary {
|
||||
epoch
|
||||
@@ -91,6 +116,101 @@ export function useRewardsPageLazyQuery(baseOptions?: Apollo.LazyQueryHookOption
|
||||
export type RewardsPageQueryHookResult = ReturnType<typeof useRewardsPageQuery>;
|
||||
export type RewardsPageLazyQueryHookResult = ReturnType<typeof useRewardsPageLazyQuery>;
|
||||
export type RewardsPageQueryResult = Apollo.QueryResult<RewardsPageQuery, RewardsPageQueryVariables>;
|
||||
export const ActiveRewardsDocument = gql`
|
||||
query ActiveRewards($isReward: Boolean, $partyId: ID, $direction: TransferDirection, $pagination: Pagination) {
|
||||
transfersConnection(
|
||||
partyId: $partyId
|
||||
isReward: $isReward
|
||||
direction: $direction
|
||||
pagination: $pagination
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
transfer {
|
||||
amount
|
||||
id
|
||||
from
|
||||
fromAccountType
|
||||
to
|
||||
toAccountType
|
||||
asset {
|
||||
id
|
||||
symbol
|
||||
decimals
|
||||
name
|
||||
quantum
|
||||
status
|
||||
}
|
||||
reference
|
||||
status
|
||||
timestamp
|
||||
kind {
|
||||
... on RecurringTransfer {
|
||||
startEpoch
|
||||
endEpoch
|
||||
dispatchStrategy {
|
||||
dispatchMetric
|
||||
dispatchMetricAssetId
|
||||
marketIdsInScope
|
||||
entityScope
|
||||
individualScope
|
||||
teamScope
|
||||
nTopPerformers
|
||||
stakingRequirement
|
||||
notionalTimeWeightedAveragePositionRequirement
|
||||
windowLength
|
||||
lockPeriod
|
||||
distributionStrategy
|
||||
rankTable {
|
||||
startRank
|
||||
shareRatio
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
reason
|
||||
}
|
||||
fees {
|
||||
transferId
|
||||
amount
|
||||
epoch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useActiveRewardsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useActiveRewardsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useActiveRewardsQuery` 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 } = useActiveRewardsQuery({
|
||||
* variables: {
|
||||
* isReward: // value for 'isReward'
|
||||
* partyId: // value for 'partyId'
|
||||
* direction: // value for 'direction'
|
||||
* pagination: // value for 'pagination'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useActiveRewardsQuery(baseOptions?: Apollo.QueryHookOptions<ActiveRewardsQuery, ActiveRewardsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<ActiveRewardsQuery, ActiveRewardsQueryVariables>(ActiveRewardsDocument, options);
|
||||
}
|
||||
export function useActiveRewardsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ActiveRewardsQuery, ActiveRewardsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<ActiveRewardsQuery, ActiveRewardsQueryVariables>(ActiveRewardsDocument, options);
|
||||
}
|
||||
export type ActiveRewardsQueryHookResult = ReturnType<typeof useActiveRewardsQuery>;
|
||||
export type ActiveRewardsLazyQueryHookResult = ReturnType<typeof useActiveRewardsLazyQuery>;
|
||||
export type ActiveRewardsQueryResult = Apollo.QueryResult<ActiveRewardsQuery, ActiveRewardsQueryVariables>;
|
||||
export const RewardsHistoryDocument = gql`
|
||||
query RewardsHistory($partyId: ID!, $epochRewardSummariesPagination: Pagination, $partyRewardsPagination: Pagination, $fromEpoch: Int, $toEpoch: Int) {
|
||||
epochRewardSummaries(
|
||||
@@ -202,4 +322,48 @@ export function useRewardsEpochLazyQuery(baseOptions?: Apollo.LazyQueryHookOptio
|
||||
}
|
||||
export type RewardsEpochQueryHookResult = ReturnType<typeof useRewardsEpochQuery>;
|
||||
export type RewardsEpochLazyQueryHookResult = ReturnType<typeof useRewardsEpochLazyQuery>;
|
||||
export type RewardsEpochQueryResult = Apollo.QueryResult<RewardsEpochQuery, RewardsEpochQueryVariables>;
|
||||
export type RewardsEpochQueryResult = Apollo.QueryResult<RewardsEpochQuery, RewardsEpochQueryVariables>;
|
||||
export const MarketForRewardsDocument = gql`
|
||||
query MarketForRewards($marketId: ID!) {
|
||||
market(id: $marketId) {
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
id
|
||||
name
|
||||
code
|
||||
metadata {
|
||||
tags
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useMarketForRewardsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useMarketForRewardsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useMarketForRewardsQuery` 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 } = useMarketForRewardsQuery({
|
||||
* variables: {
|
||||
* marketId: // value for 'marketId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useMarketForRewardsQuery(baseOptions: Apollo.QueryHookOptions<MarketForRewardsQuery, MarketForRewardsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<MarketForRewardsQuery, MarketForRewardsQueryVariables>(MarketForRewardsDocument, options);
|
||||
}
|
||||
export function useMarketForRewardsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<MarketForRewardsQuery, MarketForRewardsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<MarketForRewardsQuery, MarketForRewardsQueryVariables>(MarketForRewardsDocument, options);
|
||||
}
|
||||
export type MarketForRewardsQueryHookResult = ReturnType<typeof useMarketForRewardsQuery>;
|
||||
export type MarketForRewardsLazyQueryHookResult = ReturnType<typeof useMarketForRewardsLazyQuery>;
|
||||
export type MarketForRewardsQueryResult = Apollo.QueryResult<MarketForRewardsQuery, MarketForRewardsQueryVariables>;
|
||||
@@ -0,0 +1,174 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import {
|
||||
ActiveRewardCard,
|
||||
applyFilter,
|
||||
isActiveReward,
|
||||
} from './active-rewards';
|
||||
import {
|
||||
AccountType,
|
||||
AssetStatus,
|
||||
DispatchMetric,
|
||||
DistributionStrategy,
|
||||
EntityScope,
|
||||
IndividualScope,
|
||||
type RecurringTransfer,
|
||||
type TransferNode,
|
||||
TransferStatus,
|
||||
type Transfer,
|
||||
} from '@vegaprotocol/types';
|
||||
|
||||
jest.mock('./__generated__/Rewards', () => ({
|
||||
useMarketForRewardsQuery: () => ({
|
||||
data: undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('@vegaprotocol/assets', () => ({
|
||||
useAssetDataProvider: () => {
|
||||
return {
|
||||
data: {
|
||||
assetId: 'asset-1',
|
||||
},
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
describe('ActiveRewards', () => {
|
||||
const mockRecurringTransfer: RecurringTransfer = {
|
||||
__typename: 'RecurringTransfer',
|
||||
startEpoch: 115332,
|
||||
endEpoch: 115432,
|
||||
factor: '1',
|
||||
dispatchStrategy: {
|
||||
__typename: 'DispatchStrategy',
|
||||
dispatchMetric: DispatchMetric.DISPATCH_METRIC_LP_FEES_RECEIVED,
|
||||
dispatchMetricAssetId:
|
||||
'c9fe6fc24fce121b2cc72680543a886055abb560043fda394ba5376203b7527d',
|
||||
marketIdsInScope: null,
|
||||
entityScope: EntityScope.ENTITY_SCOPE_INDIVIDUALS,
|
||||
individualScope: IndividualScope.INDIVIDUAL_SCOPE_ALL,
|
||||
teamScope: null,
|
||||
nTopPerformers: '',
|
||||
stakingRequirement: '',
|
||||
notionalTimeWeightedAveragePositionRequirement: '',
|
||||
windowLength: 1,
|
||||
lockPeriod: 0,
|
||||
distributionStrategy: DistributionStrategy.DISTRIBUTION_STRATEGY_PRO_RATA,
|
||||
rankTable: null,
|
||||
},
|
||||
};
|
||||
|
||||
const mockTransferNode: TransferNode = {
|
||||
__typename: 'TransferNode',
|
||||
transfer: {
|
||||
__typename: 'Transfer',
|
||||
amount: '1613000000',
|
||||
id: 'c4e59bd389c8098e6c7669f2d3e1613b9f42d30b1c4c3793ac44380f4c522835',
|
||||
from: '69464e35bcb8e8a2900ca0f87acaf252d50cf2ab2fc73694845a16b7c8a0dc6f',
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
to: 'network',
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: 'c9fe6fc24fce121b2cc72680543a886055abb560043fda394ba5376203b7527d',
|
||||
symbol: 'tUSDC',
|
||||
decimals: 5,
|
||||
name: 'tUSDC TEST',
|
||||
quantum: '1',
|
||||
status: AssetStatus.STATUS_ENABLED,
|
||||
source: {
|
||||
__typename: 'ERC20' as const,
|
||||
contractAddress: '0x123',
|
||||
lifetimeLimit: '100',
|
||||
withdrawThreshold: '100',
|
||||
},
|
||||
},
|
||||
reference: 'reward',
|
||||
status: TransferStatus.STATUS_PENDING,
|
||||
timestamp: '2023-12-18T13:05:35.948706Z',
|
||||
kind: mockRecurringTransfer,
|
||||
reason: null,
|
||||
},
|
||||
fees: [],
|
||||
};
|
||||
|
||||
it('renders with valid props', () => {
|
||||
render(
|
||||
<ActiveRewardCard
|
||||
transferNode={mockTransferNode}
|
||||
currentEpoch={1}
|
||||
kind={mockRecurringTransfer}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText(/Liquidity provision fees received/i)
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Entity scope')).toBeInTheDocument();
|
||||
expect(screen.getByText('Average position')).toBeInTheDocument();
|
||||
expect(screen.getByText('Ends in')).toBeInTheDocument();
|
||||
expect(screen.getByText('115431 epochs')).toBeInTheDocument();
|
||||
expect(screen.getByText('Assessed over')).toBeInTheDocument();
|
||||
expect(screen.getByText('1 epoch')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('isActiveReward', () => {
|
||||
it('returns true for valid active reward', () => {
|
||||
const node = {
|
||||
transfer: {
|
||||
kind: {
|
||||
__typename: 'RecurringTransfer',
|
||||
dispatchStrategy: {},
|
||||
endEpoch: 10,
|
||||
},
|
||||
status: TransferStatus.STATUS_PENDING,
|
||||
},
|
||||
} as TransferNode;
|
||||
expect(isActiveReward(node, 5)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns false for invalid active reward', () => {
|
||||
const node = {
|
||||
transfer: {
|
||||
kind: {
|
||||
__typename: 'RecurringTransfer',
|
||||
dispatchStrategy: {},
|
||||
endEpoch: 10,
|
||||
},
|
||||
status: TransferStatus.STATUS_PENDING,
|
||||
},
|
||||
} as TransferNode;
|
||||
expect(isActiveReward(node, 15)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyFilter', () => {
|
||||
it('returns true when filter matches dispatch metric label', () => {
|
||||
const transfer = {
|
||||
kind: {
|
||||
__typename: 'RecurringTransfer',
|
||||
dispatchStrategy: {
|
||||
dispatchMetric: DispatchMetric.DISPATCH_METRIC_AVERAGE_POSITION,
|
||||
},
|
||||
},
|
||||
asset: { symbol: 'XYZ' },
|
||||
} as Transfer;
|
||||
const filter = { searchTerm: 'average position' };
|
||||
expect(applyFilter({ transfer }, filter)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns true when filter matches asset symbol', () => {
|
||||
const transfer = {
|
||||
kind: {
|
||||
__typename: 'RecurringTransfer',
|
||||
dispatchStrategy: {
|
||||
dispatchMetric: DispatchMetric.DISPATCH_METRIC_AVERAGE_POSITION,
|
||||
},
|
||||
},
|
||||
asset: { symbol: 'XYZ' },
|
||||
} as Transfer;
|
||||
const filter = { searchTerm: 'average position' };
|
||||
expect(applyFilter({ transfer }, filter)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,568 @@
|
||||
import {
|
||||
useActiveRewardsQuery,
|
||||
useMarketForRewardsQuery,
|
||||
} from './__generated__/Rewards';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
Icon,
|
||||
type IconName,
|
||||
Intent,
|
||||
Tooltip,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
type VegaIconSize,
|
||||
TradingInput,
|
||||
TinyScroll,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import {
|
||||
DistributionStrategyDescriptionMapping,
|
||||
DistributionStrategyMapping,
|
||||
EntityScope,
|
||||
EntityScopeMapping,
|
||||
type Maybe,
|
||||
type Transfer,
|
||||
type TransferNode,
|
||||
TransferStatus,
|
||||
TransferStatusMapping,
|
||||
DispatchMetric,
|
||||
DispatchMetricDescription,
|
||||
DispatchMetricLabels,
|
||||
type RecurringTransfer,
|
||||
EntityScopeLabelMapping,
|
||||
} from '@vegaprotocol/types';
|
||||
import { Card } from '../card/card';
|
||||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
type AssetFieldsFragment,
|
||||
useAssetDataProvider,
|
||||
useAssetsMapProvider,
|
||||
} from '@vegaprotocol/assets';
|
||||
import {
|
||||
type MarketFieldsFragment,
|
||||
useMarketsMapProvider,
|
||||
} from '@vegaprotocol/markets';
|
||||
|
||||
export type Filter = {
|
||||
searchTerm: string;
|
||||
};
|
||||
|
||||
export const isActiveReward = (node: TransferNode, currentEpoch: number) => {
|
||||
const { transfer } = node;
|
||||
if (transfer.kind.__typename !== 'RecurringTransfer') {
|
||||
return false;
|
||||
}
|
||||
const { dispatchStrategy } = transfer.kind;
|
||||
|
||||
if (!dispatchStrategy) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (transfer.kind.endEpoch && transfer.kind.endEpoch < currentEpoch) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (transfer.status !== TransferStatus.STATUS_PENDING) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const applyFilter = (
|
||||
node: TransferNode & {
|
||||
asset?: AssetFieldsFragment | null;
|
||||
marketIds?: (MarketFieldsFragment | null)[];
|
||||
},
|
||||
filter: Filter
|
||||
) => {
|
||||
const { transfer } = node;
|
||||
if (
|
||||
transfer.kind.__typename !== 'RecurringTransfer' ||
|
||||
!transfer.kind.dispatchStrategy?.dispatchMetric
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
DispatchMetricLabels[transfer.kind.dispatchStrategy.dispatchMetric]
|
||||
.toLowerCase()
|
||||
.includes(filter.searchTerm.toLowerCase()) ||
|
||||
transfer.asset?.symbol
|
||||
.toLowerCase()
|
||||
.includes(filter.searchTerm.toLowerCase()) ||
|
||||
EntityScopeLabelMapping[transfer.kind.dispatchStrategy.entityScope]
|
||||
.toLowerCase()
|
||||
.includes(filter.searchTerm.toLowerCase()) ||
|
||||
node.asset?.name
|
||||
.toLocaleLowerCase()
|
||||
.includes(filter.searchTerm.toLowerCase()) ||
|
||||
node.marketIds?.some((m) =>
|
||||
m?.tradableInstrument?.instrument?.name
|
||||
.toLocaleLowerCase()
|
||||
.includes(filter.searchTerm.toLowerCase())
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
|
||||
const t = useT();
|
||||
const { data: activeRewardsData } = useActiveRewardsQuery({
|
||||
variables: {
|
||||
isReward: true,
|
||||
},
|
||||
});
|
||||
|
||||
const [filter, setFilter] = useState<Filter>({
|
||||
searchTerm: '',
|
||||
});
|
||||
|
||||
const { data: assets } = useAssetsMapProvider();
|
||||
const { data: markets } = useMarketsMapProvider();
|
||||
|
||||
const transfers = activeRewardsData?.transfersConnection?.edges
|
||||
?.map((e) => e?.node as TransferNode)
|
||||
.filter((node) => isActiveReward(node, currentEpoch))
|
||||
.map((node) => {
|
||||
if (node.transfer.kind.__typename !== 'RecurringTransfer') {
|
||||
return node;
|
||||
}
|
||||
|
||||
const asset =
|
||||
assets &&
|
||||
assets[
|
||||
node.transfer.kind.dispatchStrategy?.dispatchMetricAssetId || ''
|
||||
];
|
||||
|
||||
const marketIds =
|
||||
node.transfer.kind.dispatchStrategy?.marketIdsInScope?.map(
|
||||
(id) => markets && markets[id]
|
||||
);
|
||||
|
||||
return { ...node, asset, marketIds };
|
||||
});
|
||||
|
||||
if (!transfers || !transfers.length) return null;
|
||||
|
||||
return (
|
||||
<Card title={t('Active rewards')} className="lg:col-span-full">
|
||||
{transfers.length > 1 && (
|
||||
<TradingInput
|
||||
onChange={(e) =>
|
||||
setFilter((curr) => ({ ...curr, searchTerm: e.target.value }))
|
||||
}
|
||||
value={filter.searchTerm}
|
||||
type="text"
|
||||
placeholder={t(
|
||||
'Search by reward dispatch metric, entity scope or asset name'
|
||||
)}
|
||||
data-testid="search-term"
|
||||
className="mb-4 w-20 mr-2"
|
||||
prependElement={<VegaIcon name={VegaIconNames.SEARCH} />}
|
||||
/>
|
||||
)}
|
||||
<TinyScroll className="grid gap-x-8 gap-y-10 h-fit grid-cols-[repeat(auto-fill,_minmax(230px,_1fr))] md:grid-cols-[repeat(auto-fill,_minmax(230px,_1fr))] lg:grid-cols-[repeat(auto-fill,_minmax(320px,_1fr))] xl:grid-cols-[repeat(auto-fill,_minmax(335px,_1fr))] max-h-[40rem] overflow-auto pr-2">
|
||||
{transfers
|
||||
.filter((n) => applyFilter(n, filter))
|
||||
.map((node, i) => {
|
||||
const { transfer } = node;
|
||||
if (
|
||||
transfer.kind.__typename !== 'RecurringTransfer' ||
|
||||
!transfer.kind.dispatchStrategy?.dispatchMetric
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
node && (
|
||||
<ActiveRewardCard
|
||||
key={i}
|
||||
transferNode={node}
|
||||
kind={transfer.kind}
|
||||
currentEpoch={currentEpoch}
|
||||
/>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</TinyScroll>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
// This was built to be a status indicator for the rewards based on the transfer status
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const StatusIndicator = ({
|
||||
status,
|
||||
reason,
|
||||
}: {
|
||||
status: TransferStatus;
|
||||
reason?: Maybe<string> | undefined;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const getIconIntent = (status: string) => {
|
||||
switch (status) {
|
||||
case TransferStatus.STATUS_DONE:
|
||||
return { icon: IconNames.TICK_CIRCLE, intent: Intent.Success };
|
||||
case TransferStatus.STATUS_CANCELLED:
|
||||
return { icon: IconNames.MOON, intent: Intent.None };
|
||||
case TransferStatus.STATUS_PENDING:
|
||||
return { icon: IconNames.HELP, intent: Intent.Primary };
|
||||
case TransferStatus.STATUS_REJECTED:
|
||||
return { icon: IconNames.ERROR, intent: Intent.Danger };
|
||||
case TransferStatus.STATUS_STOPPED:
|
||||
return { icon: IconNames.ERROR, intent: Intent.Danger };
|
||||
default:
|
||||
return { icon: IconNames.HELP, intent: Intent.Primary };
|
||||
}
|
||||
};
|
||||
const { icon, intent } = getIconIntent(status);
|
||||
return (
|
||||
<Tooltip
|
||||
description={
|
||||
<span>
|
||||
{t('Transfer status: {{status}} {{reason}}', {
|
||||
status: TransferStatusMapping[status],
|
||||
reason: reason ? `(${reason})` : '',
|
||||
})}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
<span
|
||||
className={classNames(
|
||||
{
|
||||
'text-gray-700 dark:text-gray-300': intent === Intent.None,
|
||||
'text-vega-blue': intent === Intent.Primary,
|
||||
'text-vega-green dark:text-vega-green': intent === Intent.Success,
|
||||
'dark:text-yellow text-yellow-600': intent === Intent.Warning,
|
||||
'text-vega-red': intent === Intent.Danger,
|
||||
},
|
||||
'flex items-start p-1 align-text-bottom'
|
||||
)}
|
||||
>
|
||||
<Icon size={3} name={icon as IconName} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const ActiveRewardCard = ({
|
||||
transferNode,
|
||||
currentEpoch,
|
||||
kind,
|
||||
}: {
|
||||
transferNode: TransferNode;
|
||||
currentEpoch: number;
|
||||
kind: RecurringTransfer;
|
||||
}) => {
|
||||
const t = useT();
|
||||
|
||||
const { transfer } = transferNode;
|
||||
const { dispatchStrategy } = kind;
|
||||
const marketIds = dispatchStrategy?.marketIdsInScope;
|
||||
|
||||
const { data: marketNameData } = useMarketForRewardsQuery({
|
||||
variables: {
|
||||
marketId: marketIds ? marketIds[0] : '',
|
||||
},
|
||||
});
|
||||
|
||||
const marketName = useMemo(() => {
|
||||
if (marketNameData && marketIds && marketIds.length > 1) {
|
||||
return 'Specific markets';
|
||||
} else if (
|
||||
marketNameData &&
|
||||
marketIds &&
|
||||
marketNameData &&
|
||||
marketIds.length === 1
|
||||
) {
|
||||
return marketNameData?.market?.tradableInstrument?.instrument?.name || '';
|
||||
}
|
||||
return '';
|
||||
}, [marketIds, marketNameData]);
|
||||
|
||||
const { data: dispatchAsset } = useAssetDataProvider(
|
||||
dispatchStrategy?.dispatchMetricAssetId || ''
|
||||
);
|
||||
|
||||
if (!dispatchStrategy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { gradientClassName, mainClassName } = getGradientClasses(
|
||||
dispatchStrategy.dispatchMetric
|
||||
);
|
||||
|
||||
const entityScope = dispatchStrategy.entityScope;
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={classNames(
|
||||
'bg-gradient-to-r col-span-full p-0.5 lg:col-auto h-full',
|
||||
'rounded-lg',
|
||||
gradientClassName
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
mainClassName,
|
||||
'bg-gradient-to-b bg-vega-clight-800 dark:bg-vega-cdark-800 h-full w-full rounded p-4 flex flex-col gap-4'
|
||||
)}
|
||||
>
|
||||
<div className="flex justify-between gap-4">
|
||||
<div className="flex flex-col gap-2 items-center text-center">
|
||||
<EntityIcon transfer={transfer} />
|
||||
{entityScope && (
|
||||
<span className="text-muted text-xs">
|
||||
{EntityScopeLabelMapping[entityScope] || t('Unspecified')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 items-center text-center">
|
||||
<h3 className="flex flex-col gap-1 text-2xl shrink-1 text-center">
|
||||
<span className="font-glitch">
|
||||
{addDecimalsFormatNumber(
|
||||
transferNode.transfer.amount,
|
||||
transferNode.transfer.asset?.decimals || 0,
|
||||
6
|
||||
)}
|
||||
</span>
|
||||
|
||||
<span className="font-alpha">
|
||||
{transferNode.transfer.asset?.symbol}
|
||||
</span>
|
||||
</h3>
|
||||
{
|
||||
<Tooltip
|
||||
description={t(
|
||||
DistributionStrategyDescriptionMapping[
|
||||
dispatchStrategy.distributionStrategy
|
||||
]
|
||||
)}
|
||||
underline={true}
|
||||
>
|
||||
<span className="text-xs">
|
||||
{
|
||||
DistributionStrategyMapping[
|
||||
dispatchStrategy.distributionStrategy
|
||||
]
|
||||
}
|
||||
</span>
|
||||
</Tooltip>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 items-center text-center">
|
||||
<CardIcon
|
||||
iconName={VegaIconNames.LOCK}
|
||||
tooltip={t(
|
||||
'Number of epochs after distribution to delay vesting of rewards by'
|
||||
)}
|
||||
/>
|
||||
<span className="text-muted text-xs whitespace-nowrap">
|
||||
{t('numberEpochs', '{{count}} epochs', {
|
||||
count: kind.dispatchStrategy?.lockPeriod,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span className="border-[0.5px] border-gray-700" />
|
||||
<span>
|
||||
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]}
|
||||
{marketName ? ` • ${marketName}` : ` • ${dispatchAsset?.name}`}
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-8 flex-wrap">
|
||||
{kind.endEpoch && (
|
||||
<span className="flex flex-col">
|
||||
<span className="text-muted text-xs">{t('Ends in')}</span>
|
||||
<span>
|
||||
{t('numberEpochs', '{{count}} epochs', {
|
||||
count: kind.endEpoch - currentEpoch,
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{
|
||||
<span className="flex flex-col">
|
||||
<span className="text-muted text-xs">{t('Assessed over')}</span>
|
||||
<span>
|
||||
{t('numberEpochs', '{{count}} epochs', {
|
||||
count: dispatchStrategy.windowLength,
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
{dispatchStrategy?.dispatchMetric && (
|
||||
<span className="text-muted text-sm h-[2rem]">
|
||||
{t(DispatchMetricDescription[dispatchStrategy?.dispatchMetric])}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<span className="border-[0.5px] border-gray-700" />
|
||||
|
||||
<div className="flex justify-between flex-wrap items-center gap-3 text-xs">
|
||||
<span className="flex flex-col gap-1">
|
||||
<span className="flex items-center gap-1 text-muted">
|
||||
{t('Entity scope')}{' '}
|
||||
</span>
|
||||
|
||||
<span className="flex items-center gap-1">
|
||||
{kind.dispatchStrategy?.teamScope && (
|
||||
<Tooltip
|
||||
description={
|
||||
<span>{kind.dispatchStrategy?.teamScope}</span>
|
||||
}
|
||||
>
|
||||
<span className="flex items-center p-1 rounded-full border border-gray-600">
|
||||
{<VegaIcon name={VegaIconNames.TEAM} size={16} />}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{kind.dispatchStrategy?.individualScope && (
|
||||
<Tooltip
|
||||
description={
|
||||
<span>{kind.dispatchStrategy?.individualScope}</span>
|
||||
}
|
||||
>
|
||||
<span className="flex items-center p-1 rounded-full border border-gray-600">
|
||||
{<VegaIcon name={VegaIconNames.MAN} size={16} />}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
{/* Shows transfer status */}
|
||||
{/* <StatusIndicator
|
||||
status={transfer.status}
|
||||
reason={transfer.reason}
|
||||
/> */}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="flex flex-col gap-1">
|
||||
<span className="flex items-center gap-1 text-muted">
|
||||
{t('Staked VEGA')}{' '}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
{addDecimalsFormatNumber(
|
||||
kind.dispatchStrategy?.stakingRequirement || 0,
|
||||
transfer.asset?.decimals || 0
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span className="flex flex-col gap-1">
|
||||
<span className="flex items-center gap-1 text-muted">
|
||||
{t('Average position')}{' '}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
{addDecimalsFormatNumber(
|
||||
kind.dispatchStrategy
|
||||
?.notionalTimeWeightedAveragePositionRequirement || 0,
|
||||
transfer.asset?.decimals || 0
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getGradientClasses = (d: DispatchMetric | undefined) => {
|
||||
switch (d) {
|
||||
case DispatchMetric.DISPATCH_METRIC_AVERAGE_POSITION:
|
||||
return {
|
||||
gradientClassName: 'from-vega-pink-500 to-vega-purple-400',
|
||||
mainClassName: 'from-vega-pink-400 dark:from-vega-pink-600 to-20%',
|
||||
};
|
||||
case DispatchMetric.DISPATCH_METRIC_LP_FEES_RECEIVED:
|
||||
return {
|
||||
gradientClassName: 'from-vega-green-500 to-vega-yellow-500',
|
||||
mainClassName: 'from-vega-green-400 dark:from-vega-green-600 to-20%',
|
||||
};
|
||||
case DispatchMetric.DISPATCH_METRIC_MAKER_FEES_PAID:
|
||||
return {
|
||||
gradientClassName: 'from-vega-orange-500 to-vega-pink-400',
|
||||
mainClassName: 'from-vega-orange-400 dark:from-vega-orange-600 to-20%',
|
||||
};
|
||||
case DispatchMetric.DISPATCH_METRIC_MARKET_VALUE:
|
||||
case DispatchMetric.DISPATCH_METRIC_RELATIVE_RETURN:
|
||||
return {
|
||||
gradientClassName: 'from-vega-purple-500 to-vega-blue-400',
|
||||
mainClassName: 'from-vega-purple-400 dark:from-vega-purple-600 to-20%',
|
||||
};
|
||||
case DispatchMetric.DISPATCH_METRIC_RETURN_VOLATILITY:
|
||||
return {
|
||||
gradientClassName: 'from-vega-blue-500 to-vega-green-400',
|
||||
mainClassName: 'from-vega-blue-400 dark:from-vega-blue-600 to-20%',
|
||||
};
|
||||
case DispatchMetric.DISPATCH_METRIC_VALIDATOR_RANKING:
|
||||
default:
|
||||
return {
|
||||
gradientClassName: 'from-vega-pink-500 to-vega-purple-400',
|
||||
mainClassName: 'from-vega-pink-400 dark:from-vega-pink-600 to-20%',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const CardIcon = ({
|
||||
size = 18,
|
||||
iconName,
|
||||
tooltip,
|
||||
}: {
|
||||
size?: VegaIconSize;
|
||||
iconName: VegaIconNames;
|
||||
tooltip: string;
|
||||
}) => {
|
||||
return (
|
||||
<Tooltip description={<span>{tooltip}</span>}>
|
||||
<span className="flex items-center p-2 rounded-full border border-gray-600">
|
||||
<VegaIcon name={iconName} size={size} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const EntityIcon = ({
|
||||
transfer,
|
||||
size = 18,
|
||||
}: {
|
||||
transfer: Transfer;
|
||||
size?: VegaIconSize;
|
||||
}) => {
|
||||
if (transfer.kind.__typename !== 'RecurringTransfer') {
|
||||
return null;
|
||||
}
|
||||
const entityScope = transfer.kind.dispatchStrategy?.entityScope;
|
||||
const getIconName = () => {
|
||||
switch (entityScope) {
|
||||
case EntityScope.ENTITY_SCOPE_TEAMS:
|
||||
return VegaIconNames.TEAM;
|
||||
case EntityScope.ENTITY_SCOPE_INDIVIDUALS:
|
||||
return VegaIconNames.MAN;
|
||||
default:
|
||||
return VegaIconNames.QUESTION_MARK;
|
||||
}
|
||||
};
|
||||
const iconName = getIconName();
|
||||
return (
|
||||
<Tooltip
|
||||
description={
|
||||
<span>{entityScope ? EntityScopeMapping[entityScope] : ''}</span>
|
||||
}
|
||||
>
|
||||
<span className="flex items-center p-2 rounded-full border border-gray-600">
|
||||
{iconName && <VegaIcon name={iconName} size={size} />}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
@@ -33,6 +33,10 @@ import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
||||
import { RewardsHistoryContainer } from './rewards-history';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { useAssetsMapProvider } from '@vegaprotocol/assets';
|
||||
import { ActiveRewards } from './active-rewards';
|
||||
import { ActivityStreak } from './streaks/activity-streaks';
|
||||
import { RewardHoarderBonus } from './streaks/reward-hoarder-bonus';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const ASSETS_WITH_INCORRECT_VESTING_REWARD_DATA = [
|
||||
'bf1e88d19db4b3ca0d1d5bdb73718a01686b18cf731ca26adedf3c8b83802bba', // USDT mainnet
|
||||
@@ -45,6 +49,7 @@ export const RewardsContainer = () => {
|
||||
const { params, loading: paramsLoading } = useNetworkParams([
|
||||
NetworkParams.reward_asset,
|
||||
NetworkParams.rewards_activityStreak_benefitTiers,
|
||||
NetworkParams.rewards_vesting_benefitTiers,
|
||||
NetworkParams.rewards_vesting_baseRate,
|
||||
]);
|
||||
|
||||
@@ -54,6 +59,14 @@ export const RewardsContainer = () => {
|
||||
|
||||
const { data: epochData } = useRewardsEpochQuery();
|
||||
|
||||
const { rewards_activityStreak_benefitTiers, rewards_vesting_benefitTiers } =
|
||||
params || {};
|
||||
|
||||
const activityStreakBenefitTiers = JSON.parse(
|
||||
rewards_activityStreak_benefitTiers
|
||||
);
|
||||
const vestingBenefitTiers = JSON.parse(rewards_vesting_benefitTiers);
|
||||
|
||||
// No need to specify the fromEpoch as it will by default give you the last
|
||||
// Note activityStreak in query will fail
|
||||
const { data: rewardsData, loading: rewardsLoading } = useRewardsPageQuery({
|
||||
@@ -68,6 +81,9 @@ export const RewardsContainer = () => {
|
||||
pollInterval: 10000,
|
||||
});
|
||||
|
||||
const partyActivityStreak = rewardsData?.party?.activityStreak;
|
||||
const vestingDetails = rewardsData?.party?.vestingStats;
|
||||
|
||||
if (!epochData?.epoch || !assetMap) return null;
|
||||
|
||||
const loading = paramsLoading || accountsLoading || rewardsLoading;
|
||||
@@ -114,74 +130,98 @@ export const RewardsContainer = () => {
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="grid auto-rows-min grid-cols-6 gap-3">
|
||||
{/* Always show reward information for vega */}
|
||||
<Card
|
||||
key={params.reward_asset}
|
||||
title={t('Vega Reward pot')}
|
||||
className="lg:col-span-3 xl:col-span-2"
|
||||
loading={loading}
|
||||
highlight={true}
|
||||
>
|
||||
<RewardPot
|
||||
pubKey={pubKey}
|
||||
accounts={accounts}
|
||||
assetId={params.reward_asset}
|
||||
vestingBalancesSummary={rewardsData?.party?.vestingBalancesSummary}
|
||||
/>
|
||||
</Card>
|
||||
<Card
|
||||
title={t('Vesting')}
|
||||
className="lg:col-span-3 xl:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<Vesting
|
||||
pubKey={pubKey}
|
||||
baseRate={params.rewards_vesting_baseRate}
|
||||
multiplier={
|
||||
rewardsData?.party?.activityStreak?.rewardVestingMultiplier
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
<Card
|
||||
title={t('Rewards multipliers')}
|
||||
className="lg:col-span-3 xl:col-span-2"
|
||||
loading={loading}
|
||||
highlight={true}
|
||||
>
|
||||
<Multipliers
|
||||
pubKey={pubKey}
|
||||
hoarderMultiplier={
|
||||
rewardsData?.party?.vestingStats?.rewardBonusMultiplier
|
||||
}
|
||||
streakMultiplier={
|
||||
rewardsData?.party?.activityStreak?.rewardDistributionMultiplier
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
<div className="flex flex-col w-full gap-3">
|
||||
<div className="grid auto-rows-min grid-cols-6 gap-3">
|
||||
{/* Always show reward information for vega */}
|
||||
<Card
|
||||
key={params.reward_asset}
|
||||
title={t('Vega Reward pot')}
|
||||
className="lg:col-span-3 xl:col-span-2"
|
||||
loading={loading}
|
||||
highlight={true}
|
||||
>
|
||||
<RewardPot
|
||||
pubKey={pubKey}
|
||||
accounts={accounts}
|
||||
assetId={params.reward_asset}
|
||||
vestingBalancesSummary={rewardsData?.party?.vestingBalancesSummary}
|
||||
/>
|
||||
</Card>
|
||||
<Card
|
||||
title={t('Vesting')}
|
||||
className="lg:col-span-3 xl:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<Vesting
|
||||
pubKey={pubKey}
|
||||
baseRate={params.rewards_vesting_baseRate}
|
||||
multiplier={
|
||||
rewardsData?.party?.activityStreak?.rewardVestingMultiplier
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
<Card
|
||||
title={t('Rewards multipliers')}
|
||||
className="lg:col-span-3 xl:col-span-2"
|
||||
loading={loading}
|
||||
highlight={true}
|
||||
>
|
||||
<Multipliers
|
||||
pubKey={pubKey}
|
||||
hoarderMultiplier={
|
||||
rewardsData?.party?.vestingStats?.rewardBonusMultiplier
|
||||
}
|
||||
streakMultiplier={
|
||||
rewardsData?.party?.activityStreak?.rewardDistributionMultiplier
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Show all other reward pots, most of the time users will not have other rewards */}
|
||||
{assets
|
||||
.filter((assetId) => assetId !== params.reward_asset)
|
||||
.map((assetId) => {
|
||||
const asset = assetMap ? assetMap[assetId] : null;
|
||||
{/* Show all other reward pots, most of the time users will not have other rewards */}
|
||||
{assets
|
||||
.filter((assetId) => assetId !== params.reward_asset)
|
||||
.map((assetId) => {
|
||||
const asset = assetMap ? assetMap[assetId] : null;
|
||||
|
||||
if (!asset) return null;
|
||||
if (!asset) return null;
|
||||
|
||||
// Following code is for mitigating an issue due to a core bug where locked and vesting
|
||||
// balances were incorrectly increased for infrastructure rewards for USDT on mainnet
|
||||
//
|
||||
// We don't want to incorrectly show the wring locked/vesting values, but we DO want to
|
||||
// show the user that they have rewards available to withdraw
|
||||
if (ASSETS_WITH_INCORRECT_VESTING_REWARD_DATA.includes(asset.id)) {
|
||||
const accountsForAsset = rewardAccountsAssetMap[asset.id];
|
||||
const vestedAccount = accountsForAsset?.find(
|
||||
(a) => a.type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS
|
||||
);
|
||||
// Following code is for mitigating an issue due to a core bug where locked and vesting
|
||||
// balances were incorrectly increased for infrastructure rewards for USDT on mainnet
|
||||
//
|
||||
// We don't want to incorrectly show the wring locked/vesting values, but we DO want to
|
||||
// show the user that they have rewards available to withdraw
|
||||
if (ASSETS_WITH_INCORRECT_VESTING_REWARD_DATA.includes(asset.id)) {
|
||||
const accountsForAsset = rewardAccountsAssetMap[asset.id];
|
||||
const vestedAccount = accountsForAsset?.find(
|
||||
(a) => a.type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS
|
||||
);
|
||||
|
||||
// No vested rewards available to withdraw, so skip over USDT
|
||||
if (!vestedAccount || Number(vestedAccount.balance) <= 0) {
|
||||
return null;
|
||||
// No vested rewards available to withdraw, so skip over USDT
|
||||
if (!vestedAccount || Number(vestedAccount.balance) <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={assetId}
|
||||
title={t('{{assetSymbol}} Reward pot', {
|
||||
assetSymbol: asset.symbol,
|
||||
})}
|
||||
className="lg:col-span-3 xl:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<RewardPot
|
||||
pubKey={pubKey}
|
||||
accounts={accounts}
|
||||
assetId={assetId}
|
||||
// Ensure that these values are shown as 0
|
||||
vestingBalancesSummary={{
|
||||
lockedBalances: [],
|
||||
vestingBalances: [],
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -197,48 +237,69 @@ export const RewardsContainer = () => {
|
||||
pubKey={pubKey}
|
||||
accounts={accounts}
|
||||
assetId={assetId}
|
||||
// Ensure that these values are shown as 0
|
||||
vestingBalancesSummary={{
|
||||
lockedBalances: [],
|
||||
vestingBalances: [],
|
||||
}}
|
||||
vestingBalancesSummary={
|
||||
rewardsData?.party?.vestingBalancesSummary
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={assetId}
|
||||
title={t('{{assetSymbol}} Reward pot', {
|
||||
assetSymbol: asset.symbol,
|
||||
})}
|
||||
className="lg:col-span-3 xl:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<RewardPot
|
||||
pubKey={pubKey}
|
||||
accounts={accounts}
|
||||
assetId={assetId}
|
||||
vestingBalancesSummary={
|
||||
rewardsData?.party?.vestingBalancesSummary
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
<div className="grid auto-rows-min grid-cols-6 gap-3">
|
||||
{pubKey && activityStreakBenefitTiers.tiers?.length > 0 && (
|
||||
<Card
|
||||
title={t('Activity Streak')}
|
||||
className={classNames(
|
||||
{
|
||||
'lg:col-span-6 xl:col-span-3':
|
||||
activityStreakBenefitTiers.tiers.length <= 4,
|
||||
'xl:col-span-6': activityStreakBenefitTiers.tiers.length > 4,
|
||||
},
|
||||
'hidden md:block'
|
||||
)}
|
||||
>
|
||||
<span className="flex flex-col mr-8 pr-4">
|
||||
<ActivityStreak
|
||||
tiers={activityStreakBenefitTiers.tiers}
|
||||
streak={partyActivityStreak}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
<Card
|
||||
title={t('Rewards history')}
|
||||
className="lg:col-span-full"
|
||||
loading={rewardsLoading}
|
||||
noBackgroundOnMobile={true}
|
||||
>
|
||||
<RewardsHistoryContainer
|
||||
epoch={Number(epochData?.epoch.id)}
|
||||
pubKey={pubKey}
|
||||
assets={assetMap}
|
||||
/>
|
||||
</Card>
|
||||
</span>
|
||||
</Card>
|
||||
)}
|
||||
{pubKey && vestingBenefitTiers.tiers?.length > 0 && (
|
||||
<Card
|
||||
title={t('Reward Hoarder Bonus')}
|
||||
className={classNames(
|
||||
{
|
||||
'lg:col-span-6 xl:col-span-3':
|
||||
vestingBenefitTiers.tiers.length <= 4,
|
||||
'xl:col-span-6': vestingBenefitTiers.tiers.length > 4,
|
||||
},
|
||||
'hidden md:block'
|
||||
)}
|
||||
>
|
||||
<span className="flex flex-col mr-8 pr-4">
|
||||
<RewardHoarderBonus
|
||||
tiers={vestingBenefitTiers.tiers}
|
||||
vestingDetails={vestingDetails}
|
||||
/>
|
||||
</span>
|
||||
</Card>
|
||||
)}
|
||||
<ActiveRewards currentEpoch={Number(epochData?.epoch.id)} />
|
||||
<Card
|
||||
title={t('Rewards history')}
|
||||
className="lg:col-span-full hidden md:block"
|
||||
loading={rewardsLoading}
|
||||
noBackgroundOnMobile={true}
|
||||
>
|
||||
<RewardsHistoryContainer
|
||||
epoch={Number(epochData?.epoch.id)}
|
||||
pubKey={pubKey}
|
||||
assets={assetMap}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -343,7 +404,7 @@ export const RewardPot = ({
|
||||
})}
|
||||
<VegaIcon name={VegaIconNames.LOCK} size={12} />
|
||||
</CardTableTH>
|
||||
<CardTableTD>
|
||||
<CardTableTD data-testid="locked-value">
|
||||
{addDecimalsFormatNumberQuantum(
|
||||
totalLocked.toString(),
|
||||
rewardAsset.decimals,
|
||||
@@ -357,7 +418,7 @@ export const RewardPot = ({
|
||||
assetSymbol: rewardAsset.symbol,
|
||||
})}
|
||||
</CardTableTH>
|
||||
<CardTableTD>
|
||||
<CardTableTD data-testid="vesting-value">
|
||||
{addDecimalsFormatNumberQuantum(
|
||||
totalVesting.toString(),
|
||||
rewardAsset.decimals,
|
||||
@@ -369,7 +430,7 @@ export const RewardPot = ({
|
||||
<CardTableTH>
|
||||
{t('Available to withdraw this epoch')}
|
||||
</CardTableTH>
|
||||
<CardTableTD>
|
||||
<CardTableTD data-testid="available-to-withdraw-value">
|
||||
{addDecimalsFormatNumberQuantum(
|
||||
totalVestedRewardsByRewardAsset.toString(),
|
||||
rewardAsset.decimals,
|
||||
@@ -388,6 +449,7 @@ export const RewardPot = ({
|
||||
)
|
||||
}
|
||||
size="small"
|
||||
data-testid="redeem-rewards-button"
|
||||
>
|
||||
{t('Redeem rewards')}
|
||||
</TradingButton>
|
||||
@@ -422,12 +484,16 @@ export const Vesting = ({
|
||||
<CardTable>
|
||||
<tr>
|
||||
<CardTableTH>{t('Base rate')}</CardTableTH>
|
||||
<CardTableTD>{baseRateFormatted}%</CardTableTD>
|
||||
<CardTableTD data-testid="base-rate-value">
|
||||
{baseRateFormatted}%
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
{pubKey && (
|
||||
<tr>
|
||||
<CardTableTH>{t('Vesting multiplier')}</CardTableTH>
|
||||
<CardTableTD>{multiplier ? `${multiplier}x` : '-'}</CardTableTD>
|
||||
<CardTableTD data-testid="vesting multiplier-value">
|
||||
{multiplier ? `${multiplier}x` : '-'}
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
)}
|
||||
</CardTable>
|
||||
@@ -467,13 +533,13 @@ export const Multipliers = ({
|
||||
<CardTable>
|
||||
<tr>
|
||||
<CardTableTH>{t('Streak reward multiplier')}</CardTableTH>
|
||||
<CardTableTD>
|
||||
<CardTableTD data-testid="streak-reward-multiplier-value">
|
||||
{streakMultiplier ? `${streakMultiplier}x` : '-'}
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
<tr>
|
||||
<CardTableTH>{t('Hoarder reward multiplier')}</CardTableTH>
|
||||
<CardTableTD>
|
||||
<CardTableTD data-testid="hoarder-reward-multiplier-value">
|
||||
{hoarderMultiplier ? `${hoarderMultiplier}x` : '-'}
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
|
||||
@@ -319,6 +319,7 @@ export const RewardHistoryTable = ({
|
||||
onClick={() => setIsParty(false)}
|
||||
size="extra-small"
|
||||
minimal={isParty}
|
||||
data-testid="total-distributed-button"
|
||||
>
|
||||
{t('Total distributed')}
|
||||
</TradingButton>
|
||||
@@ -327,6 +328,7 @@ export const RewardHistoryTable = ({
|
||||
size="extra-small"
|
||||
disabled={!pubKey}
|
||||
minimal={!isParty}
|
||||
data-testid="earned-by-me-button"
|
||||
>
|
||||
{t('Earned by me')}
|
||||
</TradingButton>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { ActivityStreak } from './activity-streaks';
|
||||
|
||||
describe('ActivityStreak', () => {
|
||||
it('renders null when streak is not active', () => {
|
||||
const tiers: {
|
||||
minimum_activity_streak: number;
|
||||
reward_multiplier: string;
|
||||
vesting_multiplier: string;
|
||||
}[] = [];
|
||||
const streak = null;
|
||||
render(<ActivityStreak tiers={tiers} streak={streak} />);
|
||||
const component = screen.queryByText(/epochs streak/i);
|
||||
expect(component).toBeNull();
|
||||
});
|
||||
|
||||
it('renders null when tiers are empty', () => {
|
||||
const tiers: {
|
||||
minimum_activity_streak: number;
|
||||
reward_multiplier: string;
|
||||
vesting_multiplier: string;
|
||||
}[] = [];
|
||||
const streak = {
|
||||
activeFor: 10,
|
||||
isActive: true,
|
||||
inactiveFor: 10,
|
||||
rewardDistributionMultiplier: '45678',
|
||||
rewardVestingMultiplier: '45678',
|
||||
epoch: 10,
|
||||
tradedVolume: '45678',
|
||||
openVolume: '45678',
|
||||
};
|
||||
render(<ActivityStreak tiers={tiers} streak={streak} />);
|
||||
const component = screen.queryByText(/epochs streak/i);
|
||||
expect(component).toBeNull();
|
||||
});
|
||||
|
||||
it('renders the component with tiers and active streak', () => {
|
||||
const tiers = [
|
||||
{
|
||||
minimum_activity_streak: 5,
|
||||
reward_multiplier: '1.5x',
|
||||
vesting_multiplier: '2x',
|
||||
},
|
||||
{
|
||||
minimum_activity_streak: 10,
|
||||
reward_multiplier: '2x',
|
||||
vesting_multiplier: '3x',
|
||||
},
|
||||
];
|
||||
const streak = {
|
||||
activeFor: 7,
|
||||
isActive: true,
|
||||
inactiveFor: 10,
|
||||
rewardDistributionMultiplier: '45678',
|
||||
rewardVestingMultiplier: '45678',
|
||||
epoch: 10,
|
||||
tradedVolume: '45678',
|
||||
openVolume: '45678',
|
||||
};
|
||||
render(<ActivityStreak tiers={tiers} streak={streak} />);
|
||||
|
||||
const tierLabels = screen.getAllByText(/Tier/i);
|
||||
expect(tierLabels.length).toBe(3); // 2 tiers + 1 label
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,226 @@
|
||||
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
||||
import { useT } from '../../../lib/use-t';
|
||||
import classNames from 'classnames';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { PartyActivityStreak } from '@vegaprotocol/types';
|
||||
|
||||
export const safeProgress = (
|
||||
i: number,
|
||||
userTierIndex: number,
|
||||
total: number | string,
|
||||
progress?: number | string
|
||||
) => {
|
||||
if (i < userTierIndex) return 100;
|
||||
if (i > userTierIndex) return 0;
|
||||
|
||||
if (!progress || !total) return 0;
|
||||
if (new BigNumber(progress).isGreaterThan(total)) return 100;
|
||||
return new BigNumber(progress)
|
||||
.multipliedBy(100)
|
||||
.dividedBy(total || 1)
|
||||
.toNumber();
|
||||
};
|
||||
|
||||
export const useGetUserTier = (
|
||||
tiers: {
|
||||
minimum_activity_streak: number;
|
||||
reward_multiplier: string;
|
||||
vesting_multiplier: string;
|
||||
}[],
|
||||
progress?: number
|
||||
) => {
|
||||
if (!progress) return 0;
|
||||
if (!tiers || tiers.length === 0) return 0;
|
||||
|
||||
let userTier = 0;
|
||||
let i = 0;
|
||||
while (
|
||||
i < tiers.length &&
|
||||
tiers[userTier].minimum_activity_streak < progress
|
||||
) {
|
||||
userTier = i;
|
||||
i++;
|
||||
}
|
||||
|
||||
if (
|
||||
i === tiers.length &&
|
||||
tiers[userTier].minimum_activity_streak <= progress
|
||||
) {
|
||||
userTier = i;
|
||||
}
|
||||
|
||||
if (userTier > tiers.length) {
|
||||
userTier--;
|
||||
}
|
||||
|
||||
return userTier;
|
||||
};
|
||||
|
||||
export const ActivityStreak = ({
|
||||
tiers,
|
||||
streak,
|
||||
}: {
|
||||
tiers: {
|
||||
minimum_activity_streak: number;
|
||||
reward_multiplier: string;
|
||||
vesting_multiplier: string;
|
||||
}[];
|
||||
streak?: PartyActivityStreak | null;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const userTierIndex = useGetUserTier(tiers, streak?.activeFor);
|
||||
|
||||
if (!tiers || tiers.length === 0) return null;
|
||||
|
||||
const progressBarHeight = 'h-10';
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div
|
||||
className="grid"
|
||||
style={{
|
||||
gridTemplateColumns:
|
||||
'repeat(' + tiers.length + ', minmax(0, 1fr))',
|
||||
}}
|
||||
>
|
||||
{tiers.map((tier, index) => {
|
||||
return (
|
||||
<div key={index} className="flex justify-end -mr-[2.85rem]">
|
||||
<span className="flex flex-col items-center gap-4 justify-between">
|
||||
<span className="flex flex-col items-center gap-1">
|
||||
<span className="flex flex-col items-center font-medium">
|
||||
<span className="text-sm">
|
||||
{t('Tier {{tier}}', {
|
||||
tier: index + 1,
|
||||
})}
|
||||
</span>
|
||||
<span className="text-muted text-xs">
|
||||
{t('numberEpochs', '{{count}} epochs', {
|
||||
count: tier.minimum_activity_streak,
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span
|
||||
className={classNames(
|
||||
'text-xs flex flex-col items-center justify-center px-2 py-1 rounded-lg text-white border',
|
||||
{
|
||||
'border-pink-600 bg-pink-900': index % 6 === 0,
|
||||
'border-purple-600 bg-purple-900': index % 6 === 1,
|
||||
'border-blue-600 bg-blue-900': index % 6 === 2,
|
||||
'border-orange-600 bg-orange-900': index % 6 === 3,
|
||||
'border-green-600 bg-green-900': index % 6 === 4,
|
||||
'border-yellow-600 bg-yellow-900': index % 6 === 5,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span>
|
||||
{t('Reward {{reward}}x', {
|
||||
reward: tier.reward_multiplier,
|
||||
})}
|
||||
</span>
|
||||
<span>
|
||||
{t('Vesting {{vesting}}x', {
|
||||
vesting: tier.vesting_multiplier,
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
className={classNames(
|
||||
{
|
||||
'text-pink-500': index % 6 === 0,
|
||||
'text-purple-500': index % 6 === 1,
|
||||
'text-blue-500': index % 6 === 2,
|
||||
'text-orange-500': index % 6 === 3,
|
||||
'text-green-500': index % 6 === 4,
|
||||
'text-yellow-500': index % 6 === 5,
|
||||
},
|
||||
'leading-[0] font-sans text-[48px]'
|
||||
)}
|
||||
>
|
||||
•
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
{tiers.map((_tier, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white dark:bg-gray-800 shadow-card rounded-[100px] grow"
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'relative w-full rounded-[100px] bg-gray-200 dark:bg-gray-800',
|
||||
progressBarHeight
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute left-0 top-0 h-full rounded-[100px] bg-gradient-to-r',
|
||||
{
|
||||
'from-vega-dark-400 to-vega-dark-200':
|
||||
userTierIndex === 0 || streak?.isActive === false,
|
||||
'from-vega-pink-600 to-vega-pink-500':
|
||||
userTierIndex % 6 === 1,
|
||||
'from-vega-purple-600 to-vega-purple-500':
|
||||
userTierIndex % 6 === 2,
|
||||
'from-vega-blue-600 to-vega-blue-500':
|
||||
userTierIndex % 6 === 3,
|
||||
'from-vega-orange-600 to-vega-orange-500':
|
||||
userTierIndex % 6 === 4,
|
||||
'from-vega-green-600 to-vega-green-500':
|
||||
userTierIndex % 6 === 5,
|
||||
'from-vega-yellow-600 to-vega-yellow-500':
|
||||
userTierIndex % 6 === 0,
|
||||
}
|
||||
)}
|
||||
style={{
|
||||
width:
|
||||
safeProgress(
|
||||
index,
|
||||
userTierIndex,
|
||||
tiers[index].minimum_activity_streak,
|
||||
streak?.activeFor
|
||||
) + '%',
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<VegaIcon name={VegaIconNames.STREAK} />
|
||||
|
||||
<span className="flex flex-col">
|
||||
{streak?.isActive && (
|
||||
<span data-testid="epoch-streak">
|
||||
{t('userActive', '{{active}} trader: {{count}} epochs so far', {
|
||||
active: streak?.isActive ? 'Active' : 'Inactive',
|
||||
count: streak?.activeFor || 0,
|
||||
})}{' '}
|
||||
{userTierIndex > 0 &&
|
||||
new BigNumber(
|
||||
tiers[0].minimum_activity_streak
|
||||
).isLessThanOrEqualTo(streak?.activeFor || 0) &&
|
||||
t('(Tier {{tier}} as of last epoch)', {
|
||||
tier: userTierIndex,
|
||||
})}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { RewardHoarderBonus } from './reward-hoarder-bonus';
|
||||
import type { PartyVestingStats } from '@vegaprotocol/types';
|
||||
|
||||
describe('RewardHoarderBonus', () => {
|
||||
it('renders null when vestingDetails is not provided', () => {
|
||||
const tiers: {
|
||||
minimum_quantum_balance: string;
|
||||
reward_multiplier: string;
|
||||
}[] = [];
|
||||
const vestingDetails = null;
|
||||
render(
|
||||
<RewardHoarderBonus tiers={tiers} vestingDetails={vestingDetails} />
|
||||
);
|
||||
const component = screen.queryByText(/Reward bonus/i);
|
||||
expect(component).toBeNull();
|
||||
});
|
||||
|
||||
it('renders null when tiers are empty', () => {
|
||||
const tiers: {
|
||||
minimum_quantum_balance: string;
|
||||
reward_multiplier: string;
|
||||
}[] = [];
|
||||
const vestingDetails: PartyVestingStats = {
|
||||
epochSeq: 0,
|
||||
rewardBonusMultiplier: '1.5',
|
||||
quantumBalance: '100',
|
||||
};
|
||||
render(
|
||||
<RewardHoarderBonus tiers={tiers} vestingDetails={vestingDetails} />
|
||||
);
|
||||
const component = screen.queryByText(/Reward bonus/i);
|
||||
expect(component).toBeNull();
|
||||
});
|
||||
|
||||
it('renders the component with tiers and vestingDetails', () => {
|
||||
const tiers = [
|
||||
{
|
||||
minimum_quantum_balance: '50',
|
||||
reward_multiplier: '1.5x',
|
||||
},
|
||||
{
|
||||
minimum_quantum_balance: '100',
|
||||
reward_multiplier: '2x',
|
||||
},
|
||||
];
|
||||
const vestingDetails = {
|
||||
epochSeq: 0,
|
||||
rewardBonusMultiplier: '1.5',
|
||||
quantumBalance: '75',
|
||||
};
|
||||
render(
|
||||
<RewardHoarderBonus tiers={tiers} vestingDetails={vestingDetails} />
|
||||
);
|
||||
const tierLabels = screen.getAllByText(/Tier/i);
|
||||
expect(tierLabels.length).toBe(3); // 2 tiers + 1 label
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,197 @@
|
||||
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
||||
import { useT } from '../../../lib/use-t';
|
||||
import classNames from 'classnames';
|
||||
import type { PartyVestingStats } from '@vegaprotocol/types';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { formatNumber } from '@vegaprotocol/utils';
|
||||
import { safeProgress } from './activity-streaks';
|
||||
|
||||
export const useGetUserTier = (
|
||||
tiers: {
|
||||
minimum_quantum_balance: string;
|
||||
reward_multiplier: string;
|
||||
}[],
|
||||
progress?: number | string
|
||||
) => {
|
||||
if (!progress) return 0;
|
||||
if (!tiers || tiers.length === 0) return 0;
|
||||
|
||||
let userTier = 0;
|
||||
let i = 0;
|
||||
let minProgress = '0';
|
||||
while (i < tiers.length && new BigNumber(minProgress).isLessThan(progress)) {
|
||||
userTier = i;
|
||||
i++;
|
||||
minProgress = tiers[userTier].minimum_quantum_balance;
|
||||
}
|
||||
|
||||
if (
|
||||
i === tiers.length &&
|
||||
new BigNumber(minProgress).isLessThanOrEqualTo(progress)
|
||||
) {
|
||||
userTier = i;
|
||||
}
|
||||
|
||||
if (userTier > tiers.length) {
|
||||
userTier--;
|
||||
}
|
||||
|
||||
return userTier;
|
||||
};
|
||||
|
||||
export const RewardHoarderBonus = ({
|
||||
tiers,
|
||||
vestingDetails,
|
||||
}: {
|
||||
tiers: {
|
||||
minimum_quantum_balance: string;
|
||||
reward_multiplier: string;
|
||||
}[];
|
||||
vestingDetails?: PartyVestingStats | null;
|
||||
}) => {
|
||||
const t = useT();
|
||||
|
||||
const userTierIndex = useGetUserTier(tiers, vestingDetails?.quantumBalance);
|
||||
|
||||
if (!tiers || tiers.length === 0) return null;
|
||||
|
||||
// There is only value to compare to the tiers that covers all the user' rewards across all assets
|
||||
const qUSD = 'qUSD';
|
||||
|
||||
const progressBarHeight = 'h-10';
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
<div className="flex flex-col gap-1">
|
||||
<div
|
||||
className="grid"
|
||||
style={{
|
||||
gridTemplateColumns:
|
||||
'repeat(' + tiers.length + ', minmax(0, 1fr))',
|
||||
}}
|
||||
>
|
||||
{tiers.map((tier, index) => {
|
||||
return (
|
||||
<div key={index} className="flex justify-end -mr-[2.95rem]">
|
||||
<span className="flex flex-col items-center gap-4 justify-between">
|
||||
<span className="flex flex-col items-center gap-1">
|
||||
<span className="flex flex-col items-center font-medium">
|
||||
<span className="text-sm">
|
||||
{t('Tier {{tier}}', {
|
||||
tier: index + 1,
|
||||
})}
|
||||
</span>
|
||||
<span className="text-muted text-xs">
|
||||
{formatNumber(tier.minimum_quantum_balance)} {qUSD}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span
|
||||
className={classNames(
|
||||
'text-xs flex flex-col items-center justify-center px-2 py-1 rounded-lg text-white border',
|
||||
{
|
||||
'border-pink-600 bg-pink-900': index % 6 === 0,
|
||||
'border-purple-600 bg-purple-900': index % 6 === 1,
|
||||
'border-blue-600 bg-blue-900': index % 6 === 2,
|
||||
'border-orange-600 bg-orange-900': index % 6 === 3,
|
||||
'border-green-600 bg-green-900': index % 6 === 4,
|
||||
'border-yellow-600 bg-yellow-900': index % 6 === 5,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<span>{t('Reward bonus')}</span>
|
||||
<span>
|
||||
{t('{{reward}}x', {
|
||||
reward: tier.reward_multiplier,
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span
|
||||
className={classNames(
|
||||
{
|
||||
'text-pink-500': index % 6 === 0,
|
||||
'text-purple-500': index % 6 === 1,
|
||||
'text-blue-500': index % 6 === 2,
|
||||
'text-orange-500': index % 6 === 3,
|
||||
'text-green-500': index % 6 === 4,
|
||||
'text-yellow-500': index % 6 === 5,
|
||||
},
|
||||
'leading-[0] font-sans text-[48px]'
|
||||
)}
|
||||
>
|
||||
•
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
{tiers.map((_tier, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white dark:bg-gray-800 shadow-card rounded-[100px] grow"
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'relative w-full rounded-[100px] bg-gray-200 dark:bg-gray-800',
|
||||
progressBarHeight
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'absolute left-0 top-0 h-full rounded-[100px] bg-gradient-to-r',
|
||||
{
|
||||
'from-vega-dark-400 to-vega-dark-200':
|
||||
userTierIndex === 0,
|
||||
'from-vega-pink-600 to-vega-pink-500':
|
||||
userTierIndex % 6 === 1,
|
||||
'from-vega-purple-600 to-vega-purple-500':
|
||||
userTierIndex % 6 === 2,
|
||||
'from-vega-blue-600 to-vega-blue-500':
|
||||
userTierIndex % 6 === 3,
|
||||
'from-vega-orange-600 to-vega-orange-500':
|
||||
userTierIndex % 6 === 4,
|
||||
'from-vega-green-600 to-vega-green-500':
|
||||
userTierIndex % 6 === 5,
|
||||
'from-vega-yellow-600 to-vega-yellow-500':
|
||||
userTierIndex % 6 === 0,
|
||||
}
|
||||
)}
|
||||
style={{
|
||||
width:
|
||||
safeProgress(
|
||||
index,
|
||||
userTierIndex,
|
||||
tiers[index].minimum_quantum_balance,
|
||||
vestingDetails?.quantumBalance
|
||||
) + '%',
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<VegaIcon name={VegaIconNames.STREAK} />
|
||||
<span data-testid="hoarder-bonus-total-hoarded">
|
||||
{formatNumber(vestingDetails?.quantumBalance || 0)} {qUSD}{' '}
|
||||
{userTierIndex > 0 &&
|
||||
new BigNumber(
|
||||
tiers[0].minimum_quantum_balance
|
||||
).isLessThanOrEqualTo(vestingDetails?.quantumBalance || 0) &&
|
||||
t('(Tier {{tier}} as of last epoch)', { tier: userTierIndex })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -11,14 +11,21 @@ type TableColumnDefinition = {
|
||||
name: string;
|
||||
tooltip?: string;
|
||||
className?: string;
|
||||
headerClassName?: string;
|
||||
testId?: string;
|
||||
};
|
||||
|
||||
type DataEntry = {
|
||||
[key: TableColumnDefinition['name']]: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
type TableProps = {
|
||||
columns: TableColumnDefinition[];
|
||||
data: Record<TableColumnDefinition['name'] | 'className', React.ReactNode>[];
|
||||
data: DataEntry[];
|
||||
noHeader?: boolean;
|
||||
noCollapse?: boolean;
|
||||
onRowClick?: (index: number) => void;
|
||||
};
|
||||
|
||||
const INNER_BORDER_STYLE = `border-b ${BORDER_COLOR}`;
|
||||
@@ -34,6 +41,7 @@ export const Table = forwardRef<
|
||||
noHeader = false,
|
||||
noCollapse = false,
|
||||
className,
|
||||
onRowClick,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
@@ -41,13 +49,14 @@ export const Table = forwardRef<
|
||||
const header = (
|
||||
<thead className={classNames({ 'max-md:hidden': !noCollapse })}>
|
||||
<tr>
|
||||
{columns.map(({ displayName, name, tooltip }) => (
|
||||
{columns.map(({ displayName, name, tooltip, headerClassName }) => (
|
||||
<th
|
||||
key={name}
|
||||
col-id={name}
|
||||
className={classNames(
|
||||
'px-5 py-3 text-xs text-vega-clight-100 dark:text-vega-cdark-100 font-normal',
|
||||
INNER_BORDER_STYLE
|
||||
INNER_BORDER_STYLE,
|
||||
headerClassName
|
||||
)}
|
||||
>
|
||||
<span className="flex flex-row items-center gap-2">
|
||||
@@ -79,12 +88,17 @@ export const Table = forwardRef<
|
||||
>
|
||||
{!noHeader && header}
|
||||
<tbody>
|
||||
{data.map((d, i) => (
|
||||
{data.map((dataEntry, i) => (
|
||||
<tr
|
||||
key={i}
|
||||
className={classNames(d['className'] as string, {
|
||||
className={classNames(dataEntry['className'] as string, {
|
||||
'max-md:flex flex-col w-full': !noCollapse,
|
||||
})}
|
||||
onClick={() => {
|
||||
if (onRowClick) {
|
||||
onRowClick(i);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{columns.map(({ name, displayName, className, testId }, j) => (
|
||||
<td
|
||||
@@ -114,7 +128,9 @@ export const Table = forwardRef<
|
||||
{displayName}
|
||||
</span>
|
||||
)}
|
||||
<span data-testid={`${testId || name}-${i}`}>{d[name]}</span>
|
||||
<span data-testid={`${testId || name}-${i}`}>
|
||||
{dataEntry[name]}
|
||||
</span>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
|
||||
VEGA_VERSION=v0.73.9
|
||||
LOCAL_SERVER=false
|
||||
VEGA_VERSION=v0.73.10
|
||||
LOCAL_SERVER=true
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
CONSOLE_IMAGE_NAME=vegaprotocol/trading:develop
|
||||
VEGA_VERSION=v0.73.9
|
||||
VEGA_VERSION=v0.73.10
|
||||
LOCAL_SERVER=false
|
||||
@@ -1,3 +1,3 @@
|
||||
CONSOLE_IMAGE_NAME=vegaprotocol/trading:main
|
||||
VEGA_VERSION=v0.73.8
|
||||
VEGA_VERSION=v0.73.10
|
||||
LOCAL_SERVER=false
|
||||
@@ -8,7 +8,7 @@ import docker
|
||||
import http.server
|
||||
import sys
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from playwright.sync_api import Error as PlaywrightError
|
||||
from docker.models.containers import Container
|
||||
from docker.errors import APIError
|
||||
from contextlib import contextmanager
|
||||
@@ -274,3 +274,18 @@ def perps_market(vega, request):
|
||||
if hasattr(request, "param"):
|
||||
kwargs.update(request.param)
|
||||
return setup_perps_market(vega, **kwargs)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def retry_on_http_error(request):
|
||||
retry_count = 3
|
||||
for i in range(retry_count):
|
||||
try:
|
||||
yield
|
||||
return
|
||||
except requests.exceptions.HTTPError:
|
||||
if i < retry_count - 1:
|
||||
print(f"Retrying due to HTTPError (attempt {i+1}/{retry_count})")
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ def setup_simple_market(
|
||||
custom_market_name=market_name,
|
||||
custom_asset_name="tDAI",
|
||||
custom_asset_symbol="tDAI",
|
||||
custom_quantum=1
|
||||
custom_quantum=1,
|
||||
):
|
||||
for wallet in wallets:
|
||||
vega.create_key(wallet.name)
|
||||
@@ -117,18 +117,30 @@ def setup_simple_successor_market(
|
||||
return market_id
|
||||
|
||||
|
||||
def setup_opening_auction_market(vega: VegaService, market_id: str = None, buy_orders=default_buy_orders, sell_orders=default_sell_orders, add_liquidity=True, **kwargs):
|
||||
def setup_opening_auction_market(
|
||||
vega: VegaService,
|
||||
market_id: str = None,
|
||||
buy_orders=default_buy_orders,
|
||||
sell_orders=default_sell_orders,
|
||||
add_liquidity=True,
|
||||
custom_market_name="BTC:DAI_2023",
|
||||
custom_asset_name="tDAI",
|
||||
custom_asset_symbol="tDAI",
|
||||
**kwargs,
|
||||
):
|
||||
if not market_exists(vega, market_id):
|
||||
market_id = setup_simple_market(vega, **kwargs)
|
||||
market_id = setup_simple_market(
|
||||
vega,
|
||||
custom_market_name=custom_market_name,
|
||||
custom_asset_name=custom_asset_name,
|
||||
custom_asset_symbol=custom_asset_symbol,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
if add_liquidity:
|
||||
submit_liquidity(vega, MM_WALLET.name, market_id)
|
||||
submit_multiple_orders(
|
||||
vega, MM_WALLET.name, market_id, "SIDE_SELL", sell_orders
|
||||
)
|
||||
submit_multiple_orders(
|
||||
vega, MM_WALLET2.name, market_id, "SIDE_BUY", buy_orders
|
||||
)
|
||||
submit_multiple_orders(vega, MM_WALLET.name, market_id, "SIDE_SELL", sell_orders)
|
||||
submit_multiple_orders(vega, MM_WALLET2.name, market_id, "SIDE_BUY", buy_orders)
|
||||
|
||||
vega.forward("10s")
|
||||
vega.wait_fn(1)
|
||||
@@ -146,13 +158,37 @@ def market_exists(vega: VegaService, market_id: str):
|
||||
|
||||
|
||||
# Add sell orders and buy orders to put on the book
|
||||
def setup_continuous_market(vega: VegaService, market_id: str = None, buy_orders=default_buy_orders, sell_orders=default_sell_orders, add_liquidity=True, **kwargs):
|
||||
if not market_exists(vega, market_id) or buy_orders != default_buy_orders or sell_orders != default_sell_orders:
|
||||
def setup_continuous_market(
|
||||
vega: VegaService,
|
||||
market_id: str = None,
|
||||
buy_orders=default_buy_orders,
|
||||
sell_orders=default_sell_orders,
|
||||
add_liquidity=True,
|
||||
custom_market_name="BTC:DAI_2023",
|
||||
custom_asset_name="tDAI",
|
||||
custom_asset_symbol="tDAI",
|
||||
**kwargs,
|
||||
):
|
||||
if (
|
||||
not market_exists(vega, market_id)
|
||||
or buy_orders != default_buy_orders
|
||||
or sell_orders != default_sell_orders
|
||||
):
|
||||
market_id = setup_opening_auction_market(
|
||||
vega, market_id, buy_orders, sell_orders, add_liquidity, **kwargs)
|
||||
vega,
|
||||
market_id,
|
||||
buy_orders,
|
||||
sell_orders,
|
||||
add_liquidity,
|
||||
custom_market_name=custom_market_name,
|
||||
custom_asset_name=custom_asset_name,
|
||||
custom_asset_symbol=custom_asset_symbol,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
submit_order(vega, "Key 1", market_id, "SIDE_BUY",
|
||||
sell_orders[0][0], sell_orders[0][1])
|
||||
submit_order(
|
||||
vega, "Key 1", market_id, "SIDE_BUY", sell_orders[0][0], sell_orders[0][1]
|
||||
)
|
||||
|
||||
vega.forward("10s")
|
||||
vega.wait_fn(1)
|
||||
@@ -250,6 +286,8 @@ def setup_perps_market(
|
||||
def market_exists(vega: VegaService, market_id: str):
|
||||
if market_id is None:
|
||||
return False
|
||||
all_markets = vega.all_markets()
|
||||
all_markets = vega.all_markets()
|
||||
market_ids = [market.id for market in all_markets]
|
||||
return market_id in market_ids
|
||||
print("Checking for market ID:", market_id)
|
||||
print("Available market IDs:", market_ids)
|
||||
return market_id in market_ids
|
||||
@@ -70,7 +70,6 @@ def test_iceberg_open_order(continuous_market, vega: VegaServiceNull, page: Page
|
||||
expect(
|
||||
page.locator(".ag-center-cols-container .ag-row [col-id='size']").first
|
||||
).to_have_text("-102")
|
||||
page.pause()
|
||||
expect(
|
||||
page.locator(".ag-center-cols-container .ag-row [col-id='type'] ").first
|
||||
).to_have_text("Limit (Iceberg)")
|
||||
|
||||
@@ -16,7 +16,6 @@ def vega(request):
|
||||
def continuous_market(vega):
|
||||
return setup_continuous_market(vega)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("auth", "risk_accepted")
|
||||
def test_liquidity_provision_amendment(continuous_market, vega: VegaServiceNull, page: Page):
|
||||
# TODO Refactor asserting the grid
|
||||
|
||||
@@ -64,7 +64,6 @@ def setup_market_monitoring_auction(vega: VegaServiceNull, simple_market):
|
||||
submit_order(vega, MM_WALLET.name, simple_market, "SIDE_SELL", 1, 1 + 0.1 / 2)
|
||||
submit_order(vega, MM_WALLET2.name, simple_market, "SIDE_SELL", 1, 1)
|
||||
|
||||
vega.forward("10s")
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
|
||||
@@ -75,7 +74,6 @@ def setup_market_monitoring_auction(vega: VegaServiceNull, simple_market):
|
||||
submit_order(vega, MM_WALLET2.name, simple_market, "SIDE_BUY", 100, 95)
|
||||
submit_order(vega, MM_WALLET2.name, simple_market, "SIDE_BUY", 1, 105)
|
||||
|
||||
vega.forward("10s")
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
|
||||
@@ -109,7 +107,6 @@ def test_market_monitoring_auction_price_volatility_limit_order(
|
||||
page.get_by_test_id("place-order").click()
|
||||
|
||||
wait_for_toast_confirmation(page)
|
||||
vega.forward("10s")
|
||||
vega.wait_fn(1)
|
||||
vega.wait_for_total_catchup()
|
||||
page.get_by_test_id("All").click()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user