@@ -240,7 +269,7 @@ const CreateCodeDialog = ({
onSubmit()}
+ onClick={() => onSubmit({ createReferralSet: { isTeam: false } })}
{...getButtonProps()}
>
{t('Yes')}
@@ -269,14 +298,17 @@ const CreateCodeDialog = ({
return (
- {(status === 'idle' || status === 'loading' || status === 'error') && (
+ {(status === 'idle' ||
+ status === 'requested' ||
+ status === 'pending' ||
+ err) && (
{t(
'Generate a referral code to share with your friends and access the commission benefits of the current program.'
)}
)}
- {status === 'success' && code && (
+ {status === 'confirmed' && code && (
diff --git a/apps/trading/client-pages/referrals/error-boundary.tsx b/apps/trading/client-pages/referrals/error-boundary.tsx
index ec1008456..a47e48cc4 100644
--- a/apps/trading/client-pages/referrals/error-boundary.tsx
+++ b/apps/trading/client-pages/referrals/error-boundary.tsx
@@ -1,53 +1,10 @@
-import { isRouteErrorResponse, useNavigate, useRouteError } from 'react-router';
-import { RainbowButton } from './buttons';
+import { useNavigate } from 'react-router';
+import { RainbowButton } from '../../components/rainbow-button';
+import { LayoutWithSky } from '../../components/layouts-inner';
import { AnimatedDudeWithWire } from './graphics/dude';
-import { LayoutWithSky } from './layout';
import { Routes } from '../../lib/links';
import { useT } from '../../lib/use-t';
-export const ErrorBoundary = () => {
- const t = useT();
- const error = useRouteError();
- const navigate = useNavigate();
-
- const title = isRouteErrorResponse(error)
- ? `${error.status} ${error.statusText}`
- : t('Something went wrong');
-
- const code = isRouteErrorResponse(error) ? error.status : 0;
-
- const messages: Record = {
- 0: t('An unknown error occurred.'),
- 404: t("The page you're looking for doesn't exists."),
- };
-
- return (
-
-
- {title}
-
- {Object.keys(messages).includes(code.toString()) ? (
- {messages[code]}
- ) : null}
-
-
- navigate('..')}
- variant="border"
- className="text-xs"
- >
- {t('Go back and try again')}
-
-
-
- );
-};
-
export const NotFound = () => {
const t = useT();
const navigate = useNavigate();
diff --git a/apps/trading/client-pages/referrals/hooks/use-referral.ts b/apps/trading/client-pages/referrals/hooks/use-referral.ts
index d243b55d5..cc8c4bc8b 100644
--- a/apps/trading/client-pages/referrals/hooks/use-referral.ts
+++ b/apps/trading/client-pages/referrals/hooks/use-referral.ts
@@ -8,13 +8,13 @@ import type {
ReferralSetsQueryVariables,
} from './__generated__/ReferralSets';
import { useReferralSetsQuery } from './__generated__/ReferralSets';
-import { useStakeAvailable } from './use-stake-available';
+import { useStakeAvailable } from '../../../lib/hooks/use-stake-available';
export const DEFAULT_AGGREGATION_DAYS = 30;
export type Role = 'referrer' | 'referee';
type UseReferralArgs = (
- | { code: string }
+ | { code: string | undefined }
| { pubKey: string | null; role: Role }
) & {
aggregationEpochs?: number;
diff --git a/apps/trading/client-pages/referrals/referral-statistics.spec.tsx b/apps/trading/client-pages/referrals/referral-statistics.spec.tsx
index 0a3d41a84..be2f08c33 100644
--- a/apps/trading/client-pages/referrals/referral-statistics.spec.tsx
+++ b/apps/trading/client-pages/referrals/referral-statistics.spec.tsx
@@ -1,6 +1,9 @@
import { MockedProvider, type MockedResponse } from '@apollo/react-testing';
-import { render, waitFor } from '@testing-library/react';
-import { type VegaWalletContextShape } from '@vegaprotocol/wallet';
+import { render, screen, waitFor } from '@testing-library/react';
+import {
+ VegaWalletContext,
+ type VegaWalletContextShape,
+} from '@vegaprotocol/wallet';
import { ReferralStatistics } from './referral-statistics';
import {
ReferralProgramDocument,
@@ -15,7 +18,7 @@ import {
StakeAvailableDocument,
type StakeAvailableQueryVariables,
type StakeAvailableQuery,
-} from './hooks/__generated__/StakeAvailable';
+} from '../../lib/hooks/__generated__/StakeAvailable';
import {
RefereesDocument,
type RefereesQueryVariables,
@@ -296,122 +299,99 @@ const refereesMock30: MockedResponse = {
},
};
-jest.mock('@vegaprotocol/wallet', () => {
- return {
- ...jest.requireActual('@vegaprotocol/wallet'),
- useVegaWallet: () => {
- const ctx: Partial = {
- pubKey: MOCK_PUBKEY,
- };
- return ctx;
- },
- };
-});
-
describe('ReferralStatistics', () => {
- it('displays apply code when no data has been found for given pubkey', () => {
- const { queryByTestId } = render(
+ const renderComponent = (mocks: MockedResponse[]) => {
+ const walletContext = {
+ pubKey: MOCK_PUBKEY,
+ isReadOnly: false,
+ sendTx: jest.fn(),
+ } as unknown as VegaWalletContextShape;
+
+ return render(
-
-
-
+
+
+
+
+
);
+ };
- expect(queryByTestId('referral-apply-code-form')).toBeInTheDocument();
+ it('displays apply code when no data has been found for given pubkey', () => {
+ renderComponent([]);
+ expect(
+ screen.queryByTestId('referral-apply-code-form')
+ ).toBeInTheDocument();
});
it('displays referrer stats when given pubkey is a referrer', async () => {
- const { queryByTestId } = render(
-
-
-
-
-
- );
+ renderComponent([
+ programMock,
+ referralSetAsReferrerMock,
+ noReferralSetAsRefereeMock,
+ stakeAvailableMock,
+ refereesMock,
+ refereesMock30,
+ ]);
await waitFor(() => {
expect(
- queryByTestId('referral-create-code-form')
+ screen.queryByTestId('referral-create-code-form')
).not.toBeInTheDocument();
- expect(queryByTestId('referral-statistics')).toBeInTheDocument();
- expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
+ expect(screen.queryByTestId('referral-statistics')).toBeInTheDocument();
+ expect(screen.queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referrer'
);
// gets commision from 30 epochs query
- expect(queryByTestId('total-commission-value')).toHaveTextContent(
+ expect(screen.queryByTestId('total-commission-value')).toHaveTextContent(
'12,340'
);
});
});
it('displays referee stats when given pubkey is a referee', async () => {
- const { queryByTestId } = render(
-
-
-
-
-
- );
-
+ renderComponent([
+ programMock,
+ noReferralSetAsReferrerMock,
+ referralSetAsRefereeMock,
+ stakeAvailableMock,
+ refereesMock,
+ ]);
await waitFor(() => {
expect(
- queryByTestId('referral-create-code-form')
+ screen.queryByTestId('referral-create-code-form')
).not.toBeInTheDocument();
- expect(queryByTestId('referral-statistics')).toBeInTheDocument();
- expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
+ expect(screen.queryByTestId('referral-statistics')).toBeInTheDocument();
+ expect(screen.queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referee'
);
});
});
it('displays eligibility warning when the set is no longer valid due to the referrers stake', async () => {
- const { queryByTestId } = render(
-
-
-
-
-
- );
+ renderComponent([
+ programMock,
+ noReferralSetAsReferrerMock,
+ referralSetAsRefereeMock,
+ nonEligibleStakeAvailableMock,
+ refereesMock,
+ ]);
await waitFor(() => {
expect(
- queryByTestId('referral-create-code-form')
+ screen.queryByTestId('referral-create-code-form')
).not.toBeInTheDocument();
- expect(queryByTestId('referral-statistics')).toBeInTheDocument();
- expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
+ expect(screen.queryByTestId('referral-statistics')).toBeInTheDocument();
+ expect(screen.queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referee'
);
- expect(queryByTestId('referral-eligibility-warning')).toBeInTheDocument();
- expect(queryByTestId('referral-apply-code-form')).toBeInTheDocument();
+ expect(
+ screen.queryByTestId('referral-eligibility-warning')
+ ).toBeInTheDocument();
+ expect(
+ screen.queryByTestId('referral-apply-code-form')
+ ).toBeInTheDocument();
});
});
});
diff --git a/apps/trading/client-pages/referrals/referral-statistics.tsx b/apps/trading/client-pages/referrals/referral-statistics.tsx
index 7d6dfb2d9..245871d8e 100644
--- a/apps/trading/client-pages/referrals/referral-statistics.tsx
+++ b/apps/trading/client-pages/referrals/referral-statistics.tsx
@@ -1,20 +1,17 @@
+import { useCallback, useLayoutEffect, useRef, useState } from 'react';
+import BigNumber from 'bignumber.js';
import minBy from 'lodash/minBy';
-import { CodeTile, StatTile } from './tile';
+import sortBy from 'lodash/sortBy';
+import compact from 'lodash/compact';
+import { Trans } from 'react-i18next';
+import classNames from 'classnames';
import {
VegaIcon,
VegaIconNames,
truncateMiddle,
TextChildrenTooltip as Tooltip,
} from '@vegaprotocol/ui-toolkit';
-
import { useVegaWallet } from '@vegaprotocol/wallet';
-import {
- DEFAULT_AGGREGATION_DAYS,
- useReferral,
- useUpdateReferees,
-} from './hooks/use-referral';
-import classNames from 'classnames';
-import { Table } from '../../components/table';
import {
addDecimalsFormatNumber,
getDateFormat,
@@ -24,17 +21,22 @@ import {
removePaginationWrapper,
} from '@vegaprotocol/utils';
import { useReferralSetStatsQuery } from './hooks/__generated__/ReferralSetStats';
-import compact from 'lodash/compact';
-import { useReferralProgram } from './hooks/use-referral-program';
-import { useStakeAvailable } from './hooks/use-stake-available';
-import sortBy from 'lodash/sortBy';
-import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
-import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
-import BigNumber from 'bignumber.js';
+import { useStakeAvailable } from '../../lib/hooks/use-stake-available';
import { useT, ns } from '../../lib/use-t';
-import { Trans } from 'react-i18next';
+import { useTeam } from '../../lib/hooks/use-team';
+import { TeamAvatar } from '../../components/competitions/team-avatar';
+import { TeamStats } from '../../components/competitions/team-stats';
+import { Table } from '../../components/table';
+import {
+ DEFAULT_AGGREGATION_DAYS,
+ useReferral,
+ useUpdateReferees,
+} from './hooks/use-referral';
import { ApplyCodeForm, ApplyCodeFormContainer } from './apply-code-form';
+import { useReferralProgram } from './hooks/use-referral-program';
+import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
import { QUSDTooltip } from './qusd-tooltip';
+import { CodeTile, StatTile, Tile } from './tile';
export const ReferralStatistics = () => {
const { pubKey } = useVegaWallet();
@@ -192,10 +194,7 @@ export const Statistics = ({
nextBenefitTierEpochsValue,
} = useStats({ data, program });
- const isApplyCodePreview = useMemo(
- () => data.referee === null,
- [data.referee]
- );
+ const isApplyCodePreview = data.referee === null;
const { benefitTiers } = useReferralProgram();
@@ -328,23 +327,6 @@ export const Statistics = ({
);
- const referrerTiles = (
- <>
-
- {baseCommissionTile}
- {stakingMultiplierTile}
- {finalCommissionTile}
-
-
-
- {codeTile}
- {referrerVolumeTile}
- {numberOfTradersTile}
- {totalCommissionTile}
-
- >
- );
-
const currentBenefitTierTile = (
);
+ const eligibilityWarningOverlay = as === 'referee' && !isEligible && (
+
+
{t('Referral code no longer valid')}
+
+ {t(
+ 'Your referral code is no longer valid as the referrer no longer meets the minimum requirements. Apply a new code to continue receiving discounts.'
+ )}
+
+
+ );
+
+ const referrerTiles = (
+ <>
+
+
+ {baseCommissionTile}
+ {stakingMultiplierTile}
+ {finalCommissionTile}
+
+
+
+ {codeTile}
+ {referrerVolumeTile}
+ {numberOfTradersTile}
+ {totalCommissionTile}
+
+ >
+ );
+
const refereeTiles = (
<>
+
{currentBenefitTierTile}
{runningVolumeTile}
@@ -432,20 +447,6 @@ export const Statistics = ({
>
);
- const eligibilityWarning = as === 'referee' && !isEligible && (
-
-
{t('Referral code no longer valid')}
-
- {t(
- 'Your referral code is no longer valid as the referrer no longer meets the minimum requirements. Apply a new code to continue receiving discounts.'
- )}
-
-
- );
-
return (
-
- {eligibilityWarning}
+ {eligibilityWarningOverlay}
);
};
@@ -574,3 +574,19 @@ export const RefereesTable = ({
>
);
};
+
+const Team = ({ teamId }: { teamId?: string }) => {
+ const { team, games, members } = useTeam(teamId);
+
+ if (!team) return null;
+
+ return (
+
+
+
+
{team.name}
+
+
+
+ );
+};
diff --git a/apps/trading/client-pages/referrals/referrals.tsx b/apps/trading/client-pages/referrals/referrals.tsx
index 10fea63bb..e41df656b 100644
--- a/apps/trading/client-pages/referrals/referrals.tsx
+++ b/apps/trading/client-pages/referrals/referrals.tsx
@@ -13,11 +13,9 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
import { useReferral } from './hooks/use-referral';
import { REFERRAL_DOCS_LINK } from './constants';
import classNames from 'classnames';
-import { usePageTitleStore } from '../../stores';
-import { useEffect } from 'react';
-import { titlefy } from '@vegaprotocol/utils';
import { useT } from '../../lib/use-t';
import { ErrorBoundary } from '../../components/error-boundary';
+import { usePageTitle } from '../../lib/hooks/use-page-title';
const Nav = () => {
const t = useT();
@@ -57,13 +55,7 @@ export const Referrals = () => {
const loading = refereeLoading || referrerLoading;
const showNav = !loading && !error && !referrer && !referee;
- const { updateTitle } = usePageTitleStore((store) => ({
- updateTitle: store.updateTitle,
- }));
-
- useEffect(() => {
- updateTitle(titlefy([t('Referrals')]));
- }, [updateTitle, t]);
+ usePageTitle(t('Referrals'));
return (
diff --git a/apps/trading/client-pages/teams/index.ts b/apps/trading/client-pages/teams/index.ts
deleted file mode 100644
index 3a518c18d..000000000
--- a/apps/trading/client-pages/teams/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export { Teams } from './teams';
diff --git a/apps/trading/client-pages/teams/teams.tsx b/apps/trading/client-pages/teams/teams.tsx
deleted file mode 100644
index edf9bee2c..000000000
--- a/apps/trading/client-pages/teams/teams.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-export const Teams = () => {
- return (
-
-
Teams
-
- );
-};
diff --git a/apps/trading/components/competitions/box.tsx b/apps/trading/components/competitions/box.tsx
new file mode 100644
index 000000000..d6c15de41
--- /dev/null
+++ b/apps/trading/components/competitions/box.tsx
@@ -0,0 +1,21 @@
+import classNames from 'classnames';
+import { type HTMLAttributes } from 'react';
+
+export const BORDER_COLOR = 'border-vega-clight-500 dark:border-vega-cdark-500';
+export const GRADIENT =
+ 'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
+
+export const Box = (props: HTMLAttributes) => {
+ return (
+
+ );
+};
diff --git a/apps/trading/components/competitions/competitions-cta.tsx b/apps/trading/components/competitions/competitions-cta.tsx
new file mode 100644
index 000000000..74b554657
--- /dev/null
+++ b/apps/trading/components/competitions/competitions-cta.tsx
@@ -0,0 +1,41 @@
+import { Box } from './box';
+import { type ComponentProps, type ReactElement, type ReactNode } from 'react';
+import { DudeBadge } from './graphics/dude-badge';
+
+export const CompetitionsActionsContainer = ({
+ children,
+}: {
+ children:
+ | ReactElement
+ | Iterable>;
+}) => (
+
+ {children}
+
+);
+
+export const CompetitionsAction = ({
+ variant,
+ title,
+ description,
+ actionElement,
+}: {
+ variant: ComponentProps['variant'];
+ title: string;
+ description?: string;
+ actionElement: ReactNode;
+}) => {
+ return (
+
+
+
+
+ {title}
+ {description && {description}
}
+ {actionElement}
+
+ );
+};
diff --git a/apps/trading/components/competitions/competitions-header.tsx b/apps/trading/components/competitions/competitions-header.tsx
new file mode 100644
index 000000000..178468f81
--- /dev/null
+++ b/apps/trading/components/competitions/competitions-header.tsx
@@ -0,0 +1,27 @@
+import { AnimatedDudeWithWire } from '../../client-pages/referrals/graphics/dude';
+import { type ReactNode } from 'react';
+
+export const CompetitionsHeader = ({
+ title,
+ children,
+}: {
+ title: string;
+ children?: ReactNode;
+}) => {
+ return (
+
+
+
+
+ {title}
+
+ {children}
+
+
+ );
+};
diff --git a/apps/trading/components/competitions/competitions-leaderboard.tsx b/apps/trading/components/competitions/competitions-leaderboard.tsx
new file mode 100644
index 000000000..cc1cfc25b
--- /dev/null
+++ b/apps/trading/components/competitions/competitions-leaderboard.tsx
@@ -0,0 +1,71 @@
+import { Link } from 'react-router-dom';
+import { Splash } from '@vegaprotocol/ui-toolkit';
+import { getNumberFormat } from '@vegaprotocol/utils';
+import { type useTeams } from '../../lib/hooks/use-teams';
+import { useT } from '../../lib/use-t';
+import { Table } from '../table';
+import { Rank } from './graphics/rank';
+import { Links } from '../../lib/links';
+import { TeamAvatar } from './team-avatar';
+
+export const CompetitionsLeaderboard = ({
+ data,
+}: {
+ data: ReturnType['data'];
+}) => {
+ const t = useT();
+
+ const num = (n?: number | string) =>
+ !n ? '-' : getNumberFormat(0).format(Number(n));
+
+ if (!data || data.length === 0) {
+ return {t('Could not find any teams')} ;
+ }
+
+ return (
+ {
+ // leaderboard place or medal
+ let rank: number | React.ReactNode = i + 1;
+ if (rank === 1) rank = ;
+ if (rank === 2) rank = ;
+ if (rank === 3) rank = ;
+
+ const avatar = (
+
+ );
+
+ return {
+ rank,
+ avatar,
+ team: (
+
+ {td.name}
+
+ ),
+ earned: num(td.totalQuantumRewards),
+ games: num(td.totalGamesPlayed),
+ status: td.closed ? t('Closed') : t('Open'),
+ volume: num(td.totalQuantumVolume),
+ };
+ })}
+ />
+ );
+};
diff --git a/apps/trading/components/competitions/games-container.tsx b/apps/trading/components/competitions/games-container.tsx
new file mode 100644
index 000000000..e59f9c576
--- /dev/null
+++ b/apps/trading/components/competitions/games-container.tsx
@@ -0,0 +1,44 @@
+import { type TransferNode } from '@vegaprotocol/types';
+import { ActiveRewardCard } from '../rewards-container/active-rewards';
+import { useT } from '../../lib/use-t';
+
+export const GamesContainer = ({
+ data,
+ currentEpoch,
+}: {
+ data: TransferNode[];
+ currentEpoch: number;
+}) => {
+ const t = useT();
+
+ if (!data || data.length === 0) {
+ return (
+
+ {t('There are currently no games available.')}
+
+ );
+ }
+
+ return (
+
+ {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 (
+
+ );
+ })}
+
+ );
+};
diff --git a/apps/trading/components/competitions/graphics/dude-badge.tsx b/apps/trading/components/competitions/graphics/dude-badge.tsx
new file mode 100644
index 000000000..082ebdced
--- /dev/null
+++ b/apps/trading/components/competitions/graphics/dude-badge.tsx
@@ -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 (
+
+
+
+ );
+};
diff --git a/apps/trading/components/competitions/graphics/dude-with-flag.tsx b/apps/trading/components/competitions/graphics/dude-with-flag.tsx
new file mode 100644
index 000000000..5d02043d9
--- /dev/null
+++ b/apps/trading/components/competitions/graphics/dude-with-flag.tsx
@@ -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 (
+
+ {withStar && (
+ <>
+
+
+
+
+ >
+ )}
+
+
+
+
+
+ );
+};
diff --git a/apps/trading/components/competitions/graphics/rank.tsx b/apps/trading/components/competitions/graphics/rank.tsx
new file mode 100644
index 000000000..8b713ba95
--- /dev/null
+++ b/apps/trading/components/competitions/graphics/rank.tsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/trading/components/competitions/team-avatar.tsx b/apps/trading/components/competitions/team-avatar.tsx
new file mode 100644
index 000000000..8074ed525
--- /dev/null
+++ b/apps/trading/components/competitions/team-avatar.tsx
@@ -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
+
+ );
+};
diff --git a/apps/trading/components/competitions/team-stats.tsx b/apps/trading/components/competitions/team-stats.tsx
new file mode 100644
index 000000000..d875e85e8
--- /dev/null
+++ b/apps/trading/components/competitions/team-stats.tsx
@@ -0,0 +1,195 @@
+import { type ReactNode } from 'react';
+import BigNumber from 'bignumber.js';
+import countBy from 'lodash/countBy';
+import {
+ Pill,
+ VegaIcon,
+ VegaIconNames,
+ Tooltip,
+} from '@vegaprotocol/ui-toolkit';
+import { formatNumberRounded } from '@vegaprotocol/utils';
+import {
+ type TeamStats as ITeamStats,
+ type Member,
+ type TeamGame,
+} from '../../lib/hooks/use-team';
+import { useT } from '../../lib/use-t';
+import { DispatchMetricLabels, type DispatchMetric } from '@vegaprotocol/types';
+
+export const TeamStats = ({
+ stats,
+ members,
+ games,
+}: {
+ stats?: ITeamStats;
+ members?: Member[];
+ games?: TeamGame[];
+}) => {
+ const t = useT();
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ {games && games.length ? (
+
+
+
+
+
+ ) : null}
+ >
+ );
+};
+
+const LatestResults = ({ games }: { games: TeamGame[] }) => {
+ const t = useT();
+ const latestGames = games.slice(0, 5);
+
+ return (
+
+
+ {t('gameCount', { count: latestGames.length })}
+
+
+ {latestGames.map((game) => {
+ return (
+
+ {t('place', { count: game.team.rank, ordinal: true })}
+
+ );
+ })}
+
+
+ );
+};
+
+const FavoriteGame = ({ games }: { games: TeamGame[] }) => {
+ const t = useT();
+
+ const rewardMetrics = games.map(
+ (game) => game.team.rewardMetric as DispatchMetric
+ );
+ const count = countBy(rewardMetrics);
+
+ let favoriteMetric = '';
+ let mostOccurances = 0;
+
+ for (const key in count) {
+ if (count[key] > mostOccurances) {
+ favoriteMetric = key;
+ mostOccurances = count[key];
+ }
+ }
+
+ if (!favoriteMetric) return null;
+
+ // rewardMetric is a string, should be typed as DispatchMetric
+ const favoriteMetricLabel =
+ DispatchMetricLabels[favoriteMetric as DispatchMetric];
+
+ return (
+
+ {t('Favorite game')}
+
+
+ {' '}
+ {favoriteMetricLabel}
+
+
+
+ );
+};
+
+const StatSection = ({ children }: { children: ReactNode }) => {
+ return (
+
+ );
+};
+
+const StatSectionSeparator = () => {
+ return
;
+};
+
+const StatList = ({ children }: { children: ReactNode }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+const Stat = ({
+ value,
+ label,
+ tooltip,
+ valueTestId,
+}: {
+ value: ReactNode;
+ label: ReactNode;
+ tooltip?: string;
+ valueTestId?: string;
+}) => {
+ return (
+
+
+ {value}
+
+
+ {tooltip ? (
+
+
+ {label}
+
+
+
+ ) : (
+ label
+ )}
+
+
+ );
+};
diff --git a/apps/trading/components/layouts-inner/index.ts b/apps/trading/components/layouts-inner/index.ts
new file mode 100644
index 000000000..13902ace0
--- /dev/null
+++ b/apps/trading/components/layouts-inner/index.ts
@@ -0,0 +1,2 @@
+export { LayoutWithSky } from './layout-with-sky';
+export { LayoutWithGradient } from './layout-with-gradient';
diff --git a/apps/trading/components/layouts-inner/layout-with-gradient.tsx b/apps/trading/components/layouts-inner/layout-with-gradient.tsx
new file mode 100644
index 000000000..5e2db01f2
--- /dev/null
+++ b/apps/trading/components/layouts-inner/layout-with-gradient.tsx
@@ -0,0 +1,14 @@
+import { type ReactNode } from 'react';
+
+export const LayoutWithGradient = ({ children }: { children: ReactNode }) => {
+ return (
+
+ );
+};
diff --git a/apps/trading/client-pages/referrals/layout.tsx b/apps/trading/components/layouts-inner/layout-with-sky.tsx
similarity index 82%
rename from apps/trading/client-pages/referrals/layout.tsx
rename to apps/trading/components/layouts-inner/layout-with-sky.tsx
index f6e87a6ae..755dfbeed 100644
--- a/apps/trading/client-pages/referrals/layout.tsx
+++ b/apps/trading/components/layouts-inner/layout-with-sky.tsx
@@ -1,10 +1,12 @@
import classNames from 'classnames';
import type { HTMLAttributes } from 'react';
-import { SKY_BACKGROUND } from './constants';
import { Outlet } from 'react-router-dom';
import { TinyScroll } from '@vegaprotocol/ui-toolkit';
-export const Layout = ({
+export const SKY_BACKGROUND =
+ 'bg-[url(/sky-light.png)] dark:bg-[url(/sky-dark.png)] bg-[37%_0px] bg-[length:1440px] bg-no-repeat bg-local';
+
+const Layout = ({
className,
children,
...props
diff --git a/apps/trading/components/layouts/index.ts b/apps/trading/components/layouts/index.ts
index adebc7fdc..40b987baf 100644
--- a/apps/trading/components/layouts/index.ts
+++ b/apps/trading/components/layouts/index.ts
@@ -1 +1,2 @@
-export * from './layout-with-sidebar';
+export { LayoutWithSidebar } from './layout-with-sidebar';
+export { LayoutCentered } from './layout-centered';
diff --git a/apps/trading/components/navbar/navbar.tsx b/apps/trading/components/navbar/navbar.tsx
index 1328f26f6..b304df2d4 100644
--- a/apps/trading/components/navbar/navbar.tsx
+++ b/apps/trading/components/navbar/navbar.tsx
@@ -204,6 +204,13 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
{t('Portfolio')}
+ {featureFlags.TEAM_COMPETITION && (
+
+
+ {t('Competitions')}
+
+
+ )}
{featureFlags.REFERRALS && (
diff --git a/apps/trading/components/rainbow-button/index.ts b/apps/trading/components/rainbow-button/index.ts
new file mode 100644
index 000000000..de9aaf6e7
--- /dev/null
+++ b/apps/trading/components/rainbow-button/index.ts
@@ -0,0 +1 @@
+export { RainbowButton } from './rainbow-button';
diff --git a/apps/trading/components/rainbow-button/rainbow-button.tsx b/apps/trading/components/rainbow-button/rainbow-button.tsx
new file mode 100644
index 000000000..1d7c5d137
--- /dev/null
+++ b/apps/trading/components/rainbow-button/rainbow-button.tsx
@@ -0,0 +1,35 @@
+import classNames from 'classnames';
+import { type ButtonHTMLAttributes } from 'react';
+
+type RainbowButtonProps = {
+ variant?: 'full' | 'border';
+};
+
+export const RainbowButton = ({
+ variant = 'full',
+ children,
+ className,
+ ...props
+}: RainbowButtonProps & ButtonHTMLAttributes) => (
+
+
+ {children}
+
+
+);
diff --git a/apps/trading/components/rewards-container/active-rewards.spec.tsx b/apps/trading/components/rewards-container/active-rewards.spec.tsx
index 4fa205a55..23aa595dd 100644
--- a/apps/trading/components/rewards-container/active-rewards.spec.tsx
+++ b/apps/trading/components/rewards-container/active-rewards.spec.tsx
@@ -105,7 +105,7 @@ describe('ActiveRewards', () => {
expect(
screen.getByText(/Liquidity provision fees received/i)
).toBeInTheDocument();
- expect(screen.getByText('Entity scope')).toBeInTheDocument();
+ expect(screen.getByText('Individual scope')).toBeInTheDocument();
expect(screen.getByText('Average position')).toBeInTheDocument();
expect(screen.getByText('Ends in')).toBeInTheDocument();
expect(screen.getByText('115431 epochs')).toBeInTheDocument();
diff --git a/apps/trading/components/rewards-container/active-rewards.tsx b/apps/trading/components/rewards-container/active-rewards.tsx
index 199fa42b4..fdc695019 100644
--- a/apps/trading/components/rewards-container/active-rewards.tsx
+++ b/apps/trading/components/rewards-container/active-rewards.tsx
@@ -12,6 +12,7 @@ import {
VegaIconNames,
TradingInput,
TinyScroll,
+ truncateMiddle,
} from '@vegaprotocol/ui-toolkit';
import { IconNames } from '@blueprintjs/icons';
import {
@@ -30,6 +31,9 @@ import {
DispatchMetricLabels,
EntityScopeLabelMapping,
MarketState,
+ type DispatchStrategy,
+ IndividualScopeMapping,
+ IndividualScopeDescriptionMapping,
} from '@vegaprotocol/types';
import { Card } from '../card/card';
import { useMemo, useState } from 'react';
@@ -308,6 +312,9 @@ export const ActiveRewardCard = ({
MarketState.STATE_CLOSED,
].includes(m.state)
);
+ if (marketSettled) {
+ return null;
+ }
const assetInSettledMarket =
allMarkets &&
@@ -326,10 +333,6 @@ export const ActiveRewardCard = ({
return false;
});
- if (marketSettled) {
- return null;
- }
-
// Gray out the cards that are related to suspended markets
const suspended = transferNode.markets?.some(
(m) =>
@@ -359,6 +362,7 @@ export const ActiveRewardCard = ({
: getGradientClasses(dispatchStrategy.dispatchMetric);
const entityScope = dispatchStrategy.entityScope;
+
return (
}
-
{dispatchStrategy?.dispatchMetric && (
{t(DispatchMetricDescription[dispatchStrategy?.dispatchMetric])}
)}
-
-
-
-
-
- {t('Entity scope')}{' '}
-
-
-
- {kind.dispatchStrategy?.teamScope && (
- {kind.dispatchStrategy?.teamScope}
- }
- >
-
- { }
-
-
- )}
- {kind.dispatchStrategy?.individualScope && (
- {kind.dispatchStrategy?.individualScope}
- }
- >
-
- { }
-
-
- )}
- {/* Shows transfer status */}
- {/* */}
-
-
-
-
-
- {t('Staked VEGA')}{' '}
-
-
- {addDecimalsFormatNumber(
- kind.dispatchStrategy?.stakingRequirement || 0,
- transfer.asset?.decimals || 0
- )}
-
-
-
-
-
- {t('Average position')}{' '}
-
-
- {addDecimalsFormatNumber(
- kind.dispatchStrategy
- ?.notionalTimeWeightedAveragePositionRequirement || 0,
- transfer.asset?.decimals || 0
- )}
-
-
-
+ {kind.dispatchStrategy && (
+
+ )}
);
};
+const RewardRequirements = ({
+ dispatchStrategy,
+ assetDecimalPlaces = 0,
+}: {
+ dispatchStrategy: DispatchStrategy;
+ assetDecimalPlaces: number | undefined;
+}) => {
+ const t = useT();
+
+ return (
+
+
+
+ {t('{{entity}} scope', {
+ entity: EntityScopeLabelMapping[dispatchStrategy.entityScope],
+ })}
+
+
+
+
+
+
+
+
+ {t('Staked VEGA')}
+
+
+ {addDecimalsFormatNumber(
+ dispatchStrategy?.stakingRequirement || 0,
+ assetDecimalPlaces
+ )}
+
+
+
+
+
+ {t('Average position')}
+
+
+ {addDecimalsFormatNumber(
+ dispatchStrategy?.notionalTimeWeightedAveragePositionRequirement ||
+ 0,
+ assetDecimalPlaces
+ )}
+
+
+
+ );
+};
+
+const RewardEntityScope = ({
+ dispatchStrategy,
+}: {
+ dispatchStrategy: DispatchStrategy;
+}) => {
+ const t = useT();
+
+ if (dispatchStrategy.entityScope === EntityScope.ENTITY_SCOPE_TEAMS) {
+ return (
+
+ {t('Eligible teams')}
+
+ {dispatchStrategy.teamScope.map((teamId) => {
+ if (!teamId) return null;
+ return {truncateMiddle(teamId)} ;
+ })}
+
+
+ ) : (
+ t('All teams are eligible')
+ )
+ }
+ >
+
+ {dispatchStrategy.teamScope?.length
+ ? t('Some teams')
+ : t('All teams')}
+
+
+ );
+ }
+
+ if (
+ dispatchStrategy.entityScope === EntityScope.ENTITY_SCOPE_INDIVIDUALS &&
+ dispatchStrategy.individualScope
+ ) {
+ return (
+
+ {IndividualScopeMapping[dispatchStrategy.individualScope]}
+
+ );
+ }
+
+ return null;
+};
+
const getGradientClasses = (d: DispatchMetric | undefined) => {
switch (d) {
case DispatchMetric.DISPATCH_METRIC_AVERAGE_POSITION:
diff --git a/apps/trading/components/table/table.tsx b/apps/trading/components/table/table.tsx
index 8a427de17..0d333c03a 100644
--- a/apps/trading/components/table/table.tsx
+++ b/apps/trading/components/table/table.tsx
@@ -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[];
+ 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 = (
- {columns.map(({ displayName, name, tooltip }) => (
+ {columns.map(({ displayName, name, tooltip, headerClassName }) => (
@@ -79,12 +88,17 @@ export const Table = forwardRef<
>
{!noHeader && header}
- {data.map((d, i) => (
+ {data.map((dataEntry, i) => (
{
+ if (onRowClick) {
+ onRowClick(i);
+ }
+ }}
>
{columns.map(({ name, displayName, className, testId }, j) => (
)}
- {d[name]}
+
+ {dataEntry[name]}
+
))}
diff --git a/apps/trading/e2e/tests/teams/test_teams.py b/apps/trading/e2e/tests/teams/test_teams.py
new file mode 100644
index 000000000..eb20e7be7
--- /dev/null
+++ b/apps/trading/e2e/tests/teams/test_teams.py
@@ -0,0 +1,200 @@
+import pytest
+from playwright.sync_api import expect, Page
+import vega_sim.proto.vega as vega_protos
+from vega_sim.null_service import VegaServiceNull
+from conftest import init_vega
+from actions.utils import next_epoch
+from fixtures.market import setup_continuous_market
+from conftest import auth_setup, init_page, init_vega, risk_accepted_setup
+from wallet_config import PARTY_A, PARTY_B, PARTY_C, PARTY_D, MM_WALLET
+
+
+@pytest.fixture(scope="module")
+def vega(request):
+ with init_vega(request) as vega:
+ yield vega
+
+@pytest.fixture(scope="module")
+def team_page(vega, browser, request, setup_teams_and_games):
+ with init_page(vega, browser, request) as page:
+ risk_accepted_setup(page)
+ auth_setup(vega, page)
+ team_id = setup_teams_and_games["team_id"]
+ page.goto(f"/#/competitions/teams/{team_id}")
+ yield page
+
+@pytest.fixture(scope="module")
+def setup_teams_and_games(vega: VegaServiceNull):
+ tDAI_market = setup_continuous_market(vega)
+ tDAI_asset_id = vega.find_asset_id(symbol="tDAI")
+ vega.mint(key_name=PARTY_B.name, asset=tDAI_asset_id, amount=100000)
+ vega.mint(key_name=PARTY_C.name, asset=tDAI_asset_id, amount=100000)
+ vega.mint(key_name=PARTY_A.name, asset=tDAI_asset_id, amount=100000)
+ vega.mint(key_name=PARTY_D.name, asset=tDAI_asset_id, amount=100000)
+ next_epoch(vega=vega)
+
+ tDAI_asset_id = vega.find_asset_id(symbol="tDAI")
+ vega.update_network_parameter(
+ MM_WALLET.name, parameter="reward.asset", new_value=tDAI_asset_id
+ )
+
+ next_epoch(vega=vega)
+ team_name = create_team(vega)
+
+ next_epoch(vega)
+ teams = vega.list_teams()
+
+ # list_teams actually returns a dictionary {"team_id": Team}
+ team_id = list(teams.keys())[0]
+
+ vega.apply_referral_code(PARTY_B.name, team_id)
+
+ # go to next epoch so we can check joinedAt and joinedAtEpoch appropriately
+ next_epoch(vega)
+
+ vega.apply_referral_code(PARTY_C.name, team_id)
+
+ next_epoch(vega)
+
+ vega.apply_referral_code(PARTY_D.name, team_id)
+
+ vega.wait_fn(1)
+ vega.wait_for_total_catchup()
+
+ current_epoch = vega.statistics().epoch_seq
+ game_start = current_epoch + 1
+ game_end = current_epoch + 11
+
+ current_epoch = vega.statistics().epoch_seq
+ print(f"[EPOCH: {current_epoch}] creating recurring transfer")
+ print(f"Game start: {game_start}")
+ print(f"Game game end: {game_end}")
+
+ vega.recurring_transfer(
+ from_key_name=PARTY_A.name,
+ from_account_type=vega_protos.vega.ACCOUNT_TYPE_GENERAL,
+ to_account_type=vega_protos.vega.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
+ asset=tDAI_asset_id,
+ reference="reward",
+ asset_for_metric=tDAI_asset_id,
+ metric=vega_protos.vega.DISPATCH_METRIC_MAKER_FEES_PAID,
+ entity_scope=vega_protos.vega.ENTITY_SCOPE_TEAMS,
+ n_top_performers=1,
+ amount=100,
+ factor=1.0,
+ start_epoch=game_start,
+ end_epoch=game_end,
+ window_length=10
+ )
+
+ next_epoch(vega)
+ print(f"[EPOCH: {vega.statistics().epoch_seq}] starting order activity")
+
+ # Team statistics will only return data when team has been active
+ # for DEFAULT_AGGREGATION_EPOCHS epochs
+ #
+ # https://vegaprotocol.slack.com/archives/C02KVKMAE82/p1706635625851769?thread_ts=1706631542.576449&cid=C02KVKMAE82
+
+ # Create trading activity for 10 epochs (which is the default)
+ for i in range(10):
+ vega.submit_order(
+ trading_key=PARTY_B.name,
+ market_id=tDAI_market,
+ order_type="TYPE_MARKET",
+ time_in_force="TIME_IN_FORCE_IOC",
+ side="SIDE_BUY",
+ volume=1,
+ )
+ vega.submit_order(
+ trading_key=PARTY_A.name,
+ market_id=tDAI_market,
+ order_type="TYPE_MARKET",
+ time_in_force="TIME_IN_FORCE_IOC",
+ side="SIDE_BUY",
+ volume=1,
+ )
+ next_epoch(vega)
+ print(f"[EPOCH: {vega.statistics().epoch_seq}] {i} epoch passed")
+
+ return {
+ "market_id": tDAI_market,
+ "asset_id": tDAI_asset_id,
+ "team_id": team_id,
+ "team_name": team_name,
+ }
+
+
+def create_team(vega: VegaServiceNull):
+ team_name = "Foobar"
+ vega.create_referral_set(
+ key_name=PARTY_A.name,
+ name=team_name,
+ team_url="https://vega.xyz",
+ avatar_url="http://placekitten.com/200/200",
+ closed=False,
+ )
+
+ return team_name
+
+def test_team_page_games_table(team_page: Page):
+ team_page.get_by_test_id("games-toggle").click()
+ expect(team_page.get_by_test_id("games-toggle")).to_have_text("Games (1)")
+ expect(team_page.get_by_test_id("rank-0")).to_have_text("1")
+ expect(team_page.get_by_test_id("epoch-0")).to_have_text("18")
+ expect(team_page.get_by_test_id("type-0")).to_have_text("Price maker fees paid")
+ expect(team_page.get_by_test_id("amount-0")).to_have_text("100,000,000")
+ expect(team_page.get_by_test_id("participatingTeams-0")).to_have_text(
+ "1"
+ )
+ expect(team_page.get_by_test_id("participatingMembers-0")).to_have_text(
+ "2"
+ )
+
+def test_team_page_members_table(team_page: Page):
+ team_page.get_by_test_id("members-toggle").click()
+ expect(team_page.get_by_test_id("members-toggle")).to_have_text("Members (3)")
+ expect(team_page.get_by_test_id("referee-0")).to_be_visible()
+ expect(team_page.get_by_test_id("joinedAt-0")).to_be_visible()
+ expect(team_page.get_by_test_id("joinedAtEpoch-0")).to_have_text("8")
+
+def test_team_page_headline(team_page: Page, setup_teams_and_games
+):
+ team_name = setup_teams_and_games["team_name"]
+ expect(team_page.get_by_test_id("team-name")).to_have_text(team_name)
+ expect(team_page.get_by_test_id("members-count-stat")).to_have_text("3")
+
+ expect(team_page.get_by_test_id("total-games-stat")).to_have_text(
+ "1"
+ )
+
+ # TODO this still seems wrong as its always 0
+ expect(team_page.get_by_test_id("total-volume-stat")).to_have_text(
+ "0"
+ )
+
+ expect(team_page.get_by_test_id("rewards-paid-stat")).to_have_text(
+ "100m"
+ )
+
+@pytest.fixture(scope="module")
+def competitions_page(vega, browser, request):
+ with init_page(vega, browser, request) as page:
+ risk_accepted_setup(page)
+ auth_setup(vega, page)
+ yield page
+
+def test_leaderboard(competitions_page: Page, setup_teams_and_games):
+ team_name = setup_teams_and_games["team_name"]
+ competitions_page.goto(f"/#/competitions/")
+ expect(competitions_page.get_by_test_id("rank-0").locator(".text-yellow-300")).to_have_count(1)
+ expect(competitions_page.get_by_test_id("team-0")).to_have_text(team_name)
+ expect(competitions_page.get_by_test_id("status-0")).to_have_text("Open")
+
+ expect(competitions_page.get_by_test_id("earned-0")).to_have_text("100,000,000")
+ expect(competitions_page.get_by_test_id("games-0")).to_have_text("1")
+
+ # TODO still odd that this is 0
+ expect(competitions_page.get_by_test_id("volume-0")).to_have_text("-")
+
+#TODO def test_games(competitions_page: Page):
+#TODO currently no games appear which i think is a bug
diff --git a/apps/trading/client-pages/referrals/hooks/StakeAvailable.graphql b/apps/trading/lib/hooks/StakeAvailable.graphql
similarity index 100%
rename from apps/trading/client-pages/referrals/hooks/StakeAvailable.graphql
rename to apps/trading/lib/hooks/StakeAvailable.graphql
diff --git a/apps/trading/lib/hooks/Team.graphql b/apps/trading/lib/hooks/Team.graphql
new file mode 100644
index 000000000..a1878cd11
--- /dev/null
+++ b/apps/trading/lib/hooks/Team.graphql
@@ -0,0 +1,90 @@
+fragment TeamFields on Team {
+ teamId
+ referrer
+ name
+ teamUrl
+ avatarUrl
+ createdAt
+ createdAtEpoch
+ closed
+ allowList
+}
+
+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, $aggregationEpochs: Int) {
+ teams(teamId: $teamId) {
+ edges {
+ node {
+ ...TeamFields
+ }
+ }
+ }
+ partyTeams: teams(partyId: $partyId) {
+ edges {
+ node {
+ ...TeamFields
+ }
+ }
+ }
+ teamsStatistics(teamId: $teamId, aggregationEpochs: $aggregationEpochs) {
+ edges {
+ node {
+ ...TeamStatsFields
+ }
+ }
+ }
+ teamReferees(teamId: $teamId) {
+ edges {
+ node {
+ ...TeamRefereeFields
+ }
+ }
+ }
+ games(entityScope: ENTITY_SCOPE_TEAMS) {
+ edges {
+ node {
+ ...TeamGameFields
+ }
+ }
+ }
+}
diff --git a/apps/trading/lib/hooks/Teams.graphql b/apps/trading/lib/hooks/Teams.graphql
new file mode 100644
index 000000000..be308f3bf
--- /dev/null
+++ b/apps/trading/lib/hooks/Teams.graphql
@@ -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
+ }
+ }
+ }
+}
diff --git a/apps/trading/lib/hooks/TeamsStatistics.graphql b/apps/trading/lib/hooks/TeamsStatistics.graphql
new file mode 100644
index 000000000..94808899a
--- /dev/null
+++ b/apps/trading/lib/hooks/TeamsStatistics.graphql
@@ -0,0 +1,13 @@
+query TeamsStatistics($teamId: ID, $aggregationEpochs: Int) {
+ teamsStatistics(teamId: $teamId, aggregationEpochs: $aggregationEpochs) {
+ edges {
+ node {
+ teamId
+ totalQuantumVolume
+ totalQuantumRewards
+ totalGamesPlayed
+ gamesPlayed
+ }
+ }
+ }
+}
diff --git a/apps/trading/client-pages/referrals/hooks/__generated__/StakeAvailable.ts b/apps/trading/lib/hooks/__generated__/StakeAvailable.ts
similarity index 100%
rename from apps/trading/client-pages/referrals/hooks/__generated__/StakeAvailable.ts
rename to apps/trading/lib/hooks/__generated__/StakeAvailable.ts
diff --git a/apps/trading/lib/hooks/__generated__/Team.ts b/apps/trading/lib/hooks/__generated__/Team.ts
new file mode 100644
index 000000000..1362bd8d1
--- /dev/null
+++ b/apps/trading/lib/hooks/__generated__/Team.ts
@@ -0,0 +1,154 @@
+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, allowList: Array };
+
+export type TeamStatsFieldsFragment = { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array, 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: Types.DispatchMetric, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } };
+
+export type TeamGameFieldsFragment = { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: Types.DispatchMetric, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } }> };
+
+export type TeamQueryVariables = Types.Exact<{
+ teamId: Types.Scalars['ID'];
+ partyId?: Types.InputMaybe;
+ aggregationEpochs?: Types.InputMaybe;
+}>;
+
+
+export type TeamQuery = { __typename?: 'Query', teams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean, allowList: Array } }> } | null, partyTeams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean, allowList: Array } }> } | null, teamsStatistics?: { __typename?: 'TeamsStatisticsConnection', edges: Array<{ __typename?: 'TeamStatisticsEdge', node: { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, total_quantum_rewards: string }> } }> } | null, teamReferees?: { __typename?: 'TeamRefereeConnection', edges: Array<{ __typename?: 'TeamRefereeEdge', node: { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number } }> } | null, games: { __typename?: 'GamesConnection', edges?: Array<{ __typename?: 'GameEdge', node: { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: Types.DispatchMetric, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } }> } } | null> | null } };
+
+export const TeamFieldsFragmentDoc = gql`
+ fragment TeamFields on Team {
+ teamId
+ referrer
+ name
+ teamUrl
+ avatarUrl
+ createdAt
+ createdAtEpoch
+ closed
+ allowList
+}
+ `;
+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, $aggregationEpochs: Int) {
+ teams(teamId: $teamId) {
+ edges {
+ node {
+ ...TeamFields
+ }
+ }
+ }
+ partyTeams: teams(partyId: $partyId) {
+ edges {
+ node {
+ ...TeamFields
+ }
+ }
+ }
+ teamsStatistics(teamId: $teamId, aggregationEpochs: $aggregationEpochs) {
+ 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'
+ * aggregationEpochs: // value for 'aggregationEpochs'
+ * },
+ * });
+ */
+export function useTeamQuery(baseOptions: Apollo.QueryHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useQuery(TeamDocument, options);
+ }
+export function useTeamLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useLazyQuery(TeamDocument, options);
+ }
+export type TeamQueryHookResult = ReturnType;
+export type TeamLazyQueryHookResult = ReturnType;
+export type TeamQueryResult = Apollo.QueryResult;
\ No newline at end of file
diff --git a/apps/trading/lib/hooks/__generated__/Teams.ts b/apps/trading/lib/hooks/__generated__/Teams.ts
new file mode 100644
index 000000000..08b2f1432
--- /dev/null
+++ b/apps/trading/lib/hooks/__generated__/Teams.ts
@@ -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;
+ partyId?: Types.InputMaybe;
+}>;
+
+
+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) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useQuery(TeamsDocument, options);
+ }
+export function useTeamsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useLazyQuery(TeamsDocument, options);
+ }
+export type TeamsQueryHookResult = ReturnType;
+export type TeamsLazyQueryHookResult = ReturnType;
+export type TeamsQueryResult = Apollo.QueryResult;
\ No newline at end of file
diff --git a/apps/trading/lib/hooks/__generated__/TeamsStatistics.ts b/apps/trading/lib/hooks/__generated__/TeamsStatistics.ts
new file mode 100644
index 000000000..36c126a6c
--- /dev/null
+++ b/apps/trading/lib/hooks/__generated__/TeamsStatistics.ts
@@ -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;
+ aggregationEpochs?: Types.InputMaybe;
+}>;
+
+
+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 } }> } | 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) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useQuery(TeamsStatisticsDocument, options);
+ }
+export function useTeamsStatisticsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useLazyQuery(TeamsStatisticsDocument, options);
+ }
+export type TeamsStatisticsQueryHookResult = ReturnType;
+export type TeamsStatisticsLazyQueryHookResult = ReturnType;
+export type TeamsStatisticsQueryResult = Apollo.QueryResult;
\ No newline at end of file
diff --git a/apps/trading/lib/hooks/use-games.ts b/apps/trading/lib/hooks/use-games.ts
new file mode 100644
index 000000000..71058812b
--- /dev/null
+++ b/apps/trading/lib/hooks/use-games.ts
@@ -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,
+ },
+ fetchPolicy: 'cache-and-network',
+ });
+
+ const games = compact(data?.transfersConnection?.edges?.map((n) => n?.node))
+ .map((n) => n as TransferNode)
+ .filter((node) => {
+ const active = onlyActive ? isActiveReward(node, currentEpoch) : true;
+ return active && isScopedToTeams(node);
+ });
+
+ return {
+ data: games,
+ loading,
+ error,
+ };
+};
diff --git a/apps/trading/lib/hooks/use-page-title.ts b/apps/trading/lib/hooks/use-page-title.ts
new file mode 100644
index 000000000..377c1a54a
--- /dev/null
+++ b/apps/trading/lib/hooks/use-page-title.ts
@@ -0,0 +1,18 @@
+import { useEffect, useMemo } from 'react';
+import { titlefy } from '@vegaprotocol/utils';
+import { usePageTitleStore } from '../../stores';
+
+export const usePageTitle = (title: string | string[]) => {
+ const { updateTitle } = usePageTitleStore((store) => ({
+ updateTitle: store.updateTitle,
+ }));
+
+ const memotitle = useMemo(
+ () => titlefy(Array.isArray(title) ? title : [title]),
+ [title]
+ );
+
+ useEffect(() => {
+ updateTitle(memotitle);
+ }, [updateTitle, memotitle]);
+};
diff --git a/apps/trading/lib/hooks/use-referral-set-transaction.ts b/apps/trading/lib/hooks/use-referral-set-transaction.ts
new file mode 100644
index 000000000..9d65267cd
--- /dev/null
+++ b/apps/trading/lib/hooks/use-referral-set-transaction.ts
@@ -0,0 +1,33 @@
+import {
+ useSimpleTransaction,
+ type Options,
+ type CreateReferralSet,
+ type UpdateReferralSet,
+} from '@vegaprotocol/wallet';
+import { useStakeAvailable } from './use-stake-available';
+
+/**
+ * Manages state for creating a referral set or team
+ */
+export const useReferralSetTransaction = (opts?: Options) => {
+ const { stakeAvailable, requiredStake, isEligible } = useStakeAvailable();
+
+ const { status, result, error, send } = useSimpleTransaction({
+ onSuccess: opts?.onSuccess,
+ onError: opts?.onError,
+ });
+
+ const onSubmit = (tx: CreateReferralSet | UpdateReferralSet) => {
+ send(tx);
+ };
+
+ return {
+ err: error ? error : null,
+ code: result ? result.id : null,
+ status,
+ stakeAvailable,
+ requiredStake,
+ onSubmit,
+ isEligible,
+ };
+};
diff --git a/apps/trading/client-pages/referrals/hooks/use-stake-available.ts b/apps/trading/lib/hooks/use-stake-available.ts
similarity index 100%
rename from apps/trading/client-pages/referrals/hooks/use-stake-available.ts
rename to apps/trading/lib/hooks/use-stake-available.ts
diff --git a/apps/trading/lib/hooks/use-team.ts b/apps/trading/lib/hooks/use-team.ts
new file mode 100644
index 000000000..f7526dc8a
--- /dev/null
+++ b/apps/trading/lib/hooks/use-team.ts
@@ -0,0 +1,73 @@
+import compact from 'lodash/compact';
+import orderBy from 'lodash/orderBy';
+import {
+ useTeamQuery,
+ type TeamFieldsFragment,
+ type TeamStatsFieldsFragment,
+ type TeamRefereeFieldsFragment,
+ type TeamEntityFragment,
+} from './__generated__/Team';
+import { DEFAULT_AGGREGATION_EPOCHS } from './use-teams';
+
+export type Team = TeamFieldsFragment;
+export type TeamStats = TeamStatsFieldsFragment;
+export type Member = TeamRefereeFieldsFragment;
+export type TeamEntity = TeamEntityFragment;
+export type TeamGame = ReturnType['games'][number];
+
+export const useTeam = (teamId?: string, partyId?: string) => {
+ const { data, loading, error, refetch } = useTeamQuery({
+ variables: {
+ teamId: teamId || '',
+ partyId,
+ aggregationEpochs: DEFAULT_AGGREGATION_EPOCHS,
+ },
+ skip: !teamId,
+ fetchPolicy: 'cache-and-network',
+ });
+
+ const teamEdge = data?.teams?.edges.find((e) => e.node.teamId === teamId);
+ const partyTeam = data?.partyTeams?.edges?.length
+ ? data.partyTeams.edges[0].node
+ : undefined;
+
+ const teamStatsEdge = data?.teamsStatistics?.edges.find(
+ (e) => e.node.teamId === teamId
+ );
+ const members = data?.teamReferees?.edges
+ .filter((e) => e.node.teamId === teamId)
+ .map((e) => e.node);
+
+ // Find games where the current team participated in
+ const gamesWithTeam = compact(data?.games.edges).map((edge) => {
+ const team = edge.node.entities.find((e) => {
+ if (e.__typename !== 'TeamGameEntity') return false;
+ if (e.team.teamId !== teamId) return false;
+ return true;
+ });
+
+ if (!team) return null;
+
+ return {
+ id: edge.node.id,
+ epoch: edge.node.epoch,
+ numberOfParticipants: edge.node.numberOfParticipants,
+ entities: edge.node.entities,
+ team: team as TeamEntity, // TS can't infer that all the game entities are teams
+ };
+ });
+
+ const games = orderBy(compact(gamesWithTeam), 'epoch', 'desc');
+
+ return {
+ data,
+ loading,
+ error,
+ refetch,
+ stats: teamStatsEdge?.node,
+ team: teamEdge?.node,
+ members,
+ games,
+ partyTeam,
+ };
+};
diff --git a/apps/trading/lib/hooks/use-teams.tsx b/apps/trading/lib/hooks/use-teams.tsx
new file mode 100644
index 000000000..9f13df3b0
--- /dev/null
+++ b/apps/trading/lib/hooks/use-teams.tsx
@@ -0,0 +1,72 @@
+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['edges']>['node'] &
+ ArrayElement<
+ NonNullable['edges']
+ >['node'],
+ '__typename'
+>;
+
+type UseTeamsArgs = {
+ aggregationEpochs?: number;
+ sortByField?: SortableField[];
+ order?: 'asc' | 'desc';
+};
+
+export const DEFAULT_AGGREGATION_EPOCHS = 10;
+
+export const useTeams = ({
+ aggregationEpochs = DEFAULT_AGGREGATION_EPOCHS,
+ sortByField = ['createdAtEpoch'],
+ order = 'asc',
+}: UseTeamsArgs) => {
+ const {
+ data: teamsData,
+ loading: teamsLoading,
+ error: teamsError,
+ } = useTeamsQuery({
+ fetchPolicy: 'cache-and-network',
+ });
+
+ const {
+ data: statsData,
+ loading: statsLoading,
+ error: statsError,
+ } = useTeamsStatisticsQuery({
+ variables: {
+ aggregationEpochs,
+ },
+ fetchPolicy: 'cache-and-network',
+ });
+
+ const teams = compact(teamsData?.teams?.edges).map((e) => e.node);
+ const stats = compact(statsData?.teamsStatistics?.edges).map((e) => e.node);
+
+ const data = useMemo(() => {
+ const data = teams.map((t) => ({
+ ...t,
+ ...stats.find((s) => s.teamId === t.teamId),
+ }));
+
+ const sorted = sortBy(data, sortByField);
+ if (order === 'desc') {
+ return sorted.reverse();
+ }
+ return sorted;
+ }, [teams, sortByField, order, stats]);
+
+ return {
+ data,
+ loading: teamsLoading && statsLoading,
+ error: teamsError || statsError,
+ };
+};
diff --git a/apps/trading/lib/links.ts b/apps/trading/lib/links.ts
index 16f05519e..1939d3b28 100644
--- a/apps/trading/lib/links.ts
+++ b/apps/trading/lib/links.ts
@@ -16,7 +16,12 @@ export const Routes = {
REFERRALS: '/referrals',
REFERRALS_APPLY_CODE: '/referrals/apply-code',
REFERRALS_CREATE_CODE: '/referrals/create-code',
- TEAMS: '/teams',
+ COMPETITIONS: '/competitions',
+ COMPETITIONS_TEAMS: '/competitions/teams',
+ COMPETITIONS_TEAM: '/competitions/teams/:teamId',
+ COMPETITIONS_CREATE_TEAM: '/competitions/teams/create',
+ COMPETITIONS_CREATE_TEAM_SOLO: '/competitions/teams/create?solo=true',
+ COMPETITIONS_UPDATE_TEAM: '/competitions/teams/:teamId/update',
FEES: '/fees',
REWARDS: '/rewards',
} as const;
@@ -41,7 +46,14 @@ export const Links: ConsoleLinks = {
REFERRALS: () => Routes.REFERRALS,
REFERRALS_APPLY_CODE: () => Routes.REFERRALS_APPLY_CODE,
REFERRALS_CREATE_CODE: () => Routes.REFERRALS_CREATE_CODE,
- TEAMS: () => Routes.TEAMS,
+ COMPETITIONS: () => Routes.COMPETITIONS,
+ COMPETITIONS_TEAMS: () => Routes.COMPETITIONS_TEAMS,
+ COMPETITIONS_TEAM: (teamId: string) =>
+ Routes.COMPETITIONS_TEAM.replace(':teamId', teamId),
+ COMPETITIONS_CREATE_TEAM: () => Routes.COMPETITIONS_CREATE_TEAM,
+ COMPETITIONS_CREATE_TEAM_SOLO: () => Routes.COMPETITIONS_CREATE_TEAM_SOLO,
+ COMPETITIONS_UPDATE_TEAM: (teamId: string) =>
+ Routes.COMPETITIONS_UPDATE_TEAM.replace(':teamId', teamId),
FEES: () => Routes.FEES,
REWARDS: () => Routes.REWARDS,
};
diff --git a/apps/trading/pages/client-router.tsx b/apps/trading/pages/client-router.tsx
index d01d9f5aa..832e9a7cc 100644
--- a/apps/trading/pages/client-router.tsx
+++ b/apps/trading/pages/client-router.tsx
@@ -2,8 +2,8 @@ import type { RouteObject } from 'react-router-dom';
import { Navigate, useRoutes } from 'react-router-dom';
import { lazy, Suspense } from 'react';
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
-import { LayoutWithSidebar } from '../components/layouts';
-import { LayoutCentered } from '../components/layouts/layout-centered';
+import { LayoutWithSidebar, LayoutCentered } from '../components/layouts';
+import { LayoutWithSky } from '../components/layouts-inner';
import { Home } from '../client-pages/home';
import { Liquidity } from '../client-pages/liquidity';
import { MarketsPage } from '../client-pages/markets';
@@ -14,9 +14,7 @@ import { Withdraw } from '../client-pages/withdraw';
import { Transfer } from '../client-pages/transfer';
import { Fees } from '../client-pages/fees';
import { Rewards } from '../client-pages/rewards';
-import { Teams } from '../client-pages/teams';
import { Routes as AppRoutes } from '../lib/links';
-import { LayoutWithSky } from '../client-pages/referrals/layout';
import { Referrals } from '../client-pages/referrals/referrals';
import { ReferralStatistics } from '../client-pages/referrals/referral-statistics';
import { ApplyCodeFormContainer } from '../client-pages/referrals/apply-code-form';
@@ -30,6 +28,11 @@ import { PortfolioSidebar } from '../client-pages/portfolio/portfolio-sidebar';
import { LiquiditySidebar } from '../client-pages/liquidity/liquidity-sidebar';
import { MarketsSidebar } from '../client-pages/markets/markets-sidebar';
import { useT } from '../lib/use-t';
+import { CompetitionsHome } from '../client-pages/competitions/competitions-home';
+import { CompetitionsTeams } from '../client-pages/competitions/competitions-teams';
+import { CompetitionsTeam } from '../client-pages/competitions/competitions-team';
+import { CompetitionsCreateTeam } from '../client-pages/competitions/competitions-create-team';
+import { CompetitionsUpdateTeam } from '../client-pages/competitions/competitions-update-team';
// These must remain dynamically imported as pennant cannot be compiled by nextjs due to ESM
// Using dynamic imports is a workaround for this until pennant is published as ESM
@@ -47,7 +50,7 @@ const NotFound = () => {
export const useRouterConfig = (): RouteObject[] => {
const featureFlags = useFeatureFlags((state) => state.flags);
- return compact([
+ const routeConfig = compact([
{
index: true,
element: ,
@@ -95,8 +98,34 @@ export const useRouterConfig = (): RouteObject[] => {
: undefined,
featureFlags.TEAM_COMPETITION
? {
- path: AppRoutes.TEAMS,
- element: ,
+ path: AppRoutes.COMPETITIONS,
+ element: } />,
+ children: [
+ // pages with planets and stars
+ {
+ element: ,
+ children: [
+ { index: true, element: },
+ {
+ path: AppRoutes.COMPETITIONS_TEAMS,
+ element: ,
+ },
+ ],
+ },
+ // pages with blurred background
+ {
+ path: AppRoutes.COMPETITIONS_TEAM,
+ element: ,
+ },
+ {
+ path: AppRoutes.COMPETITIONS_CREATE_TEAM,
+ element: ,
+ },
+ {
+ path: AppRoutes.COMPETITIONS_UPDATE_TEAM,
+ element: ,
+ },
+ ],
}
: undefined,
{
@@ -189,6 +218,8 @@ export const useRouterConfig = (): RouteObject[] => {
element: ,
},
]);
+
+ return routeConfig;
};
export const ClientRouter = () => {
diff --git a/apps/trading/public/cover.png b/apps/trading/public/cover.png
new file mode 100644
index 000000000..8fe83c013
Binary files /dev/null and b/apps/trading/public/cover.png differ
diff --git a/apps/trading/public/team-avatars/01.png b/apps/trading/public/team-avatars/01.png
new file mode 100644
index 000000000..570bba024
Binary files /dev/null and b/apps/trading/public/team-avatars/01.png differ
diff --git a/apps/trading/public/team-avatars/02.png b/apps/trading/public/team-avatars/02.png
new file mode 100644
index 000000000..29ab7bb3f
Binary files /dev/null and b/apps/trading/public/team-avatars/02.png differ
diff --git a/apps/trading/public/team-avatars/03.png b/apps/trading/public/team-avatars/03.png
new file mode 100644
index 000000000..f3aa9d68c
Binary files /dev/null and b/apps/trading/public/team-avatars/03.png differ
diff --git a/apps/trading/public/team-avatars/04.png b/apps/trading/public/team-avatars/04.png
new file mode 100644
index 000000000..55f597506
Binary files /dev/null and b/apps/trading/public/team-avatars/04.png differ
diff --git a/apps/trading/public/team-avatars/05.png b/apps/trading/public/team-avatars/05.png
new file mode 100644
index 000000000..f1353d6a1
Binary files /dev/null and b/apps/trading/public/team-avatars/05.png differ
diff --git a/apps/trading/public/team-avatars/06.png b/apps/trading/public/team-avatars/06.png
new file mode 100644
index 000000000..617842f19
Binary files /dev/null and b/apps/trading/public/team-avatars/06.png differ
diff --git a/apps/trading/public/team-avatars/07.png b/apps/trading/public/team-avatars/07.png
new file mode 100644
index 000000000..a58395da0
Binary files /dev/null and b/apps/trading/public/team-avatars/07.png differ
diff --git a/apps/trading/public/team-avatars/08.png b/apps/trading/public/team-avatars/08.png
new file mode 100644
index 000000000..ae850dcfc
Binary files /dev/null and b/apps/trading/public/team-avatars/08.png differ
diff --git a/apps/trading/public/team-avatars/09.png b/apps/trading/public/team-avatars/09.png
new file mode 100644
index 000000000..b42a21165
Binary files /dev/null and b/apps/trading/public/team-avatars/09.png differ
diff --git a/apps/trading/public/team-avatars/10.png b/apps/trading/public/team-avatars/10.png
new file mode 100644
index 000000000..5057c482b
Binary files /dev/null and b/apps/trading/public/team-avatars/10.png differ
diff --git a/apps/trading/public/team-avatars/11.png b/apps/trading/public/team-avatars/11.png
new file mode 100644
index 000000000..3eec8dbd1
Binary files /dev/null and b/apps/trading/public/team-avatars/11.png differ
diff --git a/apps/trading/public/team-avatars/12.png b/apps/trading/public/team-avatars/12.png
new file mode 100644
index 000000000..ffbd63886
Binary files /dev/null and b/apps/trading/public/team-avatars/12.png differ
diff --git a/apps/trading/public/team-avatars/13.png b/apps/trading/public/team-avatars/13.png
new file mode 100644
index 000000000..50f847462
Binary files /dev/null and b/apps/trading/public/team-avatars/13.png differ
diff --git a/apps/trading/public/team-avatars/14.png b/apps/trading/public/team-avatars/14.png
new file mode 100644
index 000000000..83de7508b
Binary files /dev/null and b/apps/trading/public/team-avatars/14.png differ
diff --git a/apps/trading/public/team-avatars/15.png b/apps/trading/public/team-avatars/15.png
new file mode 100644
index 000000000..3d38fc0b8
Binary files /dev/null and b/apps/trading/public/team-avatars/15.png differ
diff --git a/apps/trading/public/team-avatars/16.png b/apps/trading/public/team-avatars/16.png
new file mode 100644
index 000000000..4a32ea0f5
Binary files /dev/null and b/apps/trading/public/team-avatars/16.png differ
diff --git a/apps/trading/public/team-avatars/17.png b/apps/trading/public/team-avatars/17.png
new file mode 100644
index 000000000..526595a7d
Binary files /dev/null and b/apps/trading/public/team-avatars/17.png differ
diff --git a/apps/trading/public/team-avatars/18.png b/apps/trading/public/team-avatars/18.png
new file mode 100644
index 000000000..1c2cfefb3
Binary files /dev/null and b/apps/trading/public/team-avatars/18.png differ
diff --git a/apps/trading/public/team-avatars/19.png b/apps/trading/public/team-avatars/19.png
new file mode 100644
index 000000000..b449526a6
Binary files /dev/null and b/apps/trading/public/team-avatars/19.png differ
diff --git a/apps/trading/public/team-avatars/20.png b/apps/trading/public/team-avatars/20.png
new file mode 100644
index 000000000..6a893fb64
Binary files /dev/null and b/apps/trading/public/team-avatars/20.png differ
diff --git a/libs/environment/src/hooks/use-links.ts b/libs/environment/src/hooks/use-links.ts
index 5789dc1d8..bd85af8ad 100644
--- a/libs/environment/src/hooks/use-links.ts
+++ b/libs/environment/src/hooks/use-links.ts
@@ -162,6 +162,7 @@ export const useProtocolUpgradeProposalLink = () => {
export const EXPLORER_TX = '/txs/:hash';
export const EXPLORER_ORACLE = '/oracles/:id';
export const EXPLORER_MARKET = '/markets/:id';
+export const EXPLORER_PARTIES = '/parties/:id';
// Etherscan pages
export const ETHERSCAN_ADDRESS = '/address/:hash';
diff --git a/libs/i18n/src/locales/en/trading.json b/libs/i18n/src/locales/en/trading.json
index 887212695..0a7c7c726 100644
--- a/libs/i18n/src/locales/en/trading.json
+++ b/libs/i18n/src/locales/en/trading.json
@@ -7,24 +7,31 @@
"<0>No running Desktop App/CLI detected. Open your app now to connect or enter a0> <1>custom wallet location1>": "<0>No running Desktop App/CLI detected. Open your app now to connect or enter a0> <1>custom wallet location1>",
"A percentage of commission earned by the referrer": "A percentage of commission earned by the referrer",
"A successor to this market has been proposed": "A successor to this market has been proposed",
+ "As a team creator, you cannot switch teams": "As a team creator, you cannot switch teams",
"About the referral program": "About the referral program",
"Active": "Active",
"Activity Streak": "Activity Streak",
"All": "All",
+ "All teams": "All teams",
+ "All teams are eligible": "All teams are eligible",
+ "Amount earned": "Amount earned",
"An unknown error occurred.": "An unknown error occurred.",
"Anonymous": "Anonymous",
"Anyone with the referral link can apply it to their key(s) of choice via an on chain transaction": "Anyone with the referral link can apply it to their key(s) of choice via an on chain transaction",
"Assessed over": "Assessed over",
+ "Are you sure you want to join team: {{team}}": "Are you sure you want to join team: {{team}}",
"Asset (1)": "Asset (1)",
"Assets": "Assets",
"Available to withdraw this epoch": "Available to withdraw this epoch",
"Average position": "Average position",
+ "Avatar URL": "Avatar URL",
"Base commission rate": "Base commission rate",
"Base rate": "Base rate",
"Best bid": "Best bid",
"Best offer": "Best offer",
"Browse": "Browse",
"By using the Vega Console, you acknowledge that you have read and understood the <0>Vega Console Disclaimer0>": "By using the Vega Console, you acknowledge that you have read and understood the <0>Vega Console Disclaimer0>",
+ "Cancel": "Cancel",
"Change (24h)": "Change (24h)",
"Changes have been proposed for this market. <0>View proposals0>": "Changes have been proposed for this market. <0>View proposals0>",
"Chart": "Chart",
@@ -37,9 +44,12 @@
"Code must be be valid hex": "Code must be be valid hex",
"Collateral": "Collateral",
"Conduct your own due diligence and consult your financial advisor before making any investment decisions.": "Conduct your own due diligence and consult your financial advisor before making any investment decisions.",
+ "Confrim": "Confrim",
"Confirm in wallet...": "Confirm in wallet...",
+ "Confirming transaction...": "Confirming transaction...",
"Connect": "Connect",
"Connect wallet": "Connect wallet",
+ "Connect your wallet to join the team": "Connect your wallet to join the team",
"Connected node": "Connected node",
"Console": "Console",
"Continue sharing data": "Continue sharing data",
@@ -50,6 +60,10 @@
"Could not initialize app": "Could not initialize app",
"Countdown": "Countdown",
"Create a referral code": "Create a referral code",
+ "Create": "Create",
+ "Create a team": "Create a team",
+ "Create a simple referral code to enjoy the referrer commission outlined in the current referral program": "Create a simple referral code to enjoy the referrer commission outlined in the current referral program",
+ "Make your referral code a Team to compete in Competitions with your friends, appear in leaderboards on the <0>Competitions Homepage0>, and earn rewards": "Make your referral code a Team to compete in Competitions with your friends, appear in leaderboards on the <0>Competitions Homepage0>, and earn rewards",
"Current tier": "Current tier",
"DISCLAIMER_P1": "Vega is a decentralised peer-to-peer protocol that can be used to trade derivatives with cryptoassets. The Vega Protocol is an implementation layer (layer one) protocol made of free, public, open-source or source-available software. Use of the Vega Protocol involves various risks, including but not limited to, losses while digital assets are supplied to the Vega Protocol and losses due to the fluctuation of prices of assets.",
"DISCLAIMER_P2": "Before using the Vega Protocol, review the relevant documentation at docs.vega.xyz to make sure that you understand how it works. Conduct your own due diligence and consult your financial advisor before making any investment decisions.",
@@ -73,10 +87,14 @@
"Docs": "Docs",
"Earn commission & stake rewards": "Earn commission & stake rewards",
"Earned by me": "Earned by me",
+ "Eligible teams": "Eligible teams",
"Enactment date reached and usual auction exit checks pass": "Enactment date reached and usual auction exit checks pass",
"Ends in": "Ends in",
"Entity scope": "Entity scope",
+ "{{entity}} scope": "{{entity}} scope",
"Environment not configured": "Environment not configured",
+ "Epoch": "Epoch",
+ "epochs in referral set": "epochs in referral set",
"Epochs in set": "Epochs in set",
"Epochs to next tier": "Epochs to next tier",
"Expected in {{distance}}": "Expected in {{distance}}",
@@ -84,6 +102,7 @@
"Experiment for free with virtual assets on <0>Fairground Testnet0>": "Experiment for free with virtual assets on <0>Fairground Testnet0>",
"Expiry": "Expiry",
"Explore": "Explore",
+ "Favorite game": "Favorite game",
"Fees": "Fees",
"Fees paid": "Fees paid",
"Fees work like a CEX with no per-transaction gas for orders": "Fees work like a CEX with no per-transaction gas for orders",
@@ -100,12 +119,14 @@
"Funding payments": "Funding payments",
"Funding rate": "Funding rate",
"Futures": "Futures",
+ "Games ({{count}})": "Games ({{count}})",
"Generate a referral code to share with your friends and start earning commission.": "Generate a referral code to share with your friends and start earning commission.",
"Generate code": "Generate code",
- "Get rewards for providing liquidity. Get rewards for providing liquidity.": "Get rewards for providing liquidity. Get rewards for providing liquidity.",
+ "Get rewards for providing liquidity.": "Get rewards for providing liquidity.",
"Get started": "Get started",
"Give Feedback": "Give Feedback",
"Go back and try again": "Go back and try again",
+ "Got to competitions": "Go to competitions",
"Governance": "Governance",
"Governance vote for this market has been rejected": "Governance vote for this market has been rejected",
"Governance vote for this market is valid and has been accepted": "Governance vote for this market is valid and has been accepted",
@@ -127,10 +148,18 @@
"Inactive": "Inactive",
"Index Price": "Index Price",
"Indicators": "Indicators",
+ "Invalid image URL": "Invalid image URL",
+ "Invalid URL": "Invalid URL",
"Individual": "Individual",
"Infrastructure": "Infrastructure",
"Interval: {{interval}}": "Interval: {{interval}}",
"Invite friends and earn rewards from the trading fees they pay. Stake those rewards to earn multipliers on future rewards.": "Invite friends and earn rewards from the trading fees they pay. Stake those rewards to earn multipliers on future rewards.",
+ "Join team": "Join team",
+ "Joined": "Joined",
+ "Joined at": "Joined at",
+ "Joined epoch": "Joined epoch",
+ "gameCount_one": "Last game result",
+ "gameCount_other": "Last {{count}} game results",
"Learn about providing liquidity": "Learn about providing liquidity",
"Learn more": "Learn more",
"Ledger entries": "Ledger entries",
@@ -141,6 +170,7 @@
"Low fees and no cost to place orders": "Low fees and no cost to place orders",
"Mainnet status & incidents": "Mainnet status & incidents",
"Make withdrawal": "Make withdrawal",
+ "Make team private": "Make team private",
"Maker": "Maker",
"Mark Price": "Mark Price",
"Mark price": "Mark price",
@@ -149,6 +179,8 @@
"Market specification": "Market specification",
"Market triggers cancellation or governance vote has passed to cancel": "Market triggers cancellation or governance vote has passed to cancel",
"Markets": "Markets",
+ "Members": "Members",
+ "Members ({{count}})": "Members ({{count}})",
"Menu": "Menu",
"Metamask Snap <0>quick start0>": "Metamask Snap <0>quick start0>",
"Min. epochs": "Min. epochs",
@@ -157,16 +189,18 @@
"My liquidity provision": "My liquidity provision",
"My trading fees": "My trading fees",
"Name": "Name",
- "No MetaMask version that supports snaps detected. Learn more about <0>MetaMask Snaps0>": "No MetaMask version that supports snaps detected. Learn more about <0>MetaMask Snaps0>",
"No closed orders": "No closed orders",
"No data": "No data",
"No deposits": "No deposits",
"No funding history data": "No funding history data",
"No future markets.": "No future markets.",
+ "No games": "No games",
"No ledger entries to export": "No ledger entries to export",
"No market": "No market",
"No markets": "No markets",
"No markets.": "No markets.",
+ "No members": "No members",
+ "No MetaMask version that supports snaps detected. Learn more about <0>MetaMask Snaps0>": "No MetaMask version that supports snaps detected. Learn more about <0>MetaMask Snaps0>",
"No open orders": "No open orders",
"No orders": "No orders",
"No party accepts any liability for any losses whatsoever.": "No party accepts any liability for any losses whatsoever.",
@@ -179,6 +213,8 @@
"No third party has access to your funds.": "No third party has access to your funds.",
"No volume discount program active": "No volume discount program active",
"No withdrawals": "No withdrawals",
+ "No. of participating members": "No. of participating members",
+ "No. of participating teams": "No. of participating teams",
"Node: {{VEGA_URL}} is unsuitable": "Node: {{VEGA_URL}} is unsuitable",
"Non-custodial and pseudonymous": "Non-custodial and pseudonymous",
"None": "None",
@@ -192,11 +228,16 @@
"Order": "Order",
"Orderbook": "Orderbook",
"Orders": "Orders",
+ "Owner": "Owner",
"PRNT": "PRNT",
"Page not found": "Page not found",
"Parent of a market": "Parent of a market",
"Pennant": "Pennant",
"Perpetuals": "Perpetuals",
+ "place_ordinal_one": "{{count}}st",
+ "place_ordinal_two": "{{count}}nd",
+ "place_ordinal_few": "{{count}}rd",
+ "place_ordinal_other": "{{count}}th",
"Please choose another market from the <0>market list0>": "Please choose another market from the <0>market list0>",
"Please connect Vega wallet": "Please connect Vega wallet",
"Portfolio": "Portfolio",
@@ -206,12 +247,19 @@
"Propose a new market": "Propose a new market",
"Proposed final price is {{price}} {{assetSymbol}}.": "Proposed final price is {{price}} {{assetSymbol}}.",
"Proposed markets": "Proposed markets",
+ "Provide a link so users can learn more about your team": "Provide a link so users can learn more about your team",
+ "Provide a URL to a hosted image": "Provide a URL to a hosted image",
"Providing liquidity": "Providing liquidity",
+ "Public key allow list": "Public key allow list",
"Purpose built proof of stake blockchain": "Purpose built proof of stake blockchain",
+ "qUSD": "qUSD",
+ "qUSD provides a rough USD equivalent of balances across all assets using the value of \"Quantum\" for that asset": "qUSD provides a rough USD equivalent of balances across all assets using the value of \"Quantum\" for that asset",
+ "Rank": "Rank",
"Read the terms": "Read the terms",
"Ready to trade": "Ready to trade",
"Ready to trade with real funds? <0>Switch to Mainnet0>": "Ready to trade with real funds? <0>Switch to Mainnet0>",
"Redeem rewards": "Redeem rewards",
+ "Referee": "Referee",
"Referral benefits": "Referral benefits",
"Referral discount": "Referral discount",
"Referrals": "Referrals",
@@ -220,6 +268,7 @@
"Referrers earn commission based on a percentage of the taker fees their referees pay": "Referrers earn commission based on a percentage of the taker fees their referees pay",
"Referrers generate a code assigned to their key via an on chain transaction": "Referrers generate a code assigned to their key via an on chain transaction",
"Rejected": "Rejected",
+ "Required": "Required",
"Required epochs": "Required epochs",
"Required for next tier": "Required for next tier",
"Reset Columns": "Reset Columns",
@@ -260,8 +309,15 @@
"Successors to this market have been proposed": "Successors to this market have been proposed",
"Supplied stake": "Supplied stake",
"Suspended due to price or liquidity monitoring trigger": "Suspended due to price or liquidity monitoring trigger",
+ "Switch team": "Switch team",
+ "Switching team will move you from '{{fromTeam}}' to '{{toTeam}}' at the end of the epoch. Are you sure?": "Switching team will move you from '{{fromTeam}}' to '{{toTeam}}' at the end of the epoch. Are you sure?",
"Target stake": "Target stake",
"Team": "Team",
+ "Team name": "Team name",
+ "Team creation transaction successful": "Team creation transaction successful",
+ "Team joined": "Team joined",
+ "Team switch successful. You will switch team at the end of the epoch.": "Team switch successful. You will switch team at the end of the epoch.",
+
"The amount of fees paid to liquidity providers across the whole market during the last epoch {{epoch}}.": "The amount of fees paid to liquidity providers across the whole market during the last epoch {{epoch}}.",
"The commission is taken from the infrastructure fee, maker fee, and liquidity provider fee, not from the referee": "The commission is taken from the infrastructure fee, maker fee, and liquidity provider fee, not from the referee",
"The external time weighted average price (TWAP) received from the data source defined in the data sourcing specification.": "The external time weighted average price (TWAP) received from the data source defined in the data sourcing specification.",
@@ -283,10 +339,17 @@
"Tier {{userTier}}": "Tier {{userTier}}",
"To protect the network from spam, you must have at least {{requiredFunds}} qUSD of any asset on the network to proceed.": "To protect the network from spam, you must have at least {{requiredFunds}} qUSD of any asset on the network to proceed.",
"Toast location": "Toast location",
+ "Total amount of rewards paid out to this team in qUSD": "Total amount of rewards paid out to this team in qUSD",
"Total discount": "Total discount",
"Total distributed": "Total distributed",
"Total fee after discount": "Total fee after discount",
"Total fee before discount": "Total fee before discount",
+ "Total games": "Total games",
+ "Total number of games this team has participated in": "Total number of games this team has participated in",
+ "Total volume": "Total volume",
+ "totalCommission": "Total commission (<0>last {{count}} epochs0>)",
+ "totalCommission_one": "Total commission (<0>last {{count}} epoch0>)",
+ "totalCommission_other": "Total commission (<0>last {{count}} epochs0>)",
"Trader": "Trader",
"Trades": "Trades",
"Trading": "Trading",
@@ -297,11 +360,14 @@
"Trading on market {{name}} will stop on {{date}}": "Trading on market {{name}} will stop on {{date}}",
"TradingView": "TradingView",
"Transfer": "Transfer",
+ "Type": "Type",
"Unknown": "Unknown",
"Unknown settlement date": "Unknown settlement date",
+ "URL": "URL",
+ "Use a comma separated list to allow only specific public keys to join the team": "Use a comma separated list to allow only specific public keys to join the team",
+ "Vega chart": "Vega chart",
"Vega Reward pot": "Vega Reward pot",
"Vega Wallet <0>full featured<0>": "Vega Wallet <0>full featured<0>",
- "Vega chart": "Vega chart",
"Vesting": "Vesting",
"Vesting multiplier": "Vesting multiplier",
"Vesting {{assetSymbol}}": "Vesting {{assetSymbol}}",
@@ -314,6 +380,7 @@
"View proposals": "View proposals",
"View settlement asset details": "View settlement asset details",
"View successor market": "View successor market",
+ "View team": "View team",
"Volume": "Volume",
"Volume (24h)": "Volume (24h)",
"Volume discount": "Volume discount",
@@ -337,7 +404,6 @@
"checkOutProposalsAndVote_one": "Check out the terms of the proposal and vote:",
"checkOutProposalsAndVote_other": "Check out the terms of the proposals and vote:",
"epochStreak_one": "{{count}} epoch streak",
- "epochs in referral set": "epochs in referral set",
"epochsStreak": "{{count}} epochs streak",
"minTradingVolume": "Min. trading volume (last {{count}} epochs)",
"minTradingVolume_one": "Min. trading volume (last {{count}} epoch)",
@@ -347,22 +413,8 @@
"myVolume_other": "My volume (last {{count}} epochs)",
"numberEpochs": "{{count}} epochs",
"numberEpochs_one": "{{count}} epoch",
- "numberEpochs_other": "{{count}} epochs",
- "pastEpochs": "Past {{count}} epochs",
- "pastEpochs_one": "Past {{count}} epoch",
- "pastEpochs_other": "Past {{count}} epochs",
- "qUSD": "qUSD",
- "qUSD provides a rough USD equivalent of balances across all assets using the value of \"Quantum\" for that asset": "qUSD provides a rough USD equivalent of balances across all assets using the value of \"Quantum\" for that asset",
- "referralStatisticsCommission": "Commission earned in <0>qUSD0> (<1>last {{count}} epochs1>)",
- "referralStatisticsCommission_one": "Commission earned in <0>qUSD0> (<1>last {{count}} epoch1>)",
- "referralStatisticsCommission_other": "Commission earned in <0>qUSD0> (<1>last {{count}} epochs1>)",
- "runningNotionalOverEpochs": "Combined running notional over the {{count}} epochs",
- "runningNotionalOverEpochs_one": "Combined running notional over the {{count}} epoch",
- "runningNotionalOverEpochs_other": "Combined running notional over the {{count}} epochs",
- "to": "to",
- "totalCommission": "Total commission (<0>last {{count}} epochs0>)",
- "totalCommission_one": "Total commission (<0>last {{count}} epoch0>)",
- "totalCommission_other": "Total commission (<0>last {{count}} epochs0>)",
+ "Rewards paid out": "Rewards paid out",
+ "{{reward}}x": "{{reward}}x",
"userActive": "{{active}} trader: {{count}} epochs so far",
"userInactive": "{{active}} trader: {{count}} epochs so far, you will lose your streak in {{remaining}} epochs!",
"volumeLastEpochs": "Volume (last {{count}} epochs)",
@@ -375,6 +427,5 @@
"{{assetSymbol}} Reward pot": "{{assetSymbol}} Reward pot",
"{{checkedAssets}} Assets": "{{checkedAssets}} Assets",
"{{distance}} ago": "{{distance}} ago",
- "{{instrumentCode}} liquidity provision": "{{instrumentCode}} liquidity provision",
- "{{reward}}x": "{{reward}}x"
+ "{{instrumentCode}} liquidity provision": "{{instrumentCode}} liquidity provision"
}
diff --git a/libs/i18n/src/locales/en/utils.json b/libs/i18n/src/locales/en/utils.json
index 2f0bf6ccf..269a3ce1d 100644
--- a/libs/i18n/src/locales/en/utils.json
+++ b/libs/i18n/src/locales/en/utils.json
@@ -6,6 +6,7 @@
"Expired on {{date}}": "Expired on {{date}}",
"Invalid Ethereum address": "Invalid Ethereum address",
"Invalid Vega key": "Invalid Vega key",
+ "Invalid URL": "Invalid URL",
"Mark": "Mark",
"Must be valid JSON": "Must be valid JSON",
"Not time-based": "Not time-based",
diff --git a/libs/i18n/src/locales/en/wallet.json b/libs/i18n/src/locales/en/wallet.json
index a4f3cf2c5..073f373c7 100644
--- a/libs/i18n/src/locales/en/wallet.json
+++ b/libs/i18n/src/locales/en/wallet.json
@@ -48,6 +48,8 @@
"Supported browsers": "Supported browsers",
"The user rejected the wallet connection": "The user rejected the wallet connection",
"To complete your wallet connection, set your wallet network in your app to \"{{appChainId}}\".": "To complete your wallet connection, set your wallet network in your app to \"{{appChainId}}\".",
+ "Transaction could not be sent": "Transaction could not be sent",
+ "Transaction was not successful": "Transaction was not successful",
"Try again": "Try again",
"Understand the risk": "Understand the risk",
"Use the Desktop App/CLI": "Use the Desktop App/CLI",
@@ -57,6 +59,7 @@
"Verifying chain": "Verifying chain",
"View as party": "View as party",
"VIEW AS VEGA USER": "VIEW AS VEGA USER",
+ "Wallet rejected transaction": "Wallet rejected transaction",
"Wrong Network": "Wrong Network",
"Wrong network": "Wrong network",
"your browser": "your browser"
diff --git a/libs/types/src/__generated__/types.ts b/libs/types/src/__generated__/types.ts
index af219c512..7ee4b148b 100644
--- a/libs/types/src/__generated__/types.ts
+++ b/libs/types/src/__generated__/types.ts
@@ -1879,12 +1879,8 @@ export type LiquidityFeeSettings = {
/** Configuration of a market liquidity monitoring parameters */
export type LiquidityMonitoringParameters = {
__typename?: 'LiquidityMonitoringParameters';
- /** Specifies by how many seconds an auction should be extended if leaving the auction were to trigger a liquidity auction */
- auctionExtensionSecs: Scalars['Int'];
/** Specifies parameters related to target stake calculation */
targetStakeParameters: TargetStakeParameters;
- /** Specifies the triggering ratio for entering liquidity auction */
- triggeringRatio: Scalars['String'];
};
/** A special order type for liquidity providers */
@@ -4002,10 +3998,10 @@ export type Perpetual = {
fundingRateScalingFactor: Scalars['String'];
/** Upper bound for the funding-rate such that the funding-rate will never be higher than this value */
fundingRateUpperBound: Scalars['String'];
- /** Optional configuration driving the index price calculation for perpetual product */
- indexPriceConfig?: Maybe;
/** Continuously compounded interest rate used in funding rate calculation, in the range [-1, 1] */
interestRate: Scalars['String'];
+ /** Optional configuration driving the internal composite price calculation for perpetual product */
+ internalCompositePriceConfig?: Maybe;
/** Controls how much the upcoming funding payment liability contributes to party's margin, in the range [0, 1] */
marginFundingFactor: Scalars['String'];
/** Quote name of the instrument */
@@ -4023,14 +4019,14 @@ export type PerpetualData = {
fundingPayment?: Maybe;
/** Percentage difference between the time-weighted average price of the external and internal data point. */
fundingRate?: Maybe;
- /** The index price used for external VWAP calculation */
- indexPrice: Scalars['String'];
- /** The methodology used to calculated index price for perps */
- indexPriceType: CompositePriceType;
+ /** Internal composite price used as input to the internal VWAP */
+ internalCompositePrice: Scalars['String'];
+ /** The methodology used to calculated internal composite price for perpetual markets */
+ internalCompositePriceType: CompositePriceType;
/** Time-weighted average price calculated from data points for this period from the internal data source. */
internalTwap?: Maybe;
- /** RFC3339Nano time indicating the next time index price will be calculated for perps where applicable */
- nextIndexPriceCalc: Scalars['String'];
+ /** RFC3339Nano time indicating the next time internal composite price will be calculated for perpetual markets, where applicable */
+ nextInternalCompositePriceCalc: Scalars['String'];
/** Funding period sequence number */
seqNum: Scalars['Int'];
/** Time at which the funding period started */
@@ -5507,6 +5503,8 @@ export type ReferralSet = {
id: Scalars['ID'];
/** Party that created the set. */
referrer: Scalars['ID'];
+ /** Current number of members in the referral set. */
+ totalMembers: Scalars['Int'];
/** Timestamp as RFC3339Nano when the referral set was updated. */
updatedAt: Scalars['Timestamp'];
};
@@ -6333,6 +6331,8 @@ export type Team = {
teamId: Scalars['ID'];
/** Link to the team's homepage. */
teamUrl: Scalars['String'];
+ /** Current number of members in the team. */
+ totalMembers: Scalars['Int'];
};
/** Connection type for retrieving cursor-based paginated team data */
diff --git a/libs/types/src/global-types-mappings.ts b/libs/types/src/global-types-mappings.ts
index 7c2eb5cd2..084308487 100644
--- a/libs/types/src/global-types-mappings.ts
+++ b/libs/types/src/global-types-mappings.ts
@@ -3,6 +3,7 @@ import type {
EntityScope,
GovernanceTransferKind,
GovernanceTransferType,
+ IndividualScope,
PeggedReference,
ProposalChange,
TransferStatus,
@@ -700,6 +701,20 @@ export const EntityScopeLabelMapping: { [e in EntityScope]: string } = {
ENTITY_SCOPE_TEAMS: 'Team',
};
+export const IndividualScopeMapping: { [e in IndividualScope]: string } = {
+ INDIVIDUAL_SCOPE_ALL: 'All',
+ INDIVIDUAL_SCOPE_IN_TEAM: 'In team',
+ INDIVIDUAL_SCOPE_NOT_IN_TEAM: 'Not in team',
+};
+
+export const IndividualScopeDescriptionMapping: {
+ [e in IndividualScope]: string;
+} = {
+ INDIVIDUAL_SCOPE_ALL: 'All parties are eligble',
+ INDIVIDUAL_SCOPE_IN_TEAM: 'Parties in teams are eligible',
+ INDIVIDUAL_SCOPE_NOT_IN_TEAM: 'Only parties not in teams are eligible',
+};
+
export enum DistributionStrategyMapping {
/** Rewards funded using the pro-rata strategy should be distributed pro-rata by each entity's reward metric scaled by any active multipliers that party has */
DISTRIBUTION_STRATEGY_PRO_RATA = 'Pro rata',
diff --git a/libs/ui-toolkit/src/components/icon/vega-icons/vega-icon.tsx b/libs/ui-toolkit/src/components/icon/vega-icons/vega-icon.tsx
index ba5df1bb2..bd4cfe73d 100644
--- a/libs/ui-toolkit/src/components/icon/vega-icons/vega-icon.tsx
+++ b/libs/ui-toolkit/src/components/icon/vega-icons/vega-icon.tsx
@@ -8,13 +8,15 @@ export type VegaIconSize = 8 | 10 | 12 | 13 | 14 | 16 | 18 | 20 | 24 | 28 | 32;
export interface VegaIconProps {
name: VegaIconNames;
size?: VegaIconSize;
+ className?: string;
}
-export const VegaIcon = ({ size = 16, name }: VegaIconProps) => {
+export const VegaIcon = ({ size = 16, name, className }: VegaIconProps) => {
const effectiveClassName = classNames(
'inline-block',
'align-text-bottom',
- 'fill-current stroke-none'
+ 'fill-current stroke-none',
+ className
);
const Element = VegaIconNameMap[name];
return (
diff --git a/libs/ui-toolkit/src/components/splash/splash.tsx b/libs/ui-toolkit/src/components/splash/splash.tsx
index 1e0c925bf..ced798a5a 100644
--- a/libs/ui-toolkit/src/components/splash/splash.tsx
+++ b/libs/ui-toolkit/src/components/splash/splash.tsx
@@ -3,12 +3,14 @@ import type { ReactNode } from 'react';
export interface SplashProps {
children: ReactNode;
+ className?: classNames.Argument;
}
-export const Splash = ({ children }: SplashProps) => {
+export const Splash = ({ children, className }: SplashProps) => {
const splashClasses = classNames(
'w-full h-full text-xs text-center text-gray-800 dark:text-gray-200',
- 'flex items-center justify-center'
+ 'flex items-center justify-center',
+ className
);
return {children}
;
};
diff --git a/libs/ui-toolkit/src/components/trading-form-group/form-group.tsx b/libs/ui-toolkit/src/components/trading-form-group/form-group.tsx
index bb3cf26c9..cc332cb30 100644
--- a/libs/ui-toolkit/src/components/trading-form-group/form-group.tsx
+++ b/libs/ui-toolkit/src/components/trading-form-group/form-group.tsx
@@ -43,7 +43,7 @@ export const TradingFormGroup = ({
{label}
{labelDescription && (
- {labelDescription}
+ {labelDescription}
)}
)}
diff --git a/libs/utils/src/lib/format/number.spec.ts b/libs/utils/src/lib/format/number.spec.ts
index acbc71a7d..1039a86eb 100644
--- a/libs/utils/src/lib/format/number.spec.ts
+++ b/libs/utils/src/lib/format/number.spec.ts
@@ -12,6 +12,7 @@ import {
quantumDecimalPlaces,
toDecimal,
toNumberParts,
+ formatNumberRounded,
} from './number';
describe('number utils', () => {
@@ -254,3 +255,30 @@ describe('getUnlimitedThreshold', () => {
}
);
});
+
+describe('formatNumberRounded', () => {
+ it('rounds number with symbol', () => {
+ expect(formatNumberRounded(new BigNumber(1))).toBe('1');
+ expect(formatNumberRounded(new BigNumber(1_000))).toBe('1,000');
+ expect(formatNumberRounded(new BigNumber(1_000_000))).toBe('1m');
+ expect(formatNumberRounded(new BigNumber(1_000_000_000))).toBe('1b');
+ expect(formatNumberRounded(new BigNumber(1_000_000_000_000))).toBe('1t');
+ });
+
+ it('respects the limit parameter', () => {
+ expect(formatNumberRounded(new BigNumber(1_000), '1e3')).toBe('1k');
+ expect(formatNumberRounded(new BigNumber(1_000_000), '1e9')).toBe(
+ '1,000,000'
+ );
+ expect(formatNumberRounded(new BigNumber(9_999_999), '1e9')).toBe(
+ '9,999,999'
+ );
+ expect(formatNumberRounded(new BigNumber(1_000_000_000), '1e9')).toBe('1b');
+ expect(formatNumberRounded(new BigNumber(1_000_000_000), '1e12')).toBe(
+ '1,000,000,000'
+ );
+ expect(formatNumberRounded(new BigNumber(1_000_000_000_000), '1e9')).toBe(
+ '1t'
+ );
+ });
+});
diff --git a/libs/utils/src/lib/format/number.ts b/libs/utils/src/lib/format/number.ts
index 67305f66e..fa686107d 100644
--- a/libs/utils/src/lib/format/number.ts
+++ b/libs/utils/src/lib/format/number.ts
@@ -191,7 +191,10 @@ export const isNumeric = (
* Format a number greater than 1 million with m for million, b for billion
* and t for trillion
*/
-export const formatNumberRounded = (num: BigNumber) => {
+export const formatNumberRounded = (
+ num: BigNumber,
+ limit: '1e12' | '1e9' | '1e6' | '1e3' = '1e6'
+) => {
let value = '';
const format = (divisor: string) => {
@@ -201,15 +204,29 @@ export const formatNumberRounded = (num: BigNumber) => {
if (num.isGreaterThan(new BigNumber('1e14'))) {
value = '>100t';
- } else if (num.isGreaterThanOrEqualTo(new BigNumber('1e12'))) {
+ } else if (
+ num.isGreaterThanOrEqualTo(limit) &&
+ num.isGreaterThanOrEqualTo(new BigNumber('1e12'))
+ ) {
// Trillion
value = `${format('1e12')}t`;
- } else if (num.isGreaterThanOrEqualTo(new BigNumber('1e9'))) {
+ } else if (
+ num.isGreaterThanOrEqualTo(limit) &&
+ num.isGreaterThanOrEqualTo(new BigNumber('1e9'))
+ ) {
// Billion
value = `${format('1e9')}b`;
- } else if (num.isGreaterThanOrEqualTo(new BigNumber('1e6'))) {
+ } else if (
+ num.isGreaterThanOrEqualTo(limit) &&
+ num.isGreaterThanOrEqualTo(new BigNumber('1e6'))
+ ) {
// Million
value = `${format('1e6')}m`;
+ } else if (
+ num.isGreaterThanOrEqualTo(limit) &&
+ num.isGreaterThanOrEqualTo(new BigNumber('1e3'))
+ ) {
+ value = `${format('1e3')}k`;
} else {
value = formatNumber(num);
}
diff --git a/libs/utils/src/lib/validate/common.ts b/libs/utils/src/lib/validate/common.ts
index c8b901ee3..c0f0b33bb 100644
--- a/libs/utils/src/lib/validate/common.ts
+++ b/libs/utils/src/lib/validate/common.ts
@@ -29,11 +29,14 @@ export const useEthereumAddress = () => {
};
export const VEGA_ID_REGEX = /^[A-Fa-f0-9]{64}$/i;
+export const isValidVegaPublicKey = (value: string) => {
+ return VEGA_ID_REGEX.test(value);
+};
export const useVegaPublicKey = () => {
const t = useT();
return useCallback(
(value: string) => {
- if (!VEGA_ID_REGEX.test(value)) {
+ if (!isValidVegaPublicKey(value)) {
return t('Invalid Vega key');
}
return true;
@@ -91,3 +94,21 @@ export const useValidateJson = () => {
[t]
);
};
+
+export const URL_REGEX =
+ /^(https?:\/\/)?([a-zA-Z0-9.-]+(\.[a-zA-Z]{2,})+)(:[0-9]{1,5})?(\/[^\s]*)?$/;
+const isValidUrl = (value: string) => {
+ return URL_REGEX.test(value);
+};
+export const useValidateUrl = () => {
+ const t = useT();
+ return useCallback(
+ (value: string) => {
+ if (!isValidUrl(value)) {
+ return t('Invalid URL');
+ }
+ return true;
+ },
+ [t]
+ );
+};
diff --git a/libs/wallet/src/SimpleTransaction.graphql b/libs/wallet/src/SimpleTransaction.graphql
new file mode 100644
index 000000000..4ac49f2af
--- /dev/null
+++ b/libs/wallet/src/SimpleTransaction.graphql
@@ -0,0 +1,17 @@
+fragment SimpleTransactionFields on TransactionResult {
+ partyId
+ hash
+ status
+ error
+}
+
+subscription SimpleTransaction($partyId: ID!) {
+ busEvents(partyId: $partyId, batchSize: 0, types: [TransactionResult]) {
+ type
+ event {
+ ... on TransactionResult {
+ ...SimpleTransactionFields
+ }
+ }
+ }
+}
diff --git a/libs/wallet/src/__generated__/SimpleTransaction.ts b/libs/wallet/src/__generated__/SimpleTransaction.ts
new file mode 100644
index 000000000..e7da4a563
--- /dev/null
+++ b/libs/wallet/src/__generated__/SimpleTransaction.ts
@@ -0,0 +1,57 @@
+import * as Types from '@vegaprotocol/types';
+
+import { gql } from '@apollo/client';
+import * as Apollo from '@apollo/client';
+const defaultOptions = {} as const;
+export type SimpleTransactionFieldsFragment = { __typename?: 'TransactionResult', partyId: string, hash: string, status: boolean, error?: string | null };
+
+export type SimpleTransactionSubscriptionVariables = Types.Exact<{
+ partyId: Types.Scalars['ID'];
+}>;
+
+
+export type SimpleTransactionSubscription = { __typename?: 'Subscription', busEvents?: Array<{ __typename?: 'BusEvent', type: Types.BusEventType, event: { __typename?: 'Deposit' } | { __typename?: 'TimeUpdate' } | { __typename?: 'TransactionResult', partyId: string, hash: string, status: boolean, error?: string | null } | { __typename?: 'Withdrawal' } }> | null };
+
+export const SimpleTransactionFieldsFragmentDoc = gql`
+ fragment SimpleTransactionFields on TransactionResult {
+ partyId
+ hash
+ status
+ error
+}
+ `;
+export const SimpleTransactionDocument = gql`
+ subscription SimpleTransaction($partyId: ID!) {
+ busEvents(partyId: $partyId, batchSize: 0, types: [TransactionResult]) {
+ type
+ event {
+ ... on TransactionResult {
+ ...SimpleTransactionFields
+ }
+ }
+ }
+}
+ ${SimpleTransactionFieldsFragmentDoc}`;
+
+/**
+ * __useSimpleTransactionSubscription__
+ *
+ * To run a query within a React component, call `useSimpleTransactionSubscription` and pass it any options that fit your needs.
+ * When your component renders, `useSimpleTransactionSubscription` 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 subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
+ *
+ * @example
+ * const { data, loading, error } = useSimpleTransactionSubscription({
+ * variables: {
+ * partyId: // value for 'partyId'
+ * },
+ * });
+ */
+export function useSimpleTransactionSubscription(baseOptions: Apollo.SubscriptionHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useSubscription(SimpleTransactionDocument, options);
+ }
+export type SimpleTransactionSubscriptionHookResult = ReturnType;
+export type SimpleTransactionSubscriptionResult = Apollo.SubscriptionResult;
\ No newline at end of file
diff --git a/libs/wallet/src/connectors/vega-connector.ts b/libs/wallet/src/connectors/vega-connector.ts
index eb4ebaa70..0828c7fad 100644
--- a/libs/wallet/src/connectors/vega-connector.ts
+++ b/libs/wallet/src/connectors/vega-connector.ts
@@ -439,6 +439,12 @@ export type ApplyReferralCode = {
};
};
+export type JoinTeam = {
+ joinTeam: {
+ id: string;
+ };
+};
+
export type CreateReferralSet = {
createReferralSet: {
isTeam: boolean;
@@ -447,6 +453,21 @@ export type CreateReferralSet = {
teamUrl?: string;
avatarUrl?: string;
closed: boolean;
+ allowList: string[];
+ };
+ };
+};
+
+export type UpdateReferralSet = {
+ updateReferralSet: {
+ id: string;
+ isTeam: boolean;
+ team?: {
+ name: string;
+ teamUrl?: string;
+ avatarUrl?: string;
+ closed: boolean;
+ allowList: string[];
};
};
};
@@ -481,7 +502,9 @@ export type Transaction =
| TransferBody
| LiquidityProvisionSubmission
| ApplyReferralCode
- | CreateReferralSet;
+ | JoinTeam
+ | CreateReferralSet
+ | UpdateReferralSet;
export const isMarginModeUpdateTransaction = (
transaction: Transaction
diff --git a/libs/wallet/src/index.ts b/libs/wallet/src/index.ts
index a1bddd496..28ec16ab2 100644
--- a/libs/wallet/src/index.ts
+++ b/libs/wallet/src/index.ts
@@ -8,3 +8,9 @@ export * from './connect-dialog';
export * from './utils';
export * from './storage';
export * from './use-chain-id';
+export {
+ useSimpleTransaction,
+ type Status,
+ type Result,
+ type Options,
+} from './use-simple-transaction';
diff --git a/libs/wallet/src/use-simple-transaction.ts b/libs/wallet/src/use-simple-transaction.ts
new file mode 100644
index 000000000..ec41d76cc
--- /dev/null
+++ b/libs/wallet/src/use-simple-transaction.ts
@@ -0,0 +1,118 @@
+import { useState } from 'react';
+import { useVegaWallet } from './use-vega-wallet';
+import { type Transaction } from './connectors';
+import {
+ useSimpleTransactionSubscription,
+ type SimpleTransactionFieldsFragment,
+} from './__generated__/SimpleTransaction';
+import { useT } from './use-t';
+import { determineId } from './utils';
+
+export type Status = 'idle' | 'requested' | 'pending' | 'confirmed';
+
+export type Result = {
+ txHash: string;
+ signature: string;
+ id: string;
+};
+
+export type Options = {
+ onSuccess?: (result: Result) => void;
+ onError?: (msg: string) => void;
+};
+
+export const useSimpleTransaction = (opts?: Options) => {
+ const t = useT();
+ const { pubKey, isReadOnly, sendTx } = useVegaWallet();
+
+ const [status, setStatus] = useState('idle');
+ const [result, setResult] = useState();
+ const [error, setError] = useState();
+
+ const send = async (tx: Transaction) => {
+ if (!pubKey) {
+ throw new Error('no pubKey');
+ }
+
+ if (isReadOnly) {
+ throw new Error('cant submit in read only mode');
+ }
+
+ setStatus('requested');
+ setError(undefined);
+
+ try {
+ const res = await sendTx(pubKey, tx);
+
+ if (!res) {
+ throw new Error(t('Transaction could not be sent'));
+ }
+
+ setStatus('pending');
+ setResult({
+ txHash: res?.transactionHash.toLowerCase(),
+ signature: res.signature,
+ id: determineId(res.signature),
+ });
+ } catch (err) {
+ if (err instanceof Error) {
+ if (err.message.includes('user rejected')) {
+ setStatus('idle');
+ } else {
+ setError(err.message);
+ setStatus('idle');
+ opts?.onError?.(err.message);
+ }
+ } else {
+ const msg = t('Wallet rejected transaction');
+ setError(msg);
+ setStatus('idle');
+ opts?.onError?.(msg);
+ }
+ }
+ };
+
+ useSimpleTransactionSubscription({
+ variables: { partyId: pubKey || '' },
+ skip: !pubKey || !result,
+ fetchPolicy: 'no-cache',
+ onData: ({ data }) => {
+ if (!result) {
+ throw new Error('simple transaction query started before result');
+ }
+
+ const e = data.data?.busEvents?.find((event) => {
+ if (
+ event.event.__typename === 'TransactionResult' &&
+ event.event.hash.toLowerCase() === result?.txHash
+ ) {
+ return true;
+ }
+
+ return false;
+ });
+
+ if (!e) return;
+
+ // Force type narrowing
+ const event = e.event as SimpleTransactionFieldsFragment;
+
+ if (event.status && !event.error) {
+ setStatus('confirmed');
+ opts?.onSuccess?.(result);
+ } else {
+ const msg = event?.error || t('Transaction was not successful');
+ setError(msg);
+ setStatus('idle');
+ opts?.onError?.(msg);
+ }
+ },
+ });
+
+ return {
+ result,
+ error,
+ status,
+ send,
+ };
+};