Compare commits
43
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ae13a09b6 | ||
|
|
68712d6ef8 | ||
|
|
14936f2962 | ||
|
|
a47d865829 | ||
|
|
d40b6bacee | ||
|
|
eb296a185d | ||
|
|
7e2da69a63 | ||
|
|
7057439432 | ||
|
|
8175ed41b3 | ||
|
|
e8bc651f0e | ||
|
|
2cfe5e8325 | ||
|
|
9455166f7a | ||
|
|
38a813249a | ||
|
|
1adab570d4 | ||
|
|
2cd7884867 | ||
|
|
7939073826 | ||
|
|
a314581ed7 | ||
|
|
c437e706fe | ||
|
|
f231fd0676 | ||
|
|
1acff681d3 | ||
|
|
064ae55c1f | ||
|
|
654ca50a22 | ||
|
|
5b24d884c0 | ||
|
|
d40be3ceab | ||
|
|
15e535dc7b | ||
|
|
285bf6b523 | ||
|
|
451887f3bd | ||
|
|
68333d8f4a | ||
|
|
5449fa44dd | ||
|
|
c92007f26d | ||
|
|
0b830626a7 | ||
|
|
62b81fd94d | ||
|
|
a0235d7a8a | ||
|
|
00db11af9a | ||
|
|
16538f790b | ||
|
|
b0c8b41174 | ||
|
|
c0a5418815 | ||
|
|
389772f4e1 | ||
|
|
6ea9258574 | ||
|
|
0f4ba64a54 | ||
|
|
bcd576a6a5 | ||
|
|
a070504d2e | ||
|
|
f891caf08b |
+4
-1
@@ -9,4 +9,7 @@ apps/static/src/assets/devnet-tranches.json
|
||||
apps/static/src/assets/mainnet-tranches.json
|
||||
apps/static/src/assets/testnet-tranches.json
|
||||
|
||||
/.nx/cache
|
||||
/apps/**/cypress/reports/
|
||||
/apps/**/cypress/downloads/
|
||||
|
||||
/.nx/cache
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useMemo } from 'react';
|
||||
import { type AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
import { AssetTypeMapping, AssetStatusMapping } from '@vegaprotocol/assets';
|
||||
import {
|
||||
useAssetTypeMapping,
|
||||
useAssetStatusMapping,
|
||||
type AssetFieldsFragment,
|
||||
} from '@vegaprotocol/assets';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||
import { type AgGridReact } from 'ag-grid-react';
|
||||
@@ -15,6 +18,8 @@ type AssetsTableProps = {
|
||||
data: AssetFieldsFragment[] | null;
|
||||
};
|
||||
export const AssetsTable = ({ data }: AssetsTableProps) => {
|
||||
const assetTypeMapping = useAssetTypeMapping();
|
||||
const assetStatusMapping = useAssetStatusMapping();
|
||||
const navigate = useNavigate();
|
||||
const ref = useRef<AgGridReact>(null);
|
||||
const showColumnsOnDesktop = () => {
|
||||
@@ -47,14 +52,14 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
|
||||
field: 'source.__typename',
|
||||
hide: window.innerWidth < BREAKPOINT_MD,
|
||||
valueFormatter: ({ value }: { value?: string }) =>
|
||||
value ? AssetTypeMapping[value].value : '',
|
||||
value ? assetTypeMapping[value].value : '',
|
||||
},
|
||||
{
|
||||
headerName: t('Status'),
|
||||
field: 'status',
|
||||
hide: window.innerWidth < BREAKPOINT_MD,
|
||||
valueFormatter: ({ value }: { value?: string }) =>
|
||||
value ? AssetStatusMapping[value].value : '',
|
||||
value ? assetStatusMapping[value].value : '',
|
||||
},
|
||||
{
|
||||
colId: 'actions',
|
||||
@@ -69,7 +74,7 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
|
||||
}: VegaICellRendererParams<AssetFieldsFragment, 'id'>) =>
|
||||
value ? (
|
||||
<ButtonLink
|
||||
onClick={(e) => {
|
||||
onClick={() => {
|
||||
navigate(value);
|
||||
}}
|
||||
>
|
||||
@@ -80,7 +85,7 @@ export const AssetsTable = ({ data }: AssetsTableProps) => {
|
||||
),
|
||||
},
|
||||
],
|
||||
[navigate]
|
||||
[navigate, assetStatusMapping, assetTypeMapping]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { useVegaWallet, useEagerConnect } from '@vegaprotocol/wallet';
|
||||
import { FLAGS, useEnvironment } from '@vegaprotocol/environment';
|
||||
import { useWeb3React } from '@web3-react/core';
|
||||
import React from 'react';
|
||||
import React, { Suspense } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { SplashError } from './components/splash-error';
|
||||
@@ -164,13 +164,14 @@ export const AppLoader = ({ children }: { children: React.ReactElement }) => {
|
||||
);
|
||||
}
|
||||
|
||||
if (!loaded) {
|
||||
return (
|
||||
<Splash>
|
||||
<SplashLoader />
|
||||
</Splash>
|
||||
);
|
||||
}
|
||||
const loading = (
|
||||
<Splash>
|
||||
<SplashLoader />
|
||||
</Splash>
|
||||
);
|
||||
|
||||
return children;
|
||||
if (!loaded) {
|
||||
return loading;
|
||||
}
|
||||
return <Suspense fallback={loading}>{children}</Suspense>;
|
||||
};
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
useNodeSwitcherStore,
|
||||
DocsLinks,
|
||||
NodeFailure,
|
||||
AppLoader as Loader,
|
||||
} from '@vegaprotocol/environment';
|
||||
import { ENV } from './config';
|
||||
import type { InMemoryCacheConfig } from '@apollo/client';
|
||||
@@ -352,9 +353,11 @@ function App() {
|
||||
useInitializeEnv();
|
||||
|
||||
return (
|
||||
<NetworkLoader cache={cache}>
|
||||
<AppContainer />
|
||||
</NetworkLoader>
|
||||
<React.Suspense fallback={<Loader />}>
|
||||
<NetworkLoader cache={cache}>
|
||||
<AppContainer />
|
||||
</NetworkLoader>
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../../../libs/i18n/src/locales
|
||||
@@ -1,29 +1,41 @@
|
||||
import type { Module } from 'i18next';
|
||||
import i18n from 'i18next';
|
||||
import HttpBackend from 'i18next-http-backend';
|
||||
import LocizeBackend from 'i18next-locize-backend';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import dev from './translations/dev.json';
|
||||
const isInDev = process.env.NODE_ENV === 'development';
|
||||
const useLocize = isInDev && !!process.env.NX_USE_LOCIZE;
|
||||
|
||||
const backend = useLocize
|
||||
? {
|
||||
projectId: '96ac1231-4bdd-455a-b9d7-f5322a2e7430',
|
||||
apiKey: process.env.NX_LOCIZE_API_KEY,
|
||||
referenceLng: 'en',
|
||||
}
|
||||
: {
|
||||
loadPath: '/assets/locales/{{lng}}/{{ns}}.json',
|
||||
};
|
||||
|
||||
const Backend: Module = useLocize ? LocizeBackend : HttpBackend;
|
||||
|
||||
i18n
|
||||
.use(Backend)
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
// we init with resources
|
||||
resources: {
|
||||
en: {
|
||||
translations: {
|
||||
...dev,
|
||||
},
|
||||
},
|
||||
},
|
||||
lng: undefined,
|
||||
lng: 'en',
|
||||
fallbackLng: 'en',
|
||||
debug: true,
|
||||
supportedLngs: ['en'],
|
||||
load: 'languageOnly',
|
||||
debug: isInDev,
|
||||
// have a common namespace used around the full app
|
||||
ns: ['translations'],
|
||||
defaultNS: 'translations',
|
||||
ns: ['governance'],
|
||||
defaultNS: 'governance',
|
||||
keySeparator: false, // we use content as keys
|
||||
|
||||
backend,
|
||||
saveMissing: useLocize && !!process.env.NX_LOCIZE_API_KEY,
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
import dev from './i18n/translations/dev.json';
|
||||
import { locales } from '@vegaprotocol/i18n';
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
@@ -12,16 +12,10 @@ import ResizeObserver from 'resize-observer-polyfill';
|
||||
// en translations
|
||||
i18n.use(initReactI18next).init({
|
||||
// we init with resources
|
||||
resources: {
|
||||
en: {
|
||||
translations: {
|
||||
...dev,
|
||||
},
|
||||
},
|
||||
},
|
||||
resources: locales,
|
||||
fallbackLng: 'en',
|
||||
ns: ['translations'],
|
||||
defaultNS: 'translations',
|
||||
ns: ['governance'],
|
||||
defaultNS: 'governance',
|
||||
});
|
||||
|
||||
global.ResizeObserver = ResizeObserver;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export const useTranslation = () => ({
|
||||
t: (label: string, replacements?: Record<string, string>) => {
|
||||
let translatedLabel = label;
|
||||
if (typeof replacements === 'object' && replacements !== null) {
|
||||
Object.keys(replacements).forEach((key) => {
|
||||
translatedLabel = translatedLabel.replace(
|
||||
`{{${key}}}`,
|
||||
replacements[key]
|
||||
);
|
||||
});
|
||||
}
|
||||
return translatedLabel;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export { Rewards } from './rewards';
|
||||
@@ -0,0 +1,11 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { RewardsContainer } from '../../components/rewards-container';
|
||||
|
||||
export const Rewards = () => {
|
||||
return (
|
||||
<div className="container mx-auto p-4">
|
||||
<h1 className="px-4 pb-4 text-2xl">{t('Rewards')}</h1>
|
||||
<RewardsContainer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { TransferContainer } from '@vegaprotocol/accounts';
|
||||
import { GetStarted } from '../../components/welcome-dialog';
|
||||
import { GetStarted } from '../../components/welcome-dialog/get-started';
|
||||
|
||||
export const Transfer = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { GetStarted } from '../../components/welcome-dialog';
|
||||
import { GetStarted } from '../../components/welcome-dialog/get-started';
|
||||
import { WithdrawContainer } from '../../components/withdraw-container';
|
||||
|
||||
export const Withdraw = () => {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { VegaWalletProvider } from '@vegaprotocol/wallet';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Suspense, type ReactNode } from 'react';
|
||||
import { Web3Provider } from './web3-provider';
|
||||
|
||||
export const Bootstrapper = ({ children }: { children: ReactNode }) => {
|
||||
@@ -36,41 +36,43 @@ export const Bootstrapper = ({ children }: { children: ReactNode }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<NetworkLoader
|
||||
cache={cacheConfig}
|
||||
skeleton={<AppLoader />}
|
||||
failure={
|
||||
<AppFailure title={t('Could not initialize app')} error={error} />
|
||||
}
|
||||
>
|
||||
<NodeGuard
|
||||
<Suspense fallback={<AppLoader />}>
|
||||
<NetworkLoader
|
||||
cache={cacheConfig}
|
||||
skeleton={<AppLoader />}
|
||||
failure={<NodeFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
|
||||
failure={
|
||||
<AppFailure title={t('Could not initialize app')} error={error} />
|
||||
}
|
||||
>
|
||||
<Web3Provider
|
||||
<NodeGuard
|
||||
skeleton={<AppLoader />}
|
||||
failure={
|
||||
<AppFailure title={t(`Could not configure web3 provider`)} />
|
||||
}
|
||||
failure={<NodeFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
|
||||
>
|
||||
<VegaWalletProvider
|
||||
config={{
|
||||
network: VEGA_ENV,
|
||||
vegaUrl: VEGA_URL,
|
||||
vegaWalletServiceUrl: VEGA_WALLET_URL,
|
||||
links: {
|
||||
explorer: VEGA_EXPLORER_URL,
|
||||
concepts: DocsLinks.VEGA_WALLET_CONCEPTS_URL,
|
||||
chromeExtensionUrl: CHROME_EXTENSION_URL,
|
||||
mozillaExtensionUrl: MOZILLA_EXTENSION_URL,
|
||||
},
|
||||
}}
|
||||
<Web3Provider
|
||||
skeleton={<AppLoader />}
|
||||
failure={
|
||||
<AppFailure title={t(`Could not configure web3 provider`)} />
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</VegaWalletProvider>
|
||||
</Web3Provider>
|
||||
</NodeGuard>
|
||||
</NetworkLoader>
|
||||
<VegaWalletProvider
|
||||
config={{
|
||||
network: VEGA_ENV,
|
||||
vegaUrl: VEGA_URL,
|
||||
vegaWalletServiceUrl: VEGA_WALLET_URL,
|
||||
links: {
|
||||
explorer: VEGA_EXPLORER_URL,
|
||||
concepts: DocsLinks.VEGA_WALLET_CONCEPTS_URL,
|
||||
chromeExtensionUrl: CHROME_EXTENSION_URL,
|
||||
mozillaExtensionUrl: MOZILLA_EXTENSION_URL,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</VegaWalletProvider>
|
||||
</Web3Provider>
|
||||
</NodeGuard>
|
||||
</NetworkLoader>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLProps, ReactNode } from 'react';
|
||||
|
||||
export const Card = ({
|
||||
children,
|
||||
title,
|
||||
className,
|
||||
loading = false,
|
||||
highlight = false,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
title: string;
|
||||
className?: string;
|
||||
loading?: boolean;
|
||||
highlight?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'bg-vega-clight-800 dark:bg-vega-cdark-800 col-span-full p-0.5 lg:col-auto',
|
||||
'rounded-lg',
|
||||
{
|
||||
'bg-rainbow': highlight,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="bg-vega-clight-800 dark:bg-vega-cdark-800 h-full w-full rounded p-4">
|
||||
<h2 className="mb-3">{title}</h2>
|
||||
{loading ? <CardLoader /> : children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CardLoader = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="bg-vega-clight-600 dark:bg-vega-cdark-600 h-5 w-full" />
|
||||
<div className="bg-vega-clight-600 dark:bg-vega-cdark-600 h-6 w-3/4" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CardStat = ({
|
||||
value,
|
||||
text,
|
||||
highlight,
|
||||
description,
|
||||
testId,
|
||||
}: {
|
||||
value: ReactNode;
|
||||
text?: string;
|
||||
highlight?: boolean;
|
||||
description?: ReactNode;
|
||||
testId?: string;
|
||||
}) => {
|
||||
const val = (
|
||||
<span
|
||||
className={classNames('inline-block text-3xl leading-none', {
|
||||
'bg-rainbow bg-clip-text text-transparent': highlight,
|
||||
'cursor-help': description,
|
||||
})}
|
||||
data-testid={testId}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<p className="leading-none">
|
||||
{description ? <Tooltip description={description}>{val}</Tooltip> : val}
|
||||
{text && (
|
||||
<small className="text-muted mt-0.5 block text-xs">{text}</small>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export const CardTable = (props: HTMLProps<HTMLTableElement>) => {
|
||||
return (
|
||||
<table {...props} className="text-muted mt-0.5 w-full text-xs">
|
||||
<tbody>{props.children}</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export const CardTableTH = (props: HTMLProps<HTMLTableHeaderCellElement>) => {
|
||||
return (
|
||||
<th
|
||||
{...props}
|
||||
className={classNames('text-left font-normal', props.className)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const CardTableTD = (props: HTMLProps<HTMLTableCellElement>) => {
|
||||
return (
|
||||
<td {...props} className={classNames('text-right', props.className)} />
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { Card, CardStat, CardTable, CardTableTH, CardTableTD } from './card';
|
||||
@@ -1,36 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export const FeeCard = ({
|
||||
children,
|
||||
title,
|
||||
className,
|
||||
loading = false,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
title: string;
|
||||
className?: string;
|
||||
loading?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'p-4 bg-vega-clight-800 dark:bg-vega-cdark-800 col-span-full lg:col-auto',
|
||||
'rounded-lg',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<h2 className="mb-3">{title}</h2>
|
||||
{loading ? <FeeCardLoader /> : children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const FeeCardLoader = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="w-full h-5 bg-vega-clight-600 dark:bg-vega-cdark-600" />
|
||||
<div className="w-3/4 h-6 bg-vega-clight-600 dark:bg-vega-cdark-600" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -9,9 +9,8 @@ import {
|
||||
import { useMarketList } from '@vegaprotocol/markets';
|
||||
import { formatNumber, formatNumberRounded } from '@vegaprotocol/utils';
|
||||
import { useDiscountProgramsQuery, useFeesQuery } from './__generated__/Fees';
|
||||
import { FeeCard } from './fees-card';
|
||||
import { Card, CardStat, CardTable, CardTableTD, CardTableTH } from '../card';
|
||||
import { MarketFees } from './market-fees';
|
||||
import { Stat } from './stat';
|
||||
import { useVolumeStats } from './use-volume-stats';
|
||||
import { useReferralStats } from './use-referral-stats';
|
||||
import { formatPercentage, getAdjustedFee } from './utils';
|
||||
@@ -87,7 +86,7 @@ export const FeesContainer = () => {
|
||||
<div className="grid auto-rows-min grid-cols-4 gap-3">
|
||||
{isConnected && (
|
||||
<>
|
||||
<FeeCard
|
||||
<Card
|
||||
title={t('My trading fees')}
|
||||
className="sm:col-span-2"
|
||||
loading={loading}
|
||||
@@ -98,8 +97,8 @@ export const FeesContainer = () => {
|
||||
referralDiscount={referralDiscount}
|
||||
volumeDiscount={volumeDiscount}
|
||||
/>
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
</Card>
|
||||
<Card
|
||||
title={t('Total discount')}
|
||||
className="sm:col-span-2"
|
||||
loading={loading}
|
||||
@@ -110,8 +109,8 @@ export const FeesContainer = () => {
|
||||
isReferralProgramRunning={isReferralProgramRunning}
|
||||
isVolumeDiscountProgramRunning={isVolumeDiscountProgramRunning}
|
||||
/>
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
</Card>
|
||||
<Card
|
||||
title={t('My current volume')}
|
||||
className="sm:col-span-2"
|
||||
loading={loading}
|
||||
@@ -124,12 +123,12 @@ export const FeesContainer = () => {
|
||||
windowLength={volumeDiscountWindowLength}
|
||||
/>
|
||||
) : (
|
||||
<p className="pt-3 text-sm text-muted">
|
||||
<p className="text-muted pt-3 text-sm">
|
||||
{t('No volume discount program active')}
|
||||
</p>
|
||||
)}
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
</Card>
|
||||
<Card
|
||||
title={t('Referral benefits')}
|
||||
className="sm:col-span-2"
|
||||
loading={loading}
|
||||
@@ -143,14 +142,14 @@ export const FeesContainer = () => {
|
||||
epochs={referralDiscountWindowLength}
|
||||
/>
|
||||
) : (
|
||||
<p className="pt-3 text-sm text-muted">
|
||||
<p className="text-muted pt-3 text-sm">
|
||||
{t('No referral program active')}
|
||||
</p>
|
||||
)}
|
||||
</FeeCard>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
<FeeCard
|
||||
<Card
|
||||
title={t('Volume discount')}
|
||||
className="lg:col-span-full xl:col-span-2"
|
||||
loading={loading}
|
||||
@@ -161,8 +160,8 @@ export const FeesContainer = () => {
|
||||
lastEpochVolume={volumeInWindow}
|
||||
windowLength={volumeDiscountWindowLength}
|
||||
/>
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
</Card>
|
||||
<Card
|
||||
title={t('Referral discount')}
|
||||
className="lg:col-span-full xl:col-span-2"
|
||||
loading={loading}
|
||||
@@ -173,8 +172,8 @@ export const FeesContainer = () => {
|
||||
epochsInSet={epochsInSet}
|
||||
referralVolumeInWindow={referralVolumeInWindow}
|
||||
/>
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
</Card>
|
||||
<Card
|
||||
title={t('Fees by market')}
|
||||
className="lg:col-span-full"
|
||||
loading={marketsLoading}
|
||||
@@ -184,7 +183,7 @@ export const FeesContainer = () => {
|
||||
referralDiscount={referralDiscount}
|
||||
volumeDiscount={volumeDiscount}
|
||||
/>
|
||||
</FeeCard>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -244,8 +243,8 @@ export const TradingFees = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="pt-6 leading-none">
|
||||
<div className="pt-4">
|
||||
<div className="leading-none">
|
||||
<p className="block text-3xl leading-none" data-testid="adjusted-fees">
|
||||
{minAdjustedTotal !== undefined && maxAdjustedTotal !== undefined
|
||||
? `${formatPercentage(minAdjustedTotal)}%-${formatPercentage(
|
||||
@@ -253,47 +252,43 @@ export const TradingFees = ({
|
||||
)}%`
|
||||
: `${formatPercentage(adjustedTotal)}%`}
|
||||
</p>
|
||||
<table className="w-full mt-0.5 text-xs text-muted">
|
||||
<tbody>
|
||||
<CardTable>
|
||||
<tr className="text-default">
|
||||
<CardTableTH>{t('Total fee before discount')}</CardTableTH>
|
||||
<CardTableTD>
|
||||
{minTotal !== undefined && maxTotal !== undefined
|
||||
? `${formatPercentage(minTotal.toNumber())}%-${formatPercentage(
|
||||
maxTotal.toNumber()
|
||||
)}%`
|
||||
: `${formatPercentage(total.toNumber())}%`}
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
<tr>
|
||||
<CardTableTH>{t('Infrastructure')}</CardTableTH>
|
||||
<CardTableTD>
|
||||
{formatPercentage(
|
||||
Number(params.market_fee_factors_infrastructureFee)
|
||||
)}
|
||||
%
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
<tr>
|
||||
<CardTableTH>{t('Maker')}</CardTableTH>
|
||||
<CardTableTD>
|
||||
{formatPercentage(Number(params.market_fee_factors_makerFee))}%
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
{minLiq && maxLiq && (
|
||||
<tr>
|
||||
<th className="font-normal text-left text-default">
|
||||
{t('Total fee before discount')}
|
||||
</th>
|
||||
<td className="text-right text-default">
|
||||
{minTotal !== undefined && maxTotal !== undefined
|
||||
? `${formatPercentage(
|
||||
minTotal.toNumber()
|
||||
)}%-${formatPercentage(maxTotal.toNumber())}%`
|
||||
: `${formatPercentage(total.toNumber())}%`}
|
||||
</td>
|
||||
<CardTableTH>{t('Liquidity')}</CardTableTH>
|
||||
<CardTableTD>
|
||||
{formatPercentage(Number(minLiq.fees.factors.liquidityFee))}%
|
||||
{'-'}
|
||||
{formatPercentage(Number(maxLiq.fees.factors.liquidityFee))}%
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="font-normal text-left">{t('Infrastructure')}</th>
|
||||
<td className="text-right">
|
||||
{formatPercentage(
|
||||
Number(params.market_fee_factors_infrastructureFee)
|
||||
)}
|
||||
%
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="font-normal text-left ">{t('Maker')}</th>
|
||||
<td className="text-right">
|
||||
{formatPercentage(Number(params.market_fee_factors_makerFee))}%
|
||||
</td>
|
||||
</tr>
|
||||
{minLiq && maxLiq && (
|
||||
<tr>
|
||||
<th className="font-normal text-left ">{t('Liquidity')}</th>
|
||||
<td className="text-right">
|
||||
{formatPercentage(Number(minLiq.fees.factors.liquidityFee))}%
|
||||
{'-'}
|
||||
{formatPercentage(Number(maxLiq.fees.factors.liquidityFee))}%
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</CardTable>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -316,13 +311,13 @@ export const CurrentVolume = ({
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Stat
|
||||
<div className="flex flex-col gap-3 pt-4">
|
||||
<CardStat
|
||||
value={formatNumberRounded(new BigNumber(windowLengthVolume))}
|
||||
text={t('Past %s epochs', windowLength.toString())}
|
||||
/>
|
||||
{requiredForNextTier > 0 && (
|
||||
<Stat
|
||||
<CardStat
|
||||
value={formatNumber(requiredForNextTier)}
|
||||
text={t('Required for next tier')}
|
||||
/>
|
||||
@@ -341,8 +336,8 @@ const ReferralBenefits = ({
|
||||
epochs: number;
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<Stat
|
||||
<div className="flex flex-col gap-3 pt-4">
|
||||
<CardStat
|
||||
// all sets volume (not just current party)
|
||||
value={formatNumber(setRunningNotionalTakerVolume)}
|
||||
text={t(
|
||||
@@ -350,7 +345,7 @@ const ReferralBenefits = ({
|
||||
epochs.toString()
|
||||
)}
|
||||
/>
|
||||
<Stat value={epochsInSet} text={t('epochs in referral set')} />
|
||||
<CardStat value={epochsInSet} text={t('epochs in referral set')} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -377,8 +372,8 @@ const TotalDiscount = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Stat
|
||||
<div className="pt-4">
|
||||
<CardStat
|
||||
description={
|
||||
<>
|
||||
{totalDiscountDescription}
|
||||
@@ -388,38 +383,36 @@ const TotalDiscount = ({
|
||||
value={formatPercentage(totalDiscount) + '%'}
|
||||
highlight={true}
|
||||
/>
|
||||
<table className="w-full mt-0.5 text-xs text-muted">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th className="font-normal text-left">{t('Volume discount')}</th>
|
||||
<td className="text-right">
|
||||
{formatPercentage(volumeDiscount)}%
|
||||
{!isVolumeDiscountProgramRunning && (
|
||||
<Tooltip description={t('No active volume discount programme')}>
|
||||
<span className="cursor-help">
|
||||
{' '}
|
||||
<VegaIcon name={VegaIconNames.INFO} size={12} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th className="font-normal text-left ">{t('Referral discount')}</th>
|
||||
<td className="text-right">
|
||||
{formatPercentage(referralDiscount)}%
|
||||
{!isReferralProgramRunning && (
|
||||
<Tooltip description={t('No active referral programme')}>
|
||||
<span className="cursor-help">
|
||||
{' '}
|
||||
<VegaIcon name={VegaIconNames.INFO} size={12} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<CardTable>
|
||||
<tr>
|
||||
<CardTableTH>{t('Volume discount')}</CardTableTH>
|
||||
<CardTableTD>
|
||||
{formatPercentage(volumeDiscount)}%
|
||||
{!isVolumeDiscountProgramRunning && (
|
||||
<Tooltip description={t('No active volume discount programme')}>
|
||||
<span className="cursor-help">
|
||||
{' '}
|
||||
<VegaIcon name={VegaIconNames.INFO} size={12} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
<tr>
|
||||
<CardTableTH>{t('Referral discount')}</CardTableTH>
|
||||
<CardTableTD>
|
||||
{formatPercentage(referralDiscount)}%
|
||||
{!isReferralProgramRunning && (
|
||||
<Tooltip description={t('No active referral programme')}>
|
||||
<span className="cursor-help">
|
||||
{' '}
|
||||
<VegaIcon name={VegaIconNames.INFO} size={12} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
</CardTable>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -440,7 +433,7 @@ const VolumeTiers = ({
|
||||
}) => {
|
||||
if (!tiers.length) {
|
||||
return (
|
||||
<p className="text-sm text-muted">
|
||||
<p className="text-muted text-sm">
|
||||
{t('No volume discount program active')}
|
||||
</p>
|
||||
);
|
||||
@@ -501,7 +494,7 @@ const ReferralTiers = ({
|
||||
}) => {
|
||||
if (!tiers.length) {
|
||||
return (
|
||||
<p className="text-sm text-muted">{t('No referral program active')}</p>
|
||||
<p className="text-muted text-sm">{t('No referral program active')}</p>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -557,20 +550,20 @@ const ReferralTiers = ({
|
||||
|
||||
const YourTier = () => {
|
||||
return (
|
||||
<span className="px-4 py-1.5 rounded-xl bg-rainbow whitespace-nowrap text-white">
|
||||
<span className="bg-rainbow whitespace-nowrap rounded-xl px-4 py-1.5 text-white">
|
||||
{t('Your tier')}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const ReferrerInfo = ({ code }: { code?: string }) => (
|
||||
<div className="pt-3 text-sm text-vega-clight-200 dark:vega-cdark-200">
|
||||
<div className="text-vega-clight-200 dark:vega-cdark-200 pt-3 text-sm">
|
||||
<p className="mb-1">
|
||||
{t('Connected key is owner of the referral set')}
|
||||
{code && (
|
||||
<>
|
||||
{' '}
|
||||
<span className="text-transparent bg-rainbow bg-clip-text">
|
||||
<span className="bg-rainbow bg-clip-text text-transparent">
|
||||
{truncateMiddle(code)}
|
||||
</span>
|
||||
</>
|
||||
@@ -581,7 +574,7 @@ const ReferrerInfo = ({ code }: { code?: string }) => (
|
||||
<p>
|
||||
{t('See')}{' '}
|
||||
<Link
|
||||
className="underline text-black dark:text-white"
|
||||
className="text-black underline dark:text-white"
|
||||
to={Links.REFERRALS()}
|
||||
>
|
||||
{t('Referrals')}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export const Stat = ({
|
||||
value,
|
||||
text,
|
||||
highlight,
|
||||
description,
|
||||
}: {
|
||||
value: string | number;
|
||||
text?: string;
|
||||
highlight?: boolean;
|
||||
description?: ReactNode;
|
||||
}) => {
|
||||
const val = (
|
||||
<span
|
||||
className={classNames('inline-block text-3xl leading-none', {
|
||||
'text-transparent bg-rainbow bg-clip-text': highlight,
|
||||
'cursor-help': description,
|
||||
})}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
return (
|
||||
<p className="pt-3 leading-none first:pt-6">
|
||||
{description ? <Tooltip description={description}>{val}</Tooltip> : val}
|
||||
{text && (
|
||||
<small className="block mt-0.5 text-xs text-muted">{text}</small>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
@@ -68,6 +68,7 @@ describe('Navbar', () => {
|
||||
['/portfolio', 'Portfolio'],
|
||||
['/referrals', 'Referrals'],
|
||||
['/fees', 'Fees'],
|
||||
['/rewards', 'Rewards'],
|
||||
[expect.stringContaining('governance'), 'Governance'],
|
||||
];
|
||||
|
||||
@@ -102,6 +103,7 @@ describe('Navbar', () => {
|
||||
['/portfolio', 'Portfolio'],
|
||||
['/referrals', 'Referrals'],
|
||||
['/fees', 'Fees'],
|
||||
['/rewards', 'Rewards'],
|
||||
[expect.stringContaining('governance'), 'Governance'],
|
||||
];
|
||||
const links = menu.getAllByRole('link');
|
||||
|
||||
@@ -73,7 +73,7 @@ export const Navbar = ({
|
||||
</div>
|
||||
|
||||
{/* Right section */}
|
||||
<div className="flex items-center justify-end ml-auto gap-2">
|
||||
<div className="ml-auto flex items-center justify-end gap-2">
|
||||
<ProtocolUpgradeCountdown />
|
||||
<NavbarMobileButton
|
||||
onClick={() => {
|
||||
@@ -107,18 +107,18 @@ export const Navbar = ({
|
||||
onOpenChange={(open) => setMenu((x) => (open ? x : null))}
|
||||
>
|
||||
<D.Overlay
|
||||
className="fixed inset-0 z-20 lg:hidden dark:bg-black/80 bg-black/50"
|
||||
className="fixed inset-0 z-20 bg-black/50 dark:bg-black/80 lg:hidden"
|
||||
data-testid="navbar-menu-overlay"
|
||||
/>
|
||||
<D.Content
|
||||
className={classNames(
|
||||
'lg:hidden',
|
||||
'fixed top-0 right-0 z-20 w-3/4 h-screen border-l border-default bg-vega-clight-700 dark:bg-vega-cdark-700',
|
||||
'border-default bg-vega-clight-700 dark:bg-vega-cdark-700 fixed right-0 top-0 z-20 h-screen w-3/4 border-l',
|
||||
navTextClasses
|
||||
)}
|
||||
data-testid="navbar-menu-content"
|
||||
>
|
||||
<div className="flex items-center justify-end h-10 p-1">
|
||||
<div className="flex h-10 items-center justify-end p-1">
|
||||
<NavbarMobileButton onClick={() => setMenu(null)}>
|
||||
<span className="sr-only">{t('Close menu')}</span>
|
||||
<VegaIcon name={VegaIconNames.CROSS} size={24} />
|
||||
@@ -142,7 +142,7 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
|
||||
const marketId = useGlobalStore((store) => store.marketId);
|
||||
|
||||
return (
|
||||
<div className="lg:flex lg:h-full gap-3">
|
||||
<div className="gap-3 lg:flex lg:h-full">
|
||||
<NavbarList>
|
||||
<NavbarItem>
|
||||
<NavbarTrigger data-testid="navbar-network-switcher-trigger">
|
||||
@@ -192,6 +192,11 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
|
||||
{t('Fees')}
|
||||
</NavbarLink>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<NavbarLink to={Links.REWARDS()} onClick={onClick}>
|
||||
{t('Rewards')}
|
||||
</NavbarLink>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<NavbarLinkExternal to={useLinks(DApp.Governance)()}>
|
||||
{t('Governance')}
|
||||
@@ -241,8 +246,8 @@ const NavbarTrigger = ({
|
||||
onPointerMove={preventHover}
|
||||
onPointerLeave={preventHover}
|
||||
className={classNames(
|
||||
'w-full lg:w-auto lg:h-full',
|
||||
'flex items-center justify-between lg:justify-center gap-2 px-6 py-2 lg:p-0',
|
||||
'w-full lg:h-full lg:w-auto',
|
||||
'flex items-center justify-between gap-2 px-6 py-2 lg:justify-center lg:p-0',
|
||||
'text-lg lg:text-sm',
|
||||
'hover:text-vega-clight-100 dark:hover:text-vega-cdark-100'
|
||||
)}
|
||||
@@ -273,8 +278,8 @@ const NavbarLink = ({
|
||||
to={to}
|
||||
end={end}
|
||||
className={classNames(
|
||||
'block lg:flex lg:h-full flex-col justify-center',
|
||||
'px-6 py-2 lg:p-0 text-lg lg:text-sm',
|
||||
'block flex-col justify-center lg:flex lg:h-full',
|
||||
'px-6 py-2 text-lg lg:p-0 lg:text-sm',
|
||||
'hover:text-vega-clight-100 dark:hover:text-vega-cdark-100'
|
||||
)}
|
||||
onClick={onClick}
|
||||
@@ -297,7 +302,7 @@ const NavbarLink = ({
|
||||
</span>
|
||||
<span
|
||||
className={classNames(
|
||||
'hidden lg:block absolute left-0 bottom-0 w-full h-0',
|
||||
'absolute bottom-0 left-0 hidden h-0 w-full lg:block',
|
||||
borderClasses
|
||||
)}
|
||||
/>
|
||||
@@ -318,7 +323,7 @@ const NavbarSubItem = (props: LiHTMLAttributes<HTMLElement>) => {
|
||||
};
|
||||
|
||||
const NavbarList = (props: N.NavigationMenuListProps) => {
|
||||
return <N.List {...props} className="lg:flex lg:h-full gap-6" />;
|
||||
return <N.List {...props} className="gap-6 lg:flex lg:h-full" />;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -329,10 +334,10 @@ const NavbarContent = (props: N.NavigationMenuContentProps) => {
|
||||
<N.Content
|
||||
{...props}
|
||||
className={classNames(
|
||||
'group navbar-content',
|
||||
'lg:absolute lg:mt-2 pl-2 lg:pl-0 z-20 lg:min-w-[290px]',
|
||||
'navbar-content group',
|
||||
'z-20 pl-2 lg:absolute lg:mt-2 lg:min-w-[290px] lg:pl-0',
|
||||
'lg:bg-vega-clight-700 lg:dark:bg-vega-cdark-700',
|
||||
'lg:border border-vega-clight-500 dark:border-vega-cdark-500 lg:rounded'
|
||||
'border-vega-clight-500 dark:border-vega-cdark-500 lg:rounded lg:border'
|
||||
)}
|
||||
onPointerEnter={preventHover}
|
||||
onPointerLeave={preventHover}
|
||||
@@ -357,8 +362,8 @@ const NavbarLinkExternal = ({
|
||||
<NavLink
|
||||
to={to}
|
||||
className={classNames(
|
||||
'flex gap-2 lg:h-full items-center',
|
||||
'px-6 py-2 lg:p-0 text-lg lg:text-sm',
|
||||
'flex items-center gap-2 lg:h-full',
|
||||
'px-6 py-2 text-lg lg:p-0 lg:text-sm',
|
||||
'hover:text-vega-clight-100 dark:hover:text-vega-cdark-100'
|
||||
)}
|
||||
onClick={onClick}
|
||||
@@ -386,7 +391,7 @@ const BurgerIcon = () => (
|
||||
const NavbarListDivider = () => {
|
||||
return (
|
||||
<div className="px-6 py-2 lg:px-0" role="separator">
|
||||
<div className="w-full h-px lg:h-full lg:w-px bg-vega-clight-500 dark:bg-vega-cdark-500" />
|
||||
<div className="bg-vega-clight-500 dark:bg-vega-cdark-500 h-px w-full lg:h-full lg:w-px" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -399,7 +404,7 @@ const NavbarMobileButton = (props: ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
<button
|
||||
{...props}
|
||||
className={classNames(
|
||||
'w-8 h-8 lg:hidden flex items-center p-1 rounded ',
|
||||
'flex h-8 w-8 items-center rounded p-1 lg:hidden ',
|
||||
'hover:bg-vega-clight-500 dark:hover:bg-vega-cdark-500',
|
||||
'hover:text-vega-clight-50 dark:hover:text-vega-cdark-50'
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
query RewardsPage($partyId: ID!) {
|
||||
party(id: $partyId) {
|
||||
id
|
||||
vestingStats {
|
||||
# AKA hoarder reward multiplier
|
||||
rewardBonusMultiplier
|
||||
}
|
||||
activityStreak {
|
||||
# vesting multiplier
|
||||
rewardVestingMultiplier
|
||||
# AKA streak multiplier
|
||||
rewardDistributionMultiplier
|
||||
}
|
||||
vestingBalancesSummary {
|
||||
epoch
|
||||
vestingBalances {
|
||||
asset {
|
||||
id
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
}
|
||||
balance
|
||||
}
|
||||
lockedBalances {
|
||||
asset {
|
||||
id
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
}
|
||||
balance
|
||||
untilEpoch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query RewardsHistory(
|
||||
$partyId: ID!
|
||||
$epochRewardSummariesPagination: Pagination
|
||||
$partyRewardsPagination: Pagination
|
||||
$fromEpoch: Int
|
||||
$toEpoch: Int
|
||||
) {
|
||||
epochRewardSummaries(
|
||||
filter: { fromEpoch: $fromEpoch, toEpoch: $toEpoch }
|
||||
pagination: $epochRewardSummariesPagination
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
epoch
|
||||
assetId
|
||||
amount
|
||||
rewardType
|
||||
}
|
||||
}
|
||||
}
|
||||
party(id: $partyId) {
|
||||
id
|
||||
rewardsConnection(
|
||||
fromEpoch: $fromEpoch
|
||||
toEpoch: $toEpoch
|
||||
pagination: $partyRewardsPagination
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
amount
|
||||
percentageOfTotal
|
||||
receivedAt
|
||||
rewardType
|
||||
asset {
|
||||
id
|
||||
symbol
|
||||
name
|
||||
decimals
|
||||
}
|
||||
party {
|
||||
id
|
||||
}
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query RewardsEpoch {
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type RewardsPageQueryVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type RewardsPageQuery = { __typename?: 'Query', party?: { __typename?: 'Party', id: string, vestingStats?: { __typename?: 'PartyVestingStats', rewardBonusMultiplier: string } | null, activityStreak?: { __typename?: 'PartyActivityStreak', rewardVestingMultiplier: string, rewardDistributionMultiplier: string } | null, vestingBalancesSummary: { __typename?: 'PartyVestingBalancesSummary', epoch?: number | null, vestingBalances?: Array<{ __typename?: 'PartyVestingBalance', balance: string, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, quantum: string } }> | null, lockedBalances?: Array<{ __typename?: 'PartyLockedBalance', balance: string, untilEpoch: number, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, quantum: string } }> | null } } | null };
|
||||
|
||||
export type RewardsHistoryQueryVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
epochRewardSummariesPagination?: Types.InputMaybe<Types.Pagination>;
|
||||
partyRewardsPagination?: Types.InputMaybe<Types.Pagination>;
|
||||
fromEpoch?: Types.InputMaybe<Types.Scalars['Int']>;
|
||||
toEpoch?: Types.InputMaybe<Types.Scalars['Int']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type RewardsHistoryQuery = { __typename?: 'Query', epochRewardSummaries?: { __typename?: 'EpochRewardSummaryConnection', edges?: Array<{ __typename?: 'EpochRewardSummaryEdge', node: { __typename?: 'EpochRewardSummary', epoch: number, assetId: string, amount: string, rewardType: Types.AccountType } } | null> | null } | null, party?: { __typename?: 'Party', id: string, rewardsConnection?: { __typename?: 'RewardsConnection', edges?: Array<{ __typename?: 'RewardEdge', node: { __typename?: 'Reward', amount: string, percentageOfTotal: string, receivedAt: any, rewardType: Types.AccountType, asset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number }, party: { __typename?: 'Party', id: string }, epoch: { __typename?: 'Epoch', id: string } } } | null> | null } | null } | null };
|
||||
|
||||
export type RewardsEpochQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type RewardsEpochQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string } };
|
||||
|
||||
|
||||
export const RewardsPageDocument = gql`
|
||||
query RewardsPage($partyId: ID!) {
|
||||
party(id: $partyId) {
|
||||
id
|
||||
vestingStats {
|
||||
rewardBonusMultiplier
|
||||
}
|
||||
activityStreak {
|
||||
rewardVestingMultiplier
|
||||
rewardDistributionMultiplier
|
||||
}
|
||||
vestingBalancesSummary {
|
||||
epoch
|
||||
vestingBalances {
|
||||
asset {
|
||||
id
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
}
|
||||
balance
|
||||
}
|
||||
lockedBalances {
|
||||
asset {
|
||||
id
|
||||
symbol
|
||||
decimals
|
||||
quantum
|
||||
}
|
||||
balance
|
||||
untilEpoch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useRewardsPageQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useRewardsPageQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useRewardsPageQuery` 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 } = useRewardsPageQuery({
|
||||
* variables: {
|
||||
* partyId: // value for 'partyId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useRewardsPageQuery(baseOptions: Apollo.QueryHookOptions<RewardsPageQuery, RewardsPageQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<RewardsPageQuery, RewardsPageQueryVariables>(RewardsPageDocument, options);
|
||||
}
|
||||
export function useRewardsPageLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<RewardsPageQuery, RewardsPageQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<RewardsPageQuery, RewardsPageQueryVariables>(RewardsPageDocument, options);
|
||||
}
|
||||
export type RewardsPageQueryHookResult = ReturnType<typeof useRewardsPageQuery>;
|
||||
export type RewardsPageLazyQueryHookResult = ReturnType<typeof useRewardsPageLazyQuery>;
|
||||
export type RewardsPageQueryResult = Apollo.QueryResult<RewardsPageQuery, RewardsPageQueryVariables>;
|
||||
export const RewardsHistoryDocument = gql`
|
||||
query RewardsHistory($partyId: ID!, $epochRewardSummariesPagination: Pagination, $partyRewardsPagination: Pagination, $fromEpoch: Int, $toEpoch: Int) {
|
||||
epochRewardSummaries(
|
||||
filter: {fromEpoch: $fromEpoch, toEpoch: $toEpoch}
|
||||
pagination: $epochRewardSummariesPagination
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
epoch
|
||||
assetId
|
||||
amount
|
||||
rewardType
|
||||
}
|
||||
}
|
||||
}
|
||||
party(id: $partyId) {
|
||||
id
|
||||
rewardsConnection(
|
||||
fromEpoch: $fromEpoch
|
||||
toEpoch: $toEpoch
|
||||
pagination: $partyRewardsPagination
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
amount
|
||||
percentageOfTotal
|
||||
receivedAt
|
||||
rewardType
|
||||
asset {
|
||||
id
|
||||
symbol
|
||||
name
|
||||
decimals
|
||||
}
|
||||
party {
|
||||
id
|
||||
}
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useRewardsHistoryQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useRewardsHistoryQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useRewardsHistoryQuery` 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 } = useRewardsHistoryQuery({
|
||||
* variables: {
|
||||
* partyId: // value for 'partyId'
|
||||
* epochRewardSummariesPagination: // value for 'epochRewardSummariesPagination'
|
||||
* partyRewardsPagination: // value for 'partyRewardsPagination'
|
||||
* fromEpoch: // value for 'fromEpoch'
|
||||
* toEpoch: // value for 'toEpoch'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useRewardsHistoryQuery(baseOptions: Apollo.QueryHookOptions<RewardsHistoryQuery, RewardsHistoryQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<RewardsHistoryQuery, RewardsHistoryQueryVariables>(RewardsHistoryDocument, options);
|
||||
}
|
||||
export function useRewardsHistoryLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<RewardsHistoryQuery, RewardsHistoryQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<RewardsHistoryQuery, RewardsHistoryQueryVariables>(RewardsHistoryDocument, options);
|
||||
}
|
||||
export type RewardsHistoryQueryHookResult = ReturnType<typeof useRewardsHistoryQuery>;
|
||||
export type RewardsHistoryLazyQueryHookResult = ReturnType<typeof useRewardsHistoryLazyQuery>;
|
||||
export type RewardsHistoryQueryResult = Apollo.QueryResult<RewardsHistoryQuery, RewardsHistoryQueryVariables>;
|
||||
export const RewardsEpochDocument = gql`
|
||||
query RewardsEpoch {
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useRewardsEpochQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useRewardsEpochQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useRewardsEpochQuery` 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 } = useRewardsEpochQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useRewardsEpochQuery(baseOptions?: Apollo.QueryHookOptions<RewardsEpochQuery, RewardsEpochQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<RewardsEpochQuery, RewardsEpochQueryVariables>(RewardsEpochDocument, options);
|
||||
}
|
||||
export function useRewardsEpochLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<RewardsEpochQuery, RewardsEpochQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<RewardsEpochQuery, RewardsEpochQueryVariables>(RewardsEpochDocument, options);
|
||||
}
|
||||
export type RewardsEpochQueryHookResult = ReturnType<typeof useRewardsEpochQuery>;
|
||||
export type RewardsEpochLazyQueryHookResult = ReturnType<typeof useRewardsEpochLazyQuery>;
|
||||
export type RewardsEpochQueryResult = Apollo.QueryResult<RewardsEpochQuery, RewardsEpochQueryVariables>;
|
||||
@@ -0,0 +1 @@
|
||||
export { RewardsContainer } from './rewards-container';
|
||||
@@ -0,0 +1,215 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import type { Account } from '@vegaprotocol/accounts';
|
||||
import { AccountType, AssetStatus } from '@vegaprotocol/types';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import {
|
||||
RewardPot,
|
||||
Vesting,
|
||||
type RewardPotProps,
|
||||
Multipliers,
|
||||
} from './rewards-container';
|
||||
|
||||
const rewardAsset = {
|
||||
id: 'asset-1',
|
||||
symbol: 'ASSET 1',
|
||||
name: 'Asset 1',
|
||||
decimals: 2,
|
||||
quantum: '1',
|
||||
status: AssetStatus.STATUS_ENABLED,
|
||||
source: {
|
||||
__typename: 'ERC20' as const,
|
||||
contractAddress: '0x123',
|
||||
lifetimeLimit: '100',
|
||||
withdrawThreshold: '100',
|
||||
},
|
||||
};
|
||||
|
||||
describe('RewardPot', () => {
|
||||
const renderComponent = (props: RewardPotProps) => {
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<RewardPot {...props} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
it('Shows no rewards message if no accounts or vesting balances provided', () => {
|
||||
renderComponent({
|
||||
pubKey: 'pubkey',
|
||||
assetId: rewardAsset.id,
|
||||
accounts: [],
|
||||
vestingBalancesSummary: {
|
||||
lockedBalances: [],
|
||||
vestingBalances: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(screen.getByText(/No rewards/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Calculates all the rewards', () => {
|
||||
const asset2 = {
|
||||
id: 'asset-2',
|
||||
symbol: 'ASSET 2',
|
||||
name: 'Asset 2',
|
||||
decimals: 0,
|
||||
quantum: '1000000',
|
||||
status: AssetStatus.STATUS_ENABLED,
|
||||
source: {
|
||||
__typename: 'ERC20' as const,
|
||||
contractAddress: '0x123',
|
||||
lifetimeLimit: '100',
|
||||
withdrawThreshold: '100',
|
||||
},
|
||||
};
|
||||
|
||||
const accounts: Account[] = [
|
||||
{
|
||||
type: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
balance: '100',
|
||||
asset: rewardAsset,
|
||||
},
|
||||
{
|
||||
type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
balance: '100',
|
||||
asset: rewardAsset,
|
||||
},
|
||||
{
|
||||
type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
balance: '50',
|
||||
asset: rewardAsset,
|
||||
},
|
||||
{
|
||||
type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
balance: '500000',
|
||||
asset: asset2,
|
||||
},
|
||||
{
|
||||
type: AccountType.ACCOUNT_TYPE_VESTING_REWARDS, // should be ignored as its vesting
|
||||
balance: '100',
|
||||
asset: rewardAsset,
|
||||
},
|
||||
{
|
||||
type: AccountType.ACCOUNT_TYPE_VESTING_REWARDS, // should be ignored
|
||||
balance: '2000000',
|
||||
asset: asset2,
|
||||
},
|
||||
];
|
||||
|
||||
const props = {
|
||||
pubKey: 'pubkey',
|
||||
assetId: rewardAsset.id,
|
||||
accounts: accounts,
|
||||
vestingBalancesSummary: {
|
||||
epoch: 1,
|
||||
lockedBalances: [
|
||||
{
|
||||
balance: '150',
|
||||
asset: rewardAsset,
|
||||
untilEpoch: 1,
|
||||
},
|
||||
{
|
||||
balance: '100',
|
||||
asset: rewardAsset,
|
||||
untilEpoch: 1,
|
||||
},
|
||||
{
|
||||
balance: '100',
|
||||
asset: asset2, // should be ignored
|
||||
untilEpoch: 1,
|
||||
},
|
||||
],
|
||||
vestingBalances: [
|
||||
{
|
||||
balance: '250',
|
||||
asset: rewardAsset,
|
||||
},
|
||||
{
|
||||
balance: '200',
|
||||
asset: rewardAsset,
|
||||
},
|
||||
{
|
||||
balance: '100',
|
||||
asset: asset2, // should be ignored
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
renderComponent(props);
|
||||
|
||||
expect(screen.getByTestId('total-rewards')).toHaveTextContent(
|
||||
`7.00 ${rewardAsset.symbol}`
|
||||
);
|
||||
|
||||
expect(screen.getByText(/Locked/).nextElementSibling).toHaveTextContent(
|
||||
'2.50'
|
||||
);
|
||||
expect(screen.getByText(/Vesting/).nextElementSibling).toHaveTextContent(
|
||||
'4.50'
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByText(/Available to withdraw/).nextElementSibling
|
||||
).toHaveTextContent('1.50');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Vesting', () => {
|
||||
it('renders vesting rates', () => {
|
||||
render(<Vesting baseRate={'0.25'} pubKey="pubKey" multiplier="2" />);
|
||||
|
||||
expect(screen.getByTestId('vesting-rate')).toHaveTextContent('50%');
|
||||
|
||||
expect(screen.getByText('Base rate').nextElementSibling).toHaveTextContent(
|
||||
'25%'
|
||||
);
|
||||
expect(
|
||||
screen.getByText('Vesting multiplier').nextSibling
|
||||
).toHaveTextContent('2x');
|
||||
});
|
||||
|
||||
it('doesnt use multiplier if not connected', () => {
|
||||
render(<Vesting baseRate={'0.25'} pubKey={null} multiplier={undefined} />);
|
||||
|
||||
expect(screen.getByTestId('vesting-rate')).toHaveTextContent('25%');
|
||||
|
||||
expect(screen.getByText('Base rate').nextElementSibling).toHaveTextContent(
|
||||
'25%'
|
||||
);
|
||||
expect(screen.queryByText('Vesting multiplier')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multipliers', () => {
|
||||
it('shows combined multipliers', () => {
|
||||
render(
|
||||
<Multipliers pubKey="pubkey" streakMultiplier="3" hoarderMultiplier="2" />
|
||||
);
|
||||
expect(screen.getByTestId('combined-multipliers')).toHaveTextContent('6x');
|
||||
expect(
|
||||
screen.getByText('Streak reward multiplier').nextElementSibling
|
||||
).toHaveTextContent('3x');
|
||||
expect(
|
||||
screen.getByText('Hoarder reward multiplier').nextElementSibling
|
||||
).toHaveTextContent('2x');
|
||||
});
|
||||
|
||||
it('shows not connected state', () => {
|
||||
render(
|
||||
<Multipliers pubKey={null} streakMultiplier="3" hoarderMultiplier="2" />
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId('combined-multipliers')
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText('Streak reward multiplier')
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText('Hoarder reward multiplier')
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByText('Not connected')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,375 @@
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import type { Account } from '@vegaprotocol/accounts';
|
||||
import { useAccounts } from '@vegaprotocol/accounts';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {
|
||||
Card,
|
||||
CardStat,
|
||||
CardTable,
|
||||
CardTableTD,
|
||||
CardTableTH,
|
||||
} from '../card/card';
|
||||
import {
|
||||
type RewardsPageQuery,
|
||||
useRewardsPageQuery,
|
||||
useRewardsEpochQuery,
|
||||
} from './__generated__/Rewards';
|
||||
import {
|
||||
TradingButton,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { formatPercentage } from '../fees-container/utils';
|
||||
import { addDecimalsFormatNumberQuantum } from '@vegaprotocol/utils';
|
||||
import { ViewType, useSidebar } from '../sidebar';
|
||||
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
||||
import { RewardsHistoryContainer } from './rewards-history';
|
||||
|
||||
export const RewardsContainer = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { params, loading: paramsLoading } = useNetworkParams([
|
||||
NetworkParams.reward_asset,
|
||||
NetworkParams.rewards_activityStreak_benefitTiers,
|
||||
NetworkParams.rewards_vesting_baseRate,
|
||||
]);
|
||||
const { data: accounts, loading: accountsLoading } = useAccounts(pubKey);
|
||||
|
||||
const { data: epochData } = useRewardsEpochQuery();
|
||||
|
||||
// No need to specify the fromEpoch as it will by default give you the last
|
||||
const { data: rewardsData, loading: rewardsLoading } = useRewardsPageQuery({
|
||||
variables: {
|
||||
partyId: pubKey || '',
|
||||
},
|
||||
});
|
||||
|
||||
if (!epochData?.epoch) return null;
|
||||
|
||||
const loading = paramsLoading || accountsLoading || rewardsLoading;
|
||||
|
||||
const rewardAccounts = accounts
|
||||
? accounts.filter((a) =>
|
||||
[
|
||||
AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
AccountType.ACCOUNT_TYPE_VESTING_REWARDS,
|
||||
].includes(a.type)
|
||||
)
|
||||
: [];
|
||||
|
||||
const rewardAssetsMap = groupBy(
|
||||
rewardAccounts.filter((a) => a.asset.id !== params.reward_asset),
|
||||
'asset.id'
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid auto-rows-min grid-cols-6 gap-3">
|
||||
{/* Always show reward information for vega */}
|
||||
<Card
|
||||
key={params.reward_asset}
|
||||
title={t('Vega Reward pot')}
|
||||
className="lg:col-span-3 xl:col-span-2"
|
||||
loading={loading}
|
||||
highlight={true}
|
||||
>
|
||||
<RewardPot
|
||||
pubKey={pubKey}
|
||||
accounts={accounts}
|
||||
assetId={params.reward_asset}
|
||||
vestingBalancesSummary={rewardsData?.party?.vestingBalancesSummary}
|
||||
/>
|
||||
</Card>
|
||||
<Card
|
||||
title={t('Vesting')}
|
||||
className="lg:col-span-3 xl:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<Vesting
|
||||
pubKey={pubKey}
|
||||
baseRate={params.rewards_vesting_baseRate}
|
||||
multiplier={
|
||||
rewardsData?.party?.activityStreak?.rewardVestingMultiplier
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
<Card
|
||||
title={t('Rewards multipliers')}
|
||||
className="lg:col-span-3 xl:col-span-2"
|
||||
loading={loading}
|
||||
highlight={true}
|
||||
>
|
||||
<Multipliers
|
||||
pubKey={pubKey}
|
||||
hoarderMultiplier={
|
||||
rewardsData?.party?.vestingStats?.rewardBonusMultiplier
|
||||
}
|
||||
streakMultiplier={
|
||||
rewardsData?.party?.activityStreak?.rewardDistributionMultiplier
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Show all other reward pots, most of the time users will not have other rewards */}
|
||||
{Object.keys(rewardAssetsMap).map((assetId) => {
|
||||
const asset = rewardAssetsMap[assetId][0].asset;
|
||||
return (
|
||||
<Card
|
||||
key={assetId}
|
||||
title={t('%s Reward pot', asset.symbol)}
|
||||
className="lg:col-span-3 xl:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<RewardPot
|
||||
pubKey={pubKey}
|
||||
accounts={accounts}
|
||||
assetId={assetId}
|
||||
vestingBalancesSummary={
|
||||
rewardsData?.party?.vestingBalancesSummary
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
<Card
|
||||
title={t('Rewards history')}
|
||||
className="lg:col-span-full"
|
||||
loading={rewardsLoading}
|
||||
>
|
||||
<RewardsHistoryContainer
|
||||
epoch={Number(epochData?.epoch.id)}
|
||||
pubKey={pubKey}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type VestingBalances = NonNullable<
|
||||
RewardsPageQuery['party']
|
||||
>['vestingBalancesSummary'];
|
||||
|
||||
export type RewardPotProps = {
|
||||
pubKey: string | null;
|
||||
accounts: Account[] | null;
|
||||
assetId: string; // VEGA
|
||||
vestingBalancesSummary: VestingBalances | undefined;
|
||||
};
|
||||
|
||||
export const RewardPot = ({
|
||||
pubKey,
|
||||
accounts,
|
||||
assetId,
|
||||
vestingBalancesSummary,
|
||||
}: RewardPotProps) => {
|
||||
// TODO: Opening the sidebar for the first time works, but then clicking on redeem
|
||||
// for a different asset does not update the form
|
||||
const currentRouteId = useGetCurrentRouteId();
|
||||
const setViews = useSidebar((store) => store.setViews);
|
||||
|
||||
// All vested rewards accounts
|
||||
const availableRewardAssetAccounts = accounts
|
||||
? accounts.filter((a) => {
|
||||
return (
|
||||
a.asset.id === assetId &&
|
||||
a.type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS
|
||||
);
|
||||
})
|
||||
: [];
|
||||
|
||||
// Sum of all vested reward account balances
|
||||
const totalVestedRewardsByRewardAsset = BigNumber.sum.apply(
|
||||
null,
|
||||
availableRewardAssetAccounts.length
|
||||
? availableRewardAssetAccounts.map((a) => a.balance)
|
||||
: [0]
|
||||
);
|
||||
|
||||
const lockedEntries = vestingBalancesSummary?.lockedBalances?.filter(
|
||||
(b) => b.asset.id === assetId
|
||||
);
|
||||
const lockedBalances = lockedEntries?.length
|
||||
? lockedEntries.map((e) => e.balance)
|
||||
: [0];
|
||||
const totalLocked = BigNumber.sum.apply(null, lockedBalances);
|
||||
|
||||
const vestingEntries = vestingBalancesSummary?.vestingBalances?.filter(
|
||||
(b) => b.asset.id === assetId
|
||||
);
|
||||
const vestingBalances = vestingEntries?.length
|
||||
? vestingEntries.map((e) => e.balance)
|
||||
: [0];
|
||||
const totalVesting = BigNumber.sum.apply(null, vestingBalances);
|
||||
|
||||
const totalRewards = totalLocked.plus(totalVesting);
|
||||
|
||||
let rewardAsset = undefined;
|
||||
|
||||
if (availableRewardAssetAccounts.length) {
|
||||
rewardAsset = availableRewardAssetAccounts[0].asset;
|
||||
} else if (lockedEntries?.length) {
|
||||
rewardAsset = lockedEntries[0].asset;
|
||||
} else if (vestingEntries?.length) {
|
||||
rewardAsset = vestingEntries[0].asset;
|
||||
}
|
||||
|
||||
if (!pubKey) {
|
||||
return (
|
||||
<div className="pt-4">
|
||||
<p className="text-muted text-sm">{t('Not connected')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pt-4">
|
||||
{rewardAsset ? (
|
||||
<>
|
||||
<CardStat
|
||||
value={`${addDecimalsFormatNumberQuantum(
|
||||
totalRewards.toString(),
|
||||
rewardAsset.decimals,
|
||||
rewardAsset.quantum
|
||||
)} ${rewardAsset.symbol}`}
|
||||
testId="total-rewards"
|
||||
/>
|
||||
<div className="flex flex-col gap-4">
|
||||
<CardTable>
|
||||
<tr>
|
||||
<CardTableTH className="flex items-center gap-1">
|
||||
{t(`Locked ${rewardAsset.symbol}`)}
|
||||
<VegaIcon name={VegaIconNames.LOCK} size={12} />
|
||||
</CardTableTH>
|
||||
<CardTableTD>
|
||||
{addDecimalsFormatNumberQuantum(
|
||||
totalLocked.toString(),
|
||||
rewardAsset.decimals,
|
||||
rewardAsset.quantum
|
||||
)}
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
<tr>
|
||||
<CardTableTH>{t(`Vesting ${rewardAsset.symbol}`)}</CardTableTH>
|
||||
<CardTableTD>
|
||||
{addDecimalsFormatNumberQuantum(
|
||||
totalVesting.toString(),
|
||||
rewardAsset.decimals,
|
||||
rewardAsset.quantum
|
||||
)}
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
<tr>
|
||||
<CardTableTH>
|
||||
{t('Available to withdraw this epoch')}
|
||||
</CardTableTH>
|
||||
<CardTableTD>
|
||||
{addDecimalsFormatNumberQuantum(
|
||||
totalVestedRewardsByRewardAsset.toString(),
|
||||
rewardAsset.decimals,
|
||||
rewardAsset.quantum
|
||||
)}
|
||||
</CardTableTD>
|
||||
</tr>
|
||||
</CardTable>
|
||||
{totalVestedRewardsByRewardAsset.isGreaterThan(0) && (
|
||||
<div>
|
||||
<TradingButton
|
||||
onClick={() =>
|
||||
setViews(
|
||||
{ type: ViewType.Transfer, assetId },
|
||||
currentRouteId
|
||||
)
|
||||
}
|
||||
size="small"
|
||||
>
|
||||
{t('Redeem rewards')}
|
||||
</TradingButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-muted text-sm">{t('No rewards')}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Vesting = ({
|
||||
pubKey,
|
||||
baseRate,
|
||||
multiplier = '1',
|
||||
}: {
|
||||
pubKey: string | null;
|
||||
baseRate: string;
|
||||
multiplier?: string;
|
||||
}) => {
|
||||
const rate = new BigNumber(baseRate).times(multiplier);
|
||||
const rateFormatted = formatPercentage(Number(rate));
|
||||
const baseRateFormatted = formatPercentage(Number(baseRate));
|
||||
|
||||
return (
|
||||
<div className="pt-4">
|
||||
<CardStat value={rateFormatted + '%'} testId="vesting-rate" />
|
||||
<CardTable>
|
||||
<tr>
|
||||
<CardTableTH>{t('Base rate')}</CardTableTH>
|
||||
<CardTableTD>{baseRateFormatted}%</CardTableTD>
|
||||
</tr>
|
||||
{pubKey && (
|
||||
<tr>
|
||||
<CardTableTH>{t('Vesting multiplier')}</CardTableTH>
|
||||
<CardTableTD>{multiplier}x</CardTableTD>
|
||||
</tr>
|
||||
)}
|
||||
</CardTable>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Multipliers = ({
|
||||
pubKey,
|
||||
streakMultiplier = '1',
|
||||
hoarderMultiplier = '1',
|
||||
}: {
|
||||
pubKey: string | null;
|
||||
streakMultiplier?: string;
|
||||
hoarderMultiplier?: string;
|
||||
}) => {
|
||||
const combinedMultiplier = new BigNumber(streakMultiplier).times(
|
||||
hoarderMultiplier
|
||||
);
|
||||
|
||||
if (!pubKey) {
|
||||
return (
|
||||
<div className="pt-4">
|
||||
<p className="text-muted text-sm">{t('Not connected')}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pt-4">
|
||||
<CardStat
|
||||
value={combinedMultiplier.toString() + 'x'}
|
||||
testId="combined-multipliers"
|
||||
highlight={true}
|
||||
/>
|
||||
<CardTable>
|
||||
<tr>
|
||||
<CardTableTH>{t('Streak reward multiplier')}</CardTableTH>
|
||||
<CardTableTD>{streakMultiplier}x</CardTableTD>
|
||||
</tr>
|
||||
<tr>
|
||||
<CardTableTH>{t('Hoarder reward multiplier')}</CardTableTH>
|
||||
<CardTableTD>{hoarderMultiplier}x</CardTableTD>
|
||||
</tr>
|
||||
</CardTable>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,194 @@
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { RewardHistoryTable } from './rewards-history';
|
||||
import { AccountType, AssetStatus } from '@vegaprotocol/types';
|
||||
import { type AssetFieldsFragment } from '@vegaprotocol/assets';
|
||||
|
||||
const assets: Record<string, AssetFieldsFragment> = {
|
||||
asset1: {
|
||||
id: 'asset1',
|
||||
name: 'Asset 1',
|
||||
status: AssetStatus.STATUS_ENABLED,
|
||||
symbol: 'A ASSET',
|
||||
decimals: 0,
|
||||
quantum: '1',
|
||||
// @ts-ignore not needed
|
||||
source: {},
|
||||
},
|
||||
asset2: {
|
||||
id: 'asset2',
|
||||
name: 'Asset 2',
|
||||
status: AssetStatus.STATUS_ENABLED,
|
||||
symbol: 'B ASSET',
|
||||
decimals: 0,
|
||||
quantum: '1',
|
||||
// @ts-ignore not needed
|
||||
source: {},
|
||||
},
|
||||
};
|
||||
|
||||
const rewardSummaries = [
|
||||
{
|
||||
node: {
|
||||
epoch: 9,
|
||||
assetId: assets.asset1.id,
|
||||
amount: '60',
|
||||
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
epoch: 8,
|
||||
assetId: assets.asset1.id,
|
||||
amount: '20',
|
||||
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
epoch: 8,
|
||||
assetId: assets.asset1.id,
|
||||
amount: '20',
|
||||
rewardType: AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
epoch: 7,
|
||||
assetId: assets.asset2.id,
|
||||
amount: '300',
|
||||
rewardType: AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const getCell = (cells: HTMLElement[], colId: string) => {
|
||||
return within(
|
||||
cells.find((c) => c.getAttribute('col-id') === colId) as HTMLElement
|
||||
);
|
||||
};
|
||||
|
||||
describe('RewarsHistoryTable', () => {
|
||||
const props = {
|
||||
epochRewardSummaries: {
|
||||
edges: rewardSummaries,
|
||||
},
|
||||
partyRewards: {
|
||||
edges: [],
|
||||
},
|
||||
assets,
|
||||
pubKey: 'pubkey',
|
||||
epoch: 10,
|
||||
epochVariables: {
|
||||
from: 1,
|
||||
to: 10,
|
||||
},
|
||||
onEpochChange: jest.fn(),
|
||||
loading: false,
|
||||
};
|
||||
|
||||
it('Renders table with accounts summed up by asset', () => {
|
||||
render(<RewardHistoryTable {...props} />);
|
||||
|
||||
const container = within(
|
||||
document.querySelector('.ag-center-cols-container') as HTMLElement
|
||||
);
|
||||
const rows = container.getAllByRole('row');
|
||||
expect(rows).toHaveLength(
|
||||
Object.keys(groupBy(rewardSummaries, 'node.assetId')).length
|
||||
);
|
||||
|
||||
let row = within(rows[0]);
|
||||
let cells = row.getAllByRole('gridcell');
|
||||
|
||||
let assetCell = getCell(cells, 'asset.symbol');
|
||||
expect(assetCell.getByTestId('stack-cell-primary')).toHaveTextContent(
|
||||
assets.asset2.symbol
|
||||
);
|
||||
expect(assetCell.getByTestId('stack-cell-secondary')).toHaveTextContent(
|
||||
assets.asset2.name
|
||||
);
|
||||
|
||||
const marketCreationCell = getCell(cells, 'marketCreation');
|
||||
expect(
|
||||
marketCreationCell.getByTestId('stack-cell-primary')
|
||||
).toHaveTextContent('300');
|
||||
expect(
|
||||
marketCreationCell.getByTestId('stack-cell-secondary')
|
||||
).toHaveTextContent('100.00%');
|
||||
|
||||
let totalCell = getCell(cells, 'total');
|
||||
expect(totalCell.getByText('300.00')).toBeInTheDocument();
|
||||
|
||||
row = within(rows[1]);
|
||||
cells = row.getAllByRole('gridcell');
|
||||
|
||||
assetCell = getCell(cells, 'asset.symbol');
|
||||
expect(assetCell.getByTestId('stack-cell-primary')).toHaveTextContent(
|
||||
assets.asset1.symbol
|
||||
);
|
||||
expect(assetCell.getByTestId('stack-cell-secondary')).toHaveTextContent(
|
||||
assets.asset1.name
|
||||
);
|
||||
|
||||
// check cells are summed and percentage of totals are shown
|
||||
const priceTakingCell = getCell(cells, 'priceTaking');
|
||||
expect(priceTakingCell.getByTestId('stack-cell-primary')).toHaveTextContent(
|
||||
'80'
|
||||
);
|
||||
expect(
|
||||
priceTakingCell.getByTestId('stack-cell-secondary')
|
||||
).toHaveTextContent('80.00%');
|
||||
|
||||
const avgPositionCell = getCell(cells, 'averagePosition');
|
||||
expect(avgPositionCell.getByTestId('stack-cell-primary')).toHaveTextContent(
|
||||
'20'
|
||||
);
|
||||
expect(
|
||||
avgPositionCell.getByTestId('stack-cell-secondary')
|
||||
).toHaveTextContent('20.00%');
|
||||
|
||||
totalCell = getCell(cells, 'total');
|
||||
expect(totalCell.getByText('100.00')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('changes epochs using pagination', async () => {
|
||||
const epochVariables = {
|
||||
from: 3,
|
||||
to: 4,
|
||||
};
|
||||
const onEpochChange = jest.fn();
|
||||
|
||||
render(
|
||||
<RewardHistoryTable
|
||||
{...props}
|
||||
epoch={5}
|
||||
epochVariables={epochVariables}
|
||||
onEpochChange={onEpochChange}
|
||||
/>
|
||||
);
|
||||
const fromInput = screen.getByLabelText('From epoch');
|
||||
const toInput = screen.getByLabelText('to');
|
||||
expect(fromInput).toHaveValue(epochVariables.from);
|
||||
expect(toInput).toHaveValue(epochVariables.to);
|
||||
|
||||
const buttons = within(screen.getByTestId('fromEpoch')).getAllByRole(
|
||||
'button'
|
||||
);
|
||||
const fromInc = buttons[0];
|
||||
const decInc = buttons[1];
|
||||
|
||||
await userEvent.click(fromInc);
|
||||
expect(onEpochChange).toHaveBeenCalledWith({ from: 4, to: 4 });
|
||||
|
||||
await userEvent.click(decInc);
|
||||
expect(onEpochChange).toHaveBeenCalledWith({ from: 2, to: 4 });
|
||||
|
||||
onEpochChange.mockClear();
|
||||
|
||||
await userEvent.type(fromInput, '1');
|
||||
// no state control so typing will just append to whats there
|
||||
expect(onEpochChange).toHaveBeenCalledWith({ from: 31, to: 4 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,391 @@
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useMemo, useState } from 'react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { ColDef, ValueFormatterFunc } from 'ag-grid-community';
|
||||
import {
|
||||
useAssetsMapProvider,
|
||||
type AssetFieldsFragment,
|
||||
} from '@vegaprotocol/assets';
|
||||
import {
|
||||
addDecimalsFormatNumberQuantum,
|
||||
formatNumberPercentage,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { AgGrid, StackedCell } from '@vegaprotocol/datagrid';
|
||||
import {
|
||||
TradingButton,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
useRewardsHistoryQuery,
|
||||
type RewardsHistoryQuery,
|
||||
} from './__generated__/Rewards';
|
||||
import { useRewardsRowData } from './use-reward-row-data';
|
||||
|
||||
export const RewardsHistoryContainer = ({
|
||||
epoch,
|
||||
pubKey,
|
||||
}: {
|
||||
pubKey: string | null;
|
||||
epoch: number;
|
||||
}) => {
|
||||
const [epochVariables, setEpochVariables] = useState(() => ({
|
||||
from: epoch - 1,
|
||||
to: epoch,
|
||||
}));
|
||||
|
||||
const { data: assets } = useAssetsMapProvider();
|
||||
|
||||
// No need to specify the fromEpoch as it will by default give you the last
|
||||
const { refetch, data, loading } = useRewardsHistoryQuery({
|
||||
variables: {
|
||||
partyId: pubKey || '',
|
||||
fromEpoch: epochVariables.from,
|
||||
toEpoch: epochVariables.to,
|
||||
},
|
||||
});
|
||||
|
||||
const debouncedRefetch = useMemo(
|
||||
() => debounce((variables) => refetch(variables), 800),
|
||||
[refetch]
|
||||
);
|
||||
|
||||
const handleEpochChange = (incoming: { from: number; to: number }) => {
|
||||
if (!Number.isInteger(incoming.from) || !Number.isInteger(incoming.to)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (incoming.from > incoming.to) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Must be at least the first epoch
|
||||
if (incoming.from < 0 || incoming.to < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (incoming.from > epoch || incoming.to > epoch) {
|
||||
return;
|
||||
}
|
||||
|
||||
setEpochVariables({
|
||||
from: incoming.from,
|
||||
to: Math.min(incoming.to, epoch),
|
||||
});
|
||||
debouncedRefetch({
|
||||
partyId: pubKey || '',
|
||||
fromEpoch: incoming.from,
|
||||
toEpoch: incoming.to,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<RewardHistoryTable
|
||||
pubKey={pubKey}
|
||||
epochRewardSummaries={data?.epochRewardSummaries}
|
||||
partyRewards={data?.party?.rewardsConnection}
|
||||
onEpochChange={handleEpochChange}
|
||||
epoch={epoch}
|
||||
epochVariables={epochVariables}
|
||||
assets={assets}
|
||||
loading={loading}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const defaultColDef = {
|
||||
flex: 1,
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
};
|
||||
|
||||
interface RewardRow {
|
||||
asset: AssetFieldsFragment;
|
||||
staking: number;
|
||||
priceTaking: number;
|
||||
priceMaking: number;
|
||||
liquidityProvision: number;
|
||||
marketCreation: number;
|
||||
averagePosition: number;
|
||||
relativeReturns: number;
|
||||
returnsVolatility: number;
|
||||
validatorRanking: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export type PartyRewardsConnection = NonNullable<
|
||||
RewardsHistoryQuery['party']
|
||||
>['rewardsConnection'];
|
||||
|
||||
export const RewardHistoryTable = ({
|
||||
epochRewardSummaries,
|
||||
partyRewards,
|
||||
assets,
|
||||
pubKey,
|
||||
epochVariables,
|
||||
epoch,
|
||||
onEpochChange,
|
||||
loading,
|
||||
}: {
|
||||
epochRewardSummaries: RewardsHistoryQuery['epochRewardSummaries'];
|
||||
partyRewards: PartyRewardsConnection;
|
||||
assets: Record<string, AssetFieldsFragment> | null;
|
||||
pubKey: string | null;
|
||||
epoch: number;
|
||||
epochVariables: {
|
||||
from: number;
|
||||
to: number;
|
||||
};
|
||||
onEpochChange: (epochVariables: { from: number; to: number }) => void;
|
||||
loading: boolean;
|
||||
}) => {
|
||||
const [isParty, setIsParty] = useState(false);
|
||||
|
||||
const rowData = useRewardsRowData({
|
||||
epochRewardSummaries,
|
||||
partyRewards,
|
||||
assets,
|
||||
partyId: isParty ? pubKey : null,
|
||||
});
|
||||
|
||||
const columnDefs = useMemo<ColDef<RewardRow>[]>(() => {
|
||||
const rewardValueFormatter: ValueFormatterFunc<RewardRow> = ({
|
||||
data,
|
||||
value,
|
||||
}) => {
|
||||
if (!value || !data) {
|
||||
return '-';
|
||||
}
|
||||
return addDecimalsFormatNumberQuantum(
|
||||
value,
|
||||
data.asset.decimals,
|
||||
data.asset.quantum
|
||||
);
|
||||
};
|
||||
|
||||
const rewardCellRenderer = ({
|
||||
data,
|
||||
value,
|
||||
valueFormatted,
|
||||
}: {
|
||||
data: RewardRow;
|
||||
value: number;
|
||||
valueFormatted: string;
|
||||
}) => {
|
||||
if (!value || value <= 0 || !data) {
|
||||
return <span className="text-muted">-</span>;
|
||||
}
|
||||
|
||||
const pctOfTotal = new BigNumber(value).dividedBy(data.total).times(100);
|
||||
|
||||
return (
|
||||
<StackedCell
|
||||
primary={valueFormatted}
|
||||
secondary={formatNumberPercentage(pctOfTotal, 2)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const colDefs: ColDef[] = [
|
||||
{
|
||||
field: 'asset.symbol',
|
||||
cellRenderer: ({ value, data }: { value: string; data: RewardRow }) => {
|
||||
if (!value || !data) return <span>-</span>;
|
||||
return <StackedCell primary={value} secondary={data.asset.name} />;
|
||||
},
|
||||
sort: 'desc',
|
||||
},
|
||||
{
|
||||
field: 'staking',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
cellRenderer: rewardCellRenderer,
|
||||
},
|
||||
{
|
||||
field: 'priceTaking',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
cellRenderer: rewardCellRenderer,
|
||||
},
|
||||
{
|
||||
field: 'priceMaking',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
cellRenderer: rewardCellRenderer,
|
||||
},
|
||||
{
|
||||
field: 'liquidityProvision',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
cellRenderer: rewardCellRenderer,
|
||||
},
|
||||
{
|
||||
field: 'marketCreation',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
cellRenderer: rewardCellRenderer,
|
||||
},
|
||||
{
|
||||
field: 'averagePosition',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
cellRenderer: rewardCellRenderer,
|
||||
},
|
||||
{
|
||||
field: 'relativeReturns',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
cellRenderer: rewardCellRenderer,
|
||||
},
|
||||
{
|
||||
field: 'returnsVolatility',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
cellRenderer: rewardCellRenderer,
|
||||
},
|
||||
{
|
||||
field: 'validatorRanking',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
cellRenderer: rewardCellRenderer,
|
||||
},
|
||||
{
|
||||
field: 'total',
|
||||
type: 'rightAligned',
|
||||
valueFormatter: rewardValueFormatter,
|
||||
},
|
||||
];
|
||||
return colDefs;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between gap-2">
|
||||
<h4 className="text-muted flex items-center gap-2 text-sm">
|
||||
<label htmlFor="fromEpoch">{t('From epoch')}</label>
|
||||
<EpochInput
|
||||
id="fromEpoch"
|
||||
value={epochVariables.from}
|
||||
max={epochVariables.to}
|
||||
onChange={(value) =>
|
||||
onEpochChange({
|
||||
from: value,
|
||||
to: epochVariables.to,
|
||||
})
|
||||
}
|
||||
onIncrement={() =>
|
||||
onEpochChange({
|
||||
from: epochVariables.from + 1,
|
||||
to: epochVariables.to,
|
||||
})
|
||||
}
|
||||
onDecrement={() =>
|
||||
onEpochChange({
|
||||
from: epochVariables.from - 1,
|
||||
to: epochVariables.to,
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<label htmlFor="toEpoch">{t('to')}</label>
|
||||
|
||||
<EpochInput
|
||||
id="toEpoch"
|
||||
value={epochVariables.to}
|
||||
max={epoch}
|
||||
onChange={(value) =>
|
||||
onEpochChange({
|
||||
from: epochVariables.from,
|
||||
to: value,
|
||||
})
|
||||
}
|
||||
onIncrement={() =>
|
||||
onEpochChange({
|
||||
from: epochVariables.from,
|
||||
to: epochVariables.to + 1,
|
||||
})
|
||||
}
|
||||
onDecrement={() =>
|
||||
onEpochChange({
|
||||
from: epochVariables.from,
|
||||
to: epochVariables.to - 1,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</h4>
|
||||
|
||||
<div className="flex gap-0.5">
|
||||
<TradingButton
|
||||
onClick={() => setIsParty(false)}
|
||||
size="extra-small"
|
||||
minimal={isParty}
|
||||
>
|
||||
{t('Total distributed')}
|
||||
</TradingButton>
|
||||
<TradingButton
|
||||
onClick={() => setIsParty(true)}
|
||||
size="extra-small"
|
||||
disabled={!pubKey}
|
||||
minimal={!isParty}
|
||||
>
|
||||
{t('Earned by me')}
|
||||
</TradingButton>
|
||||
</div>
|
||||
</div>
|
||||
<AgGrid
|
||||
columnDefs={columnDefs}
|
||||
defaultColDef={defaultColDef}
|
||||
rowData={rowData}
|
||||
rowHeight={45}
|
||||
domLayout="autoHeight"
|
||||
// Show loading message without wiping out the current rows
|
||||
overlayNoRowsTemplate={loading ? t('Loading...') : t('No rows')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EpochInput = ({
|
||||
id,
|
||||
value,
|
||||
max,
|
||||
min = 1,
|
||||
step = 1,
|
||||
onChange,
|
||||
onIncrement,
|
||||
onDecrement,
|
||||
}: {
|
||||
id: string;
|
||||
value: number;
|
||||
max?: number;
|
||||
min?: number;
|
||||
step?: number;
|
||||
onChange: (value: number) => void;
|
||||
onIncrement: () => void;
|
||||
onDecrement: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<span className="flex gap-0.5" data-testid={id}>
|
||||
<span className="bg-vega-clight-600 dark:bg-vega-cdark-600 relative rounded-l-sm">
|
||||
<span className="px-2 opacity-0">{value}</span>
|
||||
<input
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
value={value}
|
||||
className="dark:focus:bg-vega-cdark-700 absolute left-0 top-0 h-full w-full appearance-none bg-transparent px-2 focus:outline-none"
|
||||
type="number"
|
||||
step={step}
|
||||
min={min}
|
||||
max={max}
|
||||
id={id}
|
||||
name={id}
|
||||
/>
|
||||
</span>
|
||||
<span className="flex flex-col gap-0.5 overflow-hidden rounded-r-sm">
|
||||
<button
|
||||
onClick={onIncrement}
|
||||
className="bg-vega-clight-600 dark:bg-vega-cdark-600 flex flex-1 items-center px-1"
|
||||
>
|
||||
<VegaIcon name={VegaIconNames.CHEVRON_UP} size={12} />
|
||||
</button>
|
||||
<button
|
||||
onClick={onDecrement}
|
||||
className="bg-vega-clight-600 dark:bg-vega-cdark-600 flex flex-1 items-center px-1"
|
||||
>
|
||||
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={12} />
|
||||
</button>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { removePaginationWrapper } from '@vegaprotocol/utils';
|
||||
import { type Asset } from '@vegaprotocol/assets';
|
||||
import { type PartyRewardsConnection } from './rewards-history';
|
||||
import { type RewardsHistoryQuery } from './__generated__/Rewards';
|
||||
|
||||
const REWARD_ACCOUNT_TYPES = [
|
||||
AccountType.ACCOUNT_TYPE_GLOBAL_REWARD,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_RELATIVE_RETURN,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY,
|
||||
AccountType.ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING,
|
||||
];
|
||||
|
||||
const getRewards = (
|
||||
rewards: Array<{
|
||||
rewardType: AccountType;
|
||||
assetId: string;
|
||||
amount: string;
|
||||
}>,
|
||||
assets: Record<string, Asset> | null
|
||||
) => {
|
||||
const assetMap = groupBy(
|
||||
rewards.filter((r) => REWARD_ACCOUNT_TYPES.includes(r.rewardType)),
|
||||
'assetId'
|
||||
);
|
||||
|
||||
return Object.keys(assetMap).map((assetId) => {
|
||||
const r = assetMap[assetId];
|
||||
const asset = assets ? assets[assetId] : undefined;
|
||||
|
||||
const totals = new Map<AccountType, number>();
|
||||
|
||||
REWARD_ACCOUNT_TYPES.forEach((type) => {
|
||||
const amountsByType = r
|
||||
.filter((a) => a.rewardType === type)
|
||||
.map((a) => a.amount);
|
||||
const typeTotal = BigNumber.sum.apply(
|
||||
null,
|
||||
amountsByType.length ? amountsByType : [0]
|
||||
);
|
||||
|
||||
totals.set(type, typeTotal.toNumber());
|
||||
});
|
||||
|
||||
const total = BigNumber.sum.apply(
|
||||
null,
|
||||
Array.from(totals).map((entry) => entry[1])
|
||||
);
|
||||
|
||||
return {
|
||||
asset,
|
||||
staking: totals.get(AccountType.ACCOUNT_TYPE_GLOBAL_REWARD),
|
||||
priceTaking: totals.get(AccountType.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES),
|
||||
priceMaking: totals.get(
|
||||
AccountType.ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES
|
||||
),
|
||||
liquidityProvision: totals.get(
|
||||
AccountType.ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES
|
||||
),
|
||||
marketCreation: totals.get(
|
||||
AccountType.ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS
|
||||
),
|
||||
averagePosition: totals.get(
|
||||
AccountType.ACCOUNT_TYPE_REWARD_AVERAGE_POSITION
|
||||
),
|
||||
relativeReturns: totals.get(
|
||||
AccountType.ACCOUNT_TYPE_REWARD_RELATIVE_RETURN
|
||||
),
|
||||
returnsVolatility: totals.get(
|
||||
AccountType.ACCOUNT_TYPE_REWARD_RETURN_VOLATILITY
|
||||
),
|
||||
validatorRanking: totals.get(
|
||||
AccountType.ACCOUNT_TYPE_REWARD_VALIDATOR_RANKING
|
||||
),
|
||||
total: total.toNumber(),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const useRewardsRowData = ({
|
||||
partyRewards,
|
||||
epochRewardSummaries,
|
||||
assets,
|
||||
partyId,
|
||||
}: {
|
||||
partyRewards: PartyRewardsConnection;
|
||||
epochRewardSummaries: RewardsHistoryQuery['epochRewardSummaries'];
|
||||
assets: Record<string, Asset> | null;
|
||||
partyId: string | null;
|
||||
}) => {
|
||||
if (partyId) {
|
||||
const rewards = removePaginationWrapper(partyRewards?.edges).map((r) => ({
|
||||
rewardType: r.rewardType,
|
||||
assetId: r.asset.id,
|
||||
amount: r.amount,
|
||||
}));
|
||||
return getRewards(rewards, assets);
|
||||
}
|
||||
|
||||
const rewards = removePaginationWrapper(epochRewardSummaries?.edges);
|
||||
return getRewards(rewards, assets);
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
import type { Module } from 'i18next';
|
||||
import i18n from 'i18next';
|
||||
import HttpBackend from 'i18next-http-backend';
|
||||
import LocizeBackend from 'i18next-locize-backend';
|
||||
import type { HttpBackendOptions, RequestCallback } from 'i18next-http-backend';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
const isInDev = process.env.NODE_ENV === 'development';
|
||||
const useLocize = isInDev && !!process.env.NX_USE_LOCIZE;
|
||||
|
||||
const backend = useLocize
|
||||
? {
|
||||
projectId: '96ac1231-4bdd-455a-b9d7-f5322a2e7430',
|
||||
apiKey: process.env.NX_LOCIZE_API_KEY,
|
||||
referenceLng: 'en',
|
||||
}
|
||||
: {
|
||||
loadPath: '/locales/{{lng}}/{{ns}}.json',
|
||||
request: (
|
||||
options: HttpBackendOptions,
|
||||
url: string,
|
||||
payload: string,
|
||||
callback: RequestCallback
|
||||
) => {
|
||||
if (typeof window === 'undefined') {
|
||||
callback(false, { status: 200, data: {} });
|
||||
return;
|
||||
}
|
||||
fetch(url).then((response) => {
|
||||
if (!response.ok) {
|
||||
return callback(response.statusText || 'Error', {
|
||||
status: response.status,
|
||||
data: {},
|
||||
});
|
||||
}
|
||||
response
|
||||
.text()
|
||||
.then((data) => {
|
||||
callback(null, { status: response.status, data });
|
||||
})
|
||||
.catch((error) => callback(error, { status: 200, data: {} }));
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const Backend: Module = useLocize ? LocizeBackend : HttpBackend;
|
||||
|
||||
i18n
|
||||
.use(Backend)
|
||||
.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
lng: 'en',
|
||||
fallbackLng: 'en',
|
||||
supportedLngs: ['en'],
|
||||
load: 'languageOnly',
|
||||
// have a common namespace used around the full app
|
||||
ns: [
|
||||
'accounts',
|
||||
'assets',
|
||||
'candles-chart',
|
||||
'datagrid',
|
||||
'deal-ticket',
|
||||
'deposits',
|
||||
'environment',
|
||||
'fills',
|
||||
'funding-payments',
|
||||
'trading',
|
||||
],
|
||||
defaultNS: 'trading',
|
||||
keySeparator: false, // we use content as keys
|
||||
backend,
|
||||
debug: isInDev,
|
||||
saveMissing: useLocize && !!process.env.NX_LOCIZE_API_KEY,
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
@@ -18,6 +18,7 @@ export const Routes = {
|
||||
REFERRALS_CREATE_CODE: '/referrals/create-code',
|
||||
TEAMS: '/teams',
|
||||
FEES: '/fees',
|
||||
REWARDS: '/rewards',
|
||||
} as const;
|
||||
|
||||
type ConsoleLinks = {
|
||||
@@ -42,4 +43,5 @@ export const Links: ConsoleLinks = {
|
||||
REFERRALS_CREATE_CODE: () => Routes.REFERRALS_CREATE_CODE,
|
||||
TEAMS: () => Routes.TEAMS,
|
||||
FEES: () => Routes.FEES,
|
||||
REWARDS: () => Routes.REWARDS,
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import Head from 'next/head';
|
||||
import type { AppProps } from 'next/app';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
envTriggerMapping,
|
||||
useEnvTriggerMapping,
|
||||
Networks,
|
||||
NodeSwitcherDialog,
|
||||
useEnvironment,
|
||||
@@ -32,6 +32,7 @@ import { SSRLoader } from './ssr-loader';
|
||||
import { PartyActiveOrdersHandler } from './party-active-orders-handler';
|
||||
import { MaybeConnectEagerly } from './maybe-connect-eagerly';
|
||||
import { TransactionHandlers } from './transaction-handlers';
|
||||
import '../lib/i18n';
|
||||
|
||||
const DEFAULT_TITLE = t('Welcome to Vega trading!');
|
||||
|
||||
@@ -39,7 +40,7 @@ const Title = () => {
|
||||
const { pageTitle } = usePageTitleStore((store) => ({
|
||||
pageTitle: store.pageTitle,
|
||||
}));
|
||||
|
||||
const envTriggerMapping = useEnvTriggerMapping();
|
||||
const { VEGA_ENV } = useEnvironment();
|
||||
const networkName = envTriggerMapping[VEGA_ENV];
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Deposit } from '../client-pages/deposit';
|
||||
import { Withdraw } from '../client-pages/withdraw';
|
||||
import { Transfer } from '../client-pages/transfer';
|
||||
import { Fees } from '../client-pages/fees';
|
||||
import { Rewards } from '../client-pages/rewards';
|
||||
import { Routes as AppRoutes } from '../lib/links';
|
||||
import { LayoutWithSky } from '../client-pages/referrals/layout';
|
||||
import { Referrals } from '../client-pages/referrals/referrals';
|
||||
@@ -96,6 +97,16 @@ export const routerConfig: RouteObject[] = compact([
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'rewards/*',
|
||||
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Rewards />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'markets/*',
|
||||
element: (
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../../libs/i18n/src/locales
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ETHERSCAN_ADDRESS, useEtherscanLink } from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useT } from './use-t';
|
||||
import {
|
||||
ActionsDropdown,
|
||||
TradingDropdownCopyItem,
|
||||
@@ -27,7 +27,7 @@ export const AccountsActionsDropdown = ({
|
||||
}) => {
|
||||
const etherscanLink = useEtherscanLink();
|
||||
const openAssetDialog = useAssetDetailsDialogStore((store) => store.open);
|
||||
|
||||
const t = useT();
|
||||
return (
|
||||
<ActionsDropdown>
|
||||
<TradingDropdownItem
|
||||
|
||||
@@ -4,6 +4,7 @@ import { marketsMapProvider } from '@vegaprotocol/markets';
|
||||
import {
|
||||
makeDataProvider,
|
||||
makeDerivedDataProvider,
|
||||
useDataProvider,
|
||||
} from '@vegaprotocol/data-provider';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { type Market } from '@vegaprotocol/markets';
|
||||
@@ -214,3 +215,13 @@ export const aggregatedAccountDataProvider = makeDerivedDataProvider<
|
||||
(account) => account.asset.id === assetId
|
||||
) || null
|
||||
);
|
||||
|
||||
export const useAccounts = (partyId: string | null) => {
|
||||
return useDataProvider({
|
||||
dataProvider: accountsDataProvider,
|
||||
variables: {
|
||||
partyId: partyId || '',
|
||||
},
|
||||
skip: !partyId,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useRef, memo, useState, useCallback } from 'react';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useT } from './use-t';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { type AgGridReact } from 'ag-grid-react';
|
||||
import {
|
||||
@@ -22,6 +22,7 @@ const AccountBreakdown = ({
|
||||
partyId: string;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const gridRef = useRef<AgGridReact>(null);
|
||||
const { data } = useDataProvider({
|
||||
dataProvider: aggregatedAccountDataProvider,
|
||||
@@ -45,10 +46,10 @@ const AccountBreakdown = ({
|
||||
</h1>
|
||||
{data && (
|
||||
<p className="mb-2 text-sm">
|
||||
{t('You have %s %s in total.', [
|
||||
addDecimalsFormatNumber(data.total, data.asset.decimals),
|
||||
data.asset.symbol,
|
||||
])}
|
||||
{t('You have {{value}} {{symbol}} in total.', {
|
||||
value: addDecimalsFormatNumber(data.total, data.asset.decimals),
|
||||
symbol: data.asset.symbol,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
<BreakdownTable
|
||||
@@ -118,6 +119,7 @@ export const AccountManager = ({
|
||||
onMarketClick,
|
||||
gridProps,
|
||||
}: AccountManagerProps) => {
|
||||
const t = useT();
|
||||
const [breakdownAssetId, setBreakdownAssetId] = useState<string>();
|
||||
const { data, error } = useDataProvider({
|
||||
dataProvider: aggregatedAccountsDataProvider,
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
isNumeric,
|
||||
toBigNum,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useT } from './use-t';
|
||||
import type {
|
||||
VegaICellRendererParams,
|
||||
VegaValueFormatterParams,
|
||||
@@ -96,6 +96,7 @@ export const AccountTable = ({
|
||||
pinnedAsset,
|
||||
...props
|
||||
}: AccountTableProps) => {
|
||||
const t = useT();
|
||||
const pinnedRow = useMemo(() => {
|
||||
if (!pinnedAsset) {
|
||||
return;
|
||||
@@ -191,7 +192,7 @@ export const AccountTable = ({
|
||||
<>
|
||||
<span className="underline">{valueFormatted}</span>
|
||||
<span className="inline-block ml-2 w-14 text-muted">
|
||||
{t('0.00%')}
|
||||
{(0).toFixed(2)}%
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
@@ -310,6 +311,7 @@ export const AccountTable = ({
|
||||
onClickTransfer,
|
||||
isReadOnly,
|
||||
showDepositButton,
|
||||
t,
|
||||
]);
|
||||
|
||||
const data = rowData?.filter((data) => data.asset.id !== pinnedAsset?.id);
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
addDecimalsFormatNumber,
|
||||
addDecimalsFormatNumberQuantum,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useT } from './use-t';
|
||||
import { Intent, TooltipCellComponent } from '@vegaprotocol/ui-toolkit';
|
||||
import { type AgGridReact, type AgGridReactProps } from 'ag-grid-react';
|
||||
import { type AccountFields } from './accounts-data-provider';
|
||||
@@ -31,6 +31,7 @@ interface BreakdownTableProps extends AgGridReactProps {
|
||||
|
||||
const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
|
||||
({ data }, ref) => {
|
||||
const t = useT();
|
||||
const coldefs = useMemo(() => {
|
||||
const defs: ColDef[] = [
|
||||
{
|
||||
@@ -53,7 +54,7 @@ const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
'None'
|
||||
t('None')
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -126,7 +127,7 @@ const BreakdownTable = forwardRef<AgGridReact, BreakdownTableProps>(
|
||||
},
|
||||
];
|
||||
return defs;
|
||||
}, []);
|
||||
}, [t]);
|
||||
|
||||
return (
|
||||
<AgGrid
|
||||
|
||||
@@ -4,9 +4,10 @@ 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 { t } from '@vegaprotocol/i18n';
|
||||
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,
|
||||
@@ -58,6 +59,7 @@ export const MarginHealthChartTooltip = ({
|
||||
decimals: number;
|
||||
marginAccountBalance?: string;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const tooltipContent = [
|
||||
<MarginHealthChartTooltipRow
|
||||
key={'maintenance'}
|
||||
@@ -169,14 +171,23 @@ export const MarginHealthChart = ({
|
||||
|
||||
return (
|
||||
<div data-testid="margin-health-chart">
|
||||
{addDecimalsFormatNumber(
|
||||
(BigInt(marginAccountBalance) - BigInt(maintenanceLevel)).toString(),
|
||||
decimals
|
||||
)}{' '}
|
||||
{t('above')}{' '}
|
||||
<ExternalLink href="https://docs.vega.xyz/testnet/concepts/trading-on-vega/positions-margin#margin-level-maintenance">
|
||||
{t('maintenance level')}
|
||||
</ExternalLink>
|
||||
<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"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { truncateByChars } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { ns, useT } from './use-t';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParams,
|
||||
@@ -21,6 +22,7 @@ export const ALLOWED_ACCOUNTS = [
|
||||
];
|
||||
|
||||
export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
||||
const t = useT();
|
||||
const { pubKey, pubKeys } = useVegaWallet();
|
||||
const { params } = useNetworkParams([
|
||||
NetworkParams.transfer_fee_factor,
|
||||
@@ -50,16 +52,20 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
||||
return (
|
||||
<>
|
||||
<p className="mb-4 text-sm" data-testid="transfer-intro-text">
|
||||
{t('Transfer funds to another Vega key')}
|
||||
{pubKey && (
|
||||
<>
|
||||
{t(' from ')}
|
||||
<Lozenge className="font-mono">
|
||||
{truncateByChars(pubKey || '')}
|
||||
</Lozenge>
|
||||
</>
|
||||
{pubKey ? (
|
||||
<Trans
|
||||
i18nKey="TRANSFER_FUNDS_TO_ANOTHER_KNOWN_VEGA_KEY"
|
||||
defaults="Transfer funds to another Vega key <0>{{pubKey}}</0>. If you are at all unsure, stop and seek advice."
|
||||
ns={ns}
|
||||
components={[<Lozenge className="font-mono">pubKey</Lozenge>]}
|
||||
values={{ pubKey: truncateByChars(pubKey || '') }}
|
||||
/>
|
||||
) : (
|
||||
t('TRANSFER_FUNDS_TO_ANOTHER_VEGA_KEY', {
|
||||
defaultValue:
|
||||
'Transfer funds to another Vega key. If you are at all unsure, stop and seek advice.',
|
||||
})
|
||||
)}
|
||||
{t('. If you are at all unsure, stop and seek advice.')}
|
||||
</p>
|
||||
<TransferForm
|
||||
pubKey={pubKey}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
addDecimalsFormatNumber,
|
||||
toBigNum,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useT } from './use-t';
|
||||
import {
|
||||
TradingFormGroup,
|
||||
TradingInput,
|
||||
@@ -66,6 +66,7 @@ export const TransferForm = ({
|
||||
accounts,
|
||||
minQuantumMultiple,
|
||||
}: TransferFormProps) => {
|
||||
const t = useT();
|
||||
const {
|
||||
control,
|
||||
register,
|
||||
@@ -300,7 +301,7 @@ export const TransferForm = ({
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup label="To Vega key" labelFor="toVegaKey">
|
||||
<TradingFormGroup label={t('To Vega key')} labelFor="toVegaKey">
|
||||
<AddressField
|
||||
onChange={() => {
|
||||
setValue('toVegaKey', '');
|
||||
@@ -317,7 +318,10 @@ export const TransferForm = ({
|
||||
{t('Please select')}
|
||||
</option>
|
||||
{pubKeys?.map((pk) => {
|
||||
const text = pk === pubKey ? t('Current key: ') + pk : pk;
|
||||
const text =
|
||||
pk === pubKey
|
||||
? t('Current key: {{pubKey}}', { pubKey: pk }) + pk
|
||||
: pk;
|
||||
|
||||
return (
|
||||
<option key={pk} value={pk}>
|
||||
@@ -351,7 +355,7 @@ export const TransferForm = ({
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup label="Amount" labelFor="amount">
|
||||
<TradingFormGroup label={t('Amount')} labelFor="amount">
|
||||
<TradingInput
|
||||
id="amount"
|
||||
autoComplete="off"
|
||||
@@ -473,6 +477,7 @@ export const TransferFee = ({
|
||||
fee?: string;
|
||||
decimals?: number;
|
||||
}) => {
|
||||
const t = useT();
|
||||
if (!feeFactor || !amount || !transferAmount || !fee) return null;
|
||||
if (
|
||||
isNaN(Number(feeFactor)) ||
|
||||
@@ -490,8 +495,8 @@ export const TransferFee = ({
|
||||
<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 %s`,
|
||||
[feeFactor]
|
||||
`The transfer fee is set by the network parameter transfer.fee.factor, currently set to {{feeFactor}}`,
|
||||
{ feeFactor }
|
||||
)}
|
||||
>
|
||||
<div>{t('Transfer fee')}</div>
|
||||
@@ -546,6 +551,7 @@ export const AddressField = ({
|
||||
mode,
|
||||
onChange,
|
||||
}: AddressInputProps) => {
|
||||
const t = useT();
|
||||
const isInput = mode === 'input';
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
export const ns = 'accounts';
|
||||
export const useT = () => useTranslation(ns).t;
|
||||
@@ -1,6 +1,21 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
import { defaultFallbackInView } from 'react-intersection-observer';
|
||||
import { locales } from '@vegaprotocol/i18n';
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
defaultFallbackInView(true);
|
||||
global.ResizeObserver = ResizeObserver;
|
||||
|
||||
// Set up i18n instance so that components have the correct default
|
||||
// en translations
|
||||
i18n.use(initReactI18next).init({
|
||||
// we init with resources
|
||||
resources: locales,
|
||||
fallbackLng: 'en',
|
||||
ns: ['accounts'],
|
||||
defaultNS: 'accounts',
|
||||
});
|
||||
|
||||
global.ResizeObserver = ResizeObserver;
|
||||
|
||||
@@ -10,4 +10,4 @@
|
||||
]
|
||||
],
|
||||
"plugins": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useT } from './use-t';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
@@ -56,6 +56,7 @@ export const AssetDetailsDialog = ({
|
||||
onChange,
|
||||
asJson = false,
|
||||
}: AssetDetailsDialogProps) => {
|
||||
const t = useT();
|
||||
const { data: asset } = useAssetDataProvider(assetId);
|
||||
|
||||
const assetSymbol = asset?.symbol || '';
|
||||
@@ -77,7 +78,7 @@ export const AssetDetailsDialog = ({
|
||||
</div>
|
||||
);
|
||||
const title = asset
|
||||
? t(`Asset details - ${asset.symbol}`)
|
||||
? t('Asset details - {{symbol}}', asset)
|
||||
: t('Asset not found');
|
||||
|
||||
return (
|
||||
@@ -100,8 +101,8 @@ export const AssetDetailsDialog = ({
|
||||
{content}
|
||||
<p className="my-4 text-xs">
|
||||
{t(
|
||||
'There is 1 unit of the settlement asset (%s) to every 1 quote unit.',
|
||||
[assetSymbol]
|
||||
'There is 1 unit of the settlement asset ({{assetSymbol}}) to every 1 quote unit.',
|
||||
{ assetSymbol }
|
||||
)}
|
||||
</p>
|
||||
<div className="w-1/4">
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { render, screen, renderHook } from '@testing-library/react';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { Asset } from './asset-data-provider';
|
||||
import {
|
||||
AssetDetail,
|
||||
AssetDetailsTable,
|
||||
rows,
|
||||
useRows,
|
||||
testId,
|
||||
} from './asset-details-table';
|
||||
import { generateBuiltinAsset, generateERC20Asset } from './test-helpers';
|
||||
@@ -67,6 +67,8 @@ describe('AssetDetailsTable', () => {
|
||||
it.each(cases)(
|
||||
"displays the available asset's data of %p with correct labels",
|
||||
async (_type, asset, details) => {
|
||||
const { result } = renderHook(() => useRows());
|
||||
const rows = result.current;
|
||||
render(<AssetDetailsTable asset={asset} />);
|
||||
for (const detail of details) {
|
||||
expect(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EtherscanLink } from '@vegaprotocol/environment';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useT } from './use-t';
|
||||
import type * as Schema from '@vegaprotocol/types';
|
||||
import type { KeyValueTableRowProps } from '@vegaprotocol/ui-toolkit';
|
||||
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
KeyValueTableRow,
|
||||
Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useMemo, type ReactNode } from 'react';
|
||||
import type { Asset } from './asset-data-provider';
|
||||
import { WITHDRAW_THRESHOLD_TOOLTIP_TEXT } from './constants';
|
||||
|
||||
@@ -52,183 +52,208 @@ const num = (asset: Asset, n: string | undefined | null) => {
|
||||
return addDecimalsFormatNumber(n, asset.decimals);
|
||||
};
|
||||
|
||||
export const rows: Rows = [
|
||||
{
|
||||
key: AssetDetail.ID,
|
||||
label: t('ID'),
|
||||
tooltip: '',
|
||||
value: (asset) => (
|
||||
<>
|
||||
{truncateMiddle(asset.id)}{' '}
|
||||
<CopyWithTooltip text={asset.id}>
|
||||
<button title={t('Copy id to clipboard')}>
|
||||
<VegaIcon size={14} name={VegaIconNames.COPY} />
|
||||
</button>
|
||||
</CopyWithTooltip>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: AssetDetail.TYPE,
|
||||
label: t('Type'),
|
||||
tooltip: '',
|
||||
value: (asset) => AssetTypeMapping[asset.source.__typename].value,
|
||||
valueTooltip: (asset) => AssetTypeMapping[asset.source.__typename].tooltip,
|
||||
},
|
||||
{
|
||||
key: AssetDetail.NAME,
|
||||
label: t('Name'),
|
||||
tooltip: '',
|
||||
value: (asset) => asset.name,
|
||||
},
|
||||
{
|
||||
key: AssetDetail.SYMBOL,
|
||||
label: t('Symbol'),
|
||||
tooltip: '',
|
||||
value: (asset) => asset.symbol,
|
||||
},
|
||||
{
|
||||
key: AssetDetail.DECIMALS,
|
||||
label: t('Decimals'),
|
||||
tooltip: t('Number of decimal / precision handled by this asset'),
|
||||
value: (asset) => asset.decimals.toString(),
|
||||
},
|
||||
{
|
||||
key: AssetDetail.QUANTUM,
|
||||
label: t('Quantum'),
|
||||
tooltip: t('The minimum economically meaningful amount of the asset'),
|
||||
value: (asset) => num(asset, asset.quantum),
|
||||
},
|
||||
{
|
||||
key: AssetDetail.STATUS,
|
||||
label: t('Status'),
|
||||
tooltip: t('The status of the asset in the Vega network'),
|
||||
value: (asset) => AssetStatusMapping[asset.status].value,
|
||||
valueTooltip: (asset) => AssetStatusMapping[asset.status].tooltip,
|
||||
},
|
||||
{
|
||||
key: AssetDetail.CONTRACT_ADDRESS,
|
||||
label: t('Contract address'),
|
||||
tooltip: t(
|
||||
'The address of the contract for the token, on the ethereum network'
|
||||
),
|
||||
value: (asset) => {
|
||||
if (asset.source.__typename !== 'ERC20') {
|
||||
return;
|
||||
}
|
||||
export const useRows = () => {
|
||||
const t = useT();
|
||||
const AssetTypeMapping = useAssetTypeMapping();
|
||||
const AssetStatusMapping = useAssetStatusMapping();
|
||||
return useMemo<Rows>(
|
||||
() => [
|
||||
{
|
||||
key: AssetDetail.ID,
|
||||
label: t('ID'),
|
||||
tooltip: '',
|
||||
value: (asset) => (
|
||||
<>
|
||||
{truncateMiddle(asset.id)}{' '}
|
||||
<CopyWithTooltip text={asset.id}>
|
||||
<button title={t('Copy id to clipboard')}>
|
||||
<VegaIcon size={14} name={VegaIconNames.COPY} />
|
||||
</button>
|
||||
</CopyWithTooltip>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: AssetDetail.TYPE,
|
||||
label: t('Type'),
|
||||
tooltip: '',
|
||||
value: (asset) => AssetTypeMapping[asset.source.__typename].value,
|
||||
valueTooltip: (asset) =>
|
||||
AssetTypeMapping[asset.source.__typename].tooltip,
|
||||
},
|
||||
{
|
||||
key: AssetDetail.NAME,
|
||||
label: t('Name'),
|
||||
tooltip: '',
|
||||
value: (asset) => asset.name,
|
||||
},
|
||||
{
|
||||
key: AssetDetail.SYMBOL,
|
||||
label: t('Symbol'),
|
||||
tooltip: '',
|
||||
value: (asset) => asset.symbol,
|
||||
},
|
||||
{
|
||||
key: AssetDetail.DECIMALS,
|
||||
label: t('Decimals'),
|
||||
tooltip: t('Number of decimal / precision handled by this asset'),
|
||||
value: (asset) => asset.decimals.toString(),
|
||||
},
|
||||
{
|
||||
key: AssetDetail.QUANTUM,
|
||||
label: t('Quantum'),
|
||||
tooltip: t('The minimum economically meaningful amount of the asset'),
|
||||
value: (asset) => num(asset, asset.quantum),
|
||||
},
|
||||
{
|
||||
key: AssetDetail.STATUS,
|
||||
label: t('Status'),
|
||||
tooltip: t('The status of the asset in the Vega network'),
|
||||
value: (asset) => AssetStatusMapping[asset.status].value,
|
||||
valueTooltip: (asset) => AssetStatusMapping[asset.status].tooltip,
|
||||
},
|
||||
{
|
||||
key: AssetDetail.CONTRACT_ADDRESS,
|
||||
label: t('Contract address'),
|
||||
tooltip: t(
|
||||
'The address of the contract for the token, on the ethereum network'
|
||||
),
|
||||
value: (asset) => {
|
||||
if (asset.source.__typename !== 'ERC20') {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<EtherscanLink address={asset.source.contractAddress}>
|
||||
{truncateMiddle(asset.source.contractAddress)}
|
||||
</EtherscanLink>{' '}
|
||||
<CopyWithTooltip text={asset.source.contractAddress}>
|
||||
<button title={t('Copy address to clipboard')}>
|
||||
<VegaIcon size={14} name={VegaIconNames.COPY} />
|
||||
</button>
|
||||
</CopyWithTooltip>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: AssetDetail.WITHDRAWAL_THRESHOLD,
|
||||
label: t('Withdrawal threshold'),
|
||||
tooltip: WITHDRAW_THRESHOLD_TOOLTIP_TEXT,
|
||||
value: (asset) =>
|
||||
num(asset, (asset.source as Schema.ERC20).withdrawThreshold),
|
||||
},
|
||||
{
|
||||
key: AssetDetail.LIFETIME_LIMIT,
|
||||
label: t('Lifetime limit'),
|
||||
tooltip: t(
|
||||
'The lifetime deposit limit per address. Note: this is a temporary measure that can be changed or removed through governance'
|
||||
),
|
||||
value: (asset) => num(asset, (asset.source as Schema.ERC20).lifetimeLimit),
|
||||
},
|
||||
{
|
||||
key: AssetDetail.MAX_FAUCET_AMOUNT_MINT,
|
||||
label: t('Max faucet amount'),
|
||||
tooltip: t(
|
||||
'Maximum amount that can be requested by a party through the built-in asset faucet at a time'
|
||||
),
|
||||
value: (asset) =>
|
||||
num(asset, (asset.source as Schema.BuiltinAsset).maxFaucetAmountMint),
|
||||
},
|
||||
{
|
||||
key: AssetDetail.INFRASTRUCTURE_FEE_ACCOUNT_BALANCE,
|
||||
label: t('Infrastructure fee account balance'),
|
||||
tooltip: t('The infrastructure fee account in this asset'),
|
||||
value: (asset) => num(asset, asset.infrastructureFeeAccount?.balance),
|
||||
},
|
||||
{
|
||||
key: AssetDetail.GLOBAL_REWARD_POOL_ACCOUNT_BALANCE,
|
||||
label: t('Global reward pool account balance'),
|
||||
tooltip: t('The global rewards acquired in this asset'),
|
||||
value: (asset) => num(asset, asset.globalRewardPoolAccount?.balance),
|
||||
},
|
||||
{
|
||||
key: AssetDetail.MAKER_PAID_FEES_ACCOUNT_BALANCE,
|
||||
label: t('Maker paid fees account balance'),
|
||||
tooltip: t(
|
||||
'The rewards acquired based on the fees paid to makers in this asset'
|
||||
),
|
||||
value: (asset) => num(asset, asset.takerFeeRewardAccount?.balance),
|
||||
},
|
||||
{
|
||||
key: AssetDetail.MAKER_RECEIVED_FEES_ACCOUNT_BALANCE,
|
||||
label: t('Maker received fees account balance'),
|
||||
tooltip: t(
|
||||
'The rewards acquired based on fees received for being a maker on trades'
|
||||
),
|
||||
value: (asset) => num(asset, asset.makerFeeRewardAccount?.balance),
|
||||
},
|
||||
{
|
||||
key: AssetDetail.LP_FEE_REWARD_ACCOUNT_BALANCE,
|
||||
label: t('Liquidity provision fee reward account balance'),
|
||||
tooltip: t(
|
||||
'The rewards acquired based on the liquidity provision fees in this asset'
|
||||
),
|
||||
value: (asset) => num(asset, asset.lpFeeRewardAccount?.balance),
|
||||
},
|
||||
{
|
||||
key: AssetDetail.MARKET_PROPOSER_REWARD_ACCOUNT_BALANCE,
|
||||
label: t('Market proposer reward account balance'),
|
||||
tooltip: t(
|
||||
'The rewards acquired based on the market proposer reward in this asset'
|
||||
),
|
||||
value: (asset) => num(asset, asset.marketProposerRewardAccount?.balance),
|
||||
},
|
||||
];
|
||||
|
||||
export const AssetStatusMapping: Mapping = {
|
||||
STATUS_ENABLED: {
|
||||
value: t('Enabled'),
|
||||
tooltip: t('Asset can be used on the Vega network'),
|
||||
},
|
||||
STATUS_PENDING_LISTING: {
|
||||
value: t('Pending listing'),
|
||||
tooltip: t('Asset needs to be added to the Ethereum bridge'),
|
||||
},
|
||||
STATUS_PROPOSED: {
|
||||
value: t('Proposed'),
|
||||
tooltip: t('Asset has been proposed to the network'),
|
||||
},
|
||||
STATUS_REJECTED: {
|
||||
value: t('Rejected'),
|
||||
tooltip: t('Asset has been rejected'),
|
||||
},
|
||||
return (
|
||||
<>
|
||||
<EtherscanLink address={asset.source.contractAddress}>
|
||||
{truncateMiddle(asset.source.contractAddress)}
|
||||
</EtherscanLink>{' '}
|
||||
<CopyWithTooltip text={asset.source.contractAddress}>
|
||||
<button title={t('Copy address to clipboard')}>
|
||||
<VegaIcon size={14} name={VegaIconNames.COPY} />
|
||||
</button>
|
||||
</CopyWithTooltip>
|
||||
</>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: AssetDetail.WITHDRAWAL_THRESHOLD,
|
||||
label: t('Withdrawal threshold'),
|
||||
tooltip: t('WITHDRAW_THRESHOLD_TOOLTIP_TEXT', {
|
||||
defaultValue: WITHDRAW_THRESHOLD_TOOLTIP_TEXT,
|
||||
}),
|
||||
value: (asset) =>
|
||||
num(asset, (asset.source as Schema.ERC20).withdrawThreshold),
|
||||
},
|
||||
{
|
||||
key: AssetDetail.LIFETIME_LIMIT,
|
||||
label: t('Lifetime limit'),
|
||||
tooltip: t(
|
||||
'The lifetime deposit limit per address. Note: this is a temporary measure that can be changed or removed through governance'
|
||||
),
|
||||
value: (asset) =>
|
||||
num(asset, (asset.source as Schema.ERC20).lifetimeLimit),
|
||||
},
|
||||
{
|
||||
key: AssetDetail.MAX_FAUCET_AMOUNT_MINT,
|
||||
label: t('Max faucet amount'),
|
||||
tooltip: t(
|
||||
'Maximum amount that can be requested by a party through the built-in asset faucet at a time'
|
||||
),
|
||||
value: (asset) =>
|
||||
num(asset, (asset.source as Schema.BuiltinAsset).maxFaucetAmountMint),
|
||||
},
|
||||
{
|
||||
key: AssetDetail.INFRASTRUCTURE_FEE_ACCOUNT_BALANCE,
|
||||
label: t('Infrastructure fee account balance'),
|
||||
tooltip: t('The infrastructure fee account in this asset'),
|
||||
value: (asset) => num(asset, asset.infrastructureFeeAccount?.balance),
|
||||
},
|
||||
{
|
||||
key: AssetDetail.GLOBAL_REWARD_POOL_ACCOUNT_BALANCE,
|
||||
label: t('Global reward pool account balance'),
|
||||
tooltip: t('The global rewards acquired in this asset'),
|
||||
value: (asset) => num(asset, asset.globalRewardPoolAccount?.balance),
|
||||
},
|
||||
{
|
||||
key: AssetDetail.MAKER_PAID_FEES_ACCOUNT_BALANCE,
|
||||
label: t('Maker paid fees account balance'),
|
||||
tooltip: t(
|
||||
'The rewards acquired based on the fees paid to makers in this asset'
|
||||
),
|
||||
value: (asset) => num(asset, asset.takerFeeRewardAccount?.balance),
|
||||
},
|
||||
{
|
||||
key: AssetDetail.MAKER_RECEIVED_FEES_ACCOUNT_BALANCE,
|
||||
label: t('Maker received fees account balance'),
|
||||
tooltip: t(
|
||||
'The rewards acquired based on fees received for being a maker on trades'
|
||||
),
|
||||
value: (asset) => num(asset, asset.makerFeeRewardAccount?.balance),
|
||||
},
|
||||
{
|
||||
key: AssetDetail.LP_FEE_REWARD_ACCOUNT_BALANCE,
|
||||
label: t('Liquidity provision fee reward account balance'),
|
||||
tooltip: t(
|
||||
'The rewards acquired based on the liquidity provision fees in this asset'
|
||||
),
|
||||
value: (asset) => num(asset, asset.lpFeeRewardAccount?.balance),
|
||||
},
|
||||
{
|
||||
key: AssetDetail.MARKET_PROPOSER_REWARD_ACCOUNT_BALANCE,
|
||||
label: t('Market proposer reward account balance'),
|
||||
tooltip: t(
|
||||
'The rewards acquired based on the market proposer reward in this asset'
|
||||
),
|
||||
value: (asset) =>
|
||||
num(asset, asset.marketProposerRewardAccount?.balance),
|
||||
},
|
||||
],
|
||||
[t, AssetTypeMapping, AssetStatusMapping]
|
||||
);
|
||||
};
|
||||
|
||||
export const AssetTypeMapping: Mapping = {
|
||||
BuiltinAsset: {
|
||||
value: 'Builtin asset',
|
||||
tooltip: t('A Vega builtin asset'),
|
||||
},
|
||||
ERC20: {
|
||||
value: 'ERC20',
|
||||
tooltip: t('An asset originated from an Ethereum ERC20 Token'),
|
||||
},
|
||||
export const useAssetStatusMapping = () => {
|
||||
const t = useT();
|
||||
return useMemo<Mapping>(
|
||||
() => ({
|
||||
STATUS_ENABLED: {
|
||||
value: t('Enabled'),
|
||||
tooltip: t('Asset can be used on the Vega network'),
|
||||
},
|
||||
STATUS_PENDING_LISTING: {
|
||||
value: t('Pending listing'),
|
||||
tooltip: t('Asset needs to be added to the Ethereum bridge'),
|
||||
},
|
||||
STATUS_PROPOSED: {
|
||||
value: t('Proposed'),
|
||||
tooltip: t('Asset has been proposed to the network'),
|
||||
},
|
||||
STATUS_REJECTED: {
|
||||
value: t('Rejected'),
|
||||
tooltip: t('Asset has been rejected'),
|
||||
},
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
};
|
||||
|
||||
export const useAssetTypeMapping = () => {
|
||||
const t = useT();
|
||||
return useMemo<Mapping>(
|
||||
() => ({
|
||||
BuiltinAsset: {
|
||||
value: t('Builtin asset'),
|
||||
tooltip: t('A Vega builtin asset'),
|
||||
},
|
||||
ERC20: {
|
||||
value: t('ERC20'),
|
||||
tooltip: t('An asset originated from an Ethereum ERC20 Token'),
|
||||
},
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
};
|
||||
|
||||
export const testId = (detail: AssetDetail, field: 'label' | 'value') =>
|
||||
@@ -248,7 +273,7 @@ export const AssetDetailsTable = ({
|
||||
? { className: 'break-all', title: value }
|
||||
: {};
|
||||
|
||||
const details = rows.map((r) => ({
|
||||
const details = useRows().map((r) => ({
|
||||
...r,
|
||||
value: r.value(asset),
|
||||
valueTooltip: r.valueTooltip?.(asset),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TradingOption, truncateMiddle } from '@vegaprotocol/ui-toolkit';
|
||||
import type { AssetFieldsFragment } from './__generated__/Asset';
|
||||
import classNames from 'classnames';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useT } from './use-t';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
type AssetOptionProps = {
|
||||
@@ -15,8 +15,9 @@ export const Balance = ({
|
||||
}: {
|
||||
balance?: string;
|
||||
symbol: string;
|
||||
}) =>
|
||||
balance ? (
|
||||
}) => {
|
||||
const t = useT();
|
||||
return balance ? (
|
||||
<div className="mt-1 font-alpha" data-testid="asset-balance">
|
||||
{balance} {symbol}
|
||||
</div>
|
||||
@@ -25,6 +26,7 @@ export const Balance = ({
|
||||
{t('Fetching balance…')}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AssetOption = ({ asset, balance }: AssetOptionProps) => {
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
export const WITHDRAW_THRESHOLD_TOOLTIP_TEXT = t(
|
||||
"The maximum you can withdraw instantly. There's no limit on the size of a withdrawal, but all withdrawals over the threshold will have a delay time added to them"
|
||||
);
|
||||
export const WITHDRAW_THRESHOLD_TOOLTIP_TEXT =
|
||||
"The maximum you can withdraw instantly. There's no limit on the size of a withdrawal, but all withdrawals over the threshold will have a delay time added to them";
|
||||
|
||||
// List of defunct and no longer used assets that were created for various testnets
|
||||
export const DENY_LIST: Record<string, string[]> = {
|
||||
|
||||
@@ -7,3 +7,4 @@ export * from './asset-option';
|
||||
export * from './assets-data-provider';
|
||||
export * from './constants';
|
||||
export * from './use-balances-store';
|
||||
export * from './utils';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export const useT = () => useTranslation('assets').t;
|
||||
@@ -0,0 +1,15 @@
|
||||
import { getQuantumValue } from './utils';
|
||||
|
||||
describe('getQuantumValue', () => {
|
||||
it('converts a value into its value in quantum AKA (qUSD)', () => {
|
||||
expect(getQuantumValue('1000000', '1000000').toString()).toEqual('1');
|
||||
expect(getQuantumValue('2000000', '1000000').toString()).toEqual('2');
|
||||
expect(getQuantumValue('2500000', '1000000').toString()).toEqual('2.5');
|
||||
expect(getQuantumValue('10000', '1000000').toString()).toEqual('0.01');
|
||||
expect(
|
||||
getQuantumValue('1000000000000000000', '1000000000000000000').toString()
|
||||
).toEqual('1');
|
||||
expect(getQuantumValue('100000000', '100000000').toString()).toEqual('1');
|
||||
expect(getQuantumValue('150000000', '100000000').toString()).toEqual('1.5');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
import { toBigNum } from '@vegaprotocol/utils';
|
||||
|
||||
export const getQuantumValue = (value: string, quantum: string) => {
|
||||
return toBigNum(value, 0).dividedBy(toBigNum(quantum, 0));
|
||||
};
|
||||
@@ -6,12 +6,12 @@ import { useMemo } from 'react';
|
||||
import debounce from 'lodash/debounce';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
STUDY_SIZE,
|
||||
useCandlesChartSettings,
|
||||
} from './use-candles-chart-settings';
|
||||
import { useT } from './use-t';
|
||||
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export type CandlesChartContainerProps = {
|
||||
marketId: string;
|
||||
@@ -25,6 +25,7 @@ export const CandlesChartContainer = ({
|
||||
const client = useApolloClient();
|
||||
const { pubKey } = useVegaWallet();
|
||||
const { theme } = useThemeSwitcher();
|
||||
const t = useT();
|
||||
|
||||
const {
|
||||
interval,
|
||||
|
||||
@@ -22,8 +22,8 @@ import {
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { type IconName } from '@blueprintjs/icons';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useCandlesChartSettings } from './use-candles-chart-settings';
|
||||
import { useT } from './use-t';
|
||||
|
||||
const chartTypeIcon = new Map<ChartType, IconName>([
|
||||
[ChartType.AREA, IconNames.TIMELINE_AREA_CHART],
|
||||
@@ -43,6 +43,7 @@ export const CandlesMenu = () => {
|
||||
setStudies,
|
||||
setOverlays,
|
||||
} = useCandlesChartSettings();
|
||||
const t = useT();
|
||||
const triggerClasses = 'text-xs';
|
||||
const contentAlign = 'end';
|
||||
const triggerButtonProps = { size: 'extra-small' } as const;
|
||||
@@ -53,7 +54,10 @@ export const CandlesMenu = () => {
|
||||
trigger={
|
||||
<TradingDropdownTrigger className={triggerClasses}>
|
||||
<TradingButton {...triggerButtonProps}>
|
||||
{t(`Interval: ${intervalLabels[interval]}`)}
|
||||
{t('Interval: {{interval}}', {
|
||||
replace: { interval: intervalLabels[interval] },
|
||||
nsSeparator: '|',
|
||||
})}
|
||||
</TradingButton>
|
||||
</TradingDropdownTrigger>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
export const useT = () => useTranslation('candles-chart').t;
|
||||
@@ -0,0 +1,15 @@
|
||||
export const useTranslation = () => ({
|
||||
t: (label: string, replacements?: Record<string, string>) => {
|
||||
const replace =
|
||||
replacements?.['replace'] && typeof replacements === 'object'
|
||||
? replacements?.['replace']
|
||||
: replacements;
|
||||
let translatedLabel = replacements?.['defaultValue'] || label;
|
||||
if (typeof replace === 'object' && replace !== null) {
|
||||
Object.keys(replace).forEach((key) => {
|
||||
translatedLabel = translatedLabel.replace(`{{${key}}}`, replace[key]);
|
||||
});
|
||||
}
|
||||
return translatedLabel;
|
||||
},
|
||||
});
|
||||
@@ -1,14 +1,12 @@
|
||||
import type { AgGridReactProps, AgReactUiProps } from 'ag-grid-react';
|
||||
import { AgGridReact } from 'ag-grid-react';
|
||||
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import classNames from 'classnames';
|
||||
import type { ColDef } from 'ag-grid-community';
|
||||
import { useT } from '../use-t';
|
||||
|
||||
const defaultProps: AgGridReactProps = {
|
||||
enableCellTextSelection: true,
|
||||
overlayLoadingTemplate: t('Loading...'),
|
||||
overlayNoRowsTemplate: t('No data'),
|
||||
suppressCellFocus: true,
|
||||
suppressColumnMoveAnimation: true,
|
||||
};
|
||||
@@ -26,6 +24,7 @@ export const AgGridThemed = ({
|
||||
style?: React.CSSProperties;
|
||||
gridRef?: React.ForwardedRef<AgGridReact>;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const { theme } = useThemeSwitcher();
|
||||
|
||||
const wrapperClasses = classNames('vega-ag-grid', 'w-full h-full', {
|
||||
@@ -38,6 +37,8 @@ export const AgGridThemed = ({
|
||||
<AgGridReact
|
||||
defaultColDef={defaultColDef}
|
||||
ref={gridRef}
|
||||
overlayLoadingTemplate={t('Loading...')}
|
||||
overlayNoRowsTemplate={t('No data')}
|
||||
{...defaultProps}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { MouseEvent } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { useT } from '../use-t';
|
||||
|
||||
interface OrderTypeCellProps {
|
||||
value?: Schema.OrderType;
|
||||
@@ -17,6 +17,7 @@ export const OrderTypeCell = ({
|
||||
onClick,
|
||||
}: OrderTypeCellProps) => {
|
||||
const id = order?.market?.id ?? '';
|
||||
const t = useT();
|
||||
|
||||
const label = useMemo(() => {
|
||||
if (!order) {
|
||||
@@ -25,7 +26,9 @@ export const OrderTypeCell = ({
|
||||
if (!value) return '-';
|
||||
|
||||
if (order?.icebergOrder) {
|
||||
return t('%s (Iceberg)', [Schema.OrderTypeMapping[value]]);
|
||||
return t('{{orderType}} (Iceberg)', {
|
||||
orderType: Schema.OrderTypeMapping[value],
|
||||
});
|
||||
}
|
||||
|
||||
if (order?.peggedOrder) {
|
||||
@@ -37,14 +40,18 @@ export const OrderTypeCell = ({
|
||||
order.peggedOrder?.offset,
|
||||
order.market.decimalPlaces
|
||||
);
|
||||
return t('%s %s %s Peg limit', [reference, side, offset]);
|
||||
return t('{{reference}} {{side}} {{offset}} Peg limit', {
|
||||
reference,
|
||||
side,
|
||||
offset,
|
||||
});
|
||||
}
|
||||
|
||||
if (order?.liquidityProvision) {
|
||||
return t('Liquidity provision');
|
||||
}
|
||||
return Schema.OrderTypeMapping[value];
|
||||
}, [order, value]);
|
||||
}, [order, value, t]);
|
||||
|
||||
const handleOnClick = useCallback(
|
||||
(ev: MouseEvent<HTMLButtonElement>) => {
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
isValid,
|
||||
} from 'date-fns';
|
||||
import { formatForInput } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { TradingInputError } from '@vegaprotocol/ui-toolkit';
|
||||
import { useT } from '../use-t';
|
||||
|
||||
const defaultValue: DateRange = {};
|
||||
export interface DateRangeFilterProps extends IFilterParams {
|
||||
@@ -27,6 +27,7 @@ export interface DateRangeFilterProps extends IFilterParams {
|
||||
|
||||
export const DateRangeFilter = forwardRef(
|
||||
(props: DateRangeFilterProps, ref) => {
|
||||
const t = useT();
|
||||
const defaultDates = props?.defaultValue || defaultValue;
|
||||
const [value, setValue] = useState<DateRange>(defaultDates);
|
||||
const valueRef = useRef<DateRange>(value);
|
||||
@@ -119,8 +120,10 @@ export const DateRangeFilter = forwardRef(
|
||||
) {
|
||||
setError(
|
||||
t(
|
||||
'The earliest data that can be queried is %s days ago.',
|
||||
String(props.maxSubDays)
|
||||
'The earliest data that can be queried is {{maxSubDays}} days ago.',
|
||||
{
|
||||
maxSubDays: String(props.maxSubDays),
|
||||
}
|
||||
)
|
||||
);
|
||||
return false;
|
||||
@@ -137,8 +140,8 @@ export const DateRangeFilter = forwardRef(
|
||||
) {
|
||||
setError(
|
||||
t(
|
||||
'The maximum time range that can be queried is %s days.',
|
||||
String(props.maxDaysRange)
|
||||
'The maximum time range that can be queried is {{maxDaysRange}} days.',
|
||||
{ maxDaysRange: String(props.maxDaysRange) }
|
||||
)
|
||||
);
|
||||
return false;
|
||||
|
||||
@@ -7,10 +7,11 @@ import {
|
||||
useRef,
|
||||
} from 'react';
|
||||
import type { IDoesFilterPassParams, IFilterParams } from 'ag-grid-community';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useT } from '../use-t';
|
||||
|
||||
export const SetFilter = forwardRef(
|
||||
(props: IFilterParams & { readonly?: boolean }, ref) => {
|
||||
const t = useT();
|
||||
const [value, setValue] = useState<string[]>([]);
|
||||
const valueRef = useRef(value);
|
||||
const { readonly } = props;
|
||||
|
||||
@@ -30,15 +30,6 @@ describe('Pagination', () => {
|
||||
expect(mockOnLoad).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders message for a single row', async () => {
|
||||
const mockOnLoad = jest.fn();
|
||||
const count = 1;
|
||||
render(<Pagination {...props} count={count} onLoad={mockOnLoad} />);
|
||||
expect(screen.getByText(`${count} row loaded`)).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Load more' }));
|
||||
expect(mockOnLoad).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders the data rentention message', () => {
|
||||
render(<Pagination {...props} showRetentionMessage={true} />);
|
||||
expect(screen.getByText(/data node retention/)).toBeInTheDocument();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useT } from './use-t';
|
||||
|
||||
export const Pagination = ({
|
||||
count,
|
||||
@@ -14,16 +14,19 @@ export const Pagination = ({
|
||||
hasDisplayedRows: boolean;
|
||||
showRetentionMessage: boolean;
|
||||
}) => {
|
||||
const t = useT();
|
||||
let rowMessage = '';
|
||||
|
||||
if (count && !pageInfo?.hasNextPage) {
|
||||
rowMessage = t('all %s rows loaded', count.toString());
|
||||
rowMessage = t('paginationAllLoaded', {
|
||||
replace: { count },
|
||||
defaultValue: 'All {{count}} rows loaded',
|
||||
});
|
||||
} else {
|
||||
if (count === 1) {
|
||||
rowMessage = t('%s row loaded', count.toString());
|
||||
} else {
|
||||
rowMessage = t('%s rows loaded', count.toString());
|
||||
}
|
||||
rowMessage = t('paginationLoaded', {
|
||||
replace: { count },
|
||||
defaultValue: '{{count}} rows loaded',
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
export const useT = () => useTranslation('datagrid').t;
|
||||
@@ -0,0 +1,14 @@
|
||||
export const useTranslation = () => ({
|
||||
t: (label: string, replacements?: Record<string, string>) => {
|
||||
let translatedLabel = label;
|
||||
if (typeof replacements === 'object' && replacements !== null) {
|
||||
Object.keys(replacements).forEach((key) => {
|
||||
translatedLabel = translatedLabel.replace(
|
||||
`{{${key}}}`,
|
||||
replacements[key]
|
||||
);
|
||||
});
|
||||
}
|
||||
return translatedLabel;
|
||||
},
|
||||
});
|
||||
@@ -1,5 +1,4 @@
|
||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
Intent,
|
||||
VegaIcon,
|
||||
@@ -7,6 +6,7 @@ import {
|
||||
Tooltip,
|
||||
TradingButton,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useT } from '../../use-t';
|
||||
|
||||
interface Props {
|
||||
margin: string;
|
||||
@@ -20,18 +20,19 @@ interface Props {
|
||||
}
|
||||
|
||||
export const MarginWarning = ({ margin, balance, asset, onDeposit }: Props) => {
|
||||
const t = useT();
|
||||
const description = (
|
||||
<div className="flex flex-col items-start gap-2 p-2">
|
||||
<p className="text-sm">
|
||||
{t('%s %s is currently required.', [
|
||||
addDecimalsFormatNumber(margin, asset.decimals),
|
||||
asset.symbol,
|
||||
])}
|
||||
{t('{{amount}} {{assetSymbol}} is currently required.', {
|
||||
amount: addDecimalsFormatNumber(margin, asset.decimals),
|
||||
assetSymbol: asset.symbol,
|
||||
})}
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
{t('You have only %s.', [
|
||||
addDecimalsFormatNumber(balance, asset.decimals),
|
||||
])}
|
||||
{t('You have only {{amount}}.', {
|
||||
amount: addDecimalsFormatNumber(balance, asset.decimals),
|
||||
})}
|
||||
</p>
|
||||
|
||||
<TradingButton
|
||||
@@ -41,7 +42,7 @@ export const MarginWarning = ({ margin, balance, asset, onDeposit }: Props) => {
|
||||
data-testid="deal-ticket-deposit-dialog-button"
|
||||
type="button"
|
||||
>
|
||||
{t('Deposit %s', [asset.symbol])}
|
||||
{t('Deposit {{assetSymbol}}', { assetSymbol: asset.symbol })}
|
||||
</TradingButton>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Intent, Notification } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useT } from '../../use-t';
|
||||
|
||||
interface ZeroBalanceErrorProps {
|
||||
asset: {
|
||||
@@ -13,6 +13,7 @@ export const ZeroBalanceError = ({
|
||||
asset,
|
||||
onDeposit,
|
||||
}: ZeroBalanceErrorProps) => {
|
||||
const t = useT();
|
||||
return (
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
@@ -20,8 +21,8 @@ export const ZeroBalanceError = ({
|
||||
message={
|
||||
<>
|
||||
{t(
|
||||
'You need %s in your wallet to trade in this market. ',
|
||||
asset.symbol
|
||||
'You need {{symbol}} in your wallet to trade in this market.',
|
||||
asset
|
||||
)}
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
useMarketPrice,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { AsyncRendererInline } from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { DealTicket } from './deal-ticket';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import { useT } from '../../use-t';
|
||||
|
||||
interface DealTicketContainerProps {
|
||||
marketId: string;
|
||||
@@ -24,6 +24,7 @@ export const DealTicketContainer = ({
|
||||
marketId,
|
||||
...props
|
||||
}: DealTicketContainerProps) => {
|
||||
const t = useT();
|
||||
const showStopOrder = useDealTicketFormValues((state) =>
|
||||
isStopOrderType(state.formValues[marketId]?.type)
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { getAsset, getQuoteName } from '@vegaprotocol/markets';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
@@ -41,8 +40,10 @@ import classNames from 'classnames';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { FeesBreakdown } from '../fees-breakdown';
|
||||
import { getTotalDiscountFactor, getDiscountedFee } from '../discounts';
|
||||
import { useT, ns } from '../../use-t';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
const emptyValue = '-';
|
||||
export const emptyValue = '-';
|
||||
|
||||
export interface DealTicketFeeDetailsProps {
|
||||
assetSymbol: string;
|
||||
@@ -57,6 +58,7 @@ export const DealTicketFeeDetails = ({
|
||||
market,
|
||||
isMarketInAuction,
|
||||
}: DealTicketFeeDetailsProps) => {
|
||||
const t = useT();
|
||||
const feeEstimate = useEstimateFees(order, isMarketInAuction);
|
||||
const asset = getAsset(market);
|
||||
const { decimals: assetDecimals, quantum } = asset;
|
||||
@@ -98,7 +100,8 @@ export const DealTicketFeeDetails = ({
|
||||
<div className="flex flex-col gap-2">
|
||||
<p>
|
||||
{t(
|
||||
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}. Fees estimated are "taker" fees and will only be payable if the order trades aggressively. Rebate equal to the maker portion will be paid to the trader if the order trades passively.`
|
||||
'An estimate of the most you would be expected to pay in fees, in the market\'s settlement asset {{assetSymbol}}. Fees estimated are "taker" fees and will only be payable if the order trades aggressively. Rebate equal to the maker portion will be paid to the trader if the order trades passively.',
|
||||
{ assetSymbol }
|
||||
)}
|
||||
</p>
|
||||
<FeesBreakdown
|
||||
@@ -136,6 +139,7 @@ export const DealTicketMarginDetails = ({
|
||||
positionEstimate,
|
||||
side,
|
||||
}: DealTicketMarginDetailsProps) => {
|
||||
const t = useT();
|
||||
const [breakdownDialog, setBreakdownDialog] = useState(false);
|
||||
const { pubKey: partyId } = useVegaWallet();
|
||||
const { data: currentMargins } = useDataProvider({
|
||||
@@ -211,7 +215,11 @@ export const DealTicketMarginDetails = ({
|
||||
quantum
|
||||
)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT(assetSymbol)}
|
||||
labelDescription={t(
|
||||
'DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT',
|
||||
DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT,
|
||||
{ assetSymbol }
|
||||
)}
|
||||
/>
|
||||
);
|
||||
projectedMargin = (
|
||||
@@ -228,7 +236,10 @@ export const DealTicketMarginDetails = ({
|
||||
quantum
|
||||
)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={EST_TOTAL_MARGIN_TOOLTIP_TEXT}
|
||||
labelDescription={t(
|
||||
'EST_TOTAL_MARGIN_TOOLTIP_TEXT',
|
||||
EST_TOTAL_MARGIN_TOOLTIP_TEXT
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -307,7 +318,13 @@ export const DealTicketMarginDetails = ({
|
||||
className="flex items-center justify-between w-full gap-2"
|
||||
>
|
||||
<div className="flex items-center text-left gap-1">
|
||||
<Tooltip description={MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol)}>
|
||||
<Tooltip
|
||||
description={t(
|
||||
'MARGIN_DIFF_TOOLTIP_TEXT',
|
||||
MARGIN_DIFF_TOOLTIP_TEXT,
|
||||
{ assetSymbol }
|
||||
)}
|
||||
>
|
||||
<span className="text-muted">{t('Margin required')}</span>
|
||||
</Tooltip>
|
||||
|
||||
@@ -346,15 +363,27 @@ export const DealTicketMarginDetails = ({
|
||||
quantum
|
||||
)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={TOTAL_MARGIN_AVAILABLE(
|
||||
formatValue(generalAccountBalance, assetDecimals, quantum),
|
||||
formatValue(marginAccountBalance, assetDecimals, quantum),
|
||||
formatValue(
|
||||
currentMargins?.maintenanceLevel,
|
||||
assetDecimals,
|
||||
quantum
|
||||
),
|
||||
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}
|
||||
@@ -368,7 +397,10 @@ export const DealTicketMarginDetails = ({
|
||||
}
|
||||
value={formatValue(marginAccountBalance, assetDecimals)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={MARGIN_ACCOUNT_TOOLTIP_TEXT}
|
||||
labelDescription={t(
|
||||
'MARGIN_ACCOUNT_TOOLTIP_TEXT',
|
||||
MARGIN_ACCOUNT_TOOLTIP_TEXT
|
||||
)}
|
||||
formattedValue={formatValue(
|
||||
marginAccountBalance,
|
||||
assetDecimals,
|
||||
@@ -386,16 +418,26 @@ export const DealTicketMarginDetails = ({
|
||||
symbol={quoteName}
|
||||
labelDescription={
|
||||
<>
|
||||
<span>{LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT}</span>{' '}
|
||||
<span>
|
||||
{t('For full details please see ')}
|
||||
<ExternalLink
|
||||
href={
|
||||
'https://github.com/vegaprotocol/specs/blob/master/non-protocol-specs/0012-NP-LIPE-liquidation-price-estimate.md'
|
||||
}
|
||||
>
|
||||
{t('liquidation price estimate documentation.')}
|
||||
</ExternalLink>
|
||||
{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>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,354 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { getAsset, getQuoteName } from '@vegaprotocol/markets';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
|
||||
import { 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 { KeyValue } from './key-value';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionChevron,
|
||||
AccordionPanel,
|
||||
ExternalLink,
|
||||
Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import { useT, ns } from '../../use-t';
|
||||
import { Trans } from 'react-i18next';
|
||||
import type { DealTicketMarginDetailsProps } from './deal-ticket-fee-details';
|
||||
import { emptyValue } from './deal-ticket-fee-details';
|
||||
|
||||
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">
|
||||
<Accordion>
|
||||
<AccordionPanel
|
||||
itemId="margin"
|
||||
trigger={
|
||||
<AccordionPrimitive.Trigger
|
||||
data-testid="accordion-toggle"
|
||||
className={classNames(
|
||||
'w-full pt-2',
|
||||
'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>
|
||||
);
|
||||
};
|
||||
@@ -2,13 +2,13 @@ import { Controller, type Control } from 'react-hook-form';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import type { OrderFormValues } from '../../hooks/use-form-values';
|
||||
import { toDecimal, validateAmount } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
TradingFormGroup,
|
||||
TradingInput,
|
||||
TradingInputError,
|
||||
Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useT } from '../../use-t';
|
||||
|
||||
export interface DealTicketSizeIcebergProps {
|
||||
control: Control<OrderFormValues>;
|
||||
@@ -27,6 +27,7 @@ export const DealTicketSizeIceberg = ({
|
||||
size,
|
||||
peakSize,
|
||||
}: DealTicketSizeIcebergProps) => {
|
||||
const t = useT();
|
||||
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
||||
|
||||
const renderPeakSizeError = () => {
|
||||
@@ -81,13 +82,15 @@ export const DealTicketSizeIceberg = ({
|
||||
required: t('You need to provide a peak size'),
|
||||
min: {
|
||||
value: sizeStep,
|
||||
message: t('Peak size cannot be lower than ' + sizeStep),
|
||||
message: t('Peak size cannot be lower than {{stepSize}}', {
|
||||
sizeStep,
|
||||
}),
|
||||
},
|
||||
max: {
|
||||
value: size,
|
||||
message: t(
|
||||
'Peak size cannot be greater than the size (%s) ',
|
||||
[size]
|
||||
'Peak size cannot be greater than the size ({{size}})',
|
||||
{ size }
|
||||
),
|
||||
},
|
||||
validate: validateAmount(sizeStep, 'peakSize'),
|
||||
@@ -138,14 +141,15 @@ export const DealTicketSizeIceberg = ({
|
||||
min: {
|
||||
value: sizeStep,
|
||||
message: t(
|
||||
'Minimum visible size cannot be lower than ' + sizeStep
|
||||
'Minimum visible size cannot be lower than {{sizeStep}}',
|
||||
{ sizeStep }
|
||||
),
|
||||
},
|
||||
max: peakSize && {
|
||||
value: peakSize,
|
||||
message: t(
|
||||
'Minimum visible size cannot be greater than the peak size (%s)',
|
||||
[peakSize]
|
||||
'Minimum visible size cannot be greater than the peak size ({{peakSize}})',
|
||||
{ peakSize }
|
||||
),
|
||||
},
|
||||
validate: validateAmount(sizeStep, 'minimumVisibleSize'),
|
||||
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
getQuoteName,
|
||||
type Market,
|
||||
} from '@vegaprotocol/markets';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { ExpirySelector } from './expiry-selector';
|
||||
import { SideSelector } from './side-selector';
|
||||
import { timeInForceLabel } from '@vegaprotocol/orders';
|
||||
@@ -60,6 +59,7 @@ import { NOTIONAL_SIZE_TOOLTIP_TEXT } from '../../constants';
|
||||
import { KeyValue } from './key-value';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { stopOrdersProvider } from '@vegaprotocol/orders';
|
||||
import { useT } from '../../use-t';
|
||||
|
||||
export interface StopOrderProps {
|
||||
market: Market;
|
||||
@@ -109,6 +109,7 @@ const Trigger = ({
|
||||
marketPrice?: string | null;
|
||||
decimalPlaces: number;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const triggerType = watch(oco ? 'ocoTriggerType' : 'triggerType');
|
||||
const triggerDirection = watch('triggerDirection');
|
||||
const isPriceTrigger = triggerType === 'price';
|
||||
@@ -161,7 +162,9 @@ const Trigger = ({
|
||||
required: t('You need provide a price'),
|
||||
min: {
|
||||
value: priceStep,
|
||||
message: t('Price cannot be lower than ' + priceStep),
|
||||
message: t('Price cannot be lower than {{priceStep}}', {
|
||||
priceStep,
|
||||
}),
|
||||
},
|
||||
validate: validateAmount(priceStep, 'Price'),
|
||||
}}
|
||||
@@ -244,8 +247,8 @@ const Trigger = ({
|
||||
min: {
|
||||
value: trailingPercentOffsetStep,
|
||||
message: t(
|
||||
'Trailing percent offset cannot be lower than ' +
|
||||
trailingPercentOffsetStep
|
||||
'Trailing percent offset cannot be lower than {{trailingPercentOffsetStep}}',
|
||||
{ trailingPercentOffsetStep }
|
||||
),
|
||||
},
|
||||
max: {
|
||||
@@ -338,6 +341,7 @@ const Size = ({
|
||||
isLimitType: boolean;
|
||||
assetUnit?: string;
|
||||
}) => {
|
||||
const t = useT();
|
||||
return (
|
||||
<Controller
|
||||
name={oco ? 'ocoSize' : 'size'}
|
||||
@@ -346,7 +350,7 @@ const Size = ({
|
||||
required: t('You need to provide a size'),
|
||||
min: {
|
||||
value: sizeStep,
|
||||
message: t('Size cannot be lower than ' + sizeStep),
|
||||
message: t('Size cannot be lower than {{sizeStep}}', { sizeStep }),
|
||||
},
|
||||
validate: validateAmount(sizeStep, 'Size'),
|
||||
}}
|
||||
@@ -397,6 +401,7 @@ const Price = ({
|
||||
quoteName: string;
|
||||
oco?: boolean;
|
||||
}) => {
|
||||
const t = useT();
|
||||
if (watch(oco ? 'ocoType' : 'type') === Schema.OrderType.TYPE_MARKET) {
|
||||
return null;
|
||||
}
|
||||
@@ -409,7 +414,7 @@ const Price = ({
|
||||
required: t('You need provide a price'),
|
||||
min: {
|
||||
value: priceStep,
|
||||
message: t('Price cannot be lower than ' + priceStep),
|
||||
message: t('Price cannot be lower than {{priceStep}}', { priceStep }),
|
||||
},
|
||||
validate: validateAmount(priceStep, 'Price'),
|
||||
}}
|
||||
@@ -452,59 +457,65 @@ const TimeInForce = ({
|
||||
}: {
|
||||
control: Control<StopOrderFormValues>;
|
||||
oco?: boolean;
|
||||
}) => (
|
||||
<Controller
|
||||
name={oco ? 'ocoTimeInForce' : 'timeInForce'}
|
||||
control={control}
|
||||
render={({ field, fieldState }) => {
|
||||
const id = `order-tif${oco ? '-oco' : ''}`;
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<FormGroup label={t('Time in force')} labelFor={id} compact={true}>
|
||||
<Select
|
||||
id={id}
|
||||
className="w-full"
|
||||
data-testid={id}
|
||||
hasError={!!fieldState.error}
|
||||
{...field}
|
||||
>
|
||||
<option
|
||||
key={Schema.OrderTimeInForce.TIME_IN_FORCE_IOC}
|
||||
value={Schema.OrderTimeInForce.TIME_IN_FORCE_IOC}
|
||||
}) => {
|
||||
const t = useT();
|
||||
return (
|
||||
<Controller
|
||||
name={oco ? 'ocoTimeInForce' : 'timeInForce'}
|
||||
control={control}
|
||||
render={({ field, fieldState }) => {
|
||||
const id = `order-tif${oco ? '-oco' : ''}`;
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<FormGroup label={t('Time in force')} labelFor={id} compact={true}>
|
||||
<Select
|
||||
id={id}
|
||||
className="w-full"
|
||||
data-testid={id}
|
||||
hasError={!!fieldState.error}
|
||||
{...field}
|
||||
>
|
||||
{timeInForceLabel(Schema.OrderTimeInForce.TIME_IN_FORCE_IOC)}
|
||||
</option>
|
||||
<option
|
||||
key={Schema.OrderTimeInForce.TIME_IN_FORCE_FOK}
|
||||
value={Schema.OrderTimeInForce.TIME_IN_FORCE_FOK}
|
||||
>
|
||||
{timeInForceLabel(Schema.OrderTimeInForce.TIME_IN_FORCE_FOK)}
|
||||
</option>
|
||||
</Select>
|
||||
</FormGroup>
|
||||
{fieldState.error && (
|
||||
<InputError testId={`stop-error-message-tif${oco ? '-oco' : ''}`}>
|
||||
{fieldState.error.message}
|
||||
</InputError>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
<option
|
||||
key={Schema.OrderTimeInForce.TIME_IN_FORCE_IOC}
|
||||
value={Schema.OrderTimeInForce.TIME_IN_FORCE_IOC}
|
||||
>
|
||||
{timeInForceLabel(Schema.OrderTimeInForce.TIME_IN_FORCE_IOC)}
|
||||
</option>
|
||||
<option
|
||||
key={Schema.OrderTimeInForce.TIME_IN_FORCE_FOK}
|
||||
value={Schema.OrderTimeInForce.TIME_IN_FORCE_FOK}
|
||||
>
|
||||
{timeInForceLabel(Schema.OrderTimeInForce.TIME_IN_FORCE_FOK)}
|
||||
</option>
|
||||
</Select>
|
||||
</FormGroup>
|
||||
{fieldState.error && (
|
||||
<InputError testId={`stop-error-message-tif${oco ? '-oco' : ''}`}>
|
||||
{fieldState.error.message}
|
||||
</InputError>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ReduceOnly = () => (
|
||||
<Tooltip description={<span>{t(REDUCE_ONLY_TOOLTIP)}</span>}>
|
||||
<div>
|
||||
<Checkbox
|
||||
name="reduce-only"
|
||||
checked={true}
|
||||
disabled={true}
|
||||
label={t('Reduce only')}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
const ReduceOnly = () => {
|
||||
const t = useT();
|
||||
return (
|
||||
<Tooltip description={<span>{t(REDUCE_ONLY_TOOLTIP)}</span>}>
|
||||
<div>
|
||||
<Checkbox
|
||||
name="reduce-only"
|
||||
checked={true}
|
||||
disabled={true}
|
||||
label={t('Reduce only')}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const NotionalAndFees = ({
|
||||
market,
|
||||
@@ -522,6 +533,7 @@ const NotionalAndFees = ({
|
||||
> &
|
||||
Pick<StopOrderProps, 'market' | 'marketPrice'> &
|
||||
Pick<StopOrderFormValues, 'triggerType' | 'triggerPrice'>) => {
|
||||
const t = useT();
|
||||
const quoteName = getQuoteName(market);
|
||||
const asset = getAsset(market);
|
||||
const isPriceTrigger = triggerType === 'price';
|
||||
@@ -548,7 +560,11 @@ const NotionalAndFees = ({
|
||||
value={formatValue(notionalSize, market.decimalPlaces)}
|
||||
formattedValue={formatValue(notionalSize, market.decimalPlaces)}
|
||||
symbol={quoteName}
|
||||
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName)}
|
||||
labelDescription={t(
|
||||
'NOTIONAL_SIZE_TOOLTIP_TEXT',
|
||||
NOTIONAL_SIZE_TOOLTIP_TEXT,
|
||||
{ quoteName }
|
||||
)}
|
||||
/>
|
||||
<DealTicketFeeDetails
|
||||
order={{
|
||||
@@ -566,49 +582,55 @@ const NotionalAndFees = ({
|
||||
);
|
||||
};
|
||||
|
||||
const formatSizeAtPrice = ({
|
||||
assetUnit,
|
||||
decimalPlaces,
|
||||
positionDecimalPlaces,
|
||||
price,
|
||||
quoteName,
|
||||
side,
|
||||
size,
|
||||
type,
|
||||
}: Pick<StopOrderFormValues, 'price' | 'side' | 'size' | 'type'> & {
|
||||
assetUnit?: string;
|
||||
decimalPlaces: number;
|
||||
positionDecimalPlaces: number;
|
||||
quoteName: string;
|
||||
}) =>
|
||||
const formatSizeAtPrice = (
|
||||
{
|
||||
assetUnit,
|
||||
decimalPlaces,
|
||||
positionDecimalPlaces,
|
||||
price,
|
||||
quoteName,
|
||||
side,
|
||||
size,
|
||||
type,
|
||||
}: Pick<StopOrderFormValues, 'price' | 'side' | 'size' | 'type'> & {
|
||||
assetUnit?: string;
|
||||
decimalPlaces: number;
|
||||
positionDecimalPlaces: number;
|
||||
quoteName: string;
|
||||
},
|
||||
t: ReturnType<typeof useT>
|
||||
) =>
|
||||
`${formatValue(
|
||||
removeDecimal(size, positionDecimalPlaces),
|
||||
positionDecimalPlaces
|
||||
)} ${assetUnit} @ ${
|
||||
type === Schema.OrderType.TYPE_MARKET
|
||||
? 'market'
|
||||
? t('sizeAtPrice-market', 'market')
|
||||
: `${formatValue(
|
||||
removeDecimal(price || '0', decimalPlaces),
|
||||
decimalPlaces
|
||||
)} ${quoteName}`
|
||||
}`;
|
||||
const formatTrigger = ({
|
||||
decimalPlaces,
|
||||
triggerDirection,
|
||||
triggerPrice,
|
||||
triggerTrailingPercentOffset,
|
||||
triggerType,
|
||||
quoteName,
|
||||
}: Pick<
|
||||
StopOrderFormValues,
|
||||
| 'triggerDirection'
|
||||
| 'triggerType'
|
||||
| 'triggerPrice'
|
||||
| 'triggerTrailingPercentOffset'
|
||||
> & {
|
||||
decimalPlaces: number;
|
||||
quoteName: string;
|
||||
}) =>
|
||||
const formatTrigger = (
|
||||
{
|
||||
decimalPlaces,
|
||||
triggerDirection,
|
||||
triggerPrice,
|
||||
triggerTrailingPercentOffset,
|
||||
triggerType,
|
||||
quoteName,
|
||||
}: Pick<
|
||||
StopOrderFormValues,
|
||||
| 'triggerDirection'
|
||||
| 'triggerType'
|
||||
| 'triggerPrice'
|
||||
| 'triggerTrailingPercentOffset'
|
||||
> & {
|
||||
decimalPlaces: number;
|
||||
quoteName: string;
|
||||
},
|
||||
t: ReturnType<typeof useT>
|
||||
) =>
|
||||
`${
|
||||
triggerDirection ===
|
||||
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE
|
||||
@@ -620,9 +642,12 @@ const formatTrigger = ({
|
||||
removeDecimal(triggerPrice || '', decimalPlaces),
|
||||
decimalPlaces
|
||||
)} ${quoteName}`
|
||||
: `${(Number(triggerTrailingPercentOffset) || 0).toFixed(1)}% ${t(
|
||||
'trailing'
|
||||
)}`
|
||||
: t('{{triggerTrailingPercentOffset}}% trailing', {
|
||||
triggerTrailingPercentOffset: (
|
||||
Number(triggerTrailingPercentOffset) || 0
|
||||
).toFixed(1),
|
||||
})
|
||||
}
|
||||
}`;
|
||||
|
||||
const SubmitButton = ({
|
||||
@@ -662,78 +687,97 @@ const SubmitButton = ({
|
||||
| 'type'
|
||||
> &
|
||||
Pick<StopOrderProps, 'market'> & { assetUnit?: string }) => {
|
||||
const t = useT();
|
||||
const quoteName = getQuoteName(market);
|
||||
const risesAbove =
|
||||
triggerDirection ===
|
||||
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE;
|
||||
const subLabel = oco ? (
|
||||
<>
|
||||
{formatSizeAtPrice({
|
||||
assetUnit,
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
price: risesAbove ? price : ocoPrice,
|
||||
quoteName,
|
||||
side,
|
||||
size: risesAbove ? size : ocoSize,
|
||||
type,
|
||||
})}{' '}
|
||||
{formatTrigger({
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
quoteName,
|
||||
triggerDirection:
|
||||
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE,
|
||||
triggerPrice: risesAbove ? triggerPrice : ocoTriggerPrice,
|
||||
triggerTrailingPercentOffset: risesAbove
|
||||
? triggerTrailingPercentOffset
|
||||
: ocoTriggerTrailingPercentOffset,
|
||||
triggerType: risesAbove ? triggerType : ocoTriggerType,
|
||||
})}
|
||||
{formatSizeAtPrice(
|
||||
{
|
||||
assetUnit,
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
price: risesAbove ? price : ocoPrice,
|
||||
quoteName,
|
||||
side,
|
||||
size: risesAbove ? size : ocoSize,
|
||||
type,
|
||||
},
|
||||
t
|
||||
)}{' '}
|
||||
{formatTrigger(
|
||||
{
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
quoteName,
|
||||
triggerDirection:
|
||||
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE,
|
||||
triggerPrice: risesAbove ? triggerPrice : ocoTriggerPrice,
|
||||
triggerTrailingPercentOffset: risesAbove
|
||||
? triggerTrailingPercentOffset
|
||||
: ocoTriggerTrailingPercentOffset,
|
||||
triggerType: risesAbove ? triggerType : ocoTriggerType,
|
||||
},
|
||||
t
|
||||
)}
|
||||
<br />
|
||||
{formatSizeAtPrice({
|
||||
assetUnit,
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
price: !risesAbove ? price : ocoPrice,
|
||||
quoteName,
|
||||
side,
|
||||
size: !risesAbove ? size : ocoSize,
|
||||
type: ocoType,
|
||||
})}{' '}
|
||||
{formatTrigger({
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
quoteName,
|
||||
triggerDirection:
|
||||
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW,
|
||||
triggerPrice: !risesAbove ? triggerPrice : ocoTriggerPrice,
|
||||
triggerTrailingPercentOffset: !risesAbove
|
||||
? triggerTrailingPercentOffset
|
||||
: ocoTriggerTrailingPercentOffset,
|
||||
triggerType: !risesAbove ? triggerType : ocoTriggerType,
|
||||
})}
|
||||
{formatSizeAtPrice(
|
||||
{
|
||||
assetUnit,
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
price: !risesAbove ? price : ocoPrice,
|
||||
quoteName,
|
||||
side,
|
||||
size: !risesAbove ? size : ocoSize,
|
||||
type: ocoType,
|
||||
},
|
||||
t
|
||||
)}{' '}
|
||||
{formatTrigger(
|
||||
{
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
quoteName,
|
||||
triggerDirection:
|
||||
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW,
|
||||
triggerPrice: !risesAbove ? triggerPrice : ocoTriggerPrice,
|
||||
triggerTrailingPercentOffset: !risesAbove
|
||||
? triggerTrailingPercentOffset
|
||||
: ocoTriggerTrailingPercentOffset,
|
||||
triggerType: !risesAbove ? triggerType : ocoTriggerType,
|
||||
},
|
||||
t
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{formatSizeAtPrice({
|
||||
assetUnit,
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
price,
|
||||
quoteName,
|
||||
side,
|
||||
size,
|
||||
type,
|
||||
})}
|
||||
{formatSizeAtPrice(
|
||||
{
|
||||
assetUnit,
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
price,
|
||||
quoteName,
|
||||
side,
|
||||
size,
|
||||
type,
|
||||
},
|
||||
t
|
||||
)}
|
||||
<br />
|
||||
{t('Trigger')}{' '}
|
||||
{formatTrigger({
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
quoteName,
|
||||
triggerDirection,
|
||||
triggerPrice,
|
||||
triggerTrailingPercentOffset,
|
||||
triggerType,
|
||||
})}
|
||||
{formatTrigger(
|
||||
{
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
quoteName,
|
||||
triggerDirection,
|
||||
triggerPrice,
|
||||
triggerTrailingPercentOffset,
|
||||
triggerType,
|
||||
},
|
||||
t
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
@@ -756,6 +800,7 @@ const SubmitButton = ({
|
||||
};
|
||||
|
||||
export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||
const t = useT();
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
const setType = useDealTicketFormValues((state) => state.setType);
|
||||
const updateStoredFormValues = useDealTicketFormValues(
|
||||
@@ -1087,14 +1132,14 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
|
||||
}
|
||||
id="expiryStrategy-submit"
|
||||
label={'Submit'}
|
||||
label={t('Submit')}
|
||||
/>
|
||||
<Radio
|
||||
value={
|
||||
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS
|
||||
}
|
||||
id="expiryStrategy-cancel"
|
||||
label={'Cancel'}
|
||||
label={t('Cancel')}
|
||||
/>
|
||||
</RadioGroup>
|
||||
);
|
||||
@@ -1107,7 +1152,11 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||
control={control}
|
||||
rules={{
|
||||
required: t('You need provide a expiry time/date'),
|
||||
validate: validateExpiration,
|
||||
validate: validateExpiration(
|
||||
t(
|
||||
'The expiry date that you have entered appears to be in the past'
|
||||
)
|
||||
),
|
||||
}}
|
||||
render={({ field }) => {
|
||||
const { value, onChange: onSelect } = field;
|
||||
@@ -1131,8 +1180,8 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||
intent={Intent.Warning}
|
||||
testId={'stop-order-warning-limit'}
|
||||
message={t(
|
||||
'There is a limit of %s active stop orders per market. Orders submitted above the limit will be immediately rejected.',
|
||||
[MAX_NUMBER_OF_ACTIVE_STOP_ORDERS.toString()]
|
||||
'There is a limit of {{maxNumberOfOrders}} active stop orders per market. Orders submitted above the limit will be immediately rejected.',
|
||||
{ maxNumberOfOrders: MAX_NUMBER_OF_ACTIVE_STOP_ORDERS.toString() }
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { type FormEventHandler } from 'react';
|
||||
import { memo, useCallback, useEffect, useRef, useMemo } from 'react';
|
||||
import { Controller, useController, useForm } from 'react-hook-form';
|
||||
import {
|
||||
DealTicketFeeDetails,
|
||||
DealTicketMarginDetails,
|
||||
} from './deal-ticket-fee-details';
|
||||
import { DealTicketFeeDetails } from './deal-ticket-fee-details';
|
||||
import { DealTicketMarginDetails } from './deal-ticket-margin-details';
|
||||
import { ExpirySelector } from './expiry-selector';
|
||||
import { SideSelector } from './side-selector';
|
||||
import { TimeInForceSelector } from './time-in-force-selector';
|
||||
@@ -45,7 +42,6 @@ import {
|
||||
} from '@vegaprotocol/markets';
|
||||
import {
|
||||
validateExpiration,
|
||||
validateMarketState,
|
||||
validateMarketTradingMode,
|
||||
validateTimeInForce,
|
||||
validateType,
|
||||
@@ -79,6 +75,7 @@ import noop from 'lodash/noop';
|
||||
import { isNonPersistentOrder } from '../../utils/time-in-force-persistance';
|
||||
import { KeyValue } from './key-value';
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
import { useT } from '../../use-t';
|
||||
|
||||
export const REDUCE_ONLY_TOOLTIP =
|
||||
'"Reduce only" will ensure that this order will not increase the size of an open position. When the order is matched, it will only trade enough volume to bring your open volume towards 0 but never change the direction of your position. If applied to a limit order that is not instantly filled, the order will be stopped.';
|
||||
@@ -142,6 +139,7 @@ export const DealTicket = ({
|
||||
submit,
|
||||
onDeposit,
|
||||
}: DealTicketProps) => {
|
||||
const t = useT();
|
||||
const { pubKey, isReadOnly } = useVegaWallet();
|
||||
const setType = useDealTicketFormValues((state) => state.setType);
|
||||
const storedFormValues = useDealTicketFormValues(
|
||||
@@ -287,7 +285,28 @@ export const DealTicket = ({
|
||||
};
|
||||
}
|
||||
|
||||
const marketStateError = validateMarketState(marketState);
|
||||
let marketStateError: true | string = true;
|
||||
|
||||
if (
|
||||
[
|
||||
Schema.MarketState.STATE_SETTLED,
|
||||
Schema.MarketState.STATE_REJECTED,
|
||||
Schema.MarketState.STATE_TRADING_TERMINATED,
|
||||
Schema.MarketState.STATE_CANCELLED,
|
||||
Schema.MarketState.STATE_CLOSED,
|
||||
].includes(marketState)
|
||||
) {
|
||||
marketStateError = t(
|
||||
`This market is {{marketState}} and not accepting orders`,
|
||||
{
|
||||
marketState:
|
||||
marketState === Schema.MarketState.STATE_TRADING_TERMINATED
|
||||
? t('terminated')
|
||||
: t(Schema.MarketStateMapping[marketState]).toLowerCase(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (marketStateError !== true) {
|
||||
return {
|
||||
message: marketStateError,
|
||||
@@ -307,7 +326,10 @@ export const DealTicket = ({
|
||||
};
|
||||
}
|
||||
|
||||
const marketTradingModeError = validateMarketTradingMode(marketTradingMode);
|
||||
const marketTradingModeError = validateMarketTradingMode(
|
||||
marketTradingMode,
|
||||
t('Trading terminated')
|
||||
);
|
||||
if (marketTradingModeError !== true) {
|
||||
return {
|
||||
message: marketTradingModeError,
|
||||
@@ -317,6 +339,7 @@ export const DealTicket = ({
|
||||
|
||||
return undefined;
|
||||
}, [
|
||||
t,
|
||||
marketState,
|
||||
marketTradingMode,
|
||||
generalAccountBalance,
|
||||
@@ -404,7 +427,7 @@ export const DealTicket = ({
|
||||
required: t('You need to provide a size'),
|
||||
min: {
|
||||
value: sizeStep,
|
||||
message: t('Size cannot be lower than ' + sizeStep),
|
||||
message: t('Size cannot be lower than {{sizeStep}}', { sizeStep }),
|
||||
},
|
||||
validate: validateAmount(sizeStep, 'Size'),
|
||||
deps: ['peakSize', 'minimumVisibleSize'],
|
||||
@@ -440,7 +463,9 @@ export const DealTicket = ({
|
||||
required: t('You need provide a price'),
|
||||
min: {
|
||||
value: priceStep,
|
||||
message: t('Price cannot be lower than ' + priceStep),
|
||||
message: t('Price cannot be lower than {{priceStep}}', {
|
||||
priceStep,
|
||||
}),
|
||||
},
|
||||
validate: validateAmount(priceStep, 'Price'),
|
||||
}}
|
||||
@@ -477,7 +502,11 @@ export const DealTicket = ({
|
||||
value={formatValue(notionalSize, market.decimalPlaces)}
|
||||
formattedValue={formatValue(notionalSize, market.decimalPlaces)}
|
||||
symbol={quoteName}
|
||||
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName)}
|
||||
labelDescription={t(
|
||||
'NOTIONAL_SIZE_TOOLTIP_TEXT',
|
||||
NOTIONAL_SIZE_TOOLTIP_TEXT,
|
||||
{ quoteName }
|
||||
)}
|
||||
/>
|
||||
<DealTicketFeeDetails
|
||||
order={
|
||||
@@ -501,7 +530,7 @@ export const DealTicket = ({
|
||||
<TimeInForceSelector
|
||||
value={field.value}
|
||||
orderType={type}
|
||||
onSelect={(value) => {
|
||||
onSelect={(value: Schema.OrderTimeInForce) => {
|
||||
// If GTT is selected and no expiresAt time is set, or its
|
||||
// behind current time then reset the value to current time
|
||||
const now = Date.now();
|
||||
@@ -534,7 +563,11 @@ export const DealTicket = ({
|
||||
control={control}
|
||||
rules={{
|
||||
required: t('You need provide a expiry time/date'),
|
||||
validate: validateExpiration,
|
||||
validate: validateExpiration(
|
||||
t(
|
||||
'The expiry date that you have entered appears to be in the past'
|
||||
)
|
||||
),
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<ExpirySelector
|
||||
@@ -629,10 +662,10 @@ export const DealTicket = ({
|
||||
<Tooltip
|
||||
description={
|
||||
<p>
|
||||
{t(`Trade only a fraction of the order size at once.
|
||||
After the peak size of the order has traded, the size is reset. This is repeated until the order is cancelled, expires, or its full volume trades away.
|
||||
For example, an iceberg order with a size of 1000 and a peak size of 100 will effectively be split into 10 orders with a size of 100 each.
|
||||
Note that the full volume of the order is not hidden and is still reflected in the order book.`)}{' '}
|
||||
{t(
|
||||
'ICEBERG_TOOLTIP',
|
||||
'Trade only a fraction of the order size at once. After the peak size of the order has traded, the size is reset. This is repeated until the order is cancelled, expires, or its full volume trades away. For example, an iceberg order with a size of 1000 and a peak size of 100 will effectively be split into 10 orders with a size of 100 each. Note that the full volume of the order is not hidden and is still reflected in the order book.'
|
||||
)}{' '}
|
||||
<ExternalLink href={DocsLinks?.ICEBERG_ORDERS}>
|
||||
{t('Find out more')}
|
||||
</ExternalLink>{' '}
|
||||
@@ -731,13 +764,14 @@ interface SummaryMessageProps {
|
||||
export const NoWalletWarning = ({
|
||||
isReadOnly,
|
||||
}: Pick<SummaryMessageProps, 'isReadOnly'>) => {
|
||||
const t = useT();
|
||||
if (isReadOnly) {
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<InputError testId="deal-ticket-error-message-summary">
|
||||
{
|
||||
{t(
|
||||
'You need to connect your own wallet to start trading on this market'
|
||||
}
|
||||
)}
|
||||
</InputError>
|
||||
</div>
|
||||
);
|
||||
@@ -756,6 +790,7 @@ const SummaryMessage = memo(
|
||||
pubKey,
|
||||
onDeposit,
|
||||
}: SummaryMessageProps) => {
|
||||
const t = useT();
|
||||
// Specific error UI for if balance is so we can
|
||||
// render a deposit dialog
|
||||
if (isReadOnly || !pubKey) {
|
||||
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
TradingInputError,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { formatForInput } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useRef } from 'react';
|
||||
import { useT } from '../../use-t';
|
||||
|
||||
interface ExpirySelectorProps {
|
||||
value?: string;
|
||||
@@ -18,6 +18,7 @@ export const ExpirySelector = ({
|
||||
onSelect,
|
||||
errorMessage,
|
||||
}: ExpirySelectorProps) => {
|
||||
const t = useT();
|
||||
const minDateRef = useRef(new Date());
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import * as RadioGroup from '@radix-ui/react-radio-group';
|
||||
import classNames from 'classnames';
|
||||
import { useT } from '../../use-t';
|
||||
|
||||
interface SideSelectorProps {
|
||||
value: Schema.Side;
|
||||
onValueChange: (side: Schema.Side) => void;
|
||||
}
|
||||
|
||||
const toggles = [
|
||||
{ label: t('Long'), value: Schema.Side.SIDE_BUY },
|
||||
{ label: t('Short'), value: Schema.Side.SIDE_SELL },
|
||||
];
|
||||
|
||||
export const SideSelector = (props: SideSelectorProps) => {
|
||||
const t = useT();
|
||||
const toggles = [
|
||||
{ label: t('Long'), value: Schema.Side.SIDE_BUY },
|
||||
{ label: t('Short'), value: Schema.Side.SIDE_SELL },
|
||||
];
|
||||
return (
|
||||
<RadioGroup.Root
|
||||
name="order-side"
|
||||
|
||||
@@ -2,15 +2,16 @@ import {
|
||||
TradingFormGroup,
|
||||
TradingInputError,
|
||||
TradingSelect,
|
||||
Tooltip,
|
||||
TextChildrenTooltip as Tooltip,
|
||||
SimpleGrid,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { timeInForceLabel } from '@vegaprotocol/orders';
|
||||
import { compileGridData } from '../trading-mode-tooltip';
|
||||
import { MarketModeValidationType } from '../../constants';
|
||||
import type { Market, StaticMarketData } from '@vegaprotocol/markets';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { useT, ns } from '../../use-t';
|
||||
|
||||
interface TimeInForceSelectorProps {
|
||||
value: Schema.OrderTimeInForce;
|
||||
@@ -36,6 +37,7 @@ export const TimeInForceSelector = ({
|
||||
marketData,
|
||||
errorMessage,
|
||||
}: TimeInForceSelectorProps) => {
|
||||
const t = useT();
|
||||
const options =
|
||||
orderType === Schema.OrderType.TYPE_LIMIT
|
||||
? typeLimitOptions
|
||||
@@ -44,25 +46,30 @@ export const TimeInForceSelector = ({
|
||||
const renderError = (errorType: string) => {
|
||||
if (errorType === MarketModeValidationType.Auction) {
|
||||
return t(
|
||||
`Until the auction ends, you can only place GFA, GTT, or GTC limit orders`
|
||||
'Until the auction ends, you can only place GFA, GTT, or GTC limit orders'
|
||||
);
|
||||
}
|
||||
|
||||
if (errorType === MarketModeValidationType.LiquidityMonitoringAuction) {
|
||||
return (
|
||||
<span>
|
||||
{t('This market is in auction until it reaches')}{' '}
|
||||
<Tooltip
|
||||
description={
|
||||
<SimpleGrid grid={compileGridData(market, marketData)} />
|
||||
}
|
||||
>
|
||||
<span>{t('sufficient liquidity')}</span>
|
||||
</Tooltip>
|
||||
{'. '}
|
||||
{t(
|
||||
`Until the auction ends, you can only place GFA, GTT, or GTC limit orders`
|
||||
)}
|
||||
<Trans
|
||||
i18nKey="TIME_IN_FORCE_SELECTOR_LIQUIDITY_MONITORING_AUCTION"
|
||||
defaults="This market is in auction until it reaches <0>sufficient liquidity</0>. Until the auction ends, you can only place GFA, GTT, or GTC limit orders."
|
||||
ns={ns}
|
||||
components={[
|
||||
<Tooltip
|
||||
description={
|
||||
<SimpleGrid
|
||||
grid={compileGridData(t, market, marketData, t)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
sufficient liquidity
|
||||
</Tooltip>,
|
||||
]}
|
||||
t={t}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -70,18 +77,23 @@ export const TimeInForceSelector = ({
|
||||
if (errorType === MarketModeValidationType.PriceMonitoringAuction) {
|
||||
return (
|
||||
<span>
|
||||
{t('This market is in auction due to')}{' '}
|
||||
<Tooltip
|
||||
description={
|
||||
<SimpleGrid grid={compileGridData(market, marketData)} />
|
||||
}
|
||||
>
|
||||
<span>{t('high price volatility')}</span>
|
||||
</Tooltip>
|
||||
{'. '}
|
||||
{t(
|
||||
`Until the auction ends, you can only place GFA, GTT, or GTC limit orders`
|
||||
)}
|
||||
<Trans
|
||||
i18nKey="TIME_IN_FORCE_SELECTOR_PRICE_MONITORING_AUCTION"
|
||||
defaults="This market is in auction due to <0>high price volatility</0>. Until the auction ends, you can only place GFA, GTT, or GTC limit orders."
|
||||
ns={ns}
|
||||
components={[
|
||||
<Tooltip
|
||||
description={
|
||||
<SimpleGrid
|
||||
grid={compileGridData(t, market, marketData, t)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
high price volatility
|
||||
</Tooltip>,
|
||||
]}
|
||||
t={t}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
TradingInputError,
|
||||
SimpleGrid,
|
||||
Tooltip,
|
||||
TextChildrenTooltip as Tooltip,
|
||||
TradingDropdown,
|
||||
TradingDropdownContent,
|
||||
TradingDropdownItemIndicator,
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { Market, StaticMarketData } from '@vegaprotocol/markets';
|
||||
import { compileGridData } from '../trading-mode-tooltip';
|
||||
import { MarketModeValidationType } from '../../constants';
|
||||
@@ -20,6 +19,8 @@ import { DealTicketType } from '../../hooks/use-form-values';
|
||||
import * as RadioGroup from '@radix-ui/react-radio-group';
|
||||
import classNames from 'classnames';
|
||||
import { FLAGS } from '@vegaprotocol/environment';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { useT, ns } from '../../use-t';
|
||||
|
||||
interface TypeSelectorProps {
|
||||
value: DealTicketType;
|
||||
@@ -29,19 +30,28 @@ interface TypeSelectorProps {
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
const toggles = [
|
||||
{ label: t('Limit'), value: DealTicketType.Limit },
|
||||
{ label: t('Market'), value: DealTicketType.Market },
|
||||
];
|
||||
const options = [
|
||||
{ label: t('Stop Limit'), value: DealTicketType.StopLimit },
|
||||
{ label: t('Stop Market'), value: DealTicketType.StopMarket },
|
||||
];
|
||||
const useToggles = () => {
|
||||
const t = useT();
|
||||
return [
|
||||
{ label: t('Limit'), value: DealTicketType.Limit },
|
||||
{ label: t('Market'), value: DealTicketType.Market },
|
||||
];
|
||||
};
|
||||
const useOptions = () => {
|
||||
const t = useT();
|
||||
return [
|
||||
{ label: t('Stop Limit'), value: DealTicketType.StopLimit },
|
||||
{ label: t('Stop Market'), value: DealTicketType.StopMarket },
|
||||
];
|
||||
};
|
||||
|
||||
export const TypeToggle = ({
|
||||
value,
|
||||
onValueChange,
|
||||
}: Pick<TypeSelectorProps, 'onValueChange' | 'value'>) => {
|
||||
const t = useT();
|
||||
const options = useOptions();
|
||||
const toggles = useToggles();
|
||||
const selectedOption = options.find((t) => t.value === value);
|
||||
return (
|
||||
<RadioGroup.Root
|
||||
@@ -84,7 +94,7 @@ export const TypeToggle = ({
|
||||
>
|
||||
<button className="flex gap-1">
|
||||
<span className="text-ellipsis whitespace-nowrap shrink overflow-hidden">
|
||||
{t(selectedOption ? selectedOption.label : 'Stop')}
|
||||
{selectedOption ? selectedOption.label : t('Stop')}
|
||||
</span>
|
||||
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={14} />
|
||||
</button>
|
||||
@@ -107,7 +117,7 @@ export const TypeToggle = ({
|
||||
id={`order-type-${itemValue}`}
|
||||
data-testid={`order-type-${itemValue}`}
|
||||
>
|
||||
{t(label)}
|
||||
{label}
|
||||
<TradingDropdownItemIndicator />
|
||||
</TradingDropdownRadioItem>
|
||||
))}
|
||||
@@ -127,6 +137,7 @@ export const TypeSelector = ({
|
||||
marketData,
|
||||
errorMessage,
|
||||
}: TypeSelectorProps) => {
|
||||
const t = useT();
|
||||
const renderError = (errorType: MarketModeValidationType) => {
|
||||
if (errorType === MarketModeValidationType.Auction) {
|
||||
return t('Only limit orders are permitted when market is in auction');
|
||||
@@ -135,16 +146,21 @@ export const TypeSelector = ({
|
||||
if (errorType === MarketModeValidationType.LiquidityMonitoringAuction) {
|
||||
return (
|
||||
<span>
|
||||
{t('This market is in auction until it reaches')}{' '}
|
||||
<Tooltip
|
||||
description={
|
||||
<SimpleGrid grid={compileGridData(market, marketData)} />
|
||||
}
|
||||
>
|
||||
<span>{t('sufficient liquidity')}</span>
|
||||
</Tooltip>
|
||||
{'. '}
|
||||
{t('Only limit orders are permitted when market is in auction')}
|
||||
<Trans
|
||||
i18nKey="TYPE_SELECTOR_LIQUIDITY_MONITORING_AUCTION"
|
||||
defaults="This market is in auction until it reaches <0>sufficient liquidity</0>. Only limit orders are permitted when market is in auction."
|
||||
ns={ns}
|
||||
components={[
|
||||
<Tooltip
|
||||
description={
|
||||
<SimpleGrid grid={compileGridData(t, market, marketData)} />
|
||||
}
|
||||
>
|
||||
sufficient liquidity
|
||||
</Tooltip>,
|
||||
]}
|
||||
t={t}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -152,16 +168,21 @@ export const TypeSelector = ({
|
||||
if (errorType === MarketModeValidationType.PriceMonitoringAuction) {
|
||||
return (
|
||||
<span>
|
||||
{t('This market is in auction due to')}{' '}
|
||||
<Tooltip
|
||||
description={
|
||||
<SimpleGrid grid={compileGridData(market, marketData)} />
|
||||
}
|
||||
>
|
||||
<span>{t('high price volatility')}</span>
|
||||
</Tooltip>
|
||||
{'. '}
|
||||
{t('Only limit orders are permitted when market is in auction')}
|
||||
<Trans
|
||||
i18nKey="TYPE_SELECTOR_PRICE_MONITORING_AUCTION"
|
||||
defaults="This market is in auction due to <0>high price volatility</0>. Only limit orders are permitted when market is in auction."
|
||||
ns={ns}
|
||||
components={[
|
||||
<Tooltip
|
||||
description={
|
||||
<SimpleGrid grid={compileGridData(t, market, marketData)} />
|
||||
}
|
||||
>
|
||||
sufficient liquidity
|
||||
</Tooltip>,
|
||||
]}
|
||||
t={t}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumberPercentage,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { getDiscountedFee } from '../discounts';
|
||||
import { useT } from '../../use-t';
|
||||
|
||||
const formatValue = (
|
||||
value: string | number | null | undefined,
|
||||
@@ -59,6 +59,7 @@ export const FeesBreakdown = ({
|
||||
referralDiscountFactor?: string;
|
||||
volumeDiscountFactor?: string;
|
||||
}) => {
|
||||
const t = useT();
|
||||
if (!fees || !totalFeeAmount || totalFeeAmount === '0') return null;
|
||||
|
||||
const { discountedFee: discountedInfrastructureFee } = getDiscountedFee(
|
||||
|
||||
@@ -2,15 +2,16 @@ import {
|
||||
getDateTimeFormat,
|
||||
addDecimalsFormatNumber,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { Link as UILink } from '@vegaprotocol/ui-toolkit';
|
||||
import type { SimpleGridProps } from '@vegaprotocol/ui-toolkit';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { getAsset, type Market, type MarketData } from '@vegaprotocol/markets';
|
||||
import type { useT } from '../../use-t';
|
||||
|
||||
export const compileGridData = (
|
||||
t: ReturnType<typeof useT>,
|
||||
market: Pick<
|
||||
Market,
|
||||
'id' | 'tradableInstrument' | 'decimalPlaces' | 'positionDecimalPlaces'
|
||||
|
||||
@@ -4,11 +4,11 @@ import classNames from 'classnames';
|
||||
import { useProposalOfMarketQuery } from '@vegaprotocol/proposals';
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { ExternalLink, SimpleGrid } from '@vegaprotocol/ui-toolkit';
|
||||
import { compileGridData } from './compile-grid-data';
|
||||
import { useMarket, useStaticMarketData } from '@vegaprotocol/markets';
|
||||
import { useT } from '../../use-t';
|
||||
|
||||
type TradingModeTooltipProps = {
|
||||
marketId?: string;
|
||||
@@ -23,6 +23,7 @@ export const TradingModeTooltip = ({
|
||||
skip,
|
||||
skipGrid,
|
||||
}: TradingModeTooltipProps) => {
|
||||
const t = useT();
|
||||
const { data: market } = useMarket(marketId);
|
||||
const { data: marketData } = useStaticMarketData(marketId, skip);
|
||||
const { marketTradingMode, trigger } = marketData || {};
|
||||
@@ -43,7 +44,7 @@ export const TradingModeTooltip = ({
|
||||
);
|
||||
|
||||
const compiledGrid =
|
||||
!skipGrid && compileGridData(market, marketData, onSelect);
|
||||
!skipGrid && compileGridData(t, market, marketData, onSelect);
|
||||
|
||||
switch (marketTradingMode) {
|
||||
case Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS: {
|
||||
@@ -88,10 +89,9 @@ export const TradingModeTooltip = ({
|
||||
>
|
||||
{`${
|
||||
Schema.MarketTradingModeMapping[marketTradingMode]
|
||||
}: ${t(
|
||||
'Closing on %s',
|
||||
getDateTimeFormat().format(enactmentDate)
|
||||
)}`}
|
||||
}: ${t('Closing on {{time}}', {
|
||||
time: getDateTimeFormat().format(enactmentDate),
|
||||
})}`}
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
@@ -118,7 +118,7 @@ export const TradingModeTooltip = ({
|
||||
return (
|
||||
<section data-testid="trading-mode-suspended-via-governance">
|
||||
{t(
|
||||
`This market has been suspended via a governance vote and can be resumed or terminated by further votes.`
|
||||
'This market has been suspended via a governance vote and can be resumed or terminated by further votes.'
|
||||
)}
|
||||
{DocsLinks && (
|
||||
<ExternalLink href={DocsLinks.MARKET_LIFECYCLE} className="ml-1">
|
||||
|
||||
@@ -1,75 +1,32 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
export const EST_MARGIN_TOOLTIP_TEXT = `A fraction of the notional position size, in the market's settlement asset {{assetSymbol}}, to cover any potential losses that you may incur. For example, for a notional size of $500, if the margin requirement is 10%, then the estimated margin would be approximately $50.`;
|
||||
export const EST_TOTAL_MARGIN_TOOLTIP_TEXT =
|
||||
'Estimated total margin that will cover open positions, active orders and this order.';
|
||||
export const MARGIN_ACCOUNT_TOOLTIP_TEXT = 'Margin account balance.';
|
||||
export const MARGIN_DIFF_TOOLTIP_TEXT =
|
||||
"The additional margin required for your new position (taking into account volume and open orders), compared to your current margin. Measured in the market's settlement asset ({{assetSymbol}}).";
|
||||
export const DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT =
|
||||
'To cover the required margin, this amount will be drawn from your general ({{assetSymbol}}) account.';
|
||||
|
||||
export const EST_MARGIN_TOOLTIP_TEXT = (settlementAsset: string) =>
|
||||
t(
|
||||
`A fraction of the notional position size, in the market's settlement asset %s, to cover any potential losses that you may incur.
|
||||
export const TOTAL_MARGIN_AVAILABLE =
|
||||
'Total margin available = general {{assetSymbol}} balance ({{generalAccountBalance}} {{assetSymbol}}) + margin balance ({{marginAccountBalance}} {{assetSymbol}}) - maintenance level ({{marginMaintenance}} {{assetSymbol}}).';
|
||||
|
||||
For example, for a notional size of $500, if the margin requirement is 10%, then the estimated margin would be approximately $50.`,
|
||||
[settlementAsset]
|
||||
);
|
||||
export const EST_TOTAL_MARGIN_TOOLTIP_TEXT = t(
|
||||
'Estimated total margin that will cover open positions, active orders and this order.'
|
||||
);
|
||||
export const MARGIN_ACCOUNT_TOOLTIP_TEXT = t('Margin account balance.');
|
||||
export const MARGIN_DIFF_TOOLTIP_TEXT = (settlementAsset: string) =>
|
||||
t(
|
||||
"The additional margin required for your new position (taking into account volume and open orders), compared to your current margin. Measured in the market's settlement asset (%s).",
|
||||
[settlementAsset]
|
||||
);
|
||||
export const DEDUCTION_FROM_COLLATERAL_TOOLTIP_TEXT = (
|
||||
settlementAsset: string
|
||||
) =>
|
||||
t(
|
||||
'To cover the required margin, this amount will be drawn from your general (%s) account.',
|
||||
[settlementAsset]
|
||||
);
|
||||
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.';
|
||||
export const EST_CLOSEOUT_TOOLTIP_TEXT =
|
||||
'If the price drops below this number, measured in the market price quote unit {{quote}}, you will be closed out, based on your current position and account balance.';
|
||||
export const NOTIONAL_SIZE_TOOLTIP_TEXT =
|
||||
'The notional size represents the position size in the settlement asset {{quoteName}} of the futures contract. This is calculated by multiplying the number of contracts by the prices of the contract. For example 10 contracts traded at a price of $50 has a notional size of $500.';
|
||||
export const EST_FEES_TOOLTIP_TEXT =
|
||||
'When you execute a new buy or sell order, you must pay a small amount of commission to the network for doing so. This fee is used to provide income to the node operates of the network and market makers who make prices on the futures market you are trading.';
|
||||
|
||||
export const TOTAL_MARGIN_AVAILABLE = (
|
||||
generalAccountBalance: string,
|
||||
marginAccountBalance: string,
|
||||
marginMaintenance: string,
|
||||
settlementAsset: string
|
||||
) =>
|
||||
t(
|
||||
'Total margin available = general %s balance (%s) + margin balance (%s) - maintenance level (%s).',
|
||||
[
|
||||
settlementAsset,
|
||||
`${generalAccountBalance} ${settlementAsset}`,
|
||||
`${marginAccountBalance} ${settlementAsset}`,
|
||||
`${marginMaintenance} ${settlementAsset}`,
|
||||
]
|
||||
);
|
||||
export const LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT =
|
||||
'This is an approximation for the liquidation price for that particular contract position, assuming nothing else changes, which may affect your margin and collateral balances.';
|
||||
|
||||
export const CONTRACTS_MARGIN_TOOLTIP_TEXT = t(
|
||||
'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.'
|
||||
);
|
||||
export const EST_CLOSEOUT_TOOLTIP_TEXT = (quote: string) =>
|
||||
t(
|
||||
`If the price drops below this number, measured in the market price quote unit %s, you will be closed out, based on your current position and account balance.`,
|
||||
[quote]
|
||||
);
|
||||
export const NOTIONAL_SIZE_TOOLTIP_TEXT = (settlementAsset: string) =>
|
||||
t(
|
||||
`The notional size represents the position size in the settlement asset %s of the futures contract. This is calculated by multiplying the number of contracts by the prices of the contract.
|
||||
export const EST_SLIPPAGE =
|
||||
'When you execute a trade on Vega, the price obtained in the market may differ from the best available price displayed at the time of placing the trade. The estimated slippage shows the difference between the best available price and the estimated execution price, determined by market liquidity and your chosen order size.';
|
||||
|
||||
For example 10 contracts traded at a price of $50 has a notional size of $500.`,
|
||||
[settlementAsset]
|
||||
);
|
||||
export const EST_FEES_TOOLTIP_TEXT = t(
|
||||
'When you execute a new buy or sell order, you must pay a small amount of commission to the network for doing so. This fee is used to provide income to the node operates of the network and market makers who make prices on the futures market you are trading.'
|
||||
);
|
||||
|
||||
export const LIQUIDATION_PRICE_ESTIMATE_TOOLTIP_TEXT = t(
|
||||
'This is an approximation for the liquidation price for that particular contract position, assuming nothing else changes, which may affect your margin and collateral balances.'
|
||||
);
|
||||
|
||||
export const EST_SLIPPAGE = t(
|
||||
'When you execute a trade on Vega, the price obtained in the market may differ from the best available price displayed at the time of placing the trade. The estimated slippage shows the difference between the best available price and the estimated execution price, determined by market liquidity and your chosen order size.'
|
||||
);
|
||||
|
||||
export const ERROR_SIZE_DECIMAL = t(
|
||||
'The size field accepts up to X decimal places.'
|
||||
);
|
||||
export const ERROR_SIZE_DECIMAL =
|
||||
'The size field accepts up to X decimal places.';
|
||||
|
||||
export enum MarketModeValidationType {
|
||||
PriceMonitoringAuction = 'PriceMonitoringAuction',
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
export const ns = 'deal-ticket';
|
||||
export const useT = () => useTranslation(ns).t;
|
||||
@@ -1,6 +1,5 @@
|
||||
export * from './get-default-order';
|
||||
export * from './validate-expiration';
|
||||
export * from './validate-market-state';
|
||||
export * from './validate-market-trading-mode';
|
||||
export * from './validate-time-in-force';
|
||||
export * from './validate-type';
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { Validate } from 'react-hook-form';
|
||||
|
||||
export const validateExpiration: Validate<string | undefined, object> = (
|
||||
value?: string
|
||||
) => {
|
||||
const now = new Date();
|
||||
const valueAsDate = value ? new Date(value) : now;
|
||||
if (now > valueAsDate) {
|
||||
return t('The expiry date that you have entered appears to be in the past');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
export const validateExpiration: (
|
||||
errorMessage: string
|
||||
) => Validate<string | undefined, object> =
|
||||
(errorMessage: string) => (value?: string) => {
|
||||
const now = new Date();
|
||||
const valueAsDate = value ? new Date(value) : now;
|
||||
if (now > valueAsDate) {
|
||||
return errorMessage;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { MarketState, MarketStateMapping } from '@vegaprotocol/types';
|
||||
|
||||
export const validateMarketState = (state: MarketState) => {
|
||||
if (
|
||||
[
|
||||
MarketState.STATE_SETTLED,
|
||||
MarketState.STATE_REJECTED,
|
||||
MarketState.STATE_TRADING_TERMINATED,
|
||||
MarketState.STATE_CANCELLED,
|
||||
MarketState.STATE_CLOSED,
|
||||
].includes(state)
|
||||
) {
|
||||
return t(
|
||||
`This market is ${marketTranslations(state)} and not accepting orders`
|
||||
);
|
||||
}
|
||||
|
||||
if (state === MarketState.STATE_PROPOSED) {
|
||||
return t(
|
||||
`This market is ${marketTranslations(
|
||||
state
|
||||
)} and only accepting liquidity commitment orders`
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const marketTranslations = (marketState: MarketState) => {
|
||||
switch (marketState) {
|
||||
case MarketState.STATE_TRADING_TERMINATED:
|
||||
return t('terminated');
|
||||
default:
|
||||
return t(MarketStateMapping[marketState]).toLowerCase();
|
||||
}
|
||||
};
|
||||
@@ -1,11 +1,11 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { MarketTradingMode } from '@vegaprotocol/types';
|
||||
|
||||
export const validateMarketTradingMode = (
|
||||
marketTradingMode: MarketTradingMode
|
||||
marketTradingMode: MarketTradingMode,
|
||||
errorMessage: string
|
||||
) => {
|
||||
if (marketTradingMode === MarketTradingMode.TRADING_MODE_NO_TRADING) {
|
||||
return t('Trading terminated');
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
export const useTranslation = () => ({
|
||||
t: (label: string, replacements?: Record<string, string>) => {
|
||||
let translatedLabel = label;
|
||||
if (typeof replacements === 'object' && replacements !== null) {
|
||||
Object.keys(replacements).forEach((key) => {
|
||||
translatedLabel = translatedLabel.replace(
|
||||
`{{${key}}}`,
|
||||
replacements[key]
|
||||
);
|
||||
});
|
||||
}
|
||||
return translatedLabel;
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { EtherscanLink } from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Intent, Notification } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
formatNumber,
|
||||
@@ -11,6 +10,7 @@ import type { EthStoredTxState } from '@vegaprotocol/web3';
|
||||
import { EthTxStatus, useEthTransactionStore } from '@vegaprotocol/web3';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { DepositBalances } from './use-deposit-balances';
|
||||
import { useT } from './use-t';
|
||||
|
||||
interface ApproveNotificationProps {
|
||||
isActive: boolean;
|
||||
@@ -33,6 +33,7 @@ export const ApproveNotification = ({
|
||||
approveTxId,
|
||||
intent = Intent.Warning,
|
||||
}: ApproveNotificationProps) => {
|
||||
const t = useT();
|
||||
const tx = useEthTransactionStore((state) => {
|
||||
return state.transactions.find((t) => t?.id === approveTxId);
|
||||
});
|
||||
@@ -55,12 +56,14 @@ export const ApproveNotification = ({
|
||||
intent={intent}
|
||||
testId="approve-default"
|
||||
message={t(
|
||||
'Before you can make a deposit of your chosen asset, %s, you need to approve its use in your Ethereum wallet',
|
||||
selectedAsset?.symbol
|
||||
'Before you can make a deposit of your chosen asset, {{assetSymbol}}, you need to approve its use in your Ethereum wallet',
|
||||
{ assetSymbol: selectedAsset?.symbol }
|
||||
)}
|
||||
buttonProps={{
|
||||
size: 'small',
|
||||
text: t('Approve %s', selectedAsset?.symbol),
|
||||
text: t('Approve {{assetSymbol}}', {
|
||||
assetSymbol: selectedAsset?.symbol,
|
||||
}),
|
||||
action: onApprove,
|
||||
dataTestId: 'approve-submit',
|
||||
}}
|
||||
@@ -72,13 +75,14 @@ export const ApproveNotification = ({
|
||||
<Notification
|
||||
intent={intent}
|
||||
testId="reapprove-default"
|
||||
message={t(
|
||||
'Approve again to deposit more than %s',
|
||||
formatNumber(balances.allowance.toString())
|
||||
)}
|
||||
message={t('Approve again to deposit more than {{allowance}}', {
|
||||
allowance: formatNumber(balances.allowance.toString()),
|
||||
})}
|
||||
buttonProps={{
|
||||
size: 'small',
|
||||
text: t('Approve %s', selectedAsset?.symbol),
|
||||
text: t('Approve {{assetSymbol}}', {
|
||||
assetSymbol: selectedAsset?.symbol,
|
||||
}),
|
||||
action: onApprove,
|
||||
dataTestId: 'reapprove-submit',
|
||||
}}
|
||||
@@ -132,6 +136,7 @@ const ApprovalTxFeedback = ({
|
||||
selectedAsset: Asset;
|
||||
allowance?: BigNumber;
|
||||
}) => {
|
||||
const t = useT();
|
||||
if (!tx) return null;
|
||||
|
||||
const txLink = tx.txHash && (
|
||||
@@ -161,8 +166,8 @@ const ApprovalTxFeedback = ({
|
||||
intent={Intent.Warning}
|
||||
testId="approve-requested"
|
||||
message={t(
|
||||
'Go to your Ethereum wallet and approve the transaction to enable the use of %s',
|
||||
selectedAsset?.symbol
|
||||
'Go to your Ethereum wallet and approve the transaction to enable the use of {{assetSymbol}}',
|
||||
{ assetSymbol: selectedAsset?.symbol }
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -179,8 +184,8 @@ const ApprovalTxFeedback = ({
|
||||
<>
|
||||
<p>
|
||||
{t(
|
||||
'Your %s approval is being confirmed by the Ethereum network. When this is complete, you can continue your deposit',
|
||||
selectedAsset?.symbol
|
||||
'Your {{assetSymbol}} approval is being confirmed by the Ethereum network. When this is complete, you can continue your deposit',
|
||||
{ assetSymbol: selectedAsset?.symbol }
|
||||
)}{' '}
|
||||
</p>
|
||||
{txLink && <p>{txLink}</p>}
|
||||
@@ -209,10 +214,15 @@ const ApprovalTxFeedback = ({
|
||||
message={
|
||||
<>
|
||||
<p>
|
||||
{t('You approved deposits of up to %s %s.', [
|
||||
selectedAsset?.symbol,
|
||||
approvedAllowanceValue,
|
||||
])}
|
||||
{t(
|
||||
'You approved deposits of up to {{assetSymbol}} {{approvedAllowanceValue}}.',
|
||||
[
|
||||
{
|
||||
assetSymbol: selectedAsset?.symbol,
|
||||
approvedAllowanceValue,
|
||||
},
|
||||
]
|
||||
)}
|
||||
</p>
|
||||
{txLink && <p>{txLink}</p>}
|
||||
</>
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
isAssetTypeERC20,
|
||||
formatNumber,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useLocalStorage } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
TradingFormGroup,
|
||||
@@ -43,6 +42,7 @@ import { FaucetNotification } from './faucet-notification';
|
||||
import { ApproveNotification } from './approve-notification';
|
||||
import { usePersistentDeposit } from './use-persistent-deposit';
|
||||
import { AssetBalance } from './asset-balance';
|
||||
import { useT } from './use-t';
|
||||
|
||||
interface FormFields {
|
||||
asset: string;
|
||||
@@ -84,6 +84,7 @@ export const DepositForm = ({
|
||||
approveTxId,
|
||||
isFaucetable,
|
||||
}: DepositFormProps) => {
|
||||
const t = useT();
|
||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||
const openDialog = useWeb3ConnectStore((store) => store.open);
|
||||
const { isActive, account } = useWeb3React();
|
||||
@@ -284,7 +285,7 @@ export const DepositForm = ({
|
||||
)}
|
||||
{isActive && isFaucetable && selectedAsset && (
|
||||
<UseButton onClick={submitFaucet}>
|
||||
{t(`Get ${selectedAsset.symbol}`)}
|
||||
{t('Get {{assetSymbol}}', { assetSymbol: selectedAsset.symbol })}
|
||||
</UseButton>
|
||||
)}
|
||||
{!errors.asset?.message && selectedAsset && (
|
||||
@@ -325,11 +326,11 @@ export const DepositForm = ({
|
||||
const allowance = new BigNumber(balances?.allowance || 0);
|
||||
if (value.isGreaterThan(allowance)) {
|
||||
return t(
|
||||
"You can't deposit more than your approved deposit amount, %s %s",
|
||||
[
|
||||
formatNumber(allowance.toString()),
|
||||
selectedAsset?.symbol || ' ',
|
||||
]
|
||||
"You can't deposit more than your approved deposit amount, {{amount}} {{assetSymbol}}",
|
||||
{
|
||||
amount: formatNumber(allowance.toString()),
|
||||
assetSymbol: selectedAsset?.symbol || ' ',
|
||||
}
|
||||
);
|
||||
}
|
||||
return true;
|
||||
@@ -347,11 +348,11 @@ export const DepositForm = ({
|
||||
|
||||
if (value.isGreaterThan(lifetimeLimit)) {
|
||||
return t(
|
||||
"You can't deposit more than your remaining deposit allowance, %s %s",
|
||||
[
|
||||
formatNumber(lifetimeLimit.toString()),
|
||||
selectedAsset?.symbol || ' ',
|
||||
]
|
||||
"You can't deposit more than your remaining deposit allowance, {{amount}} {{assetSymbol}}",
|
||||
{
|
||||
amount: formatNumber(lifetimeLimit.toString()),
|
||||
assetSymbol: selectedAsset?.symbol || ' ',
|
||||
}
|
||||
);
|
||||
}
|
||||
return true;
|
||||
@@ -361,8 +362,11 @@ export const DepositForm = ({
|
||||
const balance = new BigNumber(balances?.balance || 0);
|
||||
if (value.isGreaterThan(balance)) {
|
||||
return t(
|
||||
"You can't deposit more than you have in your Ethereum wallet, %s %s",
|
||||
[formatNumber(balance), selectedAsset?.symbol || ' ']
|
||||
"You can't deposit more than you have in your Ethereum wallet, {{amount}} {{assetSymbol}}",
|
||||
{
|
||||
amount: formatNumber(balance),
|
||||
assetSymbol: selectedAsset?.symbol || ' ',
|
||||
}
|
||||
);
|
||||
}
|
||||
return true;
|
||||
@@ -419,6 +423,7 @@ interface FormButtonProps {
|
||||
}
|
||||
|
||||
const FormButton = ({ approved, selectedAsset }: FormButtonProps) => {
|
||||
const t = useT();
|
||||
const { isActive, chainId } = useWeb3React();
|
||||
const desiredChainId = useWeb3ConnectStore((store) => store.desiredChainId);
|
||||
const invalidChain = isActive && chainId !== desiredChainId;
|
||||
@@ -429,9 +434,9 @@ const FormButton = ({ approved, selectedAsset }: FormButtonProps) => {
|
||||
<Notification
|
||||
intent={Intent.Danger}
|
||||
testId="chain-error"
|
||||
message={t(
|
||||
`This app only works on ${getChainName(desiredChainId)}.`
|
||||
)}
|
||||
message={t('This app only works on {{chainId}}.', {
|
||||
chainId: getChainName(desiredChainId),
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -464,6 +469,7 @@ const DisconnectEthereumButton = ({
|
||||
}: {
|
||||
onDisconnect: () => void;
|
||||
}) => {
|
||||
const t = useT();
|
||||
const { connector } = useWeb3React();
|
||||
const [, , removeEagerConnector] = useLocalStorage(ETHEREUM_EAGER_CONNECT);
|
||||
const disconnect = useWeb3Disconnect(connector);
|
||||
@@ -495,6 +501,7 @@ export const AddressField = ({
|
||||
input,
|
||||
onChange,
|
||||
}: AddressInputProps) => {
|
||||
const t = useT();
|
||||
const [isInput, setIsInput] = useState(() => {
|
||||
if (pubKeys && pubKeys.length <= 1) {
|
||||
return true;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { CompactNumber } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
KeyValueTable,
|
||||
@@ -8,6 +7,7 @@ import {
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type BigNumber from 'bignumber.js';
|
||||
import { formatNumber, quantumDecimalPlaces } from '@vegaprotocol/utils';
|
||||
import { useT } from './use-t';
|
||||
|
||||
// Note: all of the values here are with correct asset's decimals
|
||||
// See `libs/deposits/src/lib/use-deposit-balances.ts`
|
||||
@@ -29,6 +29,7 @@ export const DepositLimits = ({
|
||||
allowance,
|
||||
exempt,
|
||||
}: DepositLimitsProps) => {
|
||||
const t = useT();
|
||||
const limits = [
|
||||
{
|
||||
key: 'BALANCE_AVAILABLE',
|
||||
@@ -51,19 +52,23 @@ export const DepositLimits = ({
|
||||
<>
|
||||
<p>
|
||||
{t(
|
||||
'VEGA has a lifetime deposit limit of %s %s per address. This can be changed through governance',
|
||||
[formatNumber(max.toString()), asset.symbol]
|
||||
'VEGA has a lifetime deposit limit of {{amount}} {{assetSymbol}} per address. This can be changed through governance',
|
||||
{
|
||||
amount: formatNumber(max.toString()),
|
||||
assetSymbol: asset.symbol,
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{t(
|
||||
'To date, %s %s has been deposited from this Ethereum address, so you can deposit up to %s %s more.',
|
||||
[
|
||||
formatNumber(deposited.toString()),
|
||||
asset.symbol,
|
||||
formatNumber(max.minus(deposited).toString()),
|
||||
asset.symbol,
|
||||
]
|
||||
'To date, {{currentDeposit}} {{assetSymbol}} has been deposited from this Ethereum address, so you can deposit up to {{remainingDeposit}} {{assetSymbol}} more.',
|
||||
{
|
||||
currentDeposit: formatNumber(deposited.toString()),
|
||||
assetSymbol: asset.symbol,
|
||||
remainingDeposit: formatNumber(
|
||||
max.minus(deposited).toString()
|
||||
),
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
@@ -89,8 +94,8 @@ export const DepositLimits = ({
|
||||
description={
|
||||
<p>
|
||||
{t(
|
||||
'The deposit cap is set when you approve an asset for use with this app. To increase this cap, approve %s again and choose a higher cap. Check the documentation for your Ethereum wallet app for details.',
|
||||
asset.symbol
|
||||
'The deposit cap is set when you approve an asset for use with this app. To increase this cap, approve {{assetSymbol}} again and choose a higher cap. Check the documentation for your Ethereum wallet app for details.',
|
||||
{ assetSymbol: asset.symbol }
|
||||
)}
|
||||
</p>
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { Asset } from '@vegaprotocol/assets';
|
||||
import { EtherscanLink } from '@vegaprotocol/environment';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Intent, Notification } from '@vegaprotocol/ui-toolkit';
|
||||
import { EthTxStatus, useEthTransactionStore } from '@vegaprotocol/web3';
|
||||
import { getFaucetError } from './get-faucet-error';
|
||||
|
||||
interface FaucetNotificationProps {
|
||||
import { useGetFaucetError } from './get-faucet-error';
|
||||
import { useT } from './use-t';
|
||||
export interface FaucetNotificationProps {
|
||||
isActive: boolean;
|
||||
selectedAsset?: Asset;
|
||||
faucetTxId: number | null;
|
||||
@@ -14,14 +13,20 @@ interface FaucetNotificationProps {
|
||||
/**
|
||||
* Render a notification for the faucet transaction
|
||||
*/
|
||||
|
||||
export const FaucetNotification = ({
|
||||
isActive,
|
||||
selectedAsset,
|
||||
faucetTxId,
|
||||
}: FaucetNotificationProps) => {
|
||||
const t = useT();
|
||||
const tx = useEthTransactionStore((state) => {
|
||||
return state.transactions.find((t) => t?.id === faucetTxId);
|
||||
});
|
||||
const errorMessage = useGetFaucetError(
|
||||
tx?.status === EthTxStatus.Error ? tx.error : null,
|
||||
selectedAsset?.symbol
|
||||
);
|
||||
|
||||
if (!isActive) {
|
||||
return null;
|
||||
@@ -34,9 +39,7 @@ export const FaucetNotification = ({
|
||||
if (!tx) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (tx.status === EthTxStatus.Error) {
|
||||
const errorMessage = getFaucetError(tx.error, selectedAsset.symbol);
|
||||
if (errorMessage) {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<Notification
|
||||
@@ -55,7 +58,8 @@ export const FaucetNotification = ({
|
||||
intent={Intent.Warning}
|
||||
testId="faucet-requested"
|
||||
message={t(
|
||||
`Confirm the transaction in your Ethereum wallet to use the ${selectedAsset?.symbol} faucet`
|
||||
'Confirm the transaction in your Ethereum wallet to use the {{assetSymbol}} faucet',
|
||||
{ assetSymbol: selectedAsset?.symbol }
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -72,8 +76,8 @@ export const FaucetNotification = ({
|
||||
<>
|
||||
<p className="mb-2">
|
||||
{t(
|
||||
'Your request for funds from the %s faucet is being confirmed by the Ethereum network',
|
||||
selectedAsset.symbol
|
||||
'Your request for funds from the {{assetSymbol}} faucet is being confirmed by the Ethereum network',
|
||||
{ assetSymbol: selectedAsset.symbol }
|
||||
)}{' '}
|
||||
</p>
|
||||
{tx.txHash && (
|
||||
@@ -100,8 +104,10 @@ export const FaucetNotification = ({
|
||||
<>
|
||||
<p className="mb-2">
|
||||
{t(
|
||||
'%s has been deposited in your Ethereum wallet',
|
||||
selectedAsset.symbol
|
||||
'{{assetSymbol}} has been deposited in your Ethereum wallet',
|
||||
{
|
||||
assetSymbol: selectedAsset.symbol,
|
||||
}
|
||||
)}{' '}
|
||||
</p>
|
||||
{tx.txHash && (
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { TxError } from '@vegaprotocol/web3';
|
||||
import { useT } from './use-t';
|
||||
|
||||
export const getFaucetError = (error: TxError | null, symbol: string) => {
|
||||
export const useGetFaucetError = (error: TxError | null, symbol?: string) => {
|
||||
const t = useT();
|
||||
const reasonMap: {
|
||||
[reason: string]: string;
|
||||
} = {
|
||||
'faucet not enabled': t(
|
||||
'The %s faucet is not available at this time',
|
||||
symbol
|
||||
'The {{symbol}} faucet is not available at this time',
|
||||
{ symbol: symbol || '' }
|
||||
),
|
||||
'must wait faucetCallLimit between faucet calls': t(
|
||||
'You have exceeded the maximum number of faucet attempts allowed'
|
||||
@@ -20,5 +21,5 @@ export const getFaucetError = (error: TxError | null, symbol: string) => {
|
||||
// to a non generic error message
|
||||
return error && 'reason' in error && reasonMap[error.reason]
|
||||
? reasonMap[error.reason]
|
||||
: t('Faucet of %s failed', symbol);
|
||||
: t('Faucet of {{symbol}} failed', { symbol: symbol || '' });
|
||||
};
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
export const useT = () => useTranslation('deposits').t;
|
||||
@@ -0,0 +1,15 @@
|
||||
export const useTranslation = () => ({
|
||||
t: (label: string, replacements?: Record<string, string>) => {
|
||||
const replace =
|
||||
replacements?.replace && typeof replacements === 'object'
|
||||
? replacements?.replace
|
||||
: replacements;
|
||||
let translatedLabel = replacements?.defaultValue || label;
|
||||
if (typeof replace === 'object' && replace !== null) {
|
||||
Object.keys(replace).forEach((key) => {
|
||||
translatedLabel = translatedLabel.replace(`{{${key}}}`, replace[key]);
|
||||
});
|
||||
}
|
||||
return translatedLabel;
|
||||
},
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { ETHERSCAN_ADDRESS, ETHERSCAN_TX, useEtherscanLink } from '../hooks';
|
||||
import { useT } from '../use-t';
|
||||
|
||||
export const EtherscanLink = ({
|
||||
address,
|
||||
@@ -12,6 +12,7 @@ export const EtherscanLink = ({
|
||||
address?: string;
|
||||
tx?: string;
|
||||
} & ComponentProps<typeof ExternalLink>) => {
|
||||
const t = useT();
|
||||
const etherscanLink = useEtherscanLink();
|
||||
let href = '';
|
||||
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useEnvironment } from '../../hooks/use-environment';
|
||||
import {
|
||||
NetworkSwitcher,
|
||||
envNameMapping,
|
||||
envTriggerMapping,
|
||||
envDescriptionMapping,
|
||||
} from './';
|
||||
import { NetworkSwitcher } from './';
|
||||
import { Networks } from '../../';
|
||||
|
||||
jest.mock('../../hooks/use-environment');
|
||||
@@ -33,10 +27,10 @@ describe('Network switcher', () => {
|
||||
|
||||
it.each`
|
||||
network | label
|
||||
${Networks.CUSTOM} | ${envTriggerMapping[Networks.CUSTOM]}
|
||||
${Networks.DEVNET} | ${envTriggerMapping[Networks.DEVNET]}
|
||||
${Networks.TESTNET} | ${envTriggerMapping[Networks.TESTNET]}
|
||||
${Networks.MAINNET} | ${envTriggerMapping[Networks.MAINNET]}
|
||||
${Networks.CUSTOM} | ${'Custom'}
|
||||
${Networks.DEVNET} | ${'Devnet'}
|
||||
${Networks.TESTNET} | ${'Fairground'}
|
||||
${Networks.MAINNET} | ${'Mainnet'}
|
||||
`(
|
||||
'displays the correct selection label for $network',
|
||||
({ network, label }) => {
|
||||
@@ -69,13 +63,13 @@ describe('Network switcher', () => {
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
let links = screen.getAllByRole('link');
|
||||
|
||||
expect(links[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
|
||||
expect(links[1]).toHaveTextContent(envNameMapping[Networks.TESTNET]);
|
||||
expect(links[0]).not.toHaveTextContent(t('current'));
|
||||
expect(links[1]).not.toHaveTextContent(t('current'));
|
||||
expect(links[0]).not.toHaveTextContent(t('not available'));
|
||||
expect(links[1]).not.toHaveTextContent(t('not available'));
|
||||
expect(links[2]).toHaveTextContent(t('Propose a network parameter change'));
|
||||
expect(links[0]).toHaveTextContent('Mainnet');
|
||||
expect(links[1]).toHaveTextContent('Fairground testnet');
|
||||
expect(links[0]).not.toHaveTextContent('current');
|
||||
expect(links[1]).not.toHaveTextContent('current');
|
||||
expect(links[0]).not.toHaveTextContent('not available');
|
||||
expect(links[1]).not.toHaveTextContent('not available');
|
||||
expect(links[2]).toHaveTextContent('Propose a network parameter change');
|
||||
|
||||
const menuitems = screen.getAllByRole('menuitem');
|
||||
|
||||
@@ -110,8 +104,8 @@ describe('Network switcher', () => {
|
||||
|
||||
const links = screen.getAllByRole('link');
|
||||
|
||||
expect(links[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
|
||||
expect(links[0]).toHaveTextContent(t('current'));
|
||||
expect(links[0]).toHaveTextContent('Mainnet');
|
||||
expect(links[0]).toHaveTextContent('current');
|
||||
});
|
||||
|
||||
it('displays the correct selected network on the default dropdown view when it does not have an associated url', async () => {
|
||||
@@ -131,8 +125,8 @@ describe('Network switcher', () => {
|
||||
|
||||
const links = screen.getAllByRole('link');
|
||||
|
||||
expect(links[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
|
||||
expect(links[0]).toHaveTextContent(t('current'));
|
||||
expect(links[0]).toHaveTextContent('Mainnet');
|
||||
expect(links[0]).toHaveTextContent('current');
|
||||
});
|
||||
|
||||
it('displays the correct state for a network without url on the default dropdown view', async () => {
|
||||
@@ -151,13 +145,17 @@ describe('Network switcher', () => {
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
const links = screen.getAllByRole('link');
|
||||
|
||||
expect(links[0]).toHaveTextContent(envNameMapping[Networks.MAINNET]);
|
||||
expect(links[0]).toHaveTextContent(t('not available'));
|
||||
expect(links[0]).toHaveTextContent('Mainnet');
|
||||
expect(links[0]).toHaveTextContent('not available');
|
||||
});
|
||||
|
||||
it.each([Networks.MAINNET, Networks.TESTNET, Networks.DEVNET])(
|
||||
it.each([
|
||||
{ network: Networks.MAINNET, name: 'Mainnet' },
|
||||
{ network: Networks.TESTNET, name: 'Fairground testnet' },
|
||||
{ network: Networks.DEVNET, name: 'Devnet' },
|
||||
])(
|
||||
'displays the advanced view in the correct state',
|
||||
async (network) => {
|
||||
async ({ network, name }) => {
|
||||
const VEGA_NETWORKS: Record<Networks, string | undefined> = {
|
||||
[Networks.CUSTOM]: undefined,
|
||||
[Networks.MAINNET]: 'https://main.net',
|
||||
@@ -178,25 +176,14 @@ describe('Network switcher', () => {
|
||||
await userEvent.click(screen.getByTestId('network-switcher'));
|
||||
|
||||
expect(
|
||||
await screen.findByRole('menuitem', { name: t('Advanced') })
|
||||
await screen.findByRole('menuitem', { name: 'Advanced' })
|
||||
).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByRole('menuitem', { name: t('Advanced') })
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.findByText(envDescriptionMapping[network])
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('link', {
|
||||
name: new RegExp(`^${envNameMapping[network]}`),
|
||||
})
|
||||
).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByRole('menuitem', { name: 'Advanced' }));
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByRole('link', {
|
||||
name: new RegExp(`^${envNameMapping[network]}`),
|
||||
name: new RegExp(`^${name}`),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -224,15 +211,13 @@ describe('Network switcher', () => {
|
||||
render(<NetworkSwitcher />);
|
||||
|
||||
await userEvent.click(screen.getByRole('button'));
|
||||
await userEvent.click(
|
||||
screen.getByRole('menuitem', { name: t('Advanced') })
|
||||
);
|
||||
await userEvent.click(screen.getByRole('menuitem', { name: 'Advanced' }));
|
||||
|
||||
const label = screen.getByText(`(${t('current')})`);
|
||||
const label = screen.getByText('(current)');
|
||||
|
||||
expect(label).toBeInTheDocument();
|
||||
expect(label.parentNode?.parentNode?.firstElementChild).toHaveTextContent(
|
||||
envNameMapping[selectedNetwork]
|
||||
'Devnet'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -262,7 +247,7 @@ describe('Network switcher', () => {
|
||||
|
||||
expect(label).toBeInTheDocument();
|
||||
expect(label.parentNode?.parentNode?.firstElementChild).toHaveTextContent(
|
||||
envNameMapping[Networks.MAINNET]
|
||||
'Mainnet'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -13,32 +12,44 @@ import { useEnvironment } from '../../hooks/use-environment';
|
||||
import { Networks } from '../../types';
|
||||
import { DApp, TOKEN_NEW_NETWORK_PARAM_PROPOSAL, useLinks } from '../../hooks';
|
||||
import classNames from 'classnames';
|
||||
import { useT } from '../../use-t';
|
||||
|
||||
export const envNameMapping: Record<Networks, string> = {
|
||||
[Networks.VALIDATOR_TESTNET]: t('VALIDATOR_TESTNET'),
|
||||
[Networks.MAINNET_MIRROR]: t('Mainnet-mirror'),
|
||||
[Networks.CUSTOM]: t('Custom'),
|
||||
[Networks.DEVNET]: t('Devnet'),
|
||||
[Networks.STAGNET1]: t('Stagnet'),
|
||||
[Networks.TESTNET]: t('Fairground testnet'),
|
||||
[Networks.MAINNET]: t('Mainnet'),
|
||||
export const useEnvNameMapping: () => Record<Networks, string> = () => {
|
||||
const t = useT();
|
||||
return {
|
||||
[Networks.VALIDATOR_TESTNET]: t('VALIDATOR_TESTNET', {
|
||||
contextSeparator: '|',
|
||||
}),
|
||||
[Networks.MAINNET_MIRROR]: t('Mainnet-mirror'),
|
||||
[Networks.CUSTOM]: t('Custom'),
|
||||
[Networks.DEVNET]: t('Devnet'),
|
||||
[Networks.STAGNET1]: t('Stagnet'),
|
||||
[Networks.TESTNET]: t('Fairground testnet'),
|
||||
[Networks.MAINNET]: t('Mainnet'),
|
||||
};
|
||||
};
|
||||
|
||||
export const envTriggerMapping: Record<Networks, string> = {
|
||||
...envNameMapping,
|
||||
[Networks.TESTNET]: t('Fairground'),
|
||||
export const useEnvTriggerMapping: () => Record<Networks, string> = () => {
|
||||
const t = useT();
|
||||
return {
|
||||
...useEnvNameMapping(),
|
||||
[Networks.TESTNET]: t('Fairground'),
|
||||
};
|
||||
};
|
||||
|
||||
export const envDescriptionMapping: Record<Networks, string> = {
|
||||
[Networks.CUSTOM]: '',
|
||||
[Networks.VALIDATOR_TESTNET]: t('The validator deployed testnet'),
|
||||
[Networks.MAINNET_MIRROR]: t('The mainnet-mirror network'),
|
||||
[Networks.DEVNET]: t('The latest Vega code auto-deployed'),
|
||||
[Networks.STAGNET1]: t('A release candidate for the staging environment'),
|
||||
[Networks.TESTNET]: t(
|
||||
'Public testnet run by the Vega team, often used for incentives'
|
||||
),
|
||||
[Networks.MAINNET]: t('The vega mainnet'),
|
||||
export const useEnvDescriptionMapping: () => Record<Networks, string> = () => {
|
||||
const t = useT();
|
||||
return {
|
||||
[Networks.CUSTOM]: '',
|
||||
[Networks.VALIDATOR_TESTNET]: t('The validator deployed testnet'),
|
||||
[Networks.MAINNET_MIRROR]: t('The mainnet-mirror network'),
|
||||
[Networks.DEVNET]: t('The latest Vega code auto-deployed'),
|
||||
[Networks.STAGNET1]: t('A release candidate for the staging environment'),
|
||||
[Networks.TESTNET]: t(
|
||||
'Public testnet run by the Vega team, often used for incentives'
|
||||
),
|
||||
[Networks.MAINNET]: t('The vega mainnet'),
|
||||
};
|
||||
};
|
||||
|
||||
const standardNetworkKeys = [Networks.MAINNET, Networks.TESTNET];
|
||||
@@ -53,10 +64,11 @@ type NetworkLabelProps = {
|
||||
isAvailable: boolean;
|
||||
};
|
||||
|
||||
const getLabelText = ({
|
||||
const useLabelText = ({
|
||||
isCurrent = false,
|
||||
isAvailable = false,
|
||||
}: NetworkLabelProps) => {
|
||||
const t = useT();
|
||||
if (isCurrent) {
|
||||
return ` (${t('current')})`;
|
||||
}
|
||||
@@ -71,7 +83,7 @@ const NetworkLabel = ({
|
||||
isAvailable = false,
|
||||
}: NetworkLabelProps) => (
|
||||
<span className="text-vega-dark-300 dark:text-vega-light-300">
|
||||
{getLabelText({ isCurrent, isAvailable })}
|
||||
{useLabelText({ isCurrent, isAvailable })}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -86,6 +98,7 @@ export const NetworkSwitcher = ({
|
||||
currentNetwork,
|
||||
className,
|
||||
}: NetworkSwitcherProps) => {
|
||||
const t = useT();
|
||||
const { VEGA_ENV, VEGA_NETWORKS } = useEnvironment();
|
||||
const tokenLink = useLinks(DApp.Governance);
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
@@ -102,6 +115,9 @@ export const NetworkSwitcher = ({
|
||||
);
|
||||
|
||||
const current = currentNetwork || VEGA_ENV;
|
||||
const envTriggerMapping = useEnvTriggerMapping();
|
||||
const envNameMapping = useEnvNameMapping();
|
||||
const envDescriptionMapping = useEnvDescriptionMapping();
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Button, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { useNodeSwitcherStore } from '../../hooks/use-node-switcher-store';
|
||||
import { useT } from '../../use-t';
|
||||
|
||||
export const NodeFailure = ({
|
||||
title,
|
||||
@@ -10,6 +10,7 @@ export const NodeFailure = ({
|
||||
error?: string | null;
|
||||
}) => {
|
||||
const setNodeSwitcher = useNodeSwitcherStore((store) => store.setDialogOpen);
|
||||
const t = useT();
|
||||
return (
|
||||
<Splash>
|
||||
<div className="text-center">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useT } from '../../use-t';
|
||||
|
||||
type LayoutCellProps = {
|
||||
label?: string;
|
||||
@@ -17,6 +17,7 @@ export const LayoutCell = ({
|
||||
children,
|
||||
dataTestId,
|
||||
}: LayoutCellProps) => {
|
||||
const t = useT();
|
||||
const classes = [
|
||||
'lg:text-right flex justify-between lg:block',
|
||||
'my-2 lg:my-0',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { isValidUrl } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
Button,
|
||||
ButtonLink,
|
||||
@@ -15,8 +14,10 @@ import { LayoutCell } from './layout-cell';
|
||||
import { LayoutRow } from './layout-row';
|
||||
import { ApolloWrapper } from './apollo-wrapper';
|
||||
import { RowData } from './row-data';
|
||||
import { useT } from '../../use-t';
|
||||
|
||||
export const NodeSwitcher = ({ closeDialog }: { closeDialog: () => void }) => {
|
||||
const t = useT();
|
||||
const { nodes, setUrl, status, VEGA_ENV, VEGA_URL } = useEnvironment(
|
||||
(store) => ({
|
||||
status: store.status,
|
||||
@@ -73,7 +74,8 @@ export const NodeSwitcher = ({ closeDialog }: { closeDialog: () => void }) => {
|
||||
<div>
|
||||
<p className="mb-2 text-sm text-center">
|
||||
{t(
|
||||
`This app will only work on ${VEGA_ENV}. Select a node to connect to.`
|
||||
'This app will only work on {{VEGA_ENV}}. Select a node to connect to.',
|
||||
{ VEGA_ENV }
|
||||
)}
|
||||
</p>
|
||||
<TradingRadioGroup
|
||||
@@ -153,6 +155,7 @@ const CustomRowWrapper = ({
|
||||
nodeRadio,
|
||||
onBlockHeight,
|
||||
}: CustomRowWrapperProps) => {
|
||||
const t = useT();
|
||||
const [displayCustom, setDisplayCustom] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const showInput = nodeRadio === CUSTOM_NODE_KEY || nodes.length <= 0;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user