Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfb78f0af5 | ||
|
|
42a98b6a35 | ||
|
|
2002731c52 |
@@ -125,7 +125,7 @@ const CreateTeamFormContainer = ({ isSolo }: { isSolo: boolean }) => {
|
||||
onSubmit={onSubmit}
|
||||
status={status}
|
||||
err={err}
|
||||
isSolo={isSolo}
|
||||
isCreatingSoloTeam={isSolo}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -31,10 +31,7 @@ export const CompetitionsHome = () => {
|
||||
currentEpoch,
|
||||
});
|
||||
|
||||
const { data: teamsData, loading: teamsLoading } = useTeams({
|
||||
sortByField: ['totalQuantumRewards'],
|
||||
order: 'desc',
|
||||
});
|
||||
const { data: teamsData, loading: teamsLoading } = useTeams();
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
|
||||
@@ -38,12 +38,11 @@ export const CompetitionsTeam = () => {
|
||||
const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => {
|
||||
const t = useT();
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { team, partyTeam, stats, members, games, loading, refetch } = useTeam(
|
||||
teamId,
|
||||
pubKey || undefined
|
||||
);
|
||||
const { data, team, partyTeam, stats, members, games, loading, refetch } =
|
||||
useTeam(teamId, pubKey || undefined);
|
||||
|
||||
if (loading) {
|
||||
// only show spinner on first load so when users join teams its smoother
|
||||
if (!data && loading) {
|
||||
return (
|
||||
<Splash>
|
||||
<Loader />
|
||||
@@ -186,7 +185,10 @@ const Members = ({ members }: { members?: Member[] }) => {
|
||||
|
||||
const data = orderBy(
|
||||
members.map((m) => ({
|
||||
referee: <RefereeLink pubkey={m.referee} />,
|
||||
referee: <RefereeLink pubkey={m.referee} isCreator={m.isCreator} />,
|
||||
rewards: formatNumber(m.totalQuantumRewards),
|
||||
volume: formatNumber(m.totalQuantumVolume),
|
||||
gamesPlayed: formatNumber(m.totalGamesPlayed),
|
||||
joinedAt: getDateTimeFormat().format(new Date(m.joinedAt)),
|
||||
joinedAtEpoch: Number(m.joinedAtEpoch),
|
||||
})),
|
||||
@@ -197,7 +199,10 @@ const Members = ({ members }: { members?: Member[] }) => {
|
||||
return (
|
||||
<Table
|
||||
columns={[
|
||||
{ name: 'referee', displayName: t('Referee') },
|
||||
{ name: 'referee', displayName: t('Member ID') },
|
||||
{ name: 'rewards', displayName: t('Rewards earned') },
|
||||
{ name: 'volume', displayName: t('Total volume') },
|
||||
{ name: 'gamesPlayed', displayName: t('Games played') },
|
||||
{
|
||||
name: 'joinedAt',
|
||||
displayName: t('Joined at'),
|
||||
@@ -213,14 +218,24 @@ const Members = ({ members }: { members?: Member[] }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const RefereeLink = ({ pubkey }: { pubkey: string }) => {
|
||||
const RefereeLink = ({
|
||||
pubkey,
|
||||
isCreator,
|
||||
}: {
|
||||
pubkey: string;
|
||||
isCreator: boolean;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const linkCreator = useLinks(DApp.Explorer);
|
||||
const link = linkCreator(EXPLORER_PARTIES.replace(':id', pubkey));
|
||||
|
||||
return (
|
||||
<Link to={link} target="_blank" className="underline underline-offset-4">
|
||||
{truncateMiddle(pubkey)}
|
||||
</Link>
|
||||
<>
|
||||
<Link to={link} target="_blank" className="underline underline-offset-4">
|
||||
{truncateMiddle(pubkey)}
|
||||
</Link>{' '}
|
||||
<span className="text-muted text-xs">{isCreator ? t('Owner') : ''}</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -17,10 +17,7 @@ export const CompetitionsTeams = () => {
|
||||
|
||||
usePageTitle([t('Competitions'), t('Teams')]);
|
||||
|
||||
const { data: teamsData, loading: teamsLoading } = useTeams({
|
||||
sortByField: ['totalQuantumRewards'],
|
||||
order: 'desc',
|
||||
});
|
||||
const { data: teamsData, loading: teamsLoading } = useTeams();
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [filter, setFilter] = useState<string | null | undefined>(undefined);
|
||||
|
||||
@@ -98,7 +98,7 @@ const UpdateTeamFormContainer = ({
|
||||
type={TransactionType.UpdateReferralSet}
|
||||
status={status}
|
||||
err={err}
|
||||
isSolo={team.closed}
|
||||
isCreatingSoloTeam={team.closed}
|
||||
onSubmit={onSubmit}
|
||||
defaultValues={defaultValues}
|
||||
/>
|
||||
|
||||
@@ -6,11 +6,7 @@ import {
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
useSimpleTransaction,
|
||||
useVegaWallet,
|
||||
type Status,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import { useSimpleTransaction, useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { type Team } from '../../lib/hooks/use-team';
|
||||
import { useState } from 'react';
|
||||
@@ -27,19 +23,8 @@ export const JoinTeam = ({
|
||||
refetch: () => void;
|
||||
}) => {
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
const { send, status } = useSimpleTransaction({
|
||||
onSuccess: refetch,
|
||||
});
|
||||
const [confirmDialog, setConfirmDialog] = useState<JoinType>();
|
||||
|
||||
const joinTeam = () => {
|
||||
send({
|
||||
joinTeam: {
|
||||
id: team.teamId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<JoinButton
|
||||
@@ -56,11 +41,10 @@ export const JoinTeam = ({
|
||||
{confirmDialog !== undefined && (
|
||||
<DialogContent
|
||||
type={confirmDialog}
|
||||
status={status}
|
||||
team={team}
|
||||
partyTeam={partyTeam}
|
||||
onConfirm={joinTeam}
|
||||
onCancel={() => setConfirmDialog(undefined)}
|
||||
refetch={refetch}
|
||||
/>
|
||||
)}
|
||||
</Dialog>
|
||||
@@ -110,7 +94,7 @@ export const JoinButton = ({
|
||||
// Not creator of the team, but still can't switch because
|
||||
// creators cannot leave their own team
|
||||
return (
|
||||
<Tooltip description="As a team creator, you cannot switch teams">
|
||||
<Tooltip description={t('As a team creator, you cannot switch teams')}>
|
||||
<Button intent={Intent.Primary} disabled={true}>
|
||||
{t('Switch team')}{' '}
|
||||
</Button>
|
||||
@@ -149,21 +133,39 @@ export const JoinButton = ({
|
||||
|
||||
const DialogContent = ({
|
||||
type,
|
||||
status,
|
||||
team,
|
||||
partyTeam,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
refetch,
|
||||
}: {
|
||||
type: JoinType;
|
||||
status: Status;
|
||||
team: Team;
|
||||
partyTeam?: Team;
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
refetch: () => void;
|
||||
}) => {
|
||||
const t = useT();
|
||||
|
||||
const { send, status, error } = useSimpleTransaction({
|
||||
onSuccess: refetch,
|
||||
});
|
||||
|
||||
const joinTeam = () => {
|
||||
send({
|
||||
joinTeam: {
|
||||
id: team.teamId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<p className="text-vega-red break-words first-letter:capitalize">
|
||||
{error}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'requested') {
|
||||
return <p>{t('Confirm in wallet...')}</p>;
|
||||
}
|
||||
@@ -213,7 +215,7 @@ const DialogContent = ({
|
||||
</>
|
||||
)}
|
||||
<div className="flex justify-between gap-2">
|
||||
<Button onClick={onConfirm} intent={Intent.Success}>
|
||||
<Button onClick={joinTeam} intent={Intent.Success}>
|
||||
{t('Confirm')}
|
||||
</Button>
|
||||
<Button onClick={onCancel} intent={Intent.Danger}>
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
TextArea,
|
||||
TradingButton,
|
||||
Intent,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { URL_REGEX, isValidVegaPublicKey } from '@vegaprotocol/utils';
|
||||
|
||||
@@ -17,6 +19,8 @@ import type {
|
||||
UpdateReferralSet,
|
||||
Status,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import classNames from 'classnames';
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
|
||||
export type FormFields = {
|
||||
id: string;
|
||||
@@ -28,8 +32,8 @@ export type FormFields = {
|
||||
};
|
||||
|
||||
export enum TransactionType {
|
||||
CreateReferralSet,
|
||||
UpdateReferralSet,
|
||||
CreateReferralSet = 'CreateReferralSet',
|
||||
UpdateReferralSet = 'UpdateReferralSet',
|
||||
}
|
||||
|
||||
const prepareTransaction = (
|
||||
@@ -75,14 +79,14 @@ export const TeamForm = ({
|
||||
type,
|
||||
status,
|
||||
err,
|
||||
isSolo,
|
||||
isCreatingSoloTeam,
|
||||
onSubmit,
|
||||
defaultValues,
|
||||
}: {
|
||||
type: TransactionType;
|
||||
status: ReturnType<typeof useReferralSetTransaction>['status'];
|
||||
err: ReturnType<typeof useReferralSetTransaction>['err'];
|
||||
isSolo: boolean;
|
||||
isCreatingSoloTeam: boolean;
|
||||
onSubmit: ReturnType<typeof useReferralSetTransaction>['onSubmit'];
|
||||
defaultValues?: FormFields;
|
||||
}) => {
|
||||
@@ -96,8 +100,7 @@ export const TeamForm = ({
|
||||
formState: { errors },
|
||||
} = useForm<FormFields>({
|
||||
defaultValues: {
|
||||
private: isSolo,
|
||||
allowList: '',
|
||||
private: isCreatingSoloTeam,
|
||||
...defaultValues,
|
||||
},
|
||||
});
|
||||
@@ -110,12 +113,7 @@ export const TeamForm = ({
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(sendTransaction)}>
|
||||
<input
|
||||
type="hidden"
|
||||
{...register('id', {
|
||||
disabled: true,
|
||||
})}
|
||||
/>
|
||||
<input type="hidden" {...register('id')} />
|
||||
<TradingFormGroup label={t('Team name')} labelFor="name">
|
||||
<TradingInput {...register('name', { required: t('Required') })} />
|
||||
{errors.name?.message && (
|
||||
@@ -161,62 +159,65 @@ export const TeamForm = ({
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
{!isSolo && (
|
||||
<>
|
||||
<TradingFormGroup
|
||||
label={t('Make team private')}
|
||||
labelFor="private"
|
||||
hideLabel={true}
|
||||
>
|
||||
<Controller
|
||||
name="private"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<TradingCheckbox
|
||||
label={t('Make team private')}
|
||||
checked={field.value}
|
||||
onCheckedChange={(value) => {
|
||||
field.onChange(value);
|
||||
}}
|
||||
disabled={isSolo}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</TradingFormGroup>
|
||||
{isPrivate && (
|
||||
{
|
||||
// allow changing to private/public if editing, but don't show these options if making a solo team
|
||||
(type === TransactionType.UpdateReferralSet || !isCreatingSoloTeam) && (
|
||||
<>
|
||||
<TradingFormGroup
|
||||
label={t('Public key allow list')}
|
||||
labelFor="allowList"
|
||||
labelDescription={t(
|
||||
'Use a comma separated list to allow only specific public keys to join the team'
|
||||
)}
|
||||
label={t('Make team private')}
|
||||
labelFor="private"
|
||||
hideLabel={true}
|
||||
>
|
||||
<TextArea
|
||||
{...register('allowList', {
|
||||
required: t('Required'),
|
||||
disabled: isSolo,
|
||||
validate: {
|
||||
allowList: (value) => {
|
||||
const publicKeys = parseAllowListText(value);
|
||||
if (publicKeys.every((pk) => isValidVegaPublicKey(pk))) {
|
||||
return true;
|
||||
}
|
||||
return t('Invalid public key found in allow list');
|
||||
},
|
||||
},
|
||||
})}
|
||||
<Controller
|
||||
name="private"
|
||||
control={control}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<TradingCheckbox
|
||||
label={t('Make team private')}
|
||||
checked={field.value}
|
||||
onCheckedChange={(value) => {
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{errors.allowList?.message && (
|
||||
<TradingInputError forInput="avatarUrl">
|
||||
{errors.allowList.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isPrivate && (
|
||||
<TradingFormGroup
|
||||
label={t('Public key allow list')}
|
||||
labelFor="allowList"
|
||||
labelDescription={t(
|
||||
'Use a comma separated list to allow only specific public keys to join the team'
|
||||
)}
|
||||
>
|
||||
<TextArea
|
||||
{...register('allowList', {
|
||||
required: t('Required'),
|
||||
validate: {
|
||||
allowList: (value) => {
|
||||
const publicKeys = parseAllowListText(value);
|
||||
if (
|
||||
publicKeys.every((pk) => isValidVegaPublicKey(pk))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return t('Invalid public key found in allow list');
|
||||
},
|
||||
},
|
||||
})}
|
||||
/>
|
||||
{errors.allowList?.message && (
|
||||
<TradingInputError forInput="avatarUrl">
|
||||
{errors.allowList.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
{err && (
|
||||
<p className="text-danger text-xs mb-4 first-letter:capitalize">
|
||||
{err}
|
||||
@@ -242,20 +243,56 @@ const SubmitButton = ({
|
||||
text = t('Update');
|
||||
}
|
||||
|
||||
let confirmedText = t('Created');
|
||||
if (type === TransactionType.UpdateReferralSet) {
|
||||
confirmedText = t('Updated');
|
||||
}
|
||||
|
||||
if (status === 'requested') {
|
||||
text = t('Confirm in wallet...');
|
||||
} else if (status === 'pending') {
|
||||
text = t('Confirming transaction...');
|
||||
}
|
||||
|
||||
const [showConfirmed, setShowConfirmed] = useState<boolean>(false);
|
||||
useLayoutEffect(() => {
|
||||
let to: ReturnType<typeof setTimeout>;
|
||||
if (status === 'confirmed' && !showConfirmed) {
|
||||
to = setTimeout(() => {
|
||||
setShowConfirmed(true);
|
||||
}, 100);
|
||||
}
|
||||
return () => {
|
||||
clearTimeout(to);
|
||||
};
|
||||
}, [showConfirmed, status]);
|
||||
|
||||
const confirmed = (
|
||||
<span
|
||||
className={classNames('text-sm transition-opacity opacity-0', {
|
||||
'opacity-100': showConfirmed,
|
||||
})}
|
||||
>
|
||||
<VegaIcon
|
||||
name={VegaIconNames.TICK}
|
||||
size={18}
|
||||
className="text-vega-green-500"
|
||||
/>{' '}
|
||||
{confirmedText}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<TradingButton type="submit" intent={Intent.Info} disabled={disabled}>
|
||||
{text}
|
||||
</TradingButton>
|
||||
<div className="flex gap-2 items-baseline">
|
||||
<TradingButton type="submit" intent={Intent.Info} disabled={disabled}>
|
||||
{text}
|
||||
</TradingButton>
|
||||
{status === 'confirmed' && confirmed}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const parseAllowListText = (str: string) => {
|
||||
const parseAllowListText = (str: string = '') => {
|
||||
return str
|
||||
.split(',')
|
||||
.map((v) => v.trim())
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
|
||||
VEGA_VERSION=v0.74.0-preview.7
|
||||
VEGA_VERSION=v0.74.0-preview.8
|
||||
LOCAL_SERVER=false
|
||||
|
||||
@@ -137,6 +137,7 @@ def create_team(vega: VegaServiceNull):
|
||||
return team_name
|
||||
|
||||
def test_team_page_games_table(team_page: Page):
|
||||
team_page.pause()
|
||||
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")
|
||||
@@ -152,7 +153,7 @@ def test_team_page_games_table(team_page: Page):
|
||||
|
||||
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("members-toggle")).to_have_text("Members (4)")
|
||||
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")
|
||||
@@ -161,7 +162,7 @@ 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("members-count-stat")).to_have_text("4")
|
||||
|
||||
expect(team_page.get_by_test_id("total-games-stat")).to_have_text(
|
||||
"1"
|
||||
|
||||
@@ -17,7 +17,7 @@ fragment TeamStatsFields on TeamStatistics {
|
||||
totalGamesPlayed
|
||||
quantumRewards {
|
||||
epoch
|
||||
total_quantum_rewards
|
||||
totalQuantumRewards
|
||||
}
|
||||
gamesPlayed
|
||||
}
|
||||
@@ -51,6 +51,13 @@ fragment TeamGameFields on Game {
|
||||
}
|
||||
}
|
||||
|
||||
fragment TeamMemberStatsFields on TeamMemberStatistics {
|
||||
partyId
|
||||
totalQuantumVolume
|
||||
totalQuantumRewards
|
||||
totalGamesPlayed
|
||||
}
|
||||
|
||||
query Team($teamId: ID!, $partyId: ID, $aggregationEpochs: Int) {
|
||||
teams(teamId: $teamId) {
|
||||
edges {
|
||||
@@ -87,4 +94,14 @@ query Team($teamId: ID!, $partyId: ID, $aggregationEpochs: Int) {
|
||||
}
|
||||
}
|
||||
}
|
||||
teamMembersStatistics(
|
||||
teamId: $teamId
|
||||
aggregationEpochs: $aggregationEpochs
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
...TeamMemberStatsFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+22
-4
@@ -5,7 +5,7 @@ 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<string> };
|
||||
|
||||
export type TeamStatsFieldsFragment = { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string>, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, total_quantum_rewards: string }> };
|
||||
export type TeamStatsFieldsFragment = { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string>, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, totalQuantumRewards: string }> };
|
||||
|
||||
export type TeamRefereeFieldsFragment = { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number };
|
||||
|
||||
@@ -13,6 +13,8 @@ export type TeamEntityFragment = { __typename?: 'TeamGameEntity', rank: number,
|
||||
|
||||
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 TeamMemberStatsFieldsFragment = { __typename?: 'TeamMemberStatistics', partyId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number };
|
||||
|
||||
export type TeamQueryVariables = Types.Exact<{
|
||||
teamId: Types.Scalars['ID'];
|
||||
partyId?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||
@@ -20,7 +22,7 @@ export type TeamQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
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<string> } }> } | 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<string> } }> } | null, teamsStatistics?: { __typename?: 'TeamsStatisticsConnection', edges: Array<{ __typename?: 'TeamStatisticsEdge', node: { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string>, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, total_quantum_rewards: string }> } }> } | null, teamReferees?: { __typename?: 'TeamRefereeConnection', edges: Array<{ __typename?: 'TeamRefereeEdge', node: { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number } }> } | null, games: { __typename?: 'GamesConnection', edges?: Array<{ __typename?: 'GameEdge', node: { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: Types.DispatchMetric, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } }> } } | null> | null } };
|
||||
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<string> } }> } | 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<string> } }> } | null, teamsStatistics?: { __typename?: 'TeamsStatisticsConnection', edges: Array<{ __typename?: 'TeamStatisticsEdge', node: { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string>, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, totalQuantumRewards: 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 }, teamMembersStatistics?: { __typename?: 'TeamMembersStatisticsConnection', edges: Array<{ __typename?: 'TeamMemberStatisticsEdge', node: { __typename?: 'TeamMemberStatistics', partyId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number } }> } | null };
|
||||
|
||||
export const TeamFieldsFragmentDoc = gql`
|
||||
fragment TeamFields on Team {
|
||||
@@ -43,7 +45,7 @@ export const TeamStatsFieldsFragmentDoc = gql`
|
||||
totalGamesPlayed
|
||||
quantumRewards {
|
||||
epoch
|
||||
total_quantum_rewards
|
||||
totalQuantumRewards
|
||||
}
|
||||
gamesPlayed
|
||||
}
|
||||
@@ -80,6 +82,14 @@ export const TeamGameFieldsFragmentDoc = gql`
|
||||
}
|
||||
}
|
||||
${TeamEntityFragmentDoc}`;
|
||||
export const TeamMemberStatsFieldsFragmentDoc = gql`
|
||||
fragment TeamMemberStatsFields on TeamMemberStatistics {
|
||||
partyId
|
||||
totalQuantumVolume
|
||||
totalQuantumRewards
|
||||
totalGamesPlayed
|
||||
}
|
||||
`;
|
||||
export const TeamDocument = gql`
|
||||
query Team($teamId: ID!, $partyId: ID, $aggregationEpochs: Int) {
|
||||
teams(teamId: $teamId) {
|
||||
@@ -117,11 +127,19 @@ export const TeamDocument = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
teamMembersStatistics(teamId: $teamId, aggregationEpochs: $aggregationEpochs) {
|
||||
edges {
|
||||
node {
|
||||
...TeamMemberStatsFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${TeamFieldsFragmentDoc}
|
||||
${TeamStatsFieldsFragmentDoc}
|
||||
${TeamRefereeFieldsFragmentDoc}
|
||||
${TeamGameFieldsFragmentDoc}`;
|
||||
${TeamGameFieldsFragmentDoc}
|
||||
${TeamMemberStatsFieldsFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useTeamQuery__
|
||||
|
||||
@@ -6,17 +6,24 @@ import {
|
||||
type TeamStatsFieldsFragment,
|
||||
type TeamRefereeFieldsFragment,
|
||||
type TeamEntityFragment,
|
||||
type TeamMemberStatsFieldsFragment,
|
||||
} from './__generated__/Team';
|
||||
import { DEFAULT_AGGREGATION_EPOCHS } from './use-teams';
|
||||
|
||||
export type Team = TeamFieldsFragment;
|
||||
export type TeamStats = TeamStatsFieldsFragment;
|
||||
export type Member = TeamRefereeFieldsFragment;
|
||||
export type Member = TeamRefereeFieldsFragment & {
|
||||
isCreator: boolean;
|
||||
totalGamesPlayed: number;
|
||||
totalQuantumVolume: string;
|
||||
totalQuantumRewards: string;
|
||||
};
|
||||
export type TeamEntity = TeamEntityFragment;
|
||||
export type TeamGame = ReturnType<typeof useTeam>['games'][number];
|
||||
export type MemberStats = TeamMemberStatsFieldsFragment;
|
||||
|
||||
export const useTeam = (teamId?: string, partyId?: string) => {
|
||||
const { data, loading, error, refetch } = useTeamQuery({
|
||||
const queryResult = useTeamQuery({
|
||||
variables: {
|
||||
teamId: teamId || '',
|
||||
partyId,
|
||||
@@ -26,7 +33,11 @@ export const useTeam = (teamId?: string, partyId?: string) => {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
const { data } = queryResult;
|
||||
|
||||
const teamEdge = data?.teams?.edges.find((e) => e.node.teamId === teamId);
|
||||
const team = teamEdge?.node;
|
||||
|
||||
const partyTeam = data?.partyTeams?.edges?.length
|
||||
? data.partyTeams.edges[0].node
|
||||
: undefined;
|
||||
@@ -34,9 +45,40 @@ export const useTeam = (teamId?: string, partyId?: string) => {
|
||||
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);
|
||||
|
||||
const memberStats = data?.teamMembersStatistics?.edges.length
|
||||
? data.teamMembersStatistics.edges.map((e) => e.node)
|
||||
: [];
|
||||
|
||||
const members: Member[] = data?.teamReferees?.edges.length
|
||||
? data.teamReferees.edges
|
||||
.filter((e) => e.node.teamId === teamId)
|
||||
.map((e) => {
|
||||
const member = e.node;
|
||||
const stats = memberStats.find((m) => m.partyId === member.referee);
|
||||
return {
|
||||
...member,
|
||||
isCreator: false,
|
||||
totalQuantumVolume: stats ? stats.totalQuantumVolume : '0',
|
||||
totalQuantumRewards: stats ? stats.totalQuantumRewards : '0',
|
||||
totalGamesPlayed: stats ? stats.totalGamesPlayed : 0,
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
||||
if (team) {
|
||||
const ownerStats = memberStats.find((m) => m.partyId === team.referrer);
|
||||
members.unshift({
|
||||
teamId: team.teamId,
|
||||
referee: team.referrer,
|
||||
joinedAt: team?.createdAt,
|
||||
joinedAtEpoch: team?.createdAtEpoch,
|
||||
isCreator: true,
|
||||
totalQuantumVolume: ownerStats ? ownerStats.totalQuantumVolume : '0',
|
||||
totalQuantumRewards: ownerStats ? ownerStats.totalQuantumRewards : '0',
|
||||
totalGamesPlayed: ownerStats ? ownerStats.totalGamesPlayed : 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Find games where the current team participated in
|
||||
const gamesWithTeam = compact(data?.games.edges).map((edge) => {
|
||||
@@ -60,12 +102,9 @@ export const useTeam = (teamId?: string, partyId?: string) => {
|
||||
const games = orderBy(compact(gamesWithTeam), 'epoch', 'desc');
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
refetch,
|
||||
...queryResult,
|
||||
stats: teamStatsEdge?.node,
|
||||
team: teamEdge?.node,
|
||||
team,
|
||||
members,
|
||||
games,
|
||||
partyTeam,
|
||||
|
||||
@@ -1,34 +1,12 @@
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import { useMemo } from 'react';
|
||||
import { type TeamsQuery, useTeamsQuery } from './__generated__/Teams';
|
||||
import {
|
||||
type TeamsStatisticsQuery,
|
||||
useTeamsStatisticsQuery,
|
||||
} from './__generated__/TeamsStatistics';
|
||||
import { useTeamsQuery } from './__generated__/Teams';
|
||||
import { useTeamsStatisticsQuery } from './__generated__/TeamsStatistics';
|
||||
import compact from 'lodash/compact';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { type ArrayElement } from 'type-fest/source/internal';
|
||||
|
||||
type SortableField = keyof Omit<
|
||||
ArrayElement<NonNullable<TeamsQuery['teams']>['edges']>['node'] &
|
||||
ArrayElement<
|
||||
NonNullable<TeamsStatisticsQuery['teamsStatistics']>['edges']
|
||||
>['node'],
|
||||
'__typename'
|
||||
>;
|
||||
|
||||
type UseTeamsArgs = {
|
||||
aggregationEpochs?: number;
|
||||
sortByField?: SortableField[];
|
||||
order?: 'asc' | 'desc';
|
||||
};
|
||||
|
||||
export const DEFAULT_AGGREGATION_EPOCHS = 10;
|
||||
|
||||
export const useTeams = ({
|
||||
aggregationEpochs = DEFAULT_AGGREGATION_EPOCHS,
|
||||
sortByField = ['createdAtEpoch'],
|
||||
order = 'asc',
|
||||
}: UseTeamsArgs) => {
|
||||
export const useTeams = (aggregationEpochs = DEFAULT_AGGREGATION_EPOCHS) => {
|
||||
const {
|
||||
data: teamsData,
|
||||
loading: teamsLoading,
|
||||
@@ -57,12 +35,8 @@ export const useTeams = ({
|
||||
...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 orderBy(data, (d) => Number(d.totalQuantumRewards || 0), 'desc');
|
||||
}, [teams, stats]);
|
||||
|
||||
return {
|
||||
data,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AssetsDocument, type AssetsQuery } from './__generated__/Assets';
|
||||
import { AssetStatus } from '@vegaprotocol/types';
|
||||
import { type Asset } from './asset-data-provider';
|
||||
import { DENY_LIST } from './constants';
|
||||
import { type AssetFieldsFragment } from './__generated__/Asset';
|
||||
|
||||
export interface BuiltinAssetSource {
|
||||
__typename: 'BuiltinAsset';
|
||||
@@ -89,3 +90,24 @@ export const useEnabledAssets = () => {
|
||||
variables: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
/** Wrapped ETH symbol */
|
||||
const WETH = 'WETH';
|
||||
type WETHDetails = Pick<AssetFieldsFragment, 'symbol' | 'decimals' | 'quantum'>;
|
||||
/**
|
||||
* Tries to find WETH asset configuration on Vega in order to provide its
|
||||
* details, otherwise it returns hardcoded values.
|
||||
*/
|
||||
export const useWETH = (): WETHDetails => {
|
||||
const { data } = useAssetsDataProvider();
|
||||
if (data) {
|
||||
const weth = data.find((a) => a.symbol.toUpperCase() === WETH);
|
||||
if (weth) return weth;
|
||||
}
|
||||
|
||||
return {
|
||||
symbol: WETH,
|
||||
decimals: 18,
|
||||
quantum: '500000000000000', // 1 WETH ~= 2000 qUSD
|
||||
};
|
||||
};
|
||||
|
||||
@@ -182,6 +182,7 @@
|
||||
"Markets": "Markets",
|
||||
"Members": "Members",
|
||||
"Members ({{count}})": "Members ({{count}})",
|
||||
"Member ID": "Member ID",
|
||||
"Menu": "Menu",
|
||||
"Metamask Snap <0>quick start</0>": "Metamask Snap <0>quick start</0>",
|
||||
"Min. epochs": "Min. epochs",
|
||||
@@ -364,7 +365,7 @@
|
||||
"Type": "Type",
|
||||
"Unknown": "Unknown",
|
||||
"Unknown settlement date": "Unknown settlement date",
|
||||
"Update team":, "Update team",
|
||||
"Update team": "Update team",
|
||||
"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",
|
||||
|
||||
@@ -47,5 +47,11 @@
|
||||
"Withdrawals of {{threshold}} {{symbol}} or more will be delayed for {{delay}}.": "Withdrawals of {{threshold}} {{symbol}} or more will be delayed for {{delay}}.",
|
||||
"Withdrawals ready": "Withdrawals ready",
|
||||
"You have no assets to withdraw": "You have no assets to withdraw",
|
||||
"Your funds have been unlocked for withdrawal - <0>View in block explorer<0>": "Your funds have been unlocked for withdrawal - <0>View in block explorer<0>"
|
||||
"Your funds have been unlocked for withdrawal - <0>View in block explorer<0>": "Your funds have been unlocked for withdrawal - <0>View in block explorer<0>",
|
||||
"Gas fee": "Gas fee",
|
||||
"Estimated gas fee for the withdrawal transaction (refreshes each 15 seconds)": "Estimated gas fee for the withdrawal transaction (refreshes each 15 seconds)",
|
||||
"It seems that the current gas prices are exceeding the amount you're trying to withdraw": "It seems that the current gas prices are exceeding the amount you're trying to withdraw",
|
||||
"The current gas price range": "The current gas price range",
|
||||
"min": "min",
|
||||
"max": "max"
|
||||
}
|
||||
|
||||
Generated
+1
-1
@@ -4682,7 +4682,7 @@ export type QuantumRewardsPerEpoch = {
|
||||
/** Epoch for which this information is valid. */
|
||||
epoch: Scalars['Int'];
|
||||
/** Total of rewards accumulated over the epoch period expressed in quantum value. */
|
||||
total_quantum_rewards: Scalars['String'];
|
||||
totalQuantumRewards: Scalars['String'];
|
||||
};
|
||||
|
||||
/** Queries allow a caller to read data and filter data via GraphQL. */
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { EtherUnit, formatEther, unitiseEther } from './ether';
|
||||
|
||||
describe('unitiseEther', () => {
|
||||
it.each([
|
||||
[1, '1', EtherUnit.wei],
|
||||
[999, '999', EtherUnit.wei],
|
||||
[1000, '1', EtherUnit.kwei],
|
||||
[9999, '9.999', EtherUnit.kwei],
|
||||
[10000, '10', EtherUnit.kwei],
|
||||
[999999, '999.999', EtherUnit.kwei],
|
||||
[1000000, '1', EtherUnit.mwei],
|
||||
[999999999, '999.999999', EtherUnit.mwei],
|
||||
[1000000000, '1', EtherUnit.gwei],
|
||||
['999999999999999999', '999999999.999999999', EtherUnit.gwei], // max gwei
|
||||
[1e18, '1', EtherUnit.ether], // 1 ETH
|
||||
[1234e18, '1234', EtherUnit.ether], // 1234 ETH
|
||||
])('unitises %s to [%s, %s]', (value, expectedOutput, expectedUnit) => {
|
||||
const [output, unit] = unitiseEther(value);
|
||||
expect(output.toFixed()).toEqual(expectedOutput);
|
||||
expect(unit).toEqual(expectedUnit);
|
||||
});
|
||||
|
||||
it('unitises to requested unit', () => {
|
||||
const [output, unit] = unitiseEther(1, EtherUnit.kwei);
|
||||
expect(output).toEqual(BigNumber(0.001));
|
||||
expect(unit).toEqual(EtherUnit.kwei);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatEther', () => {
|
||||
it.each([
|
||||
[1, EtherUnit.wei, '1 wei'],
|
||||
[12, EtherUnit.kwei, '12 kwei'],
|
||||
[123, EtherUnit.gwei, '123 gwei'],
|
||||
[3, EtherUnit.ether, '3 ETH'],
|
||||
[234.67776331, EtherUnit.gwei, '235 gwei'],
|
||||
[12.12, EtherUnit.gwei, '12 gwei'],
|
||||
])('formats [%s, %s] to "%s"', (value, unit, expectedOutput) => {
|
||||
expect(formatEther([BigNumber(value), unit])).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import { formatNumber, toBigNum } from './number';
|
||||
import type BigNumber from 'bignumber.js';
|
||||
|
||||
export enum EtherUnit {
|
||||
/** 1 wei = 10^-18 ETH */
|
||||
wei = '0',
|
||||
/** 1 kwei = 1000 wei */
|
||||
kwei = '3',
|
||||
/** 1 mwei = 1000 kwei */
|
||||
mwei = '6',
|
||||
/** 1 gwei = 1000 kwei */
|
||||
gwei = '9',
|
||||
|
||||
// other denominations:
|
||||
// microether = '12', // aka szabo, µETH
|
||||
// milliether = '15', // aka finney, mETH
|
||||
|
||||
/** 1 ETH = 1B gwei = 10^18 wei */
|
||||
ether = '18',
|
||||
}
|
||||
|
||||
export const etherUnitMapping: Record<EtherUnit, string> = {
|
||||
[EtherUnit.wei]: 'wei',
|
||||
[EtherUnit.kwei]: 'kwei',
|
||||
[EtherUnit.mwei]: 'mwei',
|
||||
[EtherUnit.gwei]: 'gwei',
|
||||
// [EtherUnit.microether]: 'µETH', // szabo
|
||||
// [EtherUnit.milliether]: 'mETH', // finney
|
||||
[EtherUnit.ether]: 'ETH',
|
||||
};
|
||||
|
||||
type InputValue = string | number | BigNumber;
|
||||
type UnitisedTuple = [value: BigNumber, unit: EtherUnit];
|
||||
|
||||
/**
|
||||
* Converts given raw value to the unitised tuple of amount and unit
|
||||
*/
|
||||
export const unitiseEther = (
|
||||
input: InputValue,
|
||||
forceUnit?: EtherUnit
|
||||
): UnitisedTuple => {
|
||||
const units = Object.values(EtherUnit).reverse();
|
||||
|
||||
let value = toBigNum(input, Number(forceUnit || EtherUnit.ether));
|
||||
let unit = forceUnit || EtherUnit.ether;
|
||||
|
||||
if (!forceUnit) {
|
||||
for (const u of units) {
|
||||
const v = toBigNum(input, Number(u));
|
||||
value = v;
|
||||
unit = u;
|
||||
if (v.isGreaterThanOrEqualTo(1)) break;
|
||||
}
|
||||
}
|
||||
|
||||
return [value, unit];
|
||||
};
|
||||
|
||||
/**
|
||||
* `formatNumber` wrapper for unitised ether values (attaches unit name)
|
||||
*/
|
||||
export const formatEther = (
|
||||
input: UnitisedTuple,
|
||||
decimals = 0,
|
||||
noUnit = false
|
||||
) => {
|
||||
const [value, unit] = input;
|
||||
const num = formatNumber(value, decimals);
|
||||
const unitName = noUnit ? '' : etherUnitMapping[unit];
|
||||
|
||||
return `${num} ${unitName}`.trim();
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility function that formats given raw amount as ETH.
|
||||
* Example:
|
||||
* Given value of `1` this will return `0.000000000000000001 ETH`
|
||||
*/
|
||||
export const asETH = (input: InputValue, noUnit = false) =>
|
||||
formatEther(
|
||||
unitiseEther(input, EtherUnit.ether),
|
||||
Number(EtherUnit.ether),
|
||||
noUnit
|
||||
);
|
||||
@@ -4,3 +4,4 @@ export * from './range';
|
||||
export * from './size';
|
||||
export * from './strings';
|
||||
export * from './trigger';
|
||||
export * from './ether';
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
toDecimal,
|
||||
toNumberParts,
|
||||
formatNumberRounded,
|
||||
toQUSD,
|
||||
} from './number';
|
||||
|
||||
describe('number utils', () => {
|
||||
@@ -282,3 +283,22 @@ describe('formatNumberRounded', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toQUSD', () => {
|
||||
it.each([
|
||||
[0, 0, 0],
|
||||
[1, 1, 1],
|
||||
[1, 10, 0.1],
|
||||
[1, 100, 0.01],
|
||||
// real life examples
|
||||
[1000000, 1000000, 1], // USDC -> 1 USDC ~= 1 qUSD
|
||||
[500000, 1000000, 0.5], // USDC => 0.6 USDC ~= 0.5 qUSD
|
||||
[1e18, 1e18, 1], // VEGA -> 1 VEGA ~= 1 qUSD
|
||||
[123.45e18, 1e18, 123.45], // VEGA -> 1 VEGA ~= 1 qUSD
|
||||
[1e18, 5e14, 2000], // WETH -> 1 WETH ~= 2000 qUSD
|
||||
[1e9, 5e14, 0.000002], // gwei -> 1 gwei ~= 0.000002 qUSD
|
||||
[50000e9, 5e14, 0.1], // gwei -> 50000 gwei ~= 0.1 qUSD
|
||||
])('converts (%d, %d) to %d qUSD', (amount, quantum, expected) => {
|
||||
expect(toQUSD(amount, quantum).toNumber()).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ export function toDecimal(numberOfDecimals: number) {
|
||||
}
|
||||
|
||||
export function toBigNum(
|
||||
rawValue: string | number,
|
||||
rawValue: string | number | BigNumber,
|
||||
decimals: number
|
||||
): BigNumber {
|
||||
const divides = new BigNumber(10).exponentiatedBy(decimals);
|
||||
@@ -233,3 +233,24 @@ export const formatNumberRounded = (
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts given amount in one asset (determined by raw amount
|
||||
* and quantum values) to qUSD.
|
||||
* @param amount The raw amount
|
||||
* @param quantum The quantum value of the asset.
|
||||
*/
|
||||
export const toQUSD = (
|
||||
amount: string | number | BigNumber,
|
||||
quantum: string | number
|
||||
) => {
|
||||
const value = new BigNumber(amount);
|
||||
let q = new BigNumber(quantum);
|
||||
|
||||
if (q.isNaN() || q.isLessThanOrEqualTo(0)) {
|
||||
q = new BigNumber(1);
|
||||
}
|
||||
|
||||
const qUSD = value.dividedBy(q);
|
||||
return qUSD;
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@ export * from './lib/use-ethereum-transaction';
|
||||
export * from './lib/use-ethereum-withdraw-approval-toasts';
|
||||
export * from './lib/use-ethereum-withdraw-approvals-manager';
|
||||
export * from './lib/use-ethereum-withdraw-approvals-store';
|
||||
export * from './lib/use-gas-price';
|
||||
export * from './lib/use-get-withdraw-delay';
|
||||
export * from './lib/use-get-withdraw-threshold';
|
||||
export * from './lib/use-token-contract';
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import { useEthereumConfig } from './use-ethereum-config';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
const DEFAULT_INTERVAL = 15000; // 15 seconds
|
||||
|
||||
/**
|
||||
* These are the hex values of the collateral bridge contract methods.
|
||||
*
|
||||
* Collateral bridge address: 0x23872549cE10B40e31D6577e0A920088B0E0666a
|
||||
* Etherscan: https://etherscan.io/address/0x23872549cE10B40e31D6577e0A920088B0E0666a#writeContract
|
||||
*/
|
||||
export enum ContractMethod {
|
||||
DEPOSIT_ASSET = '0xf7683932',
|
||||
EXEMPT_DEPOSITOR = '0xb76fbb75',
|
||||
GLOBAL_RESUME = '0xd72ed529',
|
||||
GLOBAL_STOP = '0x9dfd3c88',
|
||||
LIST_ASSET = '0x0ff3562c',
|
||||
REMOVE_ASSET = '0xc76de358',
|
||||
REVOKE_EXEMPT_DEPOSITOR = '0x6a1c6fa4',
|
||||
SET_ASSET_LIMITS = '0x41fb776d',
|
||||
SET_WITHDRAW_DELAY = '0x5a246728',
|
||||
WITHDRAW_ASSET = '0x3ad90635',
|
||||
}
|
||||
|
||||
export type GasData = {
|
||||
/** The base (minimum) price of 1 unit of gas */
|
||||
basePrice: BigNumber;
|
||||
/** The maximum price of 1 unit of gas */
|
||||
maxPrice: BigNumber;
|
||||
/** The amount of gas (units) needed to process a transaction */
|
||||
gas: BigNumber;
|
||||
};
|
||||
|
||||
type Provider = NonNullable<ReturnType<typeof useWeb3React>['provider']>;
|
||||
|
||||
const retrieveGasData = async (
|
||||
provider: Provider,
|
||||
account: string,
|
||||
contractAddress: string,
|
||||
contractMethod: ContractMethod
|
||||
) => {
|
||||
try {
|
||||
const data = await provider.getFeeData();
|
||||
const estGasAmount = await provider.estimateGas({
|
||||
to: account,
|
||||
from: contractAddress,
|
||||
data: contractMethod,
|
||||
});
|
||||
|
||||
if (data.lastBaseFeePerGas && data.maxFeePerGas) {
|
||||
return {
|
||||
// converts also form ethers BigNumber to "normal" BigNumber
|
||||
basePrice: BigNumber(data.lastBaseFeePerGas.toString()),
|
||||
maxPrice: BigNumber(data.maxFeePerGas.toString()),
|
||||
gas: BigNumber(estGasAmount.toString()),
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
// NOOP - could not get the estimated gas or the fee data from
|
||||
// the network. This could happen if there's an issue with transaction
|
||||
// request parameters (e.g. to/from mismatch)
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the "current" gas price from the ethereum network.
|
||||
*/
|
||||
export const useGasPrice = (
|
||||
method: ContractMethod,
|
||||
interval = DEFAULT_INTERVAL
|
||||
): GasData | undefined => {
|
||||
const [gas, setGas] = useState<GasData | undefined>(undefined);
|
||||
const { provider, account } = useWeb3React();
|
||||
const { config } = useEthereumConfig();
|
||||
|
||||
useEffect(() => {
|
||||
if (!provider || !config || !account) return;
|
||||
|
||||
const retrieve = async () => {
|
||||
retrieveGasData(
|
||||
provider,
|
||||
account,
|
||||
config.collateral_bridge_contract.address,
|
||||
method
|
||||
).then((gasData) => {
|
||||
if (gasData) {
|
||||
setGas(gasData);
|
||||
}
|
||||
});
|
||||
};
|
||||
retrieve();
|
||||
|
||||
// Retrieves another estimation and prices in [interval] ms.
|
||||
let i: ReturnType<typeof setInterval>;
|
||||
if (interval > 0) {
|
||||
i = setInterval(() => {
|
||||
retrieve();
|
||||
}, interval);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (i) clearInterval(i);
|
||||
};
|
||||
}, [account, config, interval, method, provider]);
|
||||
|
||||
return gas;
|
||||
};
|
||||
@@ -27,6 +27,7 @@ import { useForm, Controller, useWatch } from 'react-hook-form';
|
||||
import { WithdrawLimits } from './withdraw-limits';
|
||||
import {
|
||||
ETHEREUM_EAGER_CONNECT,
|
||||
type GasData,
|
||||
useWeb3ConnectStore,
|
||||
useWeb3Disconnect,
|
||||
} from '@vegaprotocol/web3';
|
||||
@@ -56,6 +57,7 @@ export interface WithdrawFormProps {
|
||||
delay: number | undefined;
|
||||
onSelectAsset: (assetId: string) => void;
|
||||
submitWithdraw: (withdrawal: WithdrawalArgs) => void;
|
||||
gasPrice?: GasData;
|
||||
}
|
||||
|
||||
const WithdrawDelayNotification = ({
|
||||
@@ -117,6 +119,7 @@ export const WithdrawForm = ({
|
||||
delay,
|
||||
onSelectAsset,
|
||||
submitWithdraw,
|
||||
gasPrice,
|
||||
}: WithdrawFormProps) => {
|
||||
const t = useT();
|
||||
const ethereumAddress = useEthereumAddress();
|
||||
@@ -247,6 +250,7 @@ export const WithdrawForm = ({
|
||||
delay={delay}
|
||||
balance={balance}
|
||||
asset={selectedAsset}
|
||||
gas={gasPrice}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { CompactNumber } from '@vegaprotocol/react-helpers';
|
||||
import { WITHDRAW_THRESHOLD_TOOLTIP_TEXT } from '@vegaprotocol/assets';
|
||||
import { WITHDRAW_THRESHOLD_TOOLTIP_TEXT, useWETH } from '@vegaprotocol/assets';
|
||||
import {
|
||||
KeyValueTable,
|
||||
KeyValueTableRow,
|
||||
@@ -9,6 +9,16 @@ import {
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { useT } from './use-t';
|
||||
import { type GasData } from '@vegaprotocol/web3';
|
||||
import {
|
||||
asETH,
|
||||
formatEther,
|
||||
formatNumber,
|
||||
removeDecimal,
|
||||
toQUSD,
|
||||
unitiseEther,
|
||||
} from '@vegaprotocol/utils';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface WithdrawLimitsProps {
|
||||
amount: string;
|
||||
@@ -16,6 +26,7 @@ interface WithdrawLimitsProps {
|
||||
balance: BigNumber;
|
||||
delay: number | undefined;
|
||||
asset: Asset;
|
||||
gas?: GasData;
|
||||
}
|
||||
|
||||
export const WithdrawLimits = ({
|
||||
@@ -24,6 +35,7 @@ export const WithdrawLimits = ({
|
||||
balance,
|
||||
delay,
|
||||
asset,
|
||||
gas,
|
||||
}: WithdrawLimitsProps) => {
|
||||
const t = useT();
|
||||
const delayTime =
|
||||
@@ -64,6 +76,24 @@ export const WithdrawLimits = ({
|
||||
label: t('Delay time'),
|
||||
value: threshold && delay ? delayTime : '-',
|
||||
},
|
||||
{
|
||||
key: 'GAS_FEE',
|
||||
tooltip: t(
|
||||
'Estimated gas fee for the withdrawal transaction (refreshes each 15 seconds)'
|
||||
),
|
||||
label: t('Gas fee'),
|
||||
value: gas ? (
|
||||
<GasPrice
|
||||
gasPrice={gas}
|
||||
amount={{
|
||||
value: removeDecimal(amount, asset.decimals),
|
||||
quantum: asset.quantum,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -91,3 +121,117 @@ export const WithdrawLimits = ({
|
||||
</KeyValueTable>
|
||||
);
|
||||
};
|
||||
|
||||
const GasPrice = ({
|
||||
gasPrice,
|
||||
amount,
|
||||
}: {
|
||||
gasPrice: WithdrawLimitsProps['gas'];
|
||||
amount: { value: string; quantum: string };
|
||||
}) => {
|
||||
const t = useT();
|
||||
const { quantum: wethQuantum } = useWETH();
|
||||
const { value, quantum } = amount;
|
||||
if (gasPrice) {
|
||||
const {
|
||||
basePrice: basePricePerGas,
|
||||
maxPrice: maxPricePerGas,
|
||||
gas,
|
||||
} = gasPrice;
|
||||
const basePrice = basePricePerGas.multipliedBy(gas);
|
||||
const maxPrice = maxPricePerGas.multipliedBy(gas);
|
||||
|
||||
const basePriceQUSD = toQUSD(basePrice, wethQuantum);
|
||||
const maxPriceQUSD = toQUSD(maxPrice, wethQuantum);
|
||||
|
||||
const withdrawalAmountQUSD = toQUSD(value, quantum);
|
||||
|
||||
const isExpensive =
|
||||
!withdrawalAmountQUSD.isLessThanOrEqualTo(0) &&
|
||||
withdrawalAmountQUSD.isLessThanOrEqualTo(maxPriceQUSD);
|
||||
const expensiveClassNames = {
|
||||
'text-vega-red-500':
|
||||
isExpensive && withdrawalAmountQUSD.isLessThanOrEqualTo(basePriceQUSD),
|
||||
'text-vega-orange-500':
|
||||
isExpensive &&
|
||||
withdrawalAmountQUSD.isGreaterThan(basePriceQUSD) &&
|
||||
withdrawalAmountQUSD.isLessThanOrEqualTo(maxPriceQUSD),
|
||||
};
|
||||
|
||||
const uBasePricePerGas = unitiseEther(basePricePerGas);
|
||||
const uMaxPricePerGas = unitiseEther(
|
||||
maxPricePerGas,
|
||||
uBasePricePerGas[1] // forces the same unit as min price
|
||||
);
|
||||
|
||||
const uBasePrice = unitiseEther(basePrice);
|
||||
const uMaxPrice = unitiseEther(maxPrice, uBasePrice[1]);
|
||||
|
||||
let range = (
|
||||
<span>
|
||||
{formatEther(uBasePrice, 0, true)} - {formatEther(uMaxPrice)}
|
||||
</span>
|
||||
);
|
||||
// displays range as ETH when it's greater that 1000000 gwei
|
||||
if (uBasePrice[0].isGreaterThan(1e6)) {
|
||||
range = (
|
||||
<span className="flex flex-col font-mono md:text-[11px]">
|
||||
<span>
|
||||
{t('min')}: {asETH(basePrice)}
|
||||
</span>
|
||||
<span>
|
||||
{t('max')}: {asETH(maxPrice)}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames('flex flex-col items-end self-end')}>
|
||||
<Tooltip description={t('The current gas price range')}>
|
||||
<span>
|
||||
{/* base price per gas unit */}
|
||||
{formatEther(uBasePricePerGas, 0, true)} -{' '}
|
||||
{formatEther(uMaxPricePerGas)} / gas
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
description={
|
||||
<div className="flex flex-col gap-1">
|
||||
{isExpensive && (
|
||||
<span className={classNames(expensiveClassNames)}>
|
||||
{t(
|
||||
"It seems that the current gas prices are exceeding the amount you're trying to withdraw"
|
||||
)}{' '}
|
||||
<strong>
|
||||
(~{formatNumber(withdrawalAmountQUSD, 2)} qUSD)
|
||||
</strong>
|
||||
.
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
{formatNumber(gas)} gas × {asETH(basePricePerGas)} <br />{' '}
|
||||
= {asETH(basePrice)}
|
||||
</span>
|
||||
<span>
|
||||
{formatNumber(gas)} gas × {asETH(maxPricePerGas)} <br /> ={' '}
|
||||
{asETH(maxPrice)}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<span className={classNames(expensiveClassNames, 'text-xs')}>
|
||||
{range}
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
<span className="text-muted text-xs">
|
||||
~{formatNumber(basePriceQUSD, 2)} - {formatNumber(maxPriceQUSD, 2)}{' '}
|
||||
qUSD
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return '-';
|
||||
};
|
||||
|
||||
@@ -38,6 +38,7 @@ jest.mock('@vegaprotocol/web3', () => ({
|
||||
useGetWithdrawDelay: () => {
|
||||
return () => Promise.resolve(10000);
|
||||
},
|
||||
useGasPrice: () => undefined,
|
||||
}));
|
||||
|
||||
describe('WithdrawManager', () => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { WithdrawForm } from './withdraw-form';
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import type { AccountFieldsFragment } from '@vegaprotocol/accounts';
|
||||
import { useWithdrawAsset } from './use-withdraw-asset';
|
||||
import { ContractMethod, useGasPrice } from '@vegaprotocol/web3';
|
||||
|
||||
export interface WithdrawManagerProps {
|
||||
assets: Asset[];
|
||||
@@ -20,6 +21,8 @@ export const WithdrawManager = ({
|
||||
}: WithdrawManagerProps) => {
|
||||
const { asset, balance, min, threshold, delay, handleSelectAsset } =
|
||||
useWithdrawAsset(assets, accounts, assetId);
|
||||
const gasPrice = useGasPrice(ContractMethod.WITHDRAW_ASSET);
|
||||
|
||||
return (
|
||||
<WithdrawForm
|
||||
selectedAsset={asset}
|
||||
@@ -30,6 +33,7 @@ export const WithdrawManager = ({
|
||||
submitWithdraw={submit}
|
||||
threshold={threshold}
|
||||
delay={delay}
|
||||
gasPrice={gasPrice}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user