Compare commits

..
Author SHA1 Message Date
asiaznik d99366c885 fix: entitiy scope to individual scope in tests 2024-01-31 14:11:34 +01:00
Matthew Russell 11e337aa8e test(trading): misc competition fixes (#5674) 2024-01-31 14:11:34 +01:00
Matthew Russell fda31f1151 chore: enable teams on default env 2024-01-31 14:11:09 +01:00
Matthew Russell be9919d00c chore: enable teams on testnet 2024-01-31 14:11:09 +01:00
Matthew Russell 6f64ee198a chore: enable teams and competitions on devnet 2024-01-31 14:11:09 +01:00
Art 8a2df85458 feat(trading): competitions update team (#5673) 2024-01-31 14:11:09 +01:00
Ben efdc3b3a16 chore(trading): competitions e2e test (#5652) 2024-01-31 14:11:09 +01:00
Matthew Russell c83911aecd fix(trading): competitions and teams fixes (#5649) 2024-01-31 14:11:09 +01:00
Matthew Russell d5182a3088 feat(trading): teams information for referral page (#5643) 2024-01-31 14:11:09 +01:00
Ben e2398d1619 chore(trading): teams test for local dev (#5644) 2024-01-31 14:11:09 +01:00
Matthew Russell 89d5e1b1d7 chore(trading): competitions fixes (#5636) 2024-01-31 14:11:09 +01:00
Matthew Russell 9ba52d7c38 feat(trading): join team (#5629) 2024-01-31 14:11:09 +01:00
Matthew Russell 661284347b feat(trading): create team (#5610) 2024-01-31 14:11:08 +01:00
asiaznik b58bcacd12 chore: fallback team avatars 2024-01-31 14:11:08 +01:00
asiaznik 6a810d0057 chore: team avatar sizes 2024-01-31 14:11:08 +01:00
asiaznik 8e59efbf13 chore: sky layout, blurred layout 2024-01-31 14:11:08 +01:00
asiaznik 4f227c5275 chore: married with epic branch 2024-01-31 14:11:08 +01:00
asiaznik 5d1426da0d chore(trading): competitions home page
feat(trading): competitions
2024-01-31 14:11:08 +01:00
Matthew Russell 3c6d9661a5 Feat/5486 team page (#5620) 2024-01-31 14:11:08 +01:00
59 changed files with 1252 additions and 1429 deletions
@@ -32,7 +32,6 @@ import { TxDetailsCreateReferralSet } from './tx-create-referral-set';
import { TxDetailsApplyReferralCode } from './tx-apply-referral-code';
import { TxDetailsUpdateReferralSet } from './tx-update-referral-set';
import { TxDetailsJoinTeam } from './tx-join-team';
import { TxDetailsUpdateMarginMode } from './tx-update-margin-mode';
interface TxDetailsWrapperProps {
txData: BlockExplorerTransactionResult | undefined;
@@ -134,8 +133,6 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
return TxDetailsApplyReferralCode;
case 'Join Team':
return TxDetailsJoinTeam;
case 'Update Margin Mode':
return TxDetailsUpdateMarginMode;
default:
return TxDetailsGeneric;
}
@@ -1,60 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import type { components } from '../../../../types/explorer';
import { MarketLink } from '../../links';
interface TxDetailsUpdateMarginModeProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
type Mode = components['schemas']['UpdateMarginModeMode'];
const MarginModeLabels: Record<Mode, string> = {
MODE_CROSS_MARGIN: t('Cross margin'),
MODE_ISOLATED_MARGIN: t('Isolated margin'),
MODE_UNSPECIFIED: t('Unspecified'),
};
export const TxDetailsUpdateMarginMode = ({
txData,
pubKey,
blockData,
}: TxDetailsUpdateMarginModeProps) => {
if (!txData || !txData.command.updateMarginMode) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const u: components['schemas']['v1UpdateMarginMode'] =
txData.command.updateMarginMode;
return (
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
{u.marketId && (
<TableRow modifier="bordered">
<TableCell>{t('Market ID')}</TableCell>
<TableCell>
<MarketLink id={u.marketId} />
</TableCell>
</TableRow>
)}
{u.mode && (
<TableRow modifier="bordered">
<TableCell>{t('New margin mode')}</TableCell>
<TableCell>{MarginModeLabels[u.mode]}</TableCell>
</TableRow>
)}
{u.marginFactor && (
<TableRow modifier="bordered">
<TableCell>{t('Margin factor')}</TableCell>
<TableCell>{u.marginFactor}</TableCell>
</TableRow>
)}
</TableWithTbody>
);
};
@@ -44,7 +44,6 @@ export type FilterOption =
| 'Transfer Funds'
| 'Undelegate'
| 'Update Referral Set'
| 'Update Margin Mode'
| 'Validator Heartbeat'
| 'Vote on Proposal'
| 'Withdraw';
@@ -60,7 +59,6 @@ export const filterOptions: Record<string, FilterOption[]> = {
'Stop Orders Submission',
'Stop Orders Cancellation',
'Submit Order',
'Update Margin Mode',
],
'Transfers and Withdrawals': [
'Transfer Funds',
@@ -31,7 +31,7 @@ export const CompetitionsCreateTeam = () => {
<div className="mx-auto md:w-2/3 max-w-xl">
<Box className="flex flex-col gap-4">
<h1 className="calt text-2xl lg:text-3xl xl:text-4xl">
{isSolo ? t('Create solo team') : t('Create a team')}
{t('Create a team')}
</h1>
{pubKey && !isReadOnly ? (
<CreateTeamFormContainer isSolo={isSolo} />
@@ -125,7 +125,7 @@ const CreateTeamFormContainer = ({ isSolo }: { isSolo: boolean }) => {
onSubmit={onSubmit}
status={status}
err={err}
isCreatingSoloTeam={isSolo}
isSolo={isSolo}
/>
);
};
@@ -31,7 +31,10 @@ export const CompetitionsHome = () => {
currentEpoch,
});
const { data: teamsData, loading: teamsLoading } = useTeams();
const { data: teamsData, loading: teamsLoading } = useTeams({
sortByField: ['totalQuantumRewards'],
order: 'desc',
});
return (
<ErrorBoundary>
@@ -38,11 +38,12 @@ export const CompetitionsTeam = () => {
const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => {
const t = useT();
const { pubKey } = useVegaWallet();
const { data, team, partyTeam, stats, members, games, loading, refetch } =
useTeam(teamId, pubKey || undefined);
const { team, partyTeam, stats, members, games, loading, refetch } = useTeam(
teamId,
pubKey || undefined
);
// only show spinner on first load so when users join teams its smoother
if (!data && loading) {
if (loading) {
return (
<Splash>
<Loader />
@@ -99,10 +100,8 @@ const TeamPage = ({
>
{team.name}
</h1>
<div className="flex gap-2">
<JoinTeam team={team} partyTeam={partyTeam} refetch={refetch} />
<UpdateTeamButton team={team} />
</div>
<JoinTeam team={team} partyTeam={partyTeam} refetch={refetch} />
<UpdateTeamButton team={team} />
</div>
</header>
<TeamStats stats={stats} members={members} games={games} />
@@ -185,10 +184,7 @@ const Members = ({ members }: { members?: Member[] }) => {
const data = orderBy(
members.map((m) => ({
referee: <RefereeLink pubkey={m.referee} isCreator={m.isCreator} />,
rewards: formatNumber(m.totalQuantumRewards),
volume: formatNumber(m.totalQuantumVolume),
gamesPlayed: formatNumber(m.totalGamesPlayed),
referee: <RefereeLink pubkey={m.referee} />,
joinedAt: getDateTimeFormat().format(new Date(m.joinedAt)),
joinedAtEpoch: Number(m.joinedAtEpoch),
})),
@@ -199,10 +195,7 @@ const Members = ({ members }: { members?: Member[] }) => {
return (
<Table
columns={[
{ name: 'referee', displayName: t('Member ID') },
{ name: 'rewards', displayName: t('Rewards earned') },
{ name: 'volume', displayName: t('Total volume') },
{ name: 'gamesPlayed', displayName: t('Games played') },
{ name: 'referee', displayName: t('Referee') },
{
name: 'joinedAt',
displayName: t('Joined at'),
@@ -218,24 +211,14 @@ const Members = ({ members }: { members?: Member[] }) => {
);
};
const RefereeLink = ({
pubkey,
isCreator,
}: {
pubkey: string;
isCreator: boolean;
}) => {
const t = useT();
const RefereeLink = ({ pubkey }: { pubkey: string }) => {
const linkCreator = useLinks(DApp.Explorer);
const link = linkCreator(EXPLORER_PARTIES.replace(':id', pubkey));
return (
<>
<Link to={link} target="_blank" className="underline underline-offset-4">
{truncateMiddle(pubkey)}
</Link>{' '}
<span className="text-muted text-xs">{isCreator ? t('Owner') : ''}</span>
</>
<Link to={link} target="_blank" className="underline underline-offset-4">
{truncateMiddle(pubkey)}
</Link>
);
};
@@ -17,7 +17,10 @@ export const CompetitionsTeams = () => {
usePageTitle([t('Competitions'), t('Teams')]);
const { data: teamsData, loading: teamsLoading } = useTeams();
const { data: teamsData, loading: teamsLoading } = useTeams({
sortByField: ['totalQuantumRewards'],
order: 'desc',
});
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}
isCreatingSoloTeam={team.closed}
isSolo={team.closed}
onSubmit={onSubmit}
defaultValues={defaultValues}
/>
@@ -6,7 +6,11 @@ import {
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { useSimpleTransaction, useVegaWallet } from '@vegaprotocol/wallet';
import {
useSimpleTransaction,
useVegaWallet,
type Status,
} from '@vegaprotocol/wallet';
import { useT } from '../../lib/use-t';
import { type Team } from '../../lib/hooks/use-team';
import { useState } from 'react';
@@ -23,8 +27,19 @@ 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
@@ -41,10 +56,11 @@ export const JoinTeam = ({
{confirmDialog !== undefined && (
<DialogContent
type={confirmDialog}
status={status}
team={team}
partyTeam={partyTeam}
onConfirm={joinTeam}
onCancel={() => setConfirmDialog(undefined)}
refetch={refetch}
/>
)}
</Dialog>
@@ -94,7 +110,7 @@ export const JoinButton = ({
// Not creator of the team, but still can't switch because
// creators cannot leave their own team
return (
<Tooltip description={t('As a team creator, you cannot switch teams')}>
<Tooltip description="As a team creator, you cannot switch teams">
<Button intent={Intent.Primary} disabled={true}>
{t('Switch team')}{' '}
</Button>
@@ -133,39 +149,21 @@ 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>;
}
@@ -215,7 +213,7 @@ const DialogContent = ({
</>
)}
<div className="flex justify-between gap-2">
<Button onClick={joinTeam} intent={Intent.Success}>
<Button onClick={onConfirm} intent={Intent.Success}>
{t('Confirm')}
</Button>
<Button onClick={onCancel} intent={Intent.Danger}>
@@ -6,8 +6,6 @@ import {
TextArea,
TradingButton,
Intent,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { URL_REGEX, isValidVegaPublicKey } from '@vegaprotocol/utils';
@@ -19,8 +17,6 @@ import type {
UpdateReferralSet,
Status,
} from '@vegaprotocol/wallet';
import classNames from 'classnames';
import { useLayoutEffect, useState } from 'react';
export type FormFields = {
id: string;
@@ -32,8 +28,8 @@ export type FormFields = {
};
export enum TransactionType {
CreateReferralSet = 'CreateReferralSet',
UpdateReferralSet = 'UpdateReferralSet',
CreateReferralSet,
UpdateReferralSet,
}
const prepareTransaction = (
@@ -79,14 +75,14 @@ export const TeamForm = ({
type,
status,
err,
isCreatingSoloTeam,
isSolo,
onSubmit,
defaultValues,
}: {
type: TransactionType;
status: ReturnType<typeof useReferralSetTransaction>['status'];
err: ReturnType<typeof useReferralSetTransaction>['err'];
isCreatingSoloTeam: boolean;
isSolo: boolean;
onSubmit: ReturnType<typeof useReferralSetTransaction>['onSubmit'];
defaultValues?: FormFields;
}) => {
@@ -100,7 +96,7 @@ export const TeamForm = ({
formState: { errors },
} = useForm<FormFields>({
defaultValues: {
private: isCreatingSoloTeam,
private: isSolo,
...defaultValues,
},
});
@@ -113,7 +109,12 @@ export const TeamForm = ({
return (
<form onSubmit={handleSubmit(sendTransaction)}>
<input type="hidden" {...register('id')} />
<input
type="hidden"
{...register('id', {
disabled: true,
})}
/>
<TradingFormGroup label={t('Team name')} labelFor="name">
<TradingInput {...register('name', { required: t('Required') })} />
{errors.name?.message && (
@@ -159,70 +160,59 @@ export const TeamForm = ({
</TradingInputError>
)}
</TradingFormGroup>
{
// allow changing to private/public if editing, but don't show these options if making a solo team
(type === TransactionType.UpdateReferralSet || !isCreatingSoloTeam) && (
<>
<TradingFormGroup
label={t('Make team private')}
labelFor="private"
hideLabel={true}
>
<Controller
name="private"
control={control}
render={({ field }) => {
return (
<TradingCheckbox
label={t('Make team private')}
checked={field.value}
onCheckedChange={(value) => {
field.onChange(value);
}}
/>
);
<TradingFormGroup
label={t('Make team private')}
labelFor="private"
hideLabel={true}
>
<Controller
name="private"
control={control}
render={({ field }) => {
return (
<TradingCheckbox
label={t('Make team private')}
checked={field.value}
onCheckedChange={(value) => {
field.onChange(value);
}}
disabled={isSolo}
/>
</TradingFormGroup>
{isPrivate && (
<TradingFormGroup
label={t('Public key allow list')}
labelFor="allowList"
labelDescription={t(
'Use a comma separated list to allow only specific public keys to join the team'
)}
>
<TextArea
{...register('allowList', {
required: t('Required'),
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}
</p>
);
}}
/>
</TradingFormGroup>
{isPrivate && (
<TradingFormGroup
label={t('Public key allow list')}
labelFor="allowList"
labelDescription={t(
'Use a comma separated list to allow only specific public keys to join the team'
)}
>
<TextArea
{...register('allowList', {
required: t('Required'),
disabled: isSolo,
validate: {
allowList: (value) => {
const publicKeys = parseAllowListText(value);
if (publicKeys.every((pk) => isValidVegaPublicKey(pk))) {
return true;
}
return t('Invalid public key found in allow list');
},
},
})}
/>
{errors.allowList?.message && (
<TradingInputError forInput="avatarUrl">
{errors.allowList.message}
</TradingInputError>
)}
</TradingFormGroup>
)}
{err && <p className="text-danger text-xs mb-4 capitalize">{err}</p>}
<SubmitButton type={type} status={status} />
</form>
);
@@ -243,56 +233,20 @@ 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 (
<div className="flex gap-2 items-baseline">
<TradingButton type="submit" intent={Intent.Info} disabled={disabled}>
{text}
</TradingButton>
{status === 'confirmed' && confirmed}
</div>
<TradingButton type="submit" intent={Intent.Info} disabled={disabled}>
{text}
</TradingButton>
);
};
const parseAllowListText = (str: string = '') => {
const parseAllowListText = (str: string) => {
return str
.split(',')
.map((v) => v.trim())
@@ -2,10 +2,8 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
import { type Team } from '../../lib/hooks/use-team';
import { Intent, TradingAnchorButton } from '@vegaprotocol/ui-toolkit';
import { Links } from '../../lib/links';
import { useT } from '../../lib/use-t';
export const UpdateTeamButton = ({ team }: { team: Team }) => {
const t = useT();
const { pubKey, isReadOnly } = useVegaWallet();
if (pubKey && !isReadOnly && pubKey === team.referrer) {
@@ -14,9 +12,7 @@ export const UpdateTeamButton = ({ team }: { team: Team }) => {
data-testid="update-team-button"
href={Links.COMPETITIONS_UPDATE_TEAM(team.teamId)}
intent={Intent.Info}
>
{t('Update team')}
</TradingAnchorButton>
/>
);
}
+1 -1
View File
@@ -1,3 +1,3 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
VEGA_VERSION=v0.74.0-preview.8
VEGA_VERSION=v0.74.0-preview.7
LOCAL_SERVER=false
@@ -38,7 +38,7 @@ def test_switch_cross_isolated_margin(
expect(page.get_by_test_id("toast-content")).to_have_text(
"ConfirmedYour transaction has been confirmedView in block explorerUpdate margin modeBTC:DAI_2023Isolated margin mode, leverage: 1.0x")
expect(page.locator(margin_row).nth(1)
).to_have_text("22,109.99996Isolated1.0x")
).to_have_text("11,109.99996Isolated1.0x")
# tbd - tooltip is not visible without this wait
page.wait_for_timeout(1000)
page.get_by_test_id(tab_positions).get_by_text("Isolated").hover()
@@ -23,6 +23,7 @@ def continuous_market(vega):
return setup_continuous_market(vega)
@pytest.mark.skip("marked id issue #5681")
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_should_display_info_and_button_for_deposit(continuous_market, page: Page):
page.goto(f"/#/markets/{continuous_market}")
+2 -3
View File
@@ -137,7 +137,6 @@ 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")
@@ -153,7 +152,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 (4)")
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")
@@ -162,7 +161,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("4")
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"
+1 -18
View File
@@ -17,7 +17,7 @@ fragment TeamStatsFields on TeamStatistics {
totalGamesPlayed
quantumRewards {
epoch
totalQuantumRewards
total_quantum_rewards
}
gamesPlayed
}
@@ -51,13 +51,6 @@ fragment TeamGameFields on Game {
}
}
fragment TeamMemberStatsFields on TeamMemberStatistics {
partyId
totalQuantumVolume
totalQuantumRewards
totalGamesPlayed
}
query Team($teamId: ID!, $partyId: ID, $aggregationEpochs: Int) {
teams(teamId: $teamId) {
edges {
@@ -94,14 +87,4 @@ query Team($teamId: ID!, $partyId: ID, $aggregationEpochs: Int) {
}
}
}
teamMembersStatistics(
teamId: $teamId
aggregationEpochs: $aggregationEpochs
) {
edges {
node {
...TeamMemberStatsFields
}
}
}
}
+4 -22
View File
@@ -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, totalQuantumRewards: 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 TeamRefereeFieldsFragment = { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number };
@@ -13,8 +13,6 @@ 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']>;
@@ -22,7 +20,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, 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 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 const TeamFieldsFragmentDoc = gql`
fragment TeamFields on Team {
@@ -45,7 +43,7 @@ export const TeamStatsFieldsFragmentDoc = gql`
totalGamesPlayed
quantumRewards {
epoch
totalQuantumRewards
total_quantum_rewards
}
gamesPlayed
}
@@ -82,14 +80,6 @@ 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) {
@@ -127,19 +117,11 @@ export const TeamDocument = gql`
}
}
}
teamMembersStatistics(teamId: $teamId, aggregationEpochs: $aggregationEpochs) {
edges {
node {
...TeamMemberStatsFields
}
}
}
}
${TeamFieldsFragmentDoc}
${TeamStatsFieldsFragmentDoc}
${TeamRefereeFieldsFragmentDoc}
${TeamGameFieldsFragmentDoc}
${TeamMemberStatsFieldsFragmentDoc}`;
${TeamGameFieldsFragmentDoc}`;
/**
* __useTeamQuery__
+10 -49
View File
@@ -6,24 +6,17 @@ 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 & {
isCreator: boolean;
totalGamesPlayed: number;
totalQuantumVolume: string;
totalQuantumRewards: string;
};
export type Member = TeamRefereeFieldsFragment;
export type TeamEntity = TeamEntityFragment;
export type TeamGame = ReturnType<typeof useTeam>['games'][number];
export type MemberStats = TeamMemberStatsFieldsFragment;
export const useTeam = (teamId?: string, partyId?: string) => {
const queryResult = useTeamQuery({
const { data, loading, error, refetch } = useTeamQuery({
variables: {
teamId: teamId || '',
partyId,
@@ -33,11 +26,7 @@ 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;
@@ -45,40 +34,9 @@ export const useTeam = (teamId?: string, partyId?: string) => {
const teamStatsEdge = data?.teamsStatistics?.edges.find(
(e) => e.node.teamId === teamId
);
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,
});
}
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) => {
@@ -102,9 +60,12 @@ export const useTeam = (teamId?: string, partyId?: string) => {
const games = orderBy(compact(gamesWithTeam), 'epoch', 'desc');
return {
...queryResult,
data,
loading,
error,
refetch,
stats: teamStatsEdge?.node,
team,
team: teamEdge?.node,
members,
games,
partyTeam,
+32 -6
View File
@@ -1,12 +1,34 @@
import orderBy from 'lodash/orderBy';
import { useMemo } from 'react';
import { useTeamsQuery } from './__generated__/Teams';
import { useTeamsStatisticsQuery } from './__generated__/TeamsStatistics';
import { type TeamsQuery, useTeamsQuery } from './__generated__/Teams';
import {
type TeamsStatisticsQuery,
useTeamsStatisticsQuery,
} from './__generated__/TeamsStatistics';
import compact from 'lodash/compact';
import sortBy from 'lodash/sortBy';
import { type ArrayElement } from 'type-fest/source/internal';
type SortableField = keyof Omit<
ArrayElement<NonNullable<TeamsQuery['teams']>['edges']>['node'] &
ArrayElement<
NonNullable<TeamsStatisticsQuery['teamsStatistics']>['edges']
>['node'],
'__typename'
>;
type UseTeamsArgs = {
aggregationEpochs?: number;
sortByField?: SortableField[];
order?: 'asc' | 'desc';
};
export const DEFAULT_AGGREGATION_EPOCHS = 10;
export const useTeams = (aggregationEpochs = DEFAULT_AGGREGATION_EPOCHS) => {
export const useTeams = ({
aggregationEpochs = DEFAULT_AGGREGATION_EPOCHS,
sortByField = ['createdAtEpoch'],
order = 'asc',
}: UseTeamsArgs) => {
const {
data: teamsData,
loading: teamsLoading,
@@ -35,8 +57,12 @@ export const useTeams = (aggregationEpochs = DEFAULT_AGGREGATION_EPOCHS) => {
...stats.find((s) => s.teamId === t.teamId),
}));
return orderBy(data, (d) => Number(d.totalQuantumRewards || 0), 'desc');
}, [teams, stats]);
const sorted = sortBy(data, sortByField);
if (order === 'desc') {
return sorted.reverse();
}
return sorted;
}, [teams, sortByField, order, stats]);
return {
data,
@@ -0,0 +1,15 @@
import type { Account } from './accounts-data-provider';
import * as Schema from '@vegaprotocol/types';
interface Props {
accounts: Account[] | null;
marketId: string;
}
export const getMarketAccount = ({ accounts, marketId }: Props) =>
accounts?.find((account) => {
return (
account.market?.id === marketId &&
account.type === Schema.AccountType.ACCOUNT_TYPE_MARGIN
);
}) || null;
+2 -1
View File
@@ -6,7 +6,8 @@ export * from './accounts-manager';
export * from './breakdown-table';
export * from './use-account-balance';
export * from './get-settlement-account';
export * from './use-margin-account-balance';
export * from './use-market-account-balance';
export * from './__generated__/Margins';
export { MarginHealthChart } from './margin-health-chart';
export * from './margin-data-provider';
export * from './transfer-container';
@@ -83,25 +83,3 @@ export const marketMarginDataProvider = makeDerivedDataProvider<
(margin) => margin.market.id === marketId
) || null
);
export type MarginModeData = Pick<
MarginFieldsFragment,
'marginMode' | 'marginFactor'
>;
export const marginModeDataProvider = makeDerivedDataProvider<
MarginModeData,
never,
MarginsQueryVariables & { marketId: string }
>([marketMarginDataProvider], ([data], variables, previousData) =>
produce(previousData, (draft) => {
if (!data) {
return data;
}
const newData = {
marginMode: (data as MarginFieldsFragment).marginMode,
marginFactor: (data as MarginFieldsFragment).marginFactor,
};
return draft ? Object.assign(draft, newData) : newData;
})
);
@@ -0,0 +1,253 @@
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { Tooltip, ExternalLink } from '@vegaprotocol/ui-toolkit';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { marketMarginDataProvider } from './margin-data-provider';
import { useAssetsMapProvider } from '@vegaprotocol/assets';
import { useT, ns } from './use-t';
import { useAccountBalance } from './use-account-balance';
import { useMarketAccountBalance } from './use-market-account-balance';
import { Trans } from 'react-i18next';
const MarginHealthChartTooltipRow = ({
label,
value,
decimals,
href,
}: {
label: string;
value: string;
decimals: number;
href?: string;
}) => (
<>
<div
className="float-left clear-left"
key="label"
data-testid="margin-health-tooltip-label"
>
{href ? (
<ExternalLink href={href} target="_blank">
{label}
</ExternalLink>
) : (
label
)}
</div>
<div
className="float-right"
key="value"
data-testid="margin-health-tooltip-value"
>
{addDecimalsFormatNumber(value, decimals)}
</div>
</>
);
export const MarginHealthChartTooltip = ({
maintenanceLevel,
searchLevel,
initialLevel,
collateralReleaseLevel,
decimals,
marginAccountBalance,
}: {
maintenanceLevel: string;
searchLevel: string;
initialLevel: string;
collateralReleaseLevel: string;
decimals: number;
marginAccountBalance?: string;
}) => {
const t = useT();
const tooltipContent = [
<MarginHealthChartTooltipRow
key={'maintenance'}
label={t('maintenance level')}
href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-maintenance"
value={maintenanceLevel}
decimals={decimals}
/>,
<MarginHealthChartTooltipRow
key={'search'}
label={t('search level')}
href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-searching-for-collateral"
value={searchLevel}
decimals={decimals}
/>,
<MarginHealthChartTooltipRow
key={'initial'}
label={t('initial level')}
href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-initial"
value={initialLevel}
decimals={decimals}
/>,
<MarginHealthChartTooltipRow
key={'release'}
label={t('release level')}
href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-releasing-collateral"
value={collateralReleaseLevel}
decimals={decimals}
/>,
];
if (marginAccountBalance) {
const balance = (
<MarginHealthChartTooltipRow
key={'balance'}
label={t('balance')}
value={marginAccountBalance}
decimals={decimals}
/>
);
if (BigInt(marginAccountBalance) < BigInt(searchLevel)) {
tooltipContent.splice(1, 0, balance);
} else if (BigInt(marginAccountBalance) < BigInt(initialLevel)) {
tooltipContent.splice(2, 0, balance);
} else if (BigInt(marginAccountBalance) < BigInt(collateralReleaseLevel)) {
tooltipContent.splice(3, 0, balance);
} else {
tooltipContent.push(balance);
}
}
return (
<div className="overflow-hidden" data-testid="margin-health-tooltip">
{tooltipContent}
</div>
);
};
export const MarginHealthChart = ({
marketId,
assetId,
}: {
marketId: string;
assetId: string;
}) => {
const { data: assetsMap } = useAssetsMapProvider();
const { pubKey: partyId } = useVegaWallet();
const { data } = useDataProvider({
dataProvider: marketMarginDataProvider,
variables: { marketId, partyId: partyId ?? '' },
skip: !partyId,
});
const { accountBalance: rawGeneralAccountBalance } =
useAccountBalance(assetId);
const { accountBalance: rawMarginAccountBalance } =
useMarketAccountBalance(marketId);
const asset = assetsMap && assetsMap[assetId];
if (!data || !asset) {
return null;
}
const { decimals } = asset;
const collateralReleaseLevel = Number(data.collateralReleaseLevel);
const initialLevel = Number(data.initialLevel);
const maintenanceLevel = Number(data.maintenanceLevel);
const searchLevel = Number(data.searchLevel);
const marginAccountBalance = Number(rawMarginAccountBalance);
const generalAccountBalance = Number(rawGeneralAccountBalance);
const max = Math.max(
marginAccountBalance + generalAccountBalance,
collateralReleaseLevel
);
const red = maintenanceLevel / max;
const orange = (searchLevel - maintenanceLevel) / max;
const yellow = ((searchLevel + initialLevel) / 2 - searchLevel) / max;
const green = (collateralReleaseLevel - initialLevel) / max + yellow;
const balanceMarker = marginAccountBalance / max;
const tooltip = (
<MarginHealthChartTooltip
maintenanceLevel={data.maintenanceLevel}
searchLevel={data.searchLevel}
initialLevel={data.initialLevel}
collateralReleaseLevel={data.collateralReleaseLevel}
marginAccountBalance={rawMarginAccountBalance}
decimals={decimals}
/>
);
return (
<div data-testid="margin-health-chart">
<Trans
defaults="{{balance}} above <0>maintenance level</0>"
components={[
<ExternalLink href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-maintenance">
maintenance level
</ExternalLink>,
]}
values={{
balance: addDecimalsFormatNumber(
(
BigInt(marginAccountBalance) - BigInt(maintenanceLevel)
).toString(),
decimals
),
}}
ns={ns}
/>
<Tooltip description={tooltip}>
<div
data-testid="margin-health-chart-track"
className="relative bg-vega-green-650"
style={{
height: '6px',
marginBottom: '1px',
display: 'flex',
}}
>
<div
data-testid="margin-health-chart-red"
className="bg-vega-red-550"
style={{
height: '100%',
width: `${red * 100}%`,
}}
></div>
<div
data-testid="margin-health-chart-orange"
className="bg-vega-orange"
style={{
height: '100%',
width: `${orange * 100}%`,
}}
></div>
<div
data-testid="margin-health-chart-yellow"
className="bg-vega-yellow"
style={{
height: '100%',
width: `${yellow * 100}%`,
}}
></div>
<div
data-testid="margin-health-chart-green"
className="bg-vega-green-600"
style={{
height: '100%',
width: `${green * 100}%`,
}}
></div>
{balanceMarker > 0 && balanceMarker < 100 && (
<div
data-testid="margin-health-chart-balance"
className="absolute bg-vega-blue"
style={{
height: '8px',
width: '8px',
top: '-1px',
transform: 'translate(-4px, 0px)',
borderRadius: '50%',
border: '1px solid white',
backgroundColor: 'blue',
left: `${balanceMarker * 100}%`,
}}
></div>
)}
</div>
</Tooltip>
</div>
);
};
@@ -0,0 +1,158 @@
import {
MarginHealthChart,
MarginHealthChartTooltip,
} from './margin-health-chart';
import { act, render, screen } from '@testing-library/react';
import type { MarginFieldsFragment } from './__generated__/Margins';
import type { AssetFieldsFragment } from '@vegaprotocol/assets';
import { MarginMode } from '@vegaprotocol/types';
const asset: AssetFieldsFragment = {
id: 'assetId',
decimals: 2,
} as AssetFieldsFragment;
const margins: MarginFieldsFragment = {
asset: {
id: 'assetId',
},
collateralReleaseLevel: '1000',
initialLevel: '800',
searchLevel: '600',
maintenanceLevel: '400',
marginFactor: '',
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
orderMarginLevel: '',
market: {
id: 'marketId',
},
};
const mockGetMargins = jest.fn(() => margins);
const mockGetBalance = jest.fn(() => '0');
jest.mock('./margin-data-provider', () => ({}));
jest.mock('@vegaprotocol/assets', () => ({
useAssetsMapProvider: () => {
return {
data: {
assetId: asset,
},
};
},
}));
jest.mock('@vegaprotocol/wallet', () => ({
useVegaWallet: () => {
return {
pubKey: 'partyId',
};
},
}));
jest.mock('@vegaprotocol/data-provider', () => ({
useDataProvider: () => {
return {
data: mockGetMargins(),
};
},
}));
jest.mock('./use-account-balance', () => ({
useAccountBalance: () => {
return {
accountBalance: mockGetBalance(),
};
},
}));
jest.mock('./use-market-account-balance', () => ({
useMarketAccountBalance: () => {
return {
accountBalance: '700',
};
},
}));
describe('MarginHealthChart', () => {
it('should render correct values', async () => {
render(<MarginHealthChart marketId="marketId" assetId="assetId" />);
const chart = screen.getByTestId('margin-health-chart');
expect(chart).toHaveTextContent('3.00 above maintenance level');
const red = screen.getByTestId('margin-health-chart-red');
const orange = screen.getByTestId('margin-health-chart-orange');
const yellow = screen.getByTestId('margin-health-chart-yellow');
const green = screen.getByTestId('margin-health-chart-green');
const balance = screen.getByTestId('margin-health-chart-balance');
expect(parseInt(red.style.width)).toBe(40);
expect(parseInt(orange.style.width)).toBe(20);
expect(parseInt(yellow.style.width)).toBe(10);
expect(parseInt(green.style.width)).toBe(30);
expect(parseInt(balance.style.left)).toBe(70);
});
it('should use correct scale', async () => {
mockGetBalance.mockReturnValueOnce('1300');
await act(async () => {
render(<MarginHealthChart marketId="marketId" assetId="assetId" />);
});
await screen.findByTestId('margin-health-chart');
const red = screen.getByTestId('margin-health-chart-red');
expect(parseInt(red.style.width)).toBe(20);
});
});
describe('MarginHealthChartTooltip', () => {
it('renders correct values and labels', async () => {
await act(async () => {
render(
<MarginHealthChartTooltip
{...margins}
decimals={asset.decimals}
marginAccountBalance="500"
/>
);
});
const labels = await screen.findAllByTestId('margin-health-tooltip-label');
const expectedLabels = [
'maintenance level',
'balance',
'search level',
'initial level',
'release level',
];
labels.forEach((value, i) => {
expect(value).toHaveTextContent(expectedLabels[i]);
});
const values = await screen.findAllByTestId('margin-health-tooltip-value');
const expectedValues = ['4.00', '5.00', '6.00', '8.00', '10.00'];
values.forEach((value, i) => {
expect(value).toHaveTextContent(expectedValues[i]);
});
});
it('renders balance in correct place', async () => {
const { rerender } = render(
<MarginHealthChartTooltip
{...margins}
decimals={asset.decimals}
marginAccountBalance="700"
/>
);
let values = await screen.findAllByTestId('margin-health-tooltip-value');
expect(values[2]).toHaveTextContent('7.00');
rerender(
<MarginHealthChartTooltip
{...margins}
decimals={asset.decimals}
marginAccountBalance="900"
/>
);
values = await screen.findAllByTestId('margin-health-tooltip-value');
expect(values.length).toBe(5);
expect(values[3]).toHaveTextContent('9.00');
});
});
@@ -1,68 +0,0 @@
import { useCallback, useMemo, useState } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { accountsDataProvider } from './accounts-data-provider';
import type { Account } from './accounts-data-provider';
import { AccountType } from '@vegaprotocol/types';
export const useMarginAccountBalance = (marketId: string) => {
const { pubKey } = useVegaWallet();
const [marginAccountBalance, setMarginAccountBalance] = useState<string>('');
const [orderMarginAccountBalance, setOrderMarginAccountBalance] =
useState<string>('');
const [accountDecimals, setAccountDecimals] = useState<number | null>(null);
const update = useCallback(
({ data }: { data: Account[] | null }) => {
const marginAccount = data?.find((account) => {
return (
account.market?.id === marketId &&
account.type === AccountType.ACCOUNT_TYPE_MARGIN
);
});
const orderMarginAccount = data?.find((account) => {
return (
account.market?.id === marketId &&
account.type === AccountType.ACCOUNT_TYPE_ORDER_MARGIN
);
});
if (marginAccount?.balance) {
setMarginAccountBalance(marginAccount?.balance || '');
}
if (orderMarginAccount?.balance) {
setOrderMarginAccountBalance(orderMarginAccount?.balance || '');
}
const decimals =
orderMarginAccount?.asset.decimals || marginAccount?.asset.decimals;
if (decimals) {
setAccountDecimals(decimals);
}
return true;
},
[marketId]
);
const { loading, error } = useDataProvider({
dataProvider: accountsDataProvider,
variables: { partyId: pubKey || '' },
skip: !pubKey || !marketId,
update,
});
return useMemo(
() => ({
marginAccountBalance: pubKey ? marginAccountBalance : '',
orderMarginAccountBalance: pubKey ? orderMarginAccountBalance : '',
accountDecimals: pubKey ? accountDecimals : null,
loading,
error,
}),
[
marginAccountBalance,
orderMarginAccountBalance,
accountDecimals,
pubKey,
loading,
error,
]
);
};
@@ -0,0 +1,41 @@
import { useCallback, useMemo, useState } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { accountsDataProvider } from './accounts-data-provider';
import type { Account } from './accounts-data-provider';
import { getMarketAccount } from './get-market-account';
export const useMarketAccountBalance = (marketId: string) => {
const { pubKey } = useVegaWallet();
const [accountBalance, setAccountBalance] = useState<string>('');
const [accountDecimals, setAccountDecimals] = useState<number | null>(null);
const update = useCallback(
({ data }: { data: Account[] | null }) => {
const account = getMarketAccount({ accounts: data, marketId });
if (account?.balance) {
setAccountBalance(account?.balance || '');
}
if (account?.asset.decimals) {
setAccountDecimals(account?.asset.decimals || null);
}
return true;
},
[marketId]
);
const { loading, error } = useDataProvider({
dataProvider: accountsDataProvider,
variables: { partyId: pubKey || '' },
skip: !pubKey || !marketId,
update,
});
return useMemo(
() => ({
accountBalance: pubKey ? accountBalance : '',
accountDecimals: pubKey ? accountDecimals : null,
loading,
error,
}),
[accountBalance, accountDecimals, pubKey, loading, error]
);
};
@@ -7,7 +7,6 @@ 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';
@@ -90,24 +89,3 @@ 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
};
};
@@ -1,14 +1,47 @@
import { getAsset } from '@vegaprotocol/markets';
import { useCallback, useState } from 'react';
import { getAsset, getQuoteName } from '@vegaprotocol/markets';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { useVegaWallet } from '@vegaprotocol/wallet';
import type { Market } from '@vegaprotocol/markets';
import { formatNumberPercentage, formatValue } from '@vegaprotocol/utils';
import type { EstimatePositionQuery } from '@vegaprotocol/positions';
import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
import {
formatNumberPercentage,
formatRange,
formatValue,
} from '@vegaprotocol/utils';
import { marketMarginDataProvider } from '@vegaprotocol/accounts';
import { useDataProvider } from '@vegaprotocol/data-provider';
import * as AccordionPrimitive from '@radix-ui/react-accordion';
import * as Schema from '@vegaprotocol/types';
import {
MARGIN_DIFF_TOOLTIP_TEXT,
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
TOTAL_MARGIN_AVAILABLE,
LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT,
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
MARGIN_ACCOUNT_TOOLTIP_TEXT,
} from '../../constants';
import { useEstimateFees } from '../../hooks/use-estimate-fees';
import { KeyValue } from './key-value';
import { Intent, Pill } from '@vegaprotocol/ui-toolkit';
import {
Accordion,
AccordionChevron,
AccordionPanel,
Intent,
ExternalLink,
Pill,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import BigNumber from 'bignumber.js';
import { FeesBreakdown } from '../fees-breakdown';
import { getTotalDiscountFactor, getDiscountedFee } from '../discounts';
import { useT } from '../../use-t';
import { useT, ns } from '../../use-t';
import { Trans } from 'react-i18next';
export const emptyValue = '-';
@@ -86,3 +119,337 @@ export const DealTicketFeeDetails = ({
/>
);
};
export interface DealTicketMarginDetailsProps {
generalAccountBalance?: string;
marginAccountBalance?: string;
market: Market;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
assetSymbol: string;
positionEstimate: EstimatePositionQuery['estimatePosition'];
side: Schema.Side;
}
export const DealTicketMarginDetails = ({
marginAccountBalance,
generalAccountBalance,
assetSymbol,
market,
onMarketClick,
positionEstimate,
side,
}: DealTicketMarginDetailsProps) => {
const t = useT();
const [breakdownDialog, setBreakdownDialog] = useState(false);
const { pubKey: partyId } = useVegaWallet();
const { data: currentMargins } = useDataProvider({
dataProvider: marketMarginDataProvider,
variables: { marketId: market.id, partyId: partyId || '' },
skip: !partyId,
});
const liquidationEstimate = positionEstimate?.liquidation;
const marginEstimate = positionEstimate?.margin;
const totalBalance =
BigInt(generalAccountBalance || '0') + BigInt(marginAccountBalance || '0');
const asset = getAsset(market);
const { decimals: assetDecimals, quantum } = asset;
let marginRequiredBestCase: string | undefined = undefined;
let marginRequiredWorstCase: string | undefined = undefined;
if (marginEstimate) {
if (currentMargins) {
marginRequiredBestCase = (
BigInt(marginEstimate.bestCase.initialLevel) -
BigInt(currentMargins.initialLevel)
).toString();
if (marginRequiredBestCase.startsWith('-')) {
marginRequiredBestCase = '0';
}
marginRequiredWorstCase = (
BigInt(marginEstimate.worstCase.initialLevel) -
BigInt(currentMargins.initialLevel)
).toString();
if (marginRequiredWorstCase.startsWith('-')) {
marginRequiredWorstCase = '0';
}
} else {
marginRequiredBestCase = marginEstimate.bestCase.initialLevel;
marginRequiredWorstCase = marginEstimate.worstCase.initialLevel;
}
}
const totalMarginAvailable = (
currentMargins
? totalBalance - BigInt(currentMargins.maintenanceLevel)
: totalBalance
).toString();
let deductionFromCollateral = null;
let projectedMargin = null;
if (marginAccountBalance) {
const deductionFromCollateralBestCase =
BigInt(marginEstimate?.bestCase.initialLevel ?? 0) -
BigInt(marginAccountBalance);
const deductionFromCollateralWorstCase =
BigInt(marginEstimate?.worstCase.initialLevel ?? 0) -
BigInt(marginAccountBalance);
deductionFromCollateral = (
<KeyValue
indent
label={t('Deduction from collateral')}
value={formatRange(
deductionFromCollateralBestCase > 0
? deductionFromCollateralBestCase.toString()
: '0',
deductionFromCollateralWorstCase > 0
? deductionFromCollateralWorstCase.toString()
: '0',
assetDecimals
)}
formattedValue={formatValue(
deductionFromCollateralWorstCase > 0
? deductionFromCollateralWorstCase.toString()
: '0',
assetDecimals,
quantum
)}
symbol={assetSymbol}
labelDescription={t(
'DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT',
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
{ assetSymbol }
)}
/>
);
projectedMargin = (
<KeyValue
label={t('Projected margin')}
value={formatRange(
marginEstimate?.bestCase.initialLevel,
marginEstimate?.worstCase.initialLevel,
assetDecimals
)}
formattedValue={formatValue(
marginEstimate?.worstCase.initialLevel,
assetDecimals,
quantum
)}
symbol={assetSymbol}
labelDescription={t(
'EST_TOTAL_MARGIN_TOOLTIP_TEXT',
EST_TOTAL_MARGIN_TOOLTIP_TEXT
)}
/>
);
}
let liquidationPriceEstimate = emptyValue;
let liquidationPriceEstimateRange = emptyValue;
if (liquidationEstimate) {
const liquidationEstimateBestCaseIncludingBuyOrders = BigInt(
liquidationEstimate.bestCase.including_buy_orders.replace(/\..*/, '')
);
const liquidationEstimateBestCaseIncludingSellOrders = BigInt(
liquidationEstimate.bestCase.including_sell_orders.replace(/\..*/, '')
);
const liquidationEstimateBestCase =
side === Schema.Side.SIDE_BUY
? liquidationEstimateBestCaseIncludingBuyOrders
: liquidationEstimateBestCaseIncludingSellOrders;
const liquidationEstimateWorstCaseIncludingBuyOrders = BigInt(
liquidationEstimate.worstCase.including_buy_orders.replace(/\..*/, '')
);
const liquidationEstimateWorstCaseIncludingSellOrders = BigInt(
liquidationEstimate.worstCase.including_sell_orders.replace(/\..*/, '')
);
const liquidationEstimateWorstCase =
side === Schema.Side.SIDE_BUY
? liquidationEstimateWorstCaseIncludingBuyOrders
: liquidationEstimateWorstCaseIncludingSellOrders;
liquidationPriceEstimate = formatValue(
liquidationEstimateWorstCase.toString(),
market.decimalPlaces,
undefined,
market.decimalPlaces
);
liquidationPriceEstimateRange = formatRange(
(liquidationEstimateBestCase < liquidationEstimateWorstCase
? liquidationEstimateBestCase
: liquidationEstimateWorstCase
).toString(),
(liquidationEstimateBestCase > liquidationEstimateWorstCase
? liquidationEstimateBestCase
: liquidationEstimateWorstCase
).toString(),
market.decimalPlaces,
undefined,
market.decimalPlaces
);
}
const onAccountBreakdownDialogClose = useCallback(
() => setBreakdownDialog(false),
[]
);
const quoteName = getQuoteName(market);
return (
<div className="flex flex-col w-full gap-2 pt-2">
<Accordion>
<AccordionPanel
itemId="margin"
trigger={
<AccordionPrimitive.Trigger
data-testid="accordion-toggle"
className={classNames(
'w-full',
'flex items-center gap-2 text-xs',
'group'
)}
>
<div
data-testid={`deal-ticket-fee-margin-required`}
key={'value-dropdown'}
className="flex items-center justify-between w-full gap-2"
>
<div className="flex items-center text-left gap-1">
<Tooltip
description={t(
'MARGIN_DIFF_TOOLTIP_TEXT',
MARGIN_DIFF_TOOLTIP_TEXT,
{ assetSymbol }
)}
>
<span className="text-muted">{t('Margin required')}</span>
</Tooltip>
<AccordionChevron size={10} />
</div>
<Tooltip
description={
formatRange(
marginRequiredBestCase,
marginRequiredWorstCase,
assetDecimals
) ?? '-'
}
>
<div className="font-mono text-right">
{formatValue(
marginRequiredWorstCase,
assetDecimals,
quantum
)}{' '}
{assetSymbol || ''}
</div>
</Tooltip>
</div>
</AccordionPrimitive.Trigger>
}
>
<div className="flex flex-col w-full gap-2">
<KeyValue
label={t('Total margin available')}
indent
value={formatValue(totalMarginAvailable, assetDecimals)}
formattedValue={formatValue(
totalMarginAvailable,
assetDecimals,
quantum
)}
symbol={assetSymbol}
labelDescription={t(
'TOTAL_MARGIN_AVAILABLE',
TOTAL_MARGIN_AVAILABLE,
{
generalAccountBalance: formatValue(
generalAccountBalance,
assetDecimals,
quantum
),
marginAccountBalance: formatValue(
marginAccountBalance,
assetDecimals,
quantum
),
marginMaintenance: formatValue(
currentMargins?.maintenanceLevel,
assetDecimals,
quantum
),
assetSymbol,
}
)}
/>
{deductionFromCollateral}
<KeyValue
label={t('Current margin allocation')}
indent
onClick={
generalAccountBalance
? () => setBreakdownDialog(true)
: undefined
}
value={formatValue(marginAccountBalance, assetDecimals)}
symbol={assetSymbol}
labelDescription={t(
'MARGIN_ACCOUNT_TOOLTIP_TEXT',
MARGIN_ACCOUNT_TOOLTIP_TEXT
)}
formattedValue={formatValue(
marginAccountBalance,
assetDecimals,
quantum
)}
/>
</div>
</AccordionPanel>
</Accordion>
{projectedMargin}
<KeyValue
label={t('Liquidation')}
value={liquidationPriceEstimateRange}
formattedValue={liquidationPriceEstimate}
symbol={quoteName}
labelDescription={
<>
<span>
{t(
'LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT',
LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT
)}
</span>{' '}
<span>
<Trans
defaults="For full details please see <0>liquidation price estimate documentation</0>."
components={[
<ExternalLink
href={
'https://github.com/vegaprotocol/specs/blob/master/non-protocol-specs/0012-NP-LIPE-liquidation-price-estimate.md'
}
>
liquidation price estimate documentation
</ExternalLink>,
]}
ns={ns}
/>
</span>
</>
}
/>
{partyId && (
<AccountBreakdownDialog
assetId={breakdownDialog ? asset.id : undefined}
partyId={partyId}
onMarketClick={onMarketClick}
onClose={onAccountBreakdownDialogClose}
/>
)}
</div>
);
};
@@ -26,25 +26,12 @@ import {
import classNames from 'classnames';
import { useT, ns } from '../../use-t';
import { Trans } from 'react-i18next';
import type { Market } from '@vegaprotocol/markets';
import type { DealTicketMarginDetailsProps } from './deal-ticket-fee-details';
import { emptyValue } from './deal-ticket-fee-details';
import type { EstimatePositionQuery } from '@vegaprotocol/positions';
export interface DealTicketMarginDetailsProps {
generalAccountBalance?: string;
marginAccountBalance?: string;
orderMarginAccountBalance?: string;
market: Market;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
assetSymbol: string;
positionEstimate: EstimatePositionQuery['estimatePosition'];
side: Schema.Side;
}
export const DealTicketMarginDetails = ({
marginAccountBalance,
generalAccountBalance,
orderMarginAccountBalance,
assetSymbol,
market,
onMarketClick,
@@ -61,44 +48,31 @@ export const DealTicketMarginDetails = ({
});
const liquidationEstimate = positionEstimate?.liquidation;
const marginEstimate = positionEstimate?.margin;
const totalMarginAccountBalance =
BigInt(marginAccountBalance || '0') +
BigInt(orderMarginAccountBalance || '0');
const totalBalance =
BigInt(generalAccountBalance || '0') + totalMarginAccountBalance;
BigInt(generalAccountBalance || '0') + BigInt(marginAccountBalance || '0');
const asset = getAsset(market);
const { decimals: assetDecimals, quantum } = asset;
let marginRequiredBestCase: string | undefined = undefined;
let marginRequiredWorstCase: string | undefined = undefined;
const marginEstimateBestCase =
BigInt(marginEstimate?.bestCase.initialLevel ?? 0) +
BigInt(marginEstimate?.bestCase.orderMarginLevel ?? 0);
const marginEstimateWorstCase =
BigInt(marginEstimate?.worstCase.initialLevel ?? 0) +
BigInt(marginEstimate?.worstCase.orderMarginLevel ?? 0);
if (marginEstimate) {
if (currentMargins) {
const currentMargin =
BigInt(currentMargins.initialLevel) +
BigInt(currentMargins.orderMarginLevel);
marginRequiredBestCase = (
marginEstimateBestCase - currentMargin
BigInt(marginEstimate.bestCase.initialLevel) -
BigInt(currentMargins.initialLevel)
).toString();
if (marginRequiredBestCase.startsWith('-')) {
marginRequiredBestCase = '0';
}
marginRequiredWorstCase = (
marginEstimateWorstCase - currentMargin
BigInt(marginEstimate.worstCase.initialLevel) -
BigInt(currentMargins.initialLevel)
).toString();
if (marginRequiredWorstCase.startsWith('-')) {
marginRequiredWorstCase = '0';
}
} else {
marginRequiredBestCase = marginEstimateBestCase.toString();
marginRequiredWorstCase = marginEstimateWorstCase.toString();
marginRequiredBestCase = marginEstimate.bestCase.initialLevel;
marginRequiredWorstCase = marginEstimate.worstCase.initialLevel;
}
}
@@ -110,12 +84,14 @@ export const DealTicketMarginDetails = ({
let deductionFromCollateral = null;
let projectedMargin = null;
if (totalMarginAccountBalance) {
if (marginAccountBalance) {
const deductionFromCollateralBestCase =
marginEstimateBestCase - totalMarginAccountBalance;
BigInt(marginEstimate?.bestCase.initialLevel ?? 0) -
BigInt(marginAccountBalance);
const deductionFromCollateralWorstCase =
marginEstimateWorstCase - totalMarginAccountBalance;
BigInt(marginEstimate?.worstCase.initialLevel ?? 0) -
BigInt(marginAccountBalance);
deductionFromCollateral = (
<KeyValue
@@ -149,12 +125,12 @@ export const DealTicketMarginDetails = ({
<KeyValue
label={t('Projected margin')}
value={formatRange(
marginEstimateBestCase.toString(),
marginEstimateWorstCase.toString(),
marginEstimate?.bestCase.initialLevel,
marginEstimate?.worstCase.initialLevel,
assetDecimals
)}
formattedValue={formatValue(
marginEstimateWorstCase.toString(),
marginEstimate?.worstCase.initialLevel,
assetDecimals,
quantum
)}
@@ -300,11 +276,6 @@ export const DealTicketMarginDetails = ({
assetDecimals,
quantum
),
orderMarginAccountBalance: formatValue(
orderMarginAccountBalance,
assetDecimals,
quantum
),
marginMaintenance: formatValue(
currentMargins?.maintenanceLevel,
assetDecimals,
@@ -323,17 +294,14 @@ export const DealTicketMarginDetails = ({
? () => setBreakdownDialog(true)
: undefined
}
value={formatValue(
totalMarginAccountBalance.toString(),
assetDecimals
)}
value={formatValue(marginAccountBalance, assetDecimals)}
symbol={assetSymbol}
labelDescription={t(
'MARGIN_ACCOUNT_TOOLTIP_TEXT',
MARGIN_ACCOUNT_TOOLTIP_TEXT
)}
formattedValue={formatValue(
totalMarginAccountBalance.toString(),
marginAccountBalance,
assetDecimals,
quantum
)}
@@ -58,9 +58,8 @@ import type {
} from '@vegaprotocol/markets';
import { MarginWarning } from '../deal-ticket-validation/margin-warning';
import {
useMarginAccountBalance,
useMarketAccountBalance,
useAccountBalance,
marginModeDataProvider,
} from '@vegaprotocol/accounts';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { type OrderFormValues } from '../../hooks';
@@ -167,10 +166,9 @@ export const DealTicket = ({
const asset = getAsset(market);
const {
orderMarginAccountBalance,
marginAccountBalance,
accountBalance: marginAccountBalance,
loading: loadingMarginAccountBalance,
} = useMarginAccountBalance(market.id);
} = useMarketAccountBalance(market.id);
const {
accountBalance: generalAccountBalance,
@@ -178,9 +176,7 @@ export const DealTicket = ({
} = useAccountBalance(asset.id);
const balance = (
BigInt(marginAccountBalance) +
BigInt(generalAccountBalance) +
BigInt(orderMarginAccountBalance)
BigInt(marginAccountBalance) + BigInt(generalAccountBalance)
).toString();
const { marketState, marketTradingMode } = marketData;
@@ -245,19 +241,7 @@ export const DealTicket = ({
variables: { partyId: pubKey || '', marketId: market.id },
skip: !pubKey,
});
const { data: margin } = useDataProvider({
dataProvider: marginModeDataProvider,
variables: { partyId: pubKey || '', marketId: market.id },
skip: !pubKey,
});
const { openVolume, averageEntryPrice } = useOpenVolume(
pubKey,
market.id
) || {
openVolume: '0',
averageEntryPrice: '0',
};
const openVolume = useOpenVolume(pubKey, market.id) ?? '0';
const orders = activeOrders
? activeOrders.map<Schema.OrderInfo>((order) => ({
isMarketOrder: order.type === Schema.OrderType.TYPE_MARKET,
@@ -275,25 +259,21 @@ export const DealTicket = ({
});
}
const positionEstimate = usePositionEstimate(
{
marketId: market.id,
openVolume,
averageEntryPrice,
orders,
marginAccountBalance: marginAccountBalance || '0',
generalAccountBalance: generalAccountBalance || '0',
orderMarginAccountBalance: orderMarginAccountBalance || '0',
marginFactor: margin?.marginFactor || '1',
marginMode:
margin?.marginMode || Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN,
includeCollateralIncreaseInAvailableCollateral: true,
},
!normalizedOrder ||
const positionEstimate = usePositionEstimate({
marketId: market.id,
openVolume,
orders,
marginAccountBalance: marginAccountBalance,
generalAccountBalance: generalAccountBalance,
orderMarginAccountBalance: '0', // TODO: Get real balance
marginMode: Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN, // TODO: unhardcode this and get users margin mode for the market
averageEntryPrice: marketPrice || '0', // TODO: This assumes the order will be entirely filled at the current market price
skip:
!normalizedOrder ||
(normalizedOrder.type !== Schema.OrderType.TYPE_MARKET &&
(!normalizedOrder.price || normalizedOrder.price === '0')) ||
normalizedOrder.size === '0'
);
normalizedOrder.size === '0',
});
const assetSymbol = getAsset(market).symbol;
@@ -339,9 +319,7 @@ export const DealTicket = ({
}
const hasNoBalance =
!BigInt(generalAccountBalance) &&
!BigInt(marginAccountBalance) &&
!BigInt(orderMarginAccountBalance);
!BigInt(generalAccountBalance) && !BigInt(marginAccountBalance);
if (
hasNoBalance &&
!(loadingMarginAccountBalance || loadingGeneralAccountBalance)
@@ -371,7 +349,6 @@ export const DealTicket = ({
marketTradingMode,
generalAccountBalance,
marginAccountBalance,
orderMarginAccountBalance,
loadingMarginAccountBalance,
loadingGeneralAccountBalance,
pubKey,
@@ -730,16 +707,10 @@ export const DealTicket = ({
asset={asset}
marketTradingMode={marketData.marketTradingMode}
balance={balance}
margin={(
BigInt(
positionEstimate?.estimatePosition?.margin.bestCase.initialLevel ||
'0'
) +
BigInt(
positionEstimate?.estimatePosition?.margin.bestCase
.orderMarginLevel || '0'
)
).toString()}
margin={
positionEstimate?.estimatePosition?.margin.bestCase.initialLevel ||
'0'
}
isReadOnly={isReadOnly}
pubKey={pubKey}
onDeposit={onDeposit}
@@ -772,7 +743,6 @@ export const DealTicket = ({
onMarketClick={onMarketClick}
assetSymbol={asset.symbol}
marginAccountBalance={marginAccountBalance}
orderMarginAccountBalance={orderMarginAccountBalance}
generalAccountBalance={generalAccountBalance}
positionEstimate={positionEstimate?.estimatePosition}
market={market}
@@ -798,20 +768,8 @@ interface SummaryMessageProps {
export const NoWalletWarning = ({
isReadOnly,
noWalletConnected,
}: Pick<SummaryMessageProps, 'isReadOnly'> & {
noWalletConnected?: boolean;
}) => {
}: Pick<SummaryMessageProps, 'isReadOnly'>) => {
const t = useT();
if (noWalletConnected) {
return (
<div className="mb-2">
<InputError testId="deal-ticket-error-message-summary">
{t('You need a Vega wallet to start trading on this market')}
</InputError>
</div>
);
}
if (isReadOnly) {
return (
<div className="mb-2">
@@ -1,12 +1,9 @@
import { useDataProvider } from '@vegaprotocol/data-provider';
import * as Schema from '@vegaprotocol/types';
import {
TradingButton as Button,
TradingInput as Input,
FormGroup,
LeverageSlider,
Notification,
Intent,
} from '@vegaprotocol/ui-toolkit';
import { MarginMode, useVegaWallet } from '@vegaprotocol/wallet';
import * as Types from '@vegaprotocol/types';
@@ -18,151 +15,15 @@ import { Dialog } from '@vegaprotocol/ui-toolkit';
import { useEffect, useState } from 'react';
import { useT } from '../../use-t';
import classnames from 'classnames';
import {
marginModeDataProvider,
useAccountBalance,
useMarginAccountBalance,
} from '@vegaprotocol/accounts';
import { useMaxLeverage, useOpenVolume } from '@vegaprotocol/positions';
import { activeOrdersProvider } from '@vegaprotocol/orders';
import { usePositionEstimate } from '../../hooks/use-position-estimate';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { getAsset, useMarket } from '@vegaprotocol/markets';
import { NoWalletWarning } from './deal-ticket';
import { marketMarginDataProvider } from '@vegaprotocol/accounts';
import { useMaxLeverage } from '@vegaprotocol/positions';
const defaultLeverage = 10;
export const MarginChange = ({
partyId,
marketId,
marginMode,
marginFactor,
}: {
partyId: string | null;
marketId: string;
marginMode: Types.MarginMode;
marginFactor: string;
}) => {
const t = useT();
const { data: market } = useMarket(marketId);
const asset = market && getAsset(market);
const {
marginAccountBalance,
orderMarginAccountBalance,
loading: marginAccountBalanceLoading,
} = useMarginAccountBalance(marketId);
const {
accountBalance: generalAccountBalance,
loading: generalAccountBalanceLoading,
} = useAccountBalance(asset?.id);
const { openVolume, averageEntryPrice } = useOpenVolume(
partyId,
marketId
) || {
openVolume: '0',
averageEntryPrice: '0',
};
const { data: activeOrders } = useDataProvider({
dataProvider: activeOrdersProvider,
variables: { partyId: partyId || '', marketId },
});
const orders = activeOrders
? activeOrders.map<Schema.OrderInfo>((order) => ({
isMarketOrder: order.type === Schema.OrderType.TYPE_MARKET,
price: order.price,
remaining: order.remaining,
side: order.side,
}))
: [];
const skip =
(!orders?.length && openVolume === '0') ||
marginAccountBalanceLoading ||
generalAccountBalanceLoading;
const estimateMargin = usePositionEstimate(
{
generalAccountBalance: generalAccountBalance || '0',
marginAccountBalance: marginAccountBalance || '0',
marginFactor,
marginMode,
averageEntryPrice,
openVolume,
marketId,
orderMarginAccountBalance: orderMarginAccountBalance || '0',
includeCollateralIncreaseInAvailableCollateral: true,
orders,
},
skip
);
if (
!asset ||
!estimateMargin?.estimatePosition?.collateralIncreaseEstimate.worstCase ||
estimateMargin.estimatePosition.collateralIncreaseEstimate.worstCase === '0'
) {
return null;
}
const collateralIncreaseEstimate = BigInt(
estimateMargin.estimatePosition.collateralIncreaseEstimate.worstCase
);
if (!collateralIncreaseEstimate) {
return null;
}
let positionWarning = '';
if (orders?.length && openVolume !== '0') {
positionWarning = t(
'youHaveOpenPositionAndOrders',
'You have an existing position and open orders on this market.',
{
count: orders.length,
}
);
} else if (!orders?.length) {
positionWarning = t('You have an existing position on this market.');
} else {
positionWarning = t(
'youHaveOpenOrders',
'You have open orders on this market.',
{
count: orders.length,
}
);
}
let marginChangeWarning = '';
const amount = addDecimalsFormatNumber(
collateralIncreaseEstimate.toString(),
asset?.decimals
);
const { symbol } = asset;
const interpolation = { amount, symbol };
if (marginMode === Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN) {
marginChangeWarning = t(
'Changing the margin mode will move {{amount}} {{symbol}} from your general account to fund the position.',
interpolation
);
} else {
marginChangeWarning = t(
'Changing the margin mode and leverage will move {{amount}} {{symbol}} from your general account to fund the position.',
interpolation
);
}
return (
<div className="mb-2">
<Notification
intent={Intent.Warning}
message={
<>
<p>{positionWarning}</p>
<p>{marginChangeWarning}</p>
</>
}
/>
</div>
);
};
interface MarginDialogProps {
open: boolean;
onClose: () => void;
marketId: string;
partyId: string;
create: VegaTransactionStore['create'];
}
@@ -172,7 +33,6 @@ const CrossMarginModeDialog = ({
marketId,
create,
}: MarginDialogProps) => {
const { pubKey: partyId, isReadOnly } = useVegaWallet();
const t = useT();
return (
<Dialog
@@ -200,24 +60,15 @@ const CrossMarginModeDialog = ({
)}
</p>
</div>
<MarginChange
marketId={marketId}
partyId={partyId}
marginMode={Types.MarginMode.MARGIN_MODE_CROSS_MARGIN}
marginFactor="1"
/>
<NoWalletWarning noWalletConnected={!partyId} isReadOnly={isReadOnly} />
<Button
className="w-full"
onClick={() => {
partyId &&
!isReadOnly &&
create({
updateMarginMode: {
marketId,
mode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
},
});
create({
updateMarginMode: {
marketId,
mode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
},
});
onClose();
}}
>
@@ -231,10 +82,10 @@ const IsolatedMarginModeDialog = ({
open,
onClose,
marketId,
partyId,
marginFactor,
create,
}: MarginDialogProps & { marginFactor: string }) => {
const { pubKey: partyId, isReadOnly } = useVegaWallet();
const [leverage, setLeverage] = useState(
Number((1 / Number(marginFactor)).toFixed(1))
);
@@ -278,15 +129,13 @@ const IsolatedMarginModeDialog = ({
</div>
<form
onSubmit={() => {
partyId &&
!isReadOnly &&
create({
updateMarginMode: {
marketId,
mode: MarginMode.MARGIN_MODE_ISOLATED_MARGIN,
marginFactor: `${1 / leverage}`,
},
});
create({
updateMarginMode: {
marketId,
mode: MarginMode.MARGIN_MODE_ISOLATED_MARGIN,
marginFactor: `${1 / leverage}`,
},
});
onClose();
}}
>
@@ -295,7 +144,7 @@ const IsolatedMarginModeDialog = ({
<LeverageSlider
max={max}
step={0.1}
value={[leverage || 1]}
value={[leverage]}
onValueChange={([value]) => setLeverage(value)}
/>
</div>
@@ -305,17 +154,10 @@ const IsolatedMarginModeDialog = ({
min={1}
max={max}
step={0.1}
value={leverage || ''}
value={leverage}
onChange={(e) => setLeverage(Number(e.target.value))}
/>
</FormGroup>
<MarginChange
marketId={marketId}
partyId={partyId}
marginMode={Types.MarginMode.MARGIN_MODE_ISOLATED_MARGIN}
marginFactor={`${1 / leverage}`}
/>
<NoWalletWarning noWalletConnected={!partyId} isReadOnly={isReadOnly} />
<Button className="w-full" type="submit">
{t('Confirm')}
</Button>
@@ -327,21 +169,27 @@ const IsolatedMarginModeDialog = ({
export const MarginModeSelector = ({ marketId }: { marketId: string }) => {
const t = useT();
const [dialog, setDialog] = useState<'cross' | 'isolated' | ''>();
const { pubKey: partyId } = useVegaWallet();
const { pubKey: partyId, isReadOnly } = useVegaWallet();
const { data: margin } = useDataProvider({
dataProvider: marginModeDataProvider,
dataProvider: marketMarginDataProvider,
variables: {
partyId: partyId || '',
marketId,
},
skip: !partyId,
});
useEffect(() => {
if (!partyId) {
setDialog('');
}
}, [partyId]);
const create = useVegaTransactionStore((state) => state.create);
const marginMode = margin?.marginMode;
const marginFactor =
margin?.marginFactor && margin?.marginFactor !== '0'
? margin?.marginFactor
: undefined;
const disabled = isReadOnly;
const onClose = () => setDialog(undefined);
const enabledModeClassName = 'bg-vega-clight-500 dark:bg-vega-cdark-500';
@@ -349,8 +197,8 @@ export const MarginModeSelector = ({ marketId }: { marketId: string }) => {
<>
<div className="mb-4 grid h-8 leading-8 font-alpha text-xs grid-cols-2">
<button
type="button"
onClick={() => setDialog('cross')}
disabled={disabled}
onClick={() => partyId && setDialog('cross')}
className={classnames('rounded', {
[enabledModeClassName]:
!marginMode ||
@@ -360,8 +208,8 @@ export const MarginModeSelector = ({ marketId }: { marketId: string }) => {
{t('Cross')}
</button>
<button
type="button"
onClick={() => setDialog('isolated')}
disabled={disabled}
onClick={() => partyId && setDialog('isolated')}
className={classnames('rounded', {
[enabledModeClassName]:
marginMode === Types.MarginMode.MARGIN_MODE_ISOLATED_MARGIN,
@@ -374,23 +222,25 @@ export const MarginModeSelector = ({ marketId }: { marketId: string }) => {
})}
</button>
</div>
{
{partyId && (
<CrossMarginModeDialog
partyId={partyId}
open={dialog === 'cross'}
onClose={onClose}
marketId={marketId}
create={create}
/>
}
{
)}
{partyId && (
<IsolatedMarginModeDialog
partyId={partyId}
open={dialog === 'isolated'}
onClose={onClose}
marketId={marketId}
create={create}
marginFactor={marginFactor || `${1 / defaultLeverage}`}
/>
}
)}
</>
);
};
+1 -1
View File
@@ -8,7 +8,7 @@ export const DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT =
'To cover the required margin, this amount will be drawn from your general ({{assetSymbol}}) account.';
export const TOTAL_MARGIN_AVAILABLE =
'Total margin available = general {{assetSymbol}} balance ({{generalAccountBalance}} {{assetSymbol}}) + margin balance ({{marginAccountBalance}} {{assetSymbol}}) + order margin balance ({{orderMarginAccountBalance}} {{assetSymbol}}) - maintenance level ({{marginMaintenance}} {{assetSymbol}}).';
'Total margin available = general {{assetSymbol}} balance ({{generalAccountBalance}} {{assetSymbol}}) + margin balance ({{marginAccountBalance}} {{assetSymbol}}) - maintenance level ({{marginMaintenance}} {{assetSymbol}}).';
export const CONTRACTS_MARGIN_TOOLTIP_TEXT =
'The number of contracts determines how many units of the futures contract to buy or sell. For example, this is similar to buying one share of a listed company. The value of 1 contract is equivalent to the price of the contract. For example, if the current price is $50, then one contract is worth $50.';
@@ -5,15 +5,37 @@ import {
import { useEstimatePositionQuery } from '@vegaprotocol/positions';
import { useEffect, useState } from 'react';
export const usePositionEstimate = (
variables: EstimatePositionQueryVariables,
skip: boolean
) => {
interface PositionEstimateProps extends EstimatePositionQueryVariables {
skip: boolean;
}
export const usePositionEstimate = ({
marketId,
openVolume,
orders,
generalAccountBalance,
marginAccountBalance,
orderMarginAccountBalance,
averageEntryPrice,
marginMode,
marginFactor,
skip,
}: PositionEstimateProps) => {
const [estimates, setEstimates] = useState<EstimatePositionQuery | undefined>(
undefined
);
const { data } = useEstimatePositionQuery({
variables,
variables: {
marketId,
openVolume,
orders,
generalAccountBalance,
marginAccountBalance,
orderMarginAccountBalance,
averageEntryPrice,
marginMode,
marginFactor,
},
skip,
fetchPolicy: 'no-cache',
});
+1 -12
View File
@@ -13,8 +13,6 @@
"Any orders placed now will not trade until the auction ends": "Any orders placed now will not trade until the auction ends",
"below": "below",
"Cancel": "Cancel",
"Changing the margin mode will move {{amount}} {{symbol}} from your general account to fund the position.": "Changing the margin mode will move {{amount}} {{symbol}} from your general account to fund the position.",
"Changing the margin mode and leverage will move {{amount}} {{symbol}} from your general account to fund the position.": "Changing the margin mode and leverage will move {{amount}} {{symbol}} from your general account to fund the position.",
"Closed": "Closed",
"Closing on {{time}}": "Closing on {{time}}",
"Confirm": "Confirm",
@@ -69,13 +67,6 @@
"One cancels another": "One cancels another",
"Only limit orders are permitted when market is in auction": "Only limit orders are permitted when market is in auction",
"Only your allocated margin will be used to fund this position, and if the maintenance margin is breached you will be closed out.": "Only your allocated margin will be used to fund this position, and if the maintenance margin is breached you will be closed out.",
"You have an existing position on this market.": "You have an existing position on this market.",
"youHaveOpenOrders_one": "You have an open order on this market.",
"youHaveOpenOrders_other": "You have open orders on this market.",
"youHaveOpenOrders": "You have open orders on this market.",
"youHaveOpenPositionAndOrders_one": "You have an existing position and and open order on this market.",
"youHaveOpenPositionAndOrders_other": "You have an existing position and open orders on this market.",
"youHaveOpenPositionAndOrders": "You have an existing position and open orders on this market.",
"Peak size": "Peak size",
"Peak size cannot be greater than the size ({{size}})": "Peak size cannot be greater than the size ({{size}})",
"Peak size cannot be lower than {{stepSize}}": "Peak size cannot be lower than {{stepSize}}",
@@ -133,7 +124,7 @@
"Total": "Total",
"Total fees": "Total fees",
"Total margin available": "Total margin available",
"TOTAL_MARGIN_AVAILABLE": "Total margin available = general {{assetSymbol}} balance ({{generalAccountBalance}} {{assetSymbol}}) + margin balance ({{marginAccountBalance}} {{assetSymbol}}) + order margin balance ({{orderMarginAccountBalance}} {{assetSymbol}}) - maintenance level ({{marginMaintenance}} {{assetSymbol}}).",
"TOTAL_MARGIN_AVAILABLE": "Total margin available = general {{assetSymbol}} balance ({{generalAccountBalance}} {{assetSymbol}}) + margin balance ({{marginAccountBalance}} {{assetSymbol}}) - maintenance level ({{marginMaintenance}} {{assetSymbol}}).",
"No trading": "No trading",
"Trailing percent offset cannot be higher than 99.9": "Trailing percent offset cannot be higher than 99.9",
"Trailing percent offset cannot be lower than {{trailingPercentOffsetStep}}": "Trailing percent offset cannot be lower than {{trailingPercentOffsetStep}}",
@@ -149,10 +140,8 @@
"You are setting this market to cross-margin mode.": "You are setting this market to cross-margin mode.",
"You are setting this market to isolated margin mode.": "You are setting this market to isolated margin mode.",
"You have only {{amount}}.": "You have only {{amount}}.",
"You have an existing position and open orders on this market": "You have an existing position and open orders on this market",
"You may not have enough margin available to open this position.": "You may not have enough margin available to open this position.",
"You need {{symbol}} in your wallet to trade in this market.": "You need {{symbol}} in your wallet to trade in this market.",
"You need a Vega wallet to start trading on this market": "You need a Vega wallet to start trading on this market",
"You need provide a expiry time/date": "You need provide a expiry time/date",
"You need provide a price": "You need provide a price",
"You need provide a trailing percent offset": "You need provide a trailing percent offset",
+1 -5
View File
@@ -63,7 +63,6 @@
"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",
"Create solo team": "Create solo team",
"Make your referral code a Team to compete in Competitions with your friends, appear in leaderboards on the <0>Competitions Homepage</0>, and earn rewards": "Make your referral code a Team to compete in Competitions with your friends, appear in leaderboards on the <0>Competitions Homepage</0>, and earn rewards",
"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.",
@@ -182,7 +181,6 @@
"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",
@@ -365,13 +363,11 @@
"Type": "Type",
"Unknown": "Unknown",
"Unknown settlement date": "Unknown settlement date",
"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",
"Vega Reward pot": "Vega Reward pot",
"Vega Wallet <0>full featured</0>": "Vega Wallet <0>full featured</0>",
"Vega chart": "Vega chart",
"Vega Wallet <0>full featured<0>": "Vega Wallet <0>full featured<0>",
"Vesting": "Vesting",
"Vesting multiplier": "Vesting multiplier",
"Vesting {{assetSymbol}}": "Vesting {{assetSymbol}}",
+1 -7
View File
@@ -47,11 +47,5 @@
"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>",
"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"
"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>"
}
-2
View File
@@ -113,8 +113,6 @@ export const filterAndSortClosedMarkets = (markets: MarketMaybeWithData[]) => {
return [
MarketState.STATE_SETTLED,
MarketState.STATE_TRADING_TERMINATED,
MarketState.STATE_CLOSED,
MarketState.STATE_CANCELLED,
].includes(m.data?.marketState || m.state);
});
};
+2 -14
View File
@@ -41,26 +41,24 @@ subscription PositionsSubscription($partyId: ID!) {
query EstimatePosition(
$marketId: ID!
$openVolume: String!
$averageEntryPrice: String!
$orders: [OrderInfo!]
$averageEntryPrice: String!
$marginAccountBalance: String!
$generalAccountBalance: String!
$orderMarginAccountBalance: String!
$marginMode: MarginMode!
$marginFactor: String
$includeCollateralIncreaseInAvailableCollateral: Boolean
) {
estimatePosition(
marketId: $marketId
openVolume: $openVolume
averageEntryPrice: $averageEntryPrice
orders: $orders
averageEntryPrice: $averageEntryPrice
marginAccountBalance: $marginAccountBalance
generalAccountBalance: $generalAccountBalance
orderMarginAccountBalance: $orderMarginAccountBalance
marginMode: $marginMode
marginFactor: $marginFactor
includeCollateralIncreaseInAvailableCollateral: $includeCollateralIncreaseInAvailableCollateral
# Everywhere in the codebase we expect price values of the underlying to have the right
# number of digits for formatting with market.decimalPlaces. By default the estimatePosition
# query will return a full value requiring formatting using asset.decimals. For consistency
@@ -73,24 +71,14 @@ query EstimatePosition(
searchLevel
initialLevel
collateralReleaseLevel
marginMode
marginFactor
orderMarginLevel
}
bestCase {
maintenanceLevel
searchLevel
initialLevel
collateralReleaseLevel
marginMode
marginFactor
orderMarginLevel
}
}
collateralIncreaseEstimate {
worstCase
bestCase
}
liquidation {
worstCase {
open_volume_only
+5 -18
View File
@@ -22,18 +22,17 @@ export type PositionsSubscriptionSubscription = { __typename?: 'Subscription', p
export type EstimatePositionQueryVariables = Types.Exact<{
marketId: Types.Scalars['ID'];
openVolume: Types.Scalars['String'];
averageEntryPrice: Types.Scalars['String'];
orders?: Types.InputMaybe<Array<Types.OrderInfo> | Types.OrderInfo>;
averageEntryPrice: Types.Scalars['String'];
marginAccountBalance: Types.Scalars['String'];
generalAccountBalance: Types.Scalars['String'];
orderMarginAccountBalance: Types.Scalars['String'];
marginMode: Types.MarginMode;
marginFactor?: Types.InputMaybe<Types.Scalars['String']>;
includeCollateralIncreaseInAvailableCollateral?: Types.InputMaybe<Types.Scalars['Boolean']>;
}>;
export type EstimatePositionQuery = { __typename?: 'Query', estimatePosition?: { __typename?: 'PositionEstimate', margin: { __typename?: 'MarginEstimate', worstCase: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, marginMode: Types.MarginMode, marginFactor: string, orderMarginLevel: string }, bestCase: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string, marginMode: Types.MarginMode, marginFactor: string, orderMarginLevel: string } }, collateralIncreaseEstimate: { __typename?: 'CollateralIncreaseEstimate', worstCase: string, bestCase: string }, liquidation?: { __typename?: 'LiquidationEstimate', worstCase: { __typename?: 'LiquidationPrice', open_volume_only: string, including_buy_orders: string, including_sell_orders: string }, bestCase: { __typename?: 'LiquidationPrice', open_volume_only: string, including_buy_orders: string, including_sell_orders: string } } | null } | null };
export type EstimatePositionQuery = { __typename?: 'Query', estimatePosition?: { __typename?: 'PositionEstimate', margin: { __typename?: 'MarginEstimate', worstCase: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string }, bestCase: { __typename?: 'MarginLevels', maintenanceLevel: string, searchLevel: string, initialLevel: string, collateralReleaseLevel: string } }, liquidation?: { __typename?: 'LiquidationEstimate', worstCase: { __typename?: 'LiquidationPrice', open_volume_only: string, including_buy_orders: string, including_sell_orders: string }, bestCase: { __typename?: 'LiquidationPrice', open_volume_only: string, including_buy_orders: string, including_sell_orders: string } } | null } | null };
export const PositionFieldsFragmentDoc = gql`
fragment PositionFields on Position {
@@ -130,18 +129,17 @@ export function usePositionsSubscriptionSubscription(baseOptions: Apollo.Subscri
export type PositionsSubscriptionSubscriptionHookResult = ReturnType<typeof usePositionsSubscriptionSubscription>;
export type PositionsSubscriptionSubscriptionResult = Apollo.SubscriptionResult<PositionsSubscriptionSubscription>;
export const EstimatePositionDocument = gql`
query EstimatePosition($marketId: ID!, $openVolume: String!, $averageEntryPrice: String!, $orders: [OrderInfo!], $marginAccountBalance: String!, $generalAccountBalance: String!, $orderMarginAccountBalance: String!, $marginMode: MarginMode!, $marginFactor: String, $includeCollateralIncreaseInAvailableCollateral: Boolean) {
query EstimatePosition($marketId: ID!, $openVolume: String!, $orders: [OrderInfo!], $averageEntryPrice: String!, $marginAccountBalance: String!, $generalAccountBalance: String!, $orderMarginAccountBalance: String!, $marginMode: MarginMode!, $marginFactor: String) {
estimatePosition(
marketId: $marketId
openVolume: $openVolume
averageEntryPrice: $averageEntryPrice
orders: $orders
averageEntryPrice: $averageEntryPrice
marginAccountBalance: $marginAccountBalance
generalAccountBalance: $generalAccountBalance
orderMarginAccountBalance: $orderMarginAccountBalance
marginMode: $marginMode
marginFactor: $marginFactor
includeCollateralIncreaseInAvailableCollateral: $includeCollateralIncreaseInAvailableCollateral
scaleLiquidationPriceToMarketDecimals: true
) {
margin {
@@ -150,24 +148,14 @@ export const EstimatePositionDocument = gql`
searchLevel
initialLevel
collateralReleaseLevel
marginMode
marginFactor
orderMarginLevel
}
bestCase {
maintenanceLevel
searchLevel
initialLevel
collateralReleaseLevel
marginMode
marginFactor
orderMarginLevel
}
}
collateralIncreaseEstimate {
worstCase
bestCase
}
liquidation {
worstCase {
open_volume_only
@@ -198,14 +186,13 @@ export const EstimatePositionDocument = gql`
* variables: {
* marketId: // value for 'marketId'
* openVolume: // value for 'openVolume'
* averageEntryPrice: // value for 'averageEntryPrice'
* orders: // value for 'orders'
* averageEntryPrice: // value for 'averageEntryPrice'
* marginAccountBalance: // value for 'marginAccountBalance'
* generalAccountBalance: // value for 'generalAccountBalance'
* orderMarginAccountBalance: // value for 'orderMarginAccountBalance'
* marginMode: // value for 'marginMode'
* marginFactor: // value for 'marginFactor'
* includeCollateralIncreaseInAvailableCollateral: // value for 'includeCollateralIncreaseInAvailableCollateral'
* },
* });
*/
@@ -1,7 +1,6 @@
import type { PartialDeep } from 'type-fest';
import merge from 'lodash/merge';
import type { EstimatePositionQuery } from './__generated__/Positions';
import { MarginMode } from '@vegaprotocol/types';
export const estimatePositionQuery = (
override?: PartialDeep<EstimatePositionQuery>
@@ -15,24 +14,14 @@ export const estimatePositionQuery = (
initialLevel: '500000',
maintenanceLevel: '200000',
searchLevel: '300000',
marginFactor: '1',
orderMarginLevel: '0',
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
},
worstCase: {
collateralReleaseLevel: '1100000',
initialLevel: '600000',
maintenanceLevel: '300000',
searchLevel: '400000',
marginFactor: '1',
orderMarginLevel: '0',
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
},
},
collateralIncreaseEstimate: {
bestCase: '0',
worstCase: '0',
},
liquidation: {
bestCase: {
including_buy_orders: '1',
@@ -9,23 +9,32 @@ import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { MarginMode } from '@vegaprotocol/types';
describe('LiquidationPrice', () => {
const variables = {
const props = {
marketId: 'market-id',
openVolume: '100',
averageEntryPrice: '10',
marginAccountBalance: '500',
generalAccountBalance: '500',
orderMarginAccountBalance: '0',
decimalPlaces: 2,
averageEntryPrice: '100',
generalAccountBalance: '100',
marginAccountBalance: '100',
orderMarginAccountBalance: '100',
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
marginFactor: '1',
};
const props = { ...variables, decimalPlaces: 2 };
const worstCaseOpenVolume = '200';
const bestCaseOpenVolume = '100';
const mock: MockedResponse<EstimatePositionQuery> = {
request: {
query: EstimatePositionDocument,
variables,
variables: {
marketId: props.marketId,
openVolume: props.openVolume,
averageEntryPrice: props.averageEntryPrice,
generalAccountBalance: props.generalAccountBalance,
marginAccountBalance: props.marginAccountBalance,
orderMarginAccountBalance: props.orderMarginAccountBalance,
marginMode: props.marginMode,
marginFactor: props.marginFactor,
},
},
result: {
data: {
@@ -36,24 +45,14 @@ describe('LiquidationPrice', () => {
searchLevel: '100',
initialLevel: '100',
collateralReleaseLevel: '100',
orderMarginLevel: '0',
marginFactor: '0',
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
},
bestCase: {
maintenanceLevel: '100',
searchLevel: '100',
initialLevel: '100',
collateralReleaseLevel: '100',
orderMarginLevel: '0',
marginFactor: '0',
marginMode: MarginMode.MARGIN_MODE_CROSS_MARGIN,
},
},
collateralIncreaseEstimate: {
bestCase: '0',
worstCase: '0',
},
liquidation: {
worstCase: {
open_volume_only: worstCaseOpenVolume,
+30 -18
View File
@@ -1,35 +1,47 @@
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import {
type EstimatePositionQueryVariables,
useEstimatePositionQuery,
} from './__generated__/Positions';
import { useEstimatePositionQuery } from './__generated__/Positions';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { useT } from '../use-t';
import { MarginMode } from '@vegaprotocol/types';
export const LiquidationPrice = ({
marketId,
openVolume,
averageEntryPrice,
generalAccountBalance,
marginAccountBalance,
orderMarginAccountBalance,
marginMode = MarginMode.MARGIN_MODE_CROSS_MARGIN,
marginFactor,
decimalPlaces,
className,
...variables
}: Pick<
EstimatePositionQueryVariables,
| 'marketId'
| 'openVolume'
| 'orderMarginAccountBalance'
| 'generalAccountBalance'
| 'averageEntryPrice'
| 'marginAccountBalance'
| 'marginMode'
| 'marginFactor'
> & {
}: {
marketId: string;
openVolume: string;
averageEntryPrice: string;
generalAccountBalance: string;
marginAccountBalance: string;
orderMarginAccountBalance: string;
marginMode: MarginMode;
marginFactor: string;
decimalPlaces: number;
className?: string;
}) => {
const t = useT();
const { data: currentData, previousData } = useEstimatePositionQuery({
variables,
variables: {
marketId,
openVolume,
averageEntryPrice,
generalAccountBalance,
marginAccountBalance,
orderMarginAccountBalance,
marginMode,
marginFactor,
},
fetchPolicy: 'no-cache',
skip: !variables.openVolume || variables.openVolume === '0',
skip: !openVolume || openVolume === '0',
});
const data = currentData || previousData;
@@ -52,7 +52,7 @@ export interface Position {
quantum: string;
lossSocializationAmount: string;
marginAccountBalance: string;
orderMarginAccountBalance: string;
orderAccountBalance: string;
generalAccountBalance: string;
marketDecimalPlaces: number;
marketId: string;
@@ -67,7 +67,6 @@ export interface Position {
realisedPNL: string;
status: PositionStatus;
totalBalance: string;
totalMarginAccountBalance: string;
unrealisedPNL: string;
updatedAt: string | null;
productType: ProductType;
@@ -120,7 +119,7 @@ export const getMetrics = (
marginAccount?.balance ?? 0,
asset.decimals
);
const orderMarginAccountBalance = toBigNum(
const orderAccountBalance = toBigNum(
orderAccount?.balance ?? 0,
asset.decimals
);
@@ -138,14 +137,12 @@ export const getMetrics = (
: openVolume.multipliedBy(-1)
).multipliedBy(markPrice)
: undefined;
const totalMarginAccountBalance = marginAccountBalance.plus(
orderMarginAccountBalance
);
const totalBalance = totalMarginAccountBalance.plus(generalAccountBalance);
const totalBalance = marginAccountBalance
.plus(generalAccountBalance)
.plus(orderAccountBalance);
const marginMode =
margin?.marginMode || MarginMode.MARGIN_MODE_CROSS_MARGIN;
const marginFactor = margin?.marginFactor || '1';
const marginFactor = margin?.marginFactor;
const currentLeverage =
marginMode === MarginMode.MARGIN_MODE_ISOLATED_MARGIN
? (marginFactor && 1 / Number(marginFactor)) || undefined
@@ -156,7 +153,7 @@ export const getMetrics = (
: undefined;
metrics.push({
marginMode,
marginFactor,
marginFactor: marginFactor || '0',
maintenanceLevel: margin?.maintenanceLevel,
assetId: asset.id,
assetSymbol: asset.symbol,
@@ -166,7 +163,7 @@ export const getMetrics = (
quantum: asset.quantum,
lossSocializationAmount: position.lossSocializationAmount || '0',
marginAccountBalance: marginAccount?.balance ?? '0',
orderMarginAccountBalance: orderAccount?.balance ?? '0',
orderAccountBalance: orderAccount?.balance ?? '0',
generalAccountBalance: generalAccount?.balance ?? '0',
marketDecimalPlaces,
marketId: market.id,
@@ -183,9 +180,6 @@ export const getMetrics = (
realisedPNL: position.realisedPNL,
status: position.positionStatus,
totalBalance: totalBalance.multipliedBy(10 ** asset.decimals).toFixed(),
totalMarginAccountBalance: totalMarginAccountBalance
.multipliedBy(10 ** asset.decimals)
.toFixed(),
unrealisedPNL: position.unrealisedPNL,
updatedAt: position.updatedAt || null,
productType: market?.tradableInstrument.instrument.product
@@ -275,26 +269,13 @@ const positionDataProvider = makeDerivedDataProvider<
}
);
export type OpenVolumeData = Pick<
PositionFieldsFragment,
'openVolume' | 'averageEntryPrice'
>;
export const openVolumeDataProvider = makeDerivedDataProvider<
OpenVolumeData,
string,
never,
PositionsQueryVariables & MarketDataQueryVariables
>([positionDataProvider], ([data], variables, previousData) =>
produce(previousData, (draft) => {
if (!data) {
return data;
}
const newData = {
openVolume: (data as PositionFieldsFragment).openVolume,
averageEntryPrice: (data as PositionFieldsFragment).averageEntryPrice,
};
return draft ? Object.assign(draft, newData) : newData;
})
>(
[positionDataProvider],
(data) => (data[0] as PositionFieldsFragment | null)?.openVolume || null
);
export const rejoinPositionData = (
@@ -395,31 +376,6 @@ export const positionsMetricsProvider = makeDerivedDataProvider<
})
);
const getMaxLeverage = (market: MarketInfo | null) => {
if (!market || !market?.riskFactors) {
return 1;
}
const maxLeverage =
1 /
(Math.max(
Number(market.riskFactors.long),
Number(market.riskFactors.short)
) || 1);
return maxLeverage;
};
export const maxMarketLeverageProvider = makeDerivedDataProvider<
number,
never,
{ marketId: string }
>(
[
(callback, client, { marketId }) =>
marketInfoProvider(callback, client, { marketId }),
],
(parts) => getMaxLeverage(parts[0])
);
export const maxLeverageProvider = makeDerivedDataProvider<
number,
never,
@@ -436,7 +392,15 @@ export const maxLeverageProvider = makeDerivedDataProvider<
const market: MarketInfo | null = parts[0];
const position: PositionFieldsFragment | null = parts[1];
const margin: MarginFieldsFragment | null = parts[2];
const maxLeverage = getMaxLeverage(market);
if (!market || !market?.riskFactors) {
return 1;
}
const maxLeverage =
1 /
(Math.max(
Number(market.riskFactors.long),
Number(market.riskFactors.short)
) || 1);
if (
market &&
@@ -468,9 +432,10 @@ export const maxLeverageProvider = makeDerivedDataProvider<
}
);
export const useMaxLeverage = (marketId: string, partyId: string | null) => {
export const useMaxLeverage = (marketId: string, partyId?: string) => {
return useDataProvider({
dataProvider: partyId ? maxLeverageProvider : maxMarketLeverageProvider,
dataProvider: maxLeverageProvider,
variables: { marketId, partyId: partyId || '' },
skip: !partyId,
});
};
+49 -48
View File
@@ -137,61 +137,58 @@ const PositionMargin = ({ data }: { data: Position }) => {
? (
BigInt(data.marginAccountBalance) + BigInt(data.generalAccountBalance)
).toString()
: BigInt(data.marginAccountBalance) >
BigInt(data.orderMarginAccountBalance)
: BigInt(data.marginAccountBalance) > BigInt(data.orderAccountBalance)
? data.marginAccountBalance
: data.orderMarginAccountBalance;
: data.orderAccountBalance;
const getWidth = (balance: string) =>
BigNumber(balance).multipliedBy(100).dividedBy(max).toNumber();
const inCrossMode = data.marginMode === MarginMode.MARGIN_MODE_CROSS_MARGIN;
const hasOrderMarginAccountBalance =
!inCrossMode && data.orderMarginAccountBalance !== '0';
const hasOrderAccountBalance =
!inCrossMode && data.orderAccountBalance !== '0';
return (
<>
{data.marginAccountBalance !== '0' && (
<MarginChart
width={inCrossMode ? getWidth(data.marginAccountBalance) : undefined}
label={t('Margin: {{balance}}', {
balance: addDecimalsFormatNumberQuantum(
data.marginAccountBalance,
<MarginChart
width={inCrossMode ? getWidth(data.marginAccountBalance) : undefined}
label={t('Margin: {{balance}}', {
balance: addDecimalsFormatNumberQuantum(
data.marginAccountBalance,
data.assetDecimals,
data.quantum
),
})}
other={
inCrossMode
? t('General account: {{balance}}', {
balance: addDecimalsFormatNumberQuantum(
data.generalAccountBalance,
data.assetDecimals,
data.quantum
),
})
: undefined
}
className={classnames({ 'mb-2': hasOrderAccountBalance })}
marker={
data.maintenanceLevel ? getWidth(data.maintenanceLevel) : undefined
}
markerLabel={
data.maintenanceLevel &&
t('Liquidation: {{maintenanceLevel}}', {
maintenanceLevel: addDecimalsFormatNumberQuantum(
data.maintenanceLevel,
data.assetDecimals,
data.quantum
),
})}
other={
inCrossMode
? t('General account: {{balance}}', {
balance: addDecimalsFormatNumberQuantum(
data.generalAccountBalance,
data.assetDecimals,
data.quantum
),
})
: undefined
}
className={classnames({ 'mb-2': hasOrderMarginAccountBalance })}
marker={
data.maintenanceLevel ? getWidth(data.maintenanceLevel) : undefined
}
markerLabel={
data.maintenanceLevel &&
t('Liquidation: {{maintenanceLevel}}', {
maintenanceLevel: addDecimalsFormatNumberQuantum(
data.maintenanceLevel,
data.assetDecimals,
data.quantum
),
})
}
/>
)}
{hasOrderMarginAccountBalance ? (
})
}
/>
{hasOrderAccountBalance ? (
<MarginChart
width={getWidth(data.orderMarginAccountBalance)}
width={getWidth(data.orderAccountBalance)}
label={t('Order: {{balance}}', {
balance: addDecimalsFormatNumber(
data.orderMarginAccountBalance,
data.orderAccountBalance,
data.assetDecimals
),
})}
@@ -343,16 +340,20 @@ export const PositionsTable = ({
return !data
? undefined
: toBigNum(
data.totalMarginAccountBalance,
data.marginAccountBalance,
data.assetDecimals
).toNumber();
},
cellRenderer: ({ data }: VegaICellRendererParams<Position>) => {
if (!data || !data.totalMarginAccountBalance) {
if (
!data ||
!data.marginAccountBalance ||
!data.marketDecimalPlaces
) {
return null;
}
const margin = addDecimalsFormatNumberQuantum(
data.totalMarginAccountBalance,
data.marginAccountBalance,
data.assetDecimals,
data.quantum
);
@@ -363,7 +364,7 @@ export const PositionsTable = ({
<Tooltip
description={
data &&
data.totalMarginAccountBalance !== '0' && (
data.marginAccountBalance !== '0' && (
<PositionMargin data={data} />
)
}
@@ -409,10 +410,10 @@ export const PositionsTable = ({
className="block text-right grow"
marketId={data.marketId}
openVolume={data.openVolume}
averageEntryPrice={data.averageEntryPrice}
generalAccountBalance={data.generalAccountBalance}
marginAccountBalance={data.marginAccountBalance}
orderMarginAccountBalance={data.orderMarginAccountBalance}
orderMarginAccountBalance={data.orderAccountBalance}
averageEntryPrice={data.averageEntryPrice}
marginFactor={data.marginFactor}
marginMode={data.marginMode}
decimalPlaces={data.marketDecimalPlaces}
+2 -3
View File
@@ -181,11 +181,11 @@ const marginsFields: MarginFieldsFragment[] = [
];
export const singleRow: Position = {
marginFactor: '1',
generalAccountBalance: '12345600',
maintenanceLevel: '12300000',
marginMode: Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN,
orderMarginAccountBalance: '0',
marginFactor: '1',
orderAccountBalance: '0',
partyId: 'partyId',
assetId: 'asset-id',
assetSymbol: 'BTC',
@@ -195,7 +195,6 @@ export const singleRow: Position = {
quantum: '0.1',
lossSocializationAmount: '0',
marginAccountBalance: '12345600',
totalMarginAccountBalance: '12345600',
marketDecimalPlaces: 1,
marketId: 'string',
marketCode: 'ETHBTC.QM21',
+4 -7
View File
@@ -1,17 +1,14 @@
import { useState, useCallback } from 'react';
import {
OpenVolumeData,
openVolumeDataProvider,
} from './positions-data-providers';
import { openVolumeDataProvider } from './positions-data-providers';
import { useDataProvider } from '@vegaprotocol/data-provider';
export const useOpenVolume = (
partyId: string | null | undefined,
marketId: string
) => {
const [openVolume, setOpenVolume] = useState<OpenVolumeData | null>(null);
const update = useCallback(({ data }: { data: OpenVolumeData | null }) => {
setOpenVolume(data);
const [openVolume, setOpenVolume] = useState<string | undefined>(undefined);
const update = useCallback(({ data }: { data: string | null }) => {
setOpenVolume(data ?? undefined);
return true;
}, []);
useDataProvider({
+1 -1
View File
@@ -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. */
totalQuantumRewards: Scalars['String'];
total_quantum_rewards: Scalars['String'];
};
/** Queries allow a caller to read data and filter data via GraphQL. */
@@ -5,9 +5,7 @@ import classNames from 'classnames';
export const LeverageSlider = (
props: Omit<SliderProps, 'min' | 'max'> & Required<Pick<SliderProps, 'max'>>
) => {
const step = [2, 5, 10, 20, 25, 50, 100].find(
(step) => props.max / step <= 6
);
const step = [2, 5, 10, 20, 25].find((step) => props.max / step <= 6);
const min = 1;
const value = props.value?.[0] || props.defaultValue?.[0];
return (
@@ -30,7 +28,6 @@ export const LeverageSlider = (
const higherThanValue = value && labelValue > value;
return (
<span
key={labelValue}
className="absolute flex flex-col items-center translate-x-[-50%]"
style={{
left: `${
-42
View File
@@ -1,42 +0,0 @@
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);
});
});
-84
View File
@@ -1,84 +0,0 @@
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
);
-1
View File
@@ -4,4 +4,3 @@ export * from './range';
export * from './size';
export * from './strings';
export * from './trigger';
export * from './ether';
-20
View File
@@ -13,7 +13,6 @@ import {
toDecimal,
toNumberParts,
formatNumberRounded,
toQUSD,
} from './number';
describe('number utils', () => {
@@ -283,22 +282,3 @@ 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);
});
});
+1 -22
View File
@@ -26,7 +26,7 @@ export function toDecimal(numberOfDecimals: number) {
}
export function toBigNum(
rawValue: string | number | BigNumber,
rawValue: string | number,
decimals: number
): BigNumber {
const divides = new BigNumber(10).exponentiatedBy(decimals);
@@ -233,24 +233,3 @@ 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;
};
-1
View File
@@ -19,7 +19,6 @@ 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';
-111
View File
@@ -1,111 +0,0 @@
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;
};
-4
View File
@@ -27,7 +27,6 @@ import { useForm, Controller, useWatch } from 'react-hook-form';
import { WithdrawLimits } from './withdraw-limits';
import {
ETHEREUM_EAGER_CONNECT,
type GasData,
useWeb3ConnectStore,
useWeb3Disconnect,
} from '@vegaprotocol/web3';
@@ -57,7 +56,6 @@ export interface WithdrawFormProps {
delay: number | undefined;
onSelectAsset: (assetId: string) => void;
submitWithdraw: (withdrawal: WithdrawalArgs) => void;
gasPrice?: GasData;
}
const WithdrawDelayNotification = ({
@@ -119,7 +117,6 @@ export const WithdrawForm = ({
delay,
onSelectAsset,
submitWithdraw,
gasPrice,
}: WithdrawFormProps) => {
const t = useT();
const ethereumAddress = useEthereumAddress();
@@ -250,7 +247,6 @@ export const WithdrawForm = ({
delay={delay}
balance={balance}
asset={selectedAsset}
gas={gasPrice}
/>
</div>
)}
+1 -145
View File
@@ -1,6 +1,6 @@
import type { Asset } from '@vegaprotocol/assets';
import { CompactNumber } from '@vegaprotocol/react-helpers';
import { WITHDRAW_THRESHOLD_TOOLTIP_TEXT, useWETH } from '@vegaprotocol/assets';
import { WITHDRAW_THRESHOLD_TOOLTIP_TEXT } from '@vegaprotocol/assets';
import {
KeyValueTable,
KeyValueTableRow,
@@ -9,16 +9,6 @@ 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;
@@ -26,7 +16,6 @@ interface WithdrawLimitsProps {
balance: BigNumber;
delay: number | undefined;
asset: Asset;
gas?: GasData;
}
export const WithdrawLimits = ({
@@ -35,7 +24,6 @@ export const WithdrawLimits = ({
balance,
delay,
asset,
gas,
}: WithdrawLimitsProps) => {
const t = useT();
const delayTime =
@@ -76,24 +64,6 @@ 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 (
@@ -121,117 +91,3 @@ 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 &times; {asETH(basePricePerGas)} <br />{' '}
= {asETH(basePrice)}
</span>
<span>
{formatNumber(gas)} gas &times; {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,7 +38,6 @@ jest.mock('@vegaprotocol/web3', () => ({
useGetWithdrawDelay: () => {
return () => Promise.resolve(10000);
},
useGasPrice: () => undefined,
}));
describe('WithdrawManager', () => {
@@ -4,7 +4,6 @@ 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[];
@@ -21,8 +20,6 @@ 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}
@@ -33,7 +30,6 @@ export const WithdrawManager = ({
submitWithdraw={submit}
threshold={threshold}
delay={delay}
gasPrice={gasPrice}
/>
);
};