Compare commits

..
38 changed files with 855 additions and 250 deletions
@@ -0,0 +1,46 @@
import { t } from '@vegaprotocol/i18n';
import { TxDetailsShared } from '../shared/tx-details-shared';
import { TableWithTbody } from '../../../table';
import type { components } from '../../../../../types/explorer';
import type { BlockExplorerTransactionResult } from '../../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../../routes/blocks/tendermint-blocks-response';
import { TableCell, TableRow } from '../../../table';
type Update = components['schemas']['v1UpdatePartyProfile'];
interface TxDetailsUpdatePartyProfileProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* Party profiles can be an alias and arbitrary key/values pairs.
* This component displays the alias, if any, but not the metadata. When there is
* some wider usage, we can decide how to render it. For now, it's available in the
* full TX details.
*/
export const TxDetailsUpdatePartyProfile = ({
txData,
pubKey,
blockData,
}: TxDetailsUpdatePartyProfileProps) => {
if (!txData?.command.updatePartyProfile) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const update: Update = txData.command.updatePartyProfile;
return (
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
{update.alias && (
<TableRow modifier="bordered">
<TableCell>{t('New alias')}</TableCell>
<TableCell>{update.alias}</TableCell>
</TableRow>
)}
</TableWithTbody>
);
};
@@ -34,6 +34,7 @@ import { TxDetailsUpdateReferralSet } from './tx-update-referral-set';
import { TxDetailsJoinTeam } from './tx-join-team';
import { TxDetailsUpdateMarginMode } from './tx-update-margin-mode';
import { TxBatchProposal } from './tx-batch-proposal';
import { TxDetailsUpdatePartyProfile } from './proposal/tx-update-party-profile';
interface TxDetailsWrapperProps {
txData: BlockExplorerTransactionResult | undefined;
@@ -139,6 +140,8 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
return TxDetailsUpdateMarginMode;
case 'Batch Proposal':
return TxBatchProposal;
case 'Update Party Profile':
return TxDetailsUpdatePartyProfile;
default:
return TxDetailsGeneric;
}
@@ -44,6 +44,7 @@ export type FilterOption =
| 'Submit Order'
| 'Transfer Funds'
| 'Undelegate'
| 'Update Party Profile'
| 'Update Referral Set'
| 'Update Margin Mode'
| 'Validator Heartbeat'
@@ -79,6 +80,7 @@ export const filterOptions: Record<string, FilterOption[]> = {
'Apply Referral Code',
'Create Referral Set',
'Join Team',
'Update Party Profile',
'Update Referral Set',
],
'External Data': ['Chain Event', 'Submit Oracle Data'],
+1 -1
View File
@@ -1,4 +1,4 @@
NX_ETHEREUM_PROVIDER_URL=https://eth-mainnet.gateway.pokt.network/v1/lb/af6a2d529a11f8158bc8ca2a
NX_ETHEREUM_PROVIDER_URL=https://eth-mainnet.rpc.grove.city/v1/af6a2d529a11f8158bc8ca2a
NX_ETHERSCAN_URL=https://etherscan.io
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
NX_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.sentry.io/6300613
@@ -1,5 +1,10 @@
import { useSearchParams } from 'react-router-dom';
import { Intent, TradingAnchorButton } from '@vegaprotocol/ui-toolkit';
import { Link, useSearchParams } from 'react-router-dom';
import {
Intent,
TradingAnchorButton,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { useT } from '../../lib/use-t';
@@ -30,6 +35,19 @@ export const CompetitionsCreateTeam = () => {
<LayoutWithGradient>
<div className="mx-auto md:w-2/3 max-w-xl">
<Box className="flex flex-col gap-4">
<Link
to={Links.COMPETITIONS()}
className="text-xs inline-flex items-center gap-1 group"
>
<VegaIcon
name={VegaIconNames.CHEVRON_LEFT}
size={12}
className="text-vega-clight-100 dark:text-vega-cdark-100"
/>{' '}
<span className="group-hover:underline">
{t('Go back to the competitions')}
</span>
</Link>
<h1 className="calt text-2xl lg:text-3xl xl:text-4xl">
{isSolo ? t('Create solo team') : t('Create a team')}
</h1>
@@ -78,15 +96,17 @@ const CreateTeamFormContainer = ({ isSolo }: { isSolo: boolean }) => {
<p className="text-sm">{t('Team creation transaction successful')}</p>
{code && (
<>
<p className="text-sm">
Your team ID is:{' '}
<span
className="font-mono break-all"
data-testid="team-id-display"
>
{code}
</span>
</p>
<dl>
<dt className="text-sm">{t('Your team ID:')}</dt>
<dl>
<span
className="font-mono break-all bg-rainbow bg-clip-text text-transparent text-2xl"
data-testid="team-id-display"
>
{code}
</span>
</dl>
</dl>
<TradingAnchorButton
href={Links.COMPETITIONS_TEAM(code)}
intent={Intent.Info}
@@ -3,7 +3,14 @@ import { usePageTitle } from '../../lib/hooks/use-page-title';
import { Box } from '../../components/competitions/box';
import { useT } from '../../lib/use-t';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
import {
Intent,
Loader,
Splash,
TradingAnchorButton,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { RainbowButton } from '../../components/rainbow-button';
import { Link, Navigate, useParams } from 'react-router-dom';
import { Links } from '../../lib/links';
@@ -11,6 +18,7 @@ import { useReferralSetTransaction } from '../../lib/hooks/use-referral-set-tran
import { type FormFields, TeamForm, TransactionType } from './team-form';
import { useTeam } from '../../lib/hooks/use-team';
import { LayoutWithGradient } from '../../components/layouts-inner';
import { useEffect, useState } from 'react';
export const CompetitionsUpdateTeam = () => {
const t = useT();
@@ -29,6 +37,19 @@ export const CompetitionsUpdateTeam = () => {
<LayoutWithGradient>
<div className="mx-auto md:w-2/3 max-w-xl">
<Box className="flex flex-col gap-4">
<Link
to={Links.COMPETITIONS_TEAM(teamId)}
className="text-xs inline-flex items-center gap-1 group"
>
<VegaIcon
name={VegaIconNames.CHEVRON_LEFT}
size={12}
className="text-vega-clight-100 dark:text-vega-cdark-100"
/>{' '}
<span className="group-hover:underline">
{t('Go back to the team profile')}
</span>
</Link>
<h1 className="calt text-2xl lg:text-3xl xl:text-5xl">
{t('Update a team')}
</h1>
@@ -57,7 +78,8 @@ const UpdateTeamFormContainer = ({
pubKey: string;
}) => {
const t = useT();
const { team, loading, error } = useTeam(teamId, pubKey);
const [refetching, setRefetching] = useState<boolean>(false);
const { team, loading, error, refetch } = useTeam(teamId, pubKey);
const { err, status, onSubmit } = useReferralSetTransaction({
onSuccess: () => {
@@ -65,7 +87,15 @@ const UpdateTeamFormContainer = ({
},
});
if (loading) {
// refetch when saved
useEffect(() => {
if (refetch && status === 'confirmed') {
refetch();
setRefetching(true);
}
}, [refetch, status]);
if (loading && !refetching) {
return <Loader size="small" />;
}
if (error) {
@@ -84,6 +114,33 @@ const UpdateTeamFormContainer = ({
return <Navigate to={Links.COMPETITIONS_TEAM(teamId)} />;
}
if (status === 'confirmed') {
return (
<div
className="flex flex-col items-start gap-2"
data-testid="team-creation-success-message"
>
<p className="text-sm">
<VegaIcon
name={VegaIconNames.TICK}
size={18}
className="text-vega-green-500"
/>{' '}
{t('Changes successfully saved to your team.')}
</p>
<TradingAnchorButton
href={Links.COMPETITIONS_TEAM(teamId)}
intent={Intent.Info}
size="small"
data-testid="view-team-button"
>
{t('View team')}
</TradingAnchorButton>
</div>
);
}
const defaultValues: FormFields = {
id: team.teamId,
name: team.name,
@@ -0,0 +1,235 @@
import { Route, Routes } from 'react-router-dom';
import {
Intent,
MobileActionsDropdown,
Tooltip,
TradingButton,
TradingDropdownItem,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { type BarView, ViewType, useSidebar } from '../../components/sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { useT } from '../../lib/use-t';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import { useEffect } from 'react';
import classNames from 'classnames';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
const ViewInitializer = () => {
const currentRouteId = useGetCurrentRouteId();
const { setViews, getView } = useSidebar();
const view = getView(currentRouteId);
const { screenSize } = useScreenDimensions();
const largeScreen = ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize);
useEffect(() => {
if (largeScreen && view === undefined) {
setViews({ type: ViewType.Order }, currentRouteId);
}
}, [setViews, view, currentRouteId, largeScreen]);
return null;
};
export const MarketsMobileSidebar = () => {
const t = useT();
const currentRouteId = useGetCurrentRouteId();
const { pubKeys, isReadOnly } = useVegaWallet();
const openVegaWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
return (
<Routes>
<Route
path=":marketId"
element={
<>
<ViewInitializer />
<div className="grid grid-cols-3 grow md:grow-0 md:flex lg:flex-col items-center gap-2 lg:gap-4 p-1">
{!pubKeys || isReadOnly ? (
<>
<TradingButton
intent={Intent.Primary}
size="medium"
onClick={() => {
openVegaWalletDialog();
}}
>
{t('Connect')}
</TradingButton>
<MobileButton
view={ViewType.Order}
tooltip={t('Trade')}
routeId={currentRouteId}
/>
<MobileBarActionsDropdown currentRouteId={currentRouteId} />
</>
) : (
<>
<MobileButton
view={ViewType.Order}
tooltip={t('Trade')}
routeId={currentRouteId}
/>
<MobileButton
view={ViewType.Deposit}
tooltip={t('Deposit')}
routeId={currentRouteId}
/>
<MobileBarActionsDropdown currentRouteId={currentRouteId} />
</>
)}
</div>
</>
}
/>
</Routes>
);
};
export const MobileButton = ({
view,
tooltip: label,
disabled = false,
onClick,
routeId,
}: {
view?: ViewType;
tooltip: string;
disabled?: boolean;
onClick?: () => void;
routeId: string;
}) => {
const { setViews, getView } = useSidebar((store) => ({
setViews: store.setViews,
getView: store.getView,
}));
const currView = getView(routeId);
const onSelect = (view: BarView['type']) => {
if (view === currView?.type) {
setViews(null, routeId);
} else {
setViews({ type: view }, routeId);
}
};
const buttonClasses = classNames(
'flex items-center p-1 rounded',
'disabled:cursor-not-allowed disabled:text-vega-clight-500 dark:disabled:text-vega-cdark-500',
{
'text-vega-clight-200 dark:text-vega-cdark-200 enabled:hover:bg-vega-clight-500 dark:enabled:hover:bg-vega-cdark-500':
!view || view !== currView?.type,
'bg-vega-yellow enabled:hover:bg-vega-yellow-550 text-black':
view && view === currView?.type,
}
);
return (
<Tooltip description={label} align="center" side="right" sideOffset={10}>
<TradingButton
className={buttonClasses}
data-testid={view}
onClick={onClick || (() => onSelect(view as BarView['type']))}
disabled={disabled}
>
{label}
</TradingButton>
</Tooltip>
);
};
export const MobileDropdownItem = ({
view,
icon,
tooltip,
disabled = false,
onClick,
routeId,
}: {
view?: ViewType;
icon: VegaIconNames;
tooltip: string;
disabled?: boolean;
onClick?: () => void;
routeId: string;
}) => {
const { setViews, getView } = useSidebar((store) => ({
setViews: store.setViews,
getView: store.getView,
}));
const currView = getView(routeId);
const onSelect = (view: BarView['type']) => {
if (view === currView?.type) {
setViews(null, routeId);
} else {
setViews({ type: view }, routeId);
}
};
const buttonClasses = classNames(
'flex items-center p-1 rounded',
'disabled:cursor-not-allowed disabled:text-vega-clight-500 dark:disabled:text-vega-cdark-500',
{
'text-vega-clight-200 dark:text-vega-cdark-200 enabled:hover:bg-vega-clight-500 dark:enabled:hover:bg-vega-cdark-500':
!view || view !== currView?.type,
'bg-vega-yellow enabled:hover:bg-vega-yellow-550 text-black':
view && view === currView?.type,
}
);
return (
<Tooltip description={tooltip} align="center" side="right" sideOffset={10}>
<TradingDropdownItem
className={buttonClasses}
data-testid={view}
onClick={onClick || (() => onSelect(view as BarView['type']))}
disabled={disabled}
>
<VegaIcon name={icon} size={20} />
{tooltip}
</TradingDropdownItem>
</Tooltip>
);
};
export const MobileBarActionsDropdown = ({
currentRouteId,
}: {
currentRouteId: string;
}) => {
const t = useT();
return (
<MobileActionsDropdown>
<MobileDropdownItem
view={ViewType.Deposit}
icon={VegaIconNames.DEPOSIT}
tooltip={t('Deposit')}
routeId={currentRouteId}
/>
<MobileDropdownItem
view={ViewType.Withdraw}
icon={VegaIconNames.WITHDRAW}
tooltip={t('Withdraw')}
routeId={currentRouteId}
/>
<MobileDropdownItem
view={ViewType.Transfer}
icon={VegaIconNames.TRANSFER}
tooltip={t('Transfer')}
routeId={currentRouteId}
/>
<MobileDropdownItem
view={ViewType.Info}
icon={VegaIconNames.BREAKDOWN}
tooltip={t('Market specification')}
routeId={currentRouteId}
/>
<MobileDropdownItem
view={ViewType.Settings}
icon={VegaIconNames.COG}
tooltip={t('Settings')}
routeId={currentRouteId}
/>
</MobileActionsDropdown>
);
};
@@ -2,6 +2,7 @@ import { VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { SidebarButton, ViewType } from '../../components/sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { useT } from '../../lib/use-t';
import { MobileButton } from '../markets/mobile-buttons';
export const PortfolioSidebar = () => {
const t = useT();
@@ -30,3 +31,28 @@ export const PortfolioSidebar = () => {
</>
);
};
export const PortfolioMobileSidebar = () => {
const t = useT();
const currentRouteId = useGetCurrentRouteId();
return (
<div className="grid grid-cols-3 grow md:grow-0 md:flex lg:flex-col items-center gap-2 lg:gap-4 p-1">
<MobileButton
view={ViewType.Deposit}
tooltip={t('Deposit')}
routeId={currentRouteId}
/>
<MobileButton
view={ViewType.Withdraw}
tooltip={t('Withdraw')}
routeId={currentRouteId}
/>
<MobileButton
view={ViewType.Transfer}
tooltip={t('Transfer')}
routeId={currentRouteId}
/>
</div>
);
};
@@ -1,4 +1,4 @@
import { getNumberFormat } from '@vegaprotocol/utils';
import { formatNumber } from '@vegaprotocol/utils';
import sortBy from 'lodash/sortBy';
import omit from 'lodash/omit';
import { useReferralProgramQuery } from './__generated__/CurrentReferralProgram';
@@ -107,9 +107,7 @@ export const useReferralProgram = () => {
discountFactor: Number(t.referralDiscountFactor),
discount: BigNumber(t.referralDiscountFactor).times(100).toFixed(2) + '%',
minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
volume: getNumberFormat(0).format(
Number(t.minimumRunningNotionalTakerVolume)
),
volume: formatNumber(t.minimumRunningNotionalTakerVolume, 0),
epochs: Number(t.minimumEpochs),
};
});
@@ -14,9 +14,9 @@ import {
import { useVegaWallet } from '@vegaprotocol/wallet';
import {
addDecimalsFormatNumber,
formatNumber,
getDateFormat,
getDateTimeFormat,
getNumberFormat,
getUserLocale,
removePaginationWrapper,
} from '@vegaprotocol/utils';
@@ -323,7 +323,7 @@ export const Statistics = ({
}
description={<QUSDTooltip />}
>
{getNumberFormat(0).format(Number(totalCommissionValue))}
{formatNumber(totalCommissionValue, 0)}
</StatTile>
);
@@ -563,8 +563,8 @@ export const RefereesTable = ({
)
.map((r) => ({
...r,
volume: getNumberFormat(0).format(r.volume),
commission: getNumberFormat(0).format(r.commission),
volume: formatNumber(r.volume, 0),
commission: formatNumber(r.commission, 0),
}))
.reverse()}
/>
@@ -1,6 +1,6 @@
import { Link } from 'react-router-dom';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { getNumberFormat } from '@vegaprotocol/utils';
import { formatNumber } from '@vegaprotocol/utils';
import { type useTeams } from '../../lib/hooks/use-teams';
import { useT } from '../../lib/use-t';
import { Table } from '../table';
@@ -15,8 +15,7 @@ export const CompetitionsLeaderboard = ({
}) => {
const t = useT();
const num = (n?: number | string) =>
!n ? '-' : getNumberFormat(0).format(Number(n));
const num = (n?: number | string) => (!n ? '-' : formatNumber(n, 0));
if (!data || data.length === 0) {
return <Splash>{t('Could not find any teams')}</Splash>;
@@ -33,9 +32,9 @@ export const CompetitionsLeaderboard = ({
{ name: 'status', displayName: t('Status') },
{ name: 'volume', displayName: t('Volume') },
]}
data={data.map((td, i) => {
data={data.map((td) => {
// leaderboard place or medal
let rank: number | React.ReactNode = i + 1;
let rank: number | React.ReactNode = td.rank;
if (rank === 1) rank = <Rank variant="gold" />;
if (rank === 2) rank = <Rank variant="silver" />;
if (rank === 3) rank = <Rank variant="bronze" />;
@@ -1,4 +1,6 @@
import { isValidUrl } from '@vegaprotocol/utils';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
const NUM_AVATARS = 20;
const AVATAR_PATHNAME_PATTERN = '/team-avatars/{id}.png';
@@ -11,6 +13,26 @@ export const getFallbackAvatar = (teamId: string) => {
return AVATAR_PATHNAME_PATTERN.replace('{id}', avatarId);
};
const useAvatar = (teamId: string, url: string) => {
const fallback = getFallbackAvatar(teamId);
const [avatar, setAvatar] = useState<string>(fallback);
useEffect(() => {
if (!isValidUrl(url)) return;
fetch(url, { cache: 'force-cache' })
.then((response) => {
if (response.ok) {
setAvatar(url);
}
})
.catch(() => {
/** noop */
});
});
return avatar;
};
export const TeamAvatar = ({
teamId,
imgUrl,
@@ -22,7 +44,7 @@ export const TeamAvatar = ({
alt?: string;
size?: 'large' | 'small';
}) => {
const img = imgUrl && imgUrl.length > 0 ? imgUrl : getFallbackAvatar(teamId);
const img = useAvatar(teamId, imgUrl);
return (
// eslint-disable-next-line @next/next/no-img-element
<img
@@ -16,7 +16,7 @@ export const LayoutWithSidebar = ({
const sidebarOpen = sidebarView !== null;
const gridClasses = classNames(
'h-full relative z-0 grid',
'grid-rows-[min-content_1fr_40px]',
'grid-rows-[min-content_1fr_50px]',
'lg:grid-rows-[min-content_1fr]',
'lg:grid-cols-[1fr_280px_40px]',
'xxxl:grid-cols-[1fr_320px_40px]'
+60 -25
View File
@@ -17,6 +17,7 @@ import { useVegaWallet, useViewAsDialog } from '@vegaprotocol/wallet';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { useT } from '../../lib/use-t';
import { ErrorBoundary } from '../error-boundary';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
export enum ViewType {
Order = 'Order',
@@ -26,9 +27,10 @@ export enum ViewType {
Transfer = 'Transfer',
Settings = 'Settings',
ViewAs = 'ViewAs',
Close = 'Close',
}
type SidebarView =
export type BarView =
| {
type: ViewType.Deposit;
assetId?: string;
@@ -49,6 +51,9 @@ type SidebarView =
}
| {
type: ViewType.Settings;
}
| {
type: ViewType.Close;
};
export const Sidebar = ({ options }: { options?: ReactNode }) => {
@@ -57,26 +62,52 @@ export const Sidebar = ({ options }: { options?: ReactNode }) => {
const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1';
const setViewAsDialogOpen = useViewAsDialog((state) => state.setOpen);
const { pubKeys } = useVegaWallet();
const { isMobile } = useScreenDimensions();
const { getView } = useSidebar((store) => ({
setViews: store.setViews,
getView: store.getView,
}));
const currView = getView(currentRouteId);
return (
<div className="flex h-full p-1 lg:flex-col gap-2" data-testid="sidebar">
{options && <nav className={navClasses}>{options}</nav>}
<nav className={classNames(navClasses, 'ml-auto lg:mt-auto lg:ml-0')}>
<SidebarButton
view={ViewType.ViewAs}
onClick={() => {
setViewAsDialogOpen(true);
}}
icon={VegaIconNames.EYE}
tooltip={t('View as party')}
disabled={Boolean(pubKeys)}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Settings}
icon={VegaIconNames.COG}
tooltip={t('Settings')}
routeId={currentRouteId}
/>
<div className="flex h-full lg:flex-col gap-1" data-testid="sidebar">
{options && (
<nav className={classNames(navClasses, 'flex grow')}>{options}</nav>
)}
<nav
className={classNames(
navClasses,
'ml-auto lg:mt-auto lg:ml-0 shrink-0'
)}
>
{!isMobile ? (
<>
<SidebarButton
view={ViewType.ViewAs}
onClick={() => {
setViewAsDialogOpen(true);
}}
icon={VegaIconNames.EYE}
tooltip={t('View as party')}
disabled={Boolean(pubKeys)}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Settings}
icon={VegaIconNames.COG}
tooltip={t('Settings')}
routeId={currentRouteId}
/>
</>
) : (
currView && (
<SidebarButton
view={ViewType.Close}
icon={VegaIconNames.ARROW_LEFT}
tooltip={t('Back')}
routeId={currentRouteId}
/>
)
)}
<NodeHealthContainer />
</nav>
</div>
@@ -103,7 +134,7 @@ export const SidebarButton = ({
getView: store.getView,
}));
const currView = getView(routeId);
const onSelect = (view: SidebarView['type']) => {
const onSelect = (view: BarView['type']) => {
if (view === currView?.type) {
setViews(null, routeId);
} else {
@@ -133,7 +164,7 @@ export const SidebarButton = ({
<button
className={buttonClasses}
data-testid={view}
onClick={onClick || (() => onSelect(view as SidebarView['type']))}
onClick={onClick || (() => onSelect(view as BarView['type']))}
disabled={disabled}
>
<VegaIcon name={icon} size={20} />
@@ -180,6 +211,10 @@ export const SidebarContent = () => {
}
}
if (view.type === ViewType.Close) {
return <CloseSidebar />;
}
if (view.type === ViewType.Info) {
if (params.marketId) {
return (
@@ -267,9 +302,9 @@ const CloseSidebar = () => {
};
export const useSidebar = create<{
views: { [key: string]: SidebarView | null };
setViews: (view: SidebarView | null, routeId: string) => void;
getView: (routeId: string) => SidebarView | null | undefined;
views: { [key: string]: BarView | null };
setViews: (view: BarView | null, routeId: string) => void;
getView: (routeId: string) => BarView | null | undefined;
}>()((set, get) => ({
views: {},
setViews: (x, routeId) =>
@@ -15,7 +15,8 @@ deal_ticket_deposit_dialog_button = "deal-ticket-deposit-dialog-button"
@pytest.fixture(scope="module")
def vega(request):
with init_vega(request) as vega_instance:
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
request.addfinalizer(lambda: cleanup_container(
vega_instance)) # Register the cleanup function
yield vega_instance
@@ -24,6 +25,7 @@ def continuous_market(vega):
return setup_continuous_market(vega)
@pytest.mark.skip("tbd - issue only on the sim, should work in vega 0.74.0")
@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}")
+3 -1
View File
@@ -36,7 +36,9 @@ export const useTeams = (aggregationEpochs = DEFAULT_AGGREGATION_EPOCHS) => {
...stats.find((s) => s.teamId === t.teamId),
}));
return orderBy(data, (d) => Number(d.totalQuantumRewards || 0), 'desc');
return orderBy(data, (d) => Number(d.totalQuantumRewards || 0), 'desc').map(
(d, i) => ({ ...d, rank: i + 1 })
);
}, [teams, stats]);
return {
+23 -8
View File
@@ -24,7 +24,10 @@ import { compact } from 'lodash';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { LiquidityHeader } from '../components/liquidity-header';
import { MarketHeader, MobileMarketHeader } from '../components/market-header';
import { PortfolioSidebar } from '../client-pages/portfolio/portfolio-sidebar';
import {
PortfolioMobileSidebar,
PortfolioSidebar,
} from '../client-pages/portfolio/portfolio-sidebar';
import { LiquiditySidebar } from '../client-pages/liquidity/liquidity-sidebar';
import { MarketsSidebar } from '../client-pages/markets/markets-sidebar';
import { useT } from '../lib/use-t';
@@ -33,9 +36,10 @@ import { CompetitionsTeams } from '../client-pages/competitions/competitions-tea
import { CompetitionsTeam } from '../client-pages/competitions/competitions-team';
import { CompetitionsCreateTeam } from '../client-pages/competitions/competitions-create-team';
import { CompetitionsUpdateTeam } from '../client-pages/competitions/competitions-update-team';
import { MarketsMobileSidebar } from '../client-pages/markets/mobile-buttons';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
// These must remain dynamically imported as pennant cannot be compiled by nextjs due to ESM
// These must remain dynamically imported as pennant cannot be compiled by Next.js due to ESM
// Using dynamic imports is a workaround for this until pennant is published as ESM
const MarketPage = lazy(() => import('../client-pages/market'));
const Portfolio = lazy(() => import('../client-pages/portfolio'));
@@ -54,6 +58,17 @@ export const useRouterConfig = (): RouteObject[] => {
const { screenSize } = useScreenDimensions();
const largeScreen = ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize);
const marketHeader = largeScreen ? <MarketHeader /> : <MobileMarketHeader />;
const marketsSidebar = largeScreen ? (
<MarketsSidebar />
) : (
<MarketsMobileSidebar />
);
const portfolioSidebar = largeScreen ? (
<PortfolioSidebar />
) : (
<PortfolioMobileSidebar />
);
const routeConfig = compact([
{
index: true,
@@ -70,7 +85,7 @@ export const useRouterConfig = (): RouteObject[] => {
featureFlags.REFERRALS
? {
path: AppRoutes.REFERRALS,
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
element: <LayoutWithSidebar sidebar={portfolioSidebar} />,
children: [
{
element: (
@@ -103,7 +118,7 @@ export const useRouterConfig = (): RouteObject[] => {
featureFlags.TEAM_COMPETITION
? {
path: AppRoutes.COMPETITIONS,
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
element: <LayoutWithSidebar sidebar={portfolioSidebar} />,
children: [
// pages with planets and stars
{
@@ -134,7 +149,7 @@ export const useRouterConfig = (): RouteObject[] => {
: undefined,
{
path: 'fees/*',
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
element: <LayoutWithSidebar sidebar={portfolioSidebar} />,
children: [
{
index: true,
@@ -144,7 +159,7 @@ export const useRouterConfig = (): RouteObject[] => {
},
{
path: 'rewards/*',
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
element: <LayoutWithSidebar sidebar={portfolioSidebar} />,
children: [
{
index: true,
@@ -155,7 +170,7 @@ export const useRouterConfig = (): RouteObject[] => {
{
path: 'markets/*',
element: (
<LayoutWithSidebar header={marketHeader} sidebar={<MarketsSidebar />} />
<LayoutWithSidebar header={marketHeader} sidebar={marketsSidebar} />
),
children: [
{
@@ -176,7 +191,7 @@ export const useRouterConfig = (): RouteObject[] => {
},
{
path: 'portfolio/*',
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
element: <LayoutWithSidebar sidebar={portfolioSidebar} />,
children: [
{
index: true,
+18
View File
@@ -0,0 +1,18 @@
query TransferFee(
$fromAccount: ID!
$fromAccountType: AccountType!
$toAccount: ID!
$amount: String!
$assetId: String!
) {
estimateTransferFee(
fromAccount: $fromAccount
fromAccountType: $fromAccountType
toAccount: $toAccount
amount: $amount
assetId: $assetId
) {
fee
discount
}
}
+63
View File
@@ -0,0 +1,63 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type TransferFeeQueryVariables = Types.Exact<{
fromAccount: Types.Scalars['ID'];
fromAccountType: Types.AccountType;
toAccount: Types.Scalars['ID'];
amount: Types.Scalars['String'];
assetId: Types.Scalars['String'];
}>;
export type TransferFeeQuery = { __typename?: 'Query', estimateTransferFee?: { __typename?: 'EstimatedTransferFee', fee: string, discount: string } | null };
export const TransferFeeDocument = gql`
query TransferFee($fromAccount: ID!, $fromAccountType: AccountType!, $toAccount: ID!, $amount: String!, $assetId: String!) {
estimateTransferFee(
fromAccount: $fromAccount
fromAccountType: $fromAccountType
toAccount: $toAccount
amount: $amount
assetId: $assetId
) {
fee
discount
}
}
`;
/**
* __useTransferFeeQuery__
*
* To run a query within a React component, call `useTransferFeeQuery` and pass it any options that fit your needs.
* When your component renders, `useTransferFeeQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useTransferFeeQuery({
* variables: {
* fromAccount: // value for 'fromAccount'
* fromAccountType: // value for 'fromAccountType'
* toAccount: // value for 'toAccount'
* amount: // value for 'amount'
* assetId: // value for 'assetId'
* },
* });
*/
export function useTransferFeeQuery(baseOptions: Apollo.QueryHookOptions<TransferFeeQuery, TransferFeeQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<TransferFeeQuery, TransferFeeQueryVariables>(TransferFeeDocument, options);
}
export function useTransferFeeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<TransferFeeQuery, TransferFeeQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<TransferFeeQuery, TransferFeeQueryVariables>(TransferFeeDocument, options);
}
export type TransferFeeQueryHookResult = ReturnType<typeof useTransferFeeQuery>;
export type TransferFeeLazyQueryHookResult = ReturnType<typeof useTransferFeeLazyQuery>;
export type TransferFeeQueryResult = Apollo.QueryResult<TransferFeeQuery, TransferFeeQueryVariables>;
@@ -25,7 +25,6 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
const t = useT();
const { pubKey, pubKeys, isReadOnly } = useVegaWallet();
const { params } = useNetworkParams([
NetworkParams.transfer_fee_factor,
NetworkParams.transfer_minTransferQuantumMultiple,
]);
@@ -72,7 +71,6 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
pubKeys={pubKeys ? pubKeys?.map((pk) => pk.publicKey) : null}
isReadOnly={isReadOnly}
assetId={assetId}
feeFactor={params.transfer_fee_factor}
minQuantumMultiple={params.transfer_minTransferQuantumMultiple}
submitTransfer={transfer}
accounts={sortedAccounts}
+79 -60
View File
@@ -15,6 +15,30 @@ import {
} from './transfer-form';
import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
import { removeDecimal } from '@vegaprotocol/utils';
import type { TransferFeeQuery } from './__generated__/TransferFee';
const feeFactor = 0.001;
const mockUseTransferFeeQuery = jest.fn(
({
variables: { amount },
}: {
variables: { amount: string };
}): { data: TransferFeeQuery } => {
return {
data: {
estimateTransferFee: {
discount: '0',
fee: (Number(amount) * feeFactor).toFixed(),
},
},
};
}
);
jest.mock('./__generated__/TransferFee', () => ({
useTransferFeeQuery: (props: { variables: { amount: string } }) =>
mockUseTransferFeeQuery(props),
}));
describe('TransferForm', () => {
const renderComponent = (props: TransferFormProps) => {
@@ -56,7 +80,6 @@ describe('TransferForm', () => {
const props = {
pubKey,
pubKeys: [pubKey, '2'.repeat(64)],
feeFactor: '0.001',
submitTransfer: jest.fn(),
accounts: [
{
@@ -79,7 +102,6 @@ describe('TransferForm', () => {
pubKey,
'a4b6e3de5d7ef4e31ae1b090be49d1a2ef7bcefff60cccf7658a0d4922651cce',
],
feeFactor: '0.001',
submitTransfer: jest.fn(),
accounts: [],
minQuantumMultiple: '1',
@@ -96,10 +118,6 @@ describe('TransferForm', () => {
});
it.each([
{
targetText: 'Transfer fee',
tooltipText: /transfer\.fee\.factor/,
},
{
targetText: 'Amount to be transferred',
tooltipText: /without the fee/,
@@ -109,9 +127,6 @@ describe('TransferForm', () => {
tooltipText: /total amount taken from your account/,
},
])('Tooltip for "$targetText" shows', async (o) => {
// 1003-TRAN-015
// 1003-TRAN-016
// 1003-TRAN-017
// 1003-TRAN-018
// 1003-TRAN-019
renderComponent(props);
@@ -124,6 +139,10 @@ describe('TransferForm', () => {
// Select asset
await selectAsset(asset);
await userEvent.selectOptions(
screen.getByLabelText('From account'),
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
);
// set valid amount
const amountInput = screen.getByLabelText('Amount');
await userEvent.type(amountInput, amount);
@@ -214,9 +233,7 @@ describe('TransferForm', () => {
// set valid amount
await userEvent.clear(amountInput);
await userEvent.type(amountInput, amount);
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(
new BigNumber(props.feeFactor).times(amount).toFixed()
);
expect(screen.getByTestId('transfer-fee')).toHaveTextContent('1');
await submit();
@@ -385,47 +402,44 @@ describe('TransferForm', () => {
});
});
});
describe('IncludeFeesCheckbox', () => {
it('validates fields when checkbox is not checked', async () => {
renderComponent(props);
// check current pubkey not shown
const keySelect: HTMLSelectElement = screen.getByLabelText('To Vega key');
const pubKeyOptions = ['', pubKey, props.pubKeys[1]];
expect(keySelect.children).toHaveLength(pubKeyOptions.length);
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual(
pubKeyOptions
);
it('validates fields', async () => {
renderComponent(props);
await submit();
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value
// check current pubkey not shown
const keySelect: HTMLSelectElement = screen.getByLabelText('To Vega key');
const pubKeyOptions = ['', pubKey, props.pubKeys[1]];
expect(keySelect.children).toHaveLength(pubKeyOptions.length);
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual(
pubKeyOptions
);
// Select a pubkey
await userEvent.selectOptions(
screen.getByLabelText('To Vega key'),
props.pubKeys[1]
);
await submit();
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value
// Select asset
await selectAsset(asset);
// Select a pubkey
await userEvent.selectOptions(
screen.getByLabelText('To Vega key'),
props.pubKeys[1]
);
await userEvent.selectOptions(
screen.getByLabelText('From account'),
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
);
// Select asset
await selectAsset(asset);
const amountInput = screen.getByLabelText('Amount');
await userEvent.selectOptions(
screen.getByLabelText('From account'),
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
);
await userEvent.type(amountInput, amount);
const expectedFee = new BigNumber(amount)
.times(props.feeFactor)
.toFixed();
const total = new BigNumber(amount).plus(expectedFee).toFixed();
// 1003-TRAN-021
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(amount);
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total);
});
const amountInput = screen.getByLabelText('Amount');
await userEvent.type(amountInput, amount);
const expectedFee = new BigNumber(amount).times(feeFactor).toFixed();
const total = new BigNumber(amount).plus(expectedFee).toFixed();
// 1003-TRAN-021
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(amount);
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total);
});
describe('AddressField', () => {
@@ -457,24 +471,29 @@ describe('TransferForm', () => {
describe('TransferFee', () => {
const props = {
amount: '200',
feeFactor: '0.001',
fee: '0.2',
transferAmount: '200',
decimals: 8,
amount: '20000',
discount: '0',
fee: '20',
decimals: 2,
};
it('calculates and renders the transfer fee', () => {
it('calculates and renders amounts and fee', () => {
render(<TransferFee {...props} />);
expect(screen.queryByTestId('discount')).not.toBeInTheDocument();
expect(screen.getByTestId('transfer-fee')).toHaveTextContent('0.2');
expect(screen.getByTestId('transfer-amount')).toHaveTextContent('200.00');
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(
'200.20'
);
});
const expected = new BigNumber(props.amount)
.times(props.feeFactor)
.toFixed();
const total = new BigNumber(props.amount).plus(expected).toFixed();
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expected);
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(
props.amount
it('calculates and renders amounts, fee and discount', () => {
render(<TransferFee {...props} discount="10" />);
expect(screen.getByTestId('discount')).toHaveTextContent('0.1');
expect(screen.getByTestId('transfer-fee')).toHaveTextContent('0.2');
expect(screen.getByTestId('transfer-amount')).toHaveTextContent('200.00');
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(
'200.10'
);
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total);
});
});
});
+49 -31
View File
@@ -4,8 +4,9 @@ import {
useRequired,
useVegaPublicKey,
addDecimal,
formatNumber,
toBigNum,
removeDecimal,
addDecimalsFormatNumber,
} from '@vegaprotocol/utils';
import { useT } from './use-t';
import {
@@ -21,10 +22,11 @@ import type { Transfer } from '@vegaprotocol/wallet';
import { normalizeTransfer } from '@vegaprotocol/wallet';
import BigNumber from 'bignumber.js';
import type { ReactNode } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { AssetOption, Balance } from '@vegaprotocol/assets';
import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
import { useTransferFeeQuery } from './__generated__/TransferFee';
interface FormFields {
toVegaKey: string;
@@ -51,7 +53,6 @@ export interface TransferFormProps {
asset: Asset;
}>;
assetId?: string;
feeFactor: string | null;
minQuantumMultiple: string | null;
submitTransfer: (transfer: Transfer) => void;
}
@@ -61,7 +62,6 @@ export const TransferForm = ({
pubKeys,
isReadOnly,
assetId: initialAssetId,
feeFactor,
submitTransfer,
accounts,
minQuantumMultiple,
@@ -136,11 +136,22 @@ export const TransferForm = ({
// Max amount given selected asset and from account
const max = accountBalance ? new BigNumber(accountBalance) : new BigNumber(0);
const normalizedAmount =
(amount && asset && removeDecimal(amount, asset.decimals)) || '0';
const fee = useMemo(
() => feeFactor && new BigNumber(feeFactor).times(amount).toString(),
[amount, feeFactor]
);
const transferFeeQuery = useTransferFeeQuery({
variables: {
fromAccount: pubKey || '',
fromAccountType: accountType || AccountType.ACCOUNT_TYPE_GENERAL,
amount: normalizedAmount,
assetId: asset?.id || '',
toAccount: selectedPubKey,
},
skip: !pubKey || !amount || !asset || !selectedPubKey || fromVested,
});
const transferFee = transferFeeQuery.loading
? transferFeeQuery.data || transferFeeQuery.previousData
: transferFeeQuery.data;
const onSubmit = useCallback(
(fields: FormFields) => {
@@ -432,12 +443,14 @@ export const TransferForm = ({
</TradingInputError>
)}
</TradingFormGroup>
{amount && fee && (
{(transferFee?.estimateTransferFee || fromVested) && amount && asset && (
<TransferFee
amount={amount}
feeFactor={feeFactor}
fee={fromVested ? '0' : fee}
decimals={asset?.decimals}
amount={normalizedAmount}
fee={fromVested ? '0' : transferFee?.estimateTransferFee?.fee}
discount={
fromVested ? '0' : transferFee?.estimateTransferFee?.discount
}
decimals={asset.decimals}
/>
)}
<TradingButton type="submit" fill={true} disabled={isReadOnly}>
@@ -449,39 +462,44 @@ export const TransferForm = ({
export const TransferFee = ({
amount,
feeFactor,
fee,
discount,
decimals,
}: {
amount: string;
feeFactor: string | null;
fee?: string;
decimals?: number;
discount?: string;
decimals: number;
}) => {
const t = useT();
if (!feeFactor || !amount || !fee) return null;
if (isNaN(Number(feeFactor)) || isNaN(Number(amount)) || isNaN(Number(fee))) {
if (!amount || !fee) return null;
if (isNaN(Number(amount)) || isNaN(Number(fee))) {
return null;
}
const totalValue = new BigNumber(amount).plus(fee).toString();
const totalValue = (
BigInt(amount) +
BigInt(fee) -
BigInt(discount || '0')
).toString();
return (
<div className="mb-4 flex flex-col gap-2 text-xs">
<div className="flex flex-wrap items-center justify-between gap-1">
<Tooltip
description={t(
`The transfer fee is set by the network parameter transfer.fee.factor, currently set to {{feeFactor}}`,
{ feeFactor }
)}
>
<div>{t('Transfer fee')}</div>
</Tooltip>
<div>{t('Transfer fee')}</div>
<div data-testid="transfer-fee" className="text-muted">
{formatNumber(fee, decimals)}
{addDecimalsFormatNumber(fee, decimals)}
</div>
</div>
{discount && discount !== '0' && (
<div className="flex flex-wrap items-center justify-between gap-1">
<div>{t('Discount')}</div>
<div data-testid="discount" className="text-muted">
{addDecimalsFormatNumber(discount, decimals)}
</div>
</div>
)}
<div className="flex flex-wrap items-center justify-between gap-1">
<Tooltip
description={t(
@@ -492,7 +510,7 @@ export const TransferFee = ({
</Tooltip>
<div data-testid="transfer-amount" className="text-muted">
{formatNumber(amount, decimals)}
{addDecimalsFormatNumber(amount, decimals)}
</div>
</div>
<div className="flex flex-wrap items-center justify-between gap-1">
@@ -505,7 +523,7 @@ export const TransferFee = ({
</Tooltip>
<div data-testid="total-transfer-fee" className="text-muted">
{formatNumber(totalValue, decimals)}
{addDecimalsFormatNumber(totalValue, decimals)}
</div>
</div>
</div>
@@ -281,7 +281,7 @@ export const DealTicket = ({
marginFactor: margin?.marginFactor || '1',
marginMode:
margin?.marginMode || Schema.MarginMode.MARGIN_MODE_CROSS_MARGIN,
includeCollateralIncreaseInAvailableCollateral: true,
includeRequiredPositionMarginInAvailableCollateral: true,
},
!normalizedOrder ||
(normalizedOrder.type !== Schema.OrderType.TYPE_MARKET &&
@@ -89,7 +89,7 @@ export const MarginChange = ({
openVolume,
marketId,
orderMarginAccountBalance: orderMarginAccountBalance || '0',
includeCollateralIncreaseInAvailableCollateral: true,
includeRequiredPositionMarginInAvailableCollateral: true,
orders,
},
skip
-1
View File
@@ -35,7 +35,6 @@
"The total amount of each asset on this key. Includes used and available collateral.": "The total amount of each asset on this key. Includes used and available collateral.",
"The total amount taken from your account. The amount to be transferred plus the fee.": "The total amount taken from your account. The amount to be transferred plus the fee.",
"The total amount to be transferred (without the fee)": "The total amount to be transferred (without the fee)",
"The transfer fee is set by the network parameter transfer.fee.factor, currently set to {{feeFactor}}": "The transfer fee is set by the network parameter transfer.fee.factor, currently set to {{feeFactor}}",
"To account": "To account",
"To Vega key": "To Vega key",
"Total": "Total",
+5 -1
View File
@@ -446,5 +446,9 @@
"Choose a team": "Choose a team",
"Join a team": "Join a team",
"Solo team / lone wolf": "Solo team / lone wolf",
"Choose a team to get involved": "Choose a team to get involved"
"Choose a team to get involved": "Choose a team to get involved",
"Go back to the team's profile": "Go back to the team's profile",
"Go back to the competitions": "Go back to the competitions",
"Your team ID:": "Your team ID:",
"Changes successfully saved to your team.": "Changes successfully saved to your team."
}
+6 -6
View File
@@ -94,7 +94,7 @@ export const LiquidityTable = ({
return `${addDecimalsFormatNumberQuantum(
value,
assetDecimalPlaces ?? 0,
quantum ?? 0
quantum ?? 1
)}`;
};
@@ -165,7 +165,7 @@ export const LiquidityTable = ({
return `${addDecimalsFormatNumberQuantum(
newValue,
assetDecimalPlaces ?? 0,
quantum ?? 0
quantum ?? 1
)}`;
};
@@ -227,7 +227,7 @@ export const LiquidityTable = ({
addDecimalsFormatNumberQuantum(
pendingCommitmentAmount,
assetDecimalPlaces ?? 0,
quantum ?? 0
quantum ?? 1
);
if (
@@ -238,7 +238,7 @@ export const LiquidityTable = ({
addDecimalsFormatNumberQuantum(
currentCommitmentAmount,
assetDecimalPlaces ?? 0,
quantum ?? 0
quantum ?? 1
);
return (
@@ -286,7 +286,7 @@ export const LiquidityTable = ({
addDecimalsFormatNumberQuantum(
pendingCommitmentAmount,
assetDecimalPlaces ?? 0,
quantum ?? 0
quantum ?? 1
);
if (
@@ -297,7 +297,7 @@ export const LiquidityTable = ({
addDecimalsFormatNumberQuantum(
currentCommitmentAmount,
assetDecimalPlaces ?? 0,
quantum ?? 0
quantum ?? 1
);
return (
+3 -4
View File
@@ -1,7 +1,7 @@
import { DepthChart } from 'pennant';
import throttle from 'lodash/throttle';
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
import { addDecimal, getNumberFormat } from '@vegaprotocol/utils';
import { addDecimal, formatNumber } from '@vegaprotocol/utils';
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { marketDepthProvider } from './market-depth-provider';
@@ -216,13 +216,12 @@ export const DepthChartContainer = ({ marketId }: DepthChartManagerProps) => {
const volumeFormat = useCallback(
(volume: number) =>
getNumberFormat(market?.positionDecimalPlaces || 0).format(volume),
formatNumber(volume, market?.positionDecimalPlaces || 0),
[market?.positionDecimalPlaces]
);
const priceFormat = useCallback(
(price: number) =>
getNumberFormat(market?.decimalPlaces || 0).format(price),
(price: number) => formatNumber(price, market?.decimalPlaces || 0),
[market?.decimalPlaces]
);
+2 -2
View File
@@ -48,7 +48,7 @@ query EstimatePosition(
$orderMarginAccountBalance: String!
$marginMode: MarginMode!
$marginFactor: String
$includeCollateralIncreaseInAvailableCollateral: Boolean
$includeRequiredPositionMarginInAvailableCollateral: Boolean
) {
estimatePosition(
marketId: $marketId
@@ -60,7 +60,7 @@ query EstimatePosition(
orderMarginAccountBalance: $orderMarginAccountBalance
marginMode: $marginMode
marginFactor: $marginFactor
includeCollateralIncreaseInAvailableCollateral: $includeCollateralIncreaseInAvailableCollateral
includeRequiredPositionMarginInAvailableCollateral: $includeRequiredPositionMarginInAvailableCollateral
# 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
+4 -4
View File
@@ -29,7 +29,7 @@ export type EstimatePositionQueryVariables = Types.Exact<{
orderMarginAccountBalance: Types.Scalars['String'];
marginMode: Types.MarginMode;
marginFactor?: Types.InputMaybe<Types.Scalars['String']>;
includeCollateralIncreaseInAvailableCollateral?: Types.InputMaybe<Types.Scalars['Boolean']>;
includeRequiredPositionMarginInAvailableCollateral?: Types.InputMaybe<Types.Scalars['Boolean']>;
}>;
@@ -130,7 +130,7 @@ 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!, $averageEntryPrice: String!, $orders: [OrderInfo!], $marginAccountBalance: String!, $generalAccountBalance: String!, $orderMarginAccountBalance: String!, $marginMode: MarginMode!, $marginFactor: String, $includeRequiredPositionMarginInAvailableCollateral: Boolean) {
estimatePosition(
marketId: $marketId
openVolume: $openVolume
@@ -141,7 +141,7 @@ export const EstimatePositionDocument = gql`
orderMarginAccountBalance: $orderMarginAccountBalance
marginMode: $marginMode
marginFactor: $marginFactor
includeCollateralIncreaseInAvailableCollateral: $includeCollateralIncreaseInAvailableCollateral
includeRequiredPositionMarginInAvailableCollateral: $includeRequiredPositionMarginInAvailableCollateral
scaleLiquidationPriceToMarketDecimals: true
) {
collateralIncreaseEstimate {
@@ -185,7 +185,7 @@ export const EstimatePositionDocument = gql`
* orderMarginAccountBalance: // value for 'orderMarginAccountBalance'
* marginMode: // value for 'marginMode'
* marginFactor: // value for 'marginFactor'
* includeCollateralIncreaseInAvailableCollateral: // value for 'includeCollateralIncreaseInAvailableCollateral'
* includeRequiredPositionMarginInAvailableCollateral: // value for 'includeRequiredPositionMarginInAvailableCollateral'
* },
* });
*/
+1 -1
View File
@@ -5015,7 +5015,7 @@ export type QueryestimateOrderArgs = {
export type QueryestimatePositionArgs = {
averageEntryPrice: Scalars['String'];
generalAccountBalance: Scalars['String'];
includeCollateralIncreaseInAvailableCollateral?: InputMaybe<Scalars['Boolean']>;
includeRequiredPositionMarginInAvailableCollateral?: InputMaybe<Scalars['Boolean']>;
marginAccountBalance: Scalars['String'];
marginFactor?: InputMaybe<Scalars['String']>;
marginMode: MarginMode;
@@ -38,7 +38,7 @@ export function Dialog({
);
const wrapperClasses = classNames(
// Dimensions
'w-screen sm:max-w-[90vw] p-4 md:p-8',
'max-w-[95vw] sm:max-w-[90vw] p-4 md:p-8',
// Need to apply background and text colors again as content is rendered in a portal
'dark:bg-black bg-white dark:text-white',
getIntentBorder(intent),
@@ -1,4 +1,5 @@
import { VegaIcon, VegaIconNames } from '../icon';
import { TradingButton } from '../trading-button';
import {
TradingDropdown,
TradingDropdownContent,
@@ -15,6 +16,16 @@ export const ActionsDropdownTrigger = () => {
);
};
export const MobileActionsDropdownTrigger = () => {
return (
<TradingDropdownTrigger data-testid="dropdown-menu">
<TradingButton size="medium">
<VegaIcon name={VegaIconNames.KEBAB} />
</TradingButton>
</TradingDropdownTrigger>
);
};
type ActionMenuContentProps = React.ComponentProps<
typeof TradingDropdownContent
>;
@@ -26,3 +37,11 @@ export const ActionsDropdown = (props: ActionMenuContentProps) => {
</TradingDropdown>
);
};
export const MobileActionsDropdown = (props: ActionMenuContentProps) => {
return (
<TradingDropdown trigger={<MobileActionsDropdownTrigger />}>
<TradingDropdownContent {...props} side="bottom" align="end" />
</TradingDropdown>
);
};
@@ -1,2 +1,2 @@
export * from './trading-dropdown';
export * from './actions-dropdown';
export * from './trading-dropdown';
+5 -21
View File
@@ -23,6 +23,7 @@ describe('number utils', () => {
{ v: new BigNumber(123000), d: 1, o: '12,300.0' },
{ v: new BigNumber(123001), d: 2, o: '1,230.01' },
{ v: new BigNumber(123001000), d: 2, o: '1,230,010.00' },
{ v: '100000000000000000001', d: 18, o: '100.000000000000000001' },
])(
'formats with addDecimalsFormatNumber given number correctly',
({ v, d, o }) => {
@@ -31,27 +32,10 @@ describe('number utils', () => {
);
it.each([
{ v: new BigNumber(123000), d: 5, o: '1.23', q: 0.1 },
{ v: new BigNumber(123000), d: 3, o: '123.00', q: 0.1 },
{ v: new BigNumber(123000), d: 1, o: '12,300.00', q: 0.1 },
{ v: new BigNumber(123001000), d: 2, o: '1,230,010.00', q: 0.1 },
{ v: new BigNumber(123001), d: 2, o: '1,230.01', q: 100 },
{ v: new BigNumber(123001), d: 2, o: '1,230.01', q: 0.1 },
{ v: new BigNumber(123001), d: 2, o: '1,230.01', q: 1 },
{
v: BigNumber('123456789123456789'),
d: 10,
o: '12,345,678.91234568',
q: '0.00003846',
},
{
v: BigNumber('123456789123456789'),
d: 10,
o: '12,345,678.91234568',
q: '1',
},
// USDT / USDC
{ v: new BigNumber(12345678), d: 6, o: '12.35', q: 1000000 },
{ v: '1234000000000000000', d: 18, q: '1000000000000000000', o: '1.23' }, //vega
{ v: '1235000000000000000', d: 18, q: '1000000000000000000', o: '1.24' }, //vega
{ v: '1230012', d: 6, q: '1000000', o: '1.23' }, // USDT
{ v: '1234560000000000000', d: 18, q: '500000000000000', o: '1.2346' }, // WEth
])(
'formats with addDecimalsFormatNumberQuantum given number correctly',
({ v, d, o, q }) => {
+58 -34
View File
@@ -1,5 +1,4 @@
import { BigNumber } from 'bignumber.js';
import isNil from 'lodash/isNil';
import memoize from 'lodash/memoize';
import { getUserLocale } from '../get-user-locale';
@@ -53,36 +52,36 @@ export function removeDecimal(
return new BigNumber(value || 0).times(times).toFixed(0);
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
export const getNumberFormat = memoize((digits: number) => {
if (isNil(digits) || digits < 0) {
return new Intl.NumberFormat(getUserLocale());
}
return new Intl.NumberFormat(getUserLocale(), {
minimumFractionDigits: Math.min(Math.max(0, digits), MIN_FRACTION_DIGITS),
maximumFractionDigits: Math.min(Math.max(0, digits), MAX_FRACTION_DIGITS),
});
});
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
export const getFixedNumberFormat = memoize((digits: number) => {
if (isNil(digits) || digits < 0) {
return new Intl.NumberFormat(getUserLocale());
}
return new Intl.NumberFormat(getUserLocale(), {
minimumFractionDigits: Math.min(Math.max(0, digits), MAX_FRACTION_DIGITS),
maximumFractionDigits: Math.min(Math.max(0, digits), MAX_FRACTION_DIGITS),
});
});
export const getDecimalSeparator = memoize(
() =>
getNumberFormat(1)
new Intl.NumberFormat(getUserLocale())
.formatToParts(1.1)
.find((part) => part.type === 'decimal')?.value
.find((part) => part.type === 'decimal')?.value ?? '.'
);
/** formatNumber will format the number with fixed decimals
export const getGroupFormat = memoize(() => {
const parts = new Intl.NumberFormat(getUserLocale()).formatToParts(
100000000000.1
);
const groupSeparator = parts.find((part) => part.type === 'group')?.value;
const groupSize =
(groupSeparator &&
parts.reverse().find((part) => part.type === 'integer')?.value.length) ||
0;
return {
groupSize,
groupSeparator,
};
});
const getFormat = memoize(() => ({
decimalSeparator: getDecimalSeparator(),
...getGroupFormat(),
}));
/**
* formatNumber will format the number with maximum number of decimals
* trailing zeros are removed but min(MIN_FRACTION_DIGITS, formatDecimals) decimal places will be kept
* @param rawValue - should be a number that is not outside the safe range fail as in https://mikemcl.github.io/bignumber.js/#toN
* @param formatDecimals - number of decimals to use
*/
@@ -90,7 +89,23 @@ export const formatNumber = (
rawValue: string | number | BigNumber,
formatDecimals = 0
) => {
return getNumberFormat(formatDecimals).format(Number(rawValue));
const decimalPlaces = Math.min(
Math.max(0, formatDecimals),
MAX_FRACTION_DIGITS
);
const format = getFormat();
const formatted = new BigNumber(rawValue).toFormat(decimalPlaces, format);
// if there are no decimal places just return formatted value
if (!decimalPlaces) {
return formatted;
}
// minimum number of decimal places to keep when removing trailing zeros
const minimumFractionDigits = Math.min(decimalPlaces, MIN_FRACTION_DIGITS);
const parts = formatted.split(format.decimalSeparator);
parts[1] = (parts[1] || '')
.replace(/0+$/, '')
.padEnd(minimumFractionDigits, '0');
return parts.join(format.decimalSeparator);
};
/** formatNumberFixed will format the number with fixed decimals
@@ -101,7 +116,10 @@ export const formatNumberFixed = (
rawValue: string | number | BigNumber,
formatDecimals = 0
) => {
return getFixedNumberFormat(formatDecimals).format(Number(rawValue));
return new BigNumber(rawValue).toFormat(
Math.min(Math.max(0, formatDecimals), MAX_FRACTION_DIGITS),
getFormat()
);
};
export const quantumDecimalPlaces = (
@@ -131,9 +149,14 @@ export const addDecimalsFormatNumberQuantum = (
if (isNaN(Number(quantum))) {
return addDecimalsFormatNumber(rawValue, decimalPlaces);
}
const quantumValue = addDecimal(quantum, decimalPlaces);
const numberDP = Math.max(0, Math.log10(100 / Number(quantumValue)));
return addDecimalsFormatNumber(rawValue, decimalPlaces, Math.ceil(numberDP));
const numberDP = Math.ceil(
Math.abs(Math.log10(toBigNum(quantum, decimalPlaces).toNumber()))
);
return addDecimalsFormatNumber(
rawValue,
decimalPlaces,
Math.max(MIN_FRACTION_DIGITS, numberDP)
);
};
export const addDecimalsFormatNumber = (
@@ -141,9 +164,10 @@ export const addDecimalsFormatNumber = (
decimalPlaces: number,
formatDecimals: number = decimalPlaces
) => {
const x = addDecimal(rawValue, decimalPlaces);
return formatNumber(x, formatDecimals);
return formatNumber(
new BigNumber(rawValue || 0).dividedBy(Math.pow(10, decimalPlaces)),
formatDecimals
);
};
export const addDecimalsFixedFormatNumber = (
+12 -12
View File
@@ -10,24 +10,24 @@ describe('formatValue', () => {
{
v: '123456789123456789',
d: 10,
o: '12,345,678.91234568',
o: '12,345,678.9123456789',
},
])('formats values correctly', ({ v, d, o }) => {
expect(formatValue(v, d)).toStrictEqual(o);
});
it.each([
{ v: 123000, d: 5, o: '1.23', q: '0.1' },
{ v: 123000, d: 3, o: '123.00', q: '0.1' },
{ v: 123000, d: 1, o: '12,300.00', q: '0.1' },
{ v: 123001000, d: 2, o: '1,230,010.00', q: '0.1' },
{ v: 123000, d: 5, o: '1.23', q: '1' },
{ v: 123000, d: 3, o: '123.00', q: '1' },
{ v: 123000, d: 1, o: '12,300.00', q: '1' },
{ v: 123001000, d: 2, o: '1,230,010.00', q: '1' },
{ v: 123001, d: 2, o: '1,230.01', q: '100' },
{ v: 123001, d: 2, o: '1,230.01', q: '0.1' },
{ v: 123001, d: 2, o: '1,230.01', q: '1' },
{
v: '123456789123456789',
d: 10,
o: '12,345,678.91234568',
q: '0.00003846',
o: '12,345,678.91235',
q: '384600',
},
])(
'formats with formatValue with quantum given number correctly',
@@ -42,15 +42,15 @@ describe('formatRange', () => {
min: 123000,
max: 12300011111,
d: 5,
o: '1.23 - 123,000.11111',
q: '0.1',
o: '1.23 - 123,000.11',
q: '1000',
},
{
min: 123000,
max: 12300011111,
d: 3,
o: '123.00 - 12,300,011.111',
q: '0.1',
o: '123.00 - 12,300,011.11',
q: '100',
},
{
min: 123000,
-2
View File
@@ -40,8 +40,6 @@
## Transfer
- **Must** display tooltip for "Transfer fee when hovered over.(<a name="1003-TRAN-017" href="#1003-TRAN-017">1003-TRAN-017</a>)
- **Must** display tooltip for "Amount to be transferred" when hovered over.(<a name="1003-TRAN-018" href="#1003-TRAN-018">1003-TRAN-018</a>)
- **Must** display tooltip for "Total amount (with fee)" when hovered over.(<a name="1003-TRAN-019" href="#1003-TRAN-019">1003-TRAN-019</a>)