Compare commits

...
Author SHA1 Message Date
Matthew Russell 2ae13a09b6 chore: add missing prop 2023-11-14 21:56:48 -08:00
Matthew Russell 68712d6ef8 chore: format 2023-11-14 21:55:36 -08:00
Matthew Russell 14936f2962 feat: add not connected state for multipliers 2023-11-14 21:55:36 -08:00
Matthew Russell a47d865829 feat: add minimal prop to button to enable a selected state 2023-11-14 21:55:36 -08:00
Matthew Russell d40b6bacee chore: add specs for rewards page 2023-11-14 21:55:36 -08:00
Matthew Russell eb296a185d feat: show loading state, allow 0 to make typing experience better 2023-11-14 21:55:36 -08:00
Matthew Russell 7e2da69a63 fix: formatting 2023-11-14 21:55:36 -08:00
Matthew Russell 7057439432 test: add tests for reward multiplier card 2023-11-14 21:55:36 -08:00
Matthew Russell 8175ed41b3 fix: use correct multiplier for vesting 2023-11-14 21:55:36 -08:00
Matthew Russell e8bc651f0e chore: remove duplicate fragment 2023-11-14 21:55:36 -08:00
Matthew Russell 2cfe5e8325 chore: remove duplicate export 2023-11-14 21:55:36 -08:00
Matthew Russell 9455166f7a chore: update vesting card tests 2023-11-14 21:55:36 -08:00
Matthew Russell 38a813249a chore: add missing props for tests 2023-11-14 21:55:36 -08:00
Matthew Russell 1adab570d4 feat: apply multiplier to vesting rate 2023-11-14 21:55:36 -08:00
Matthew Russell 2cd7884867 feat: wire up vesting information 2023-11-14 21:55:36 -08:00
Matthew Russell 7939073826 fix: prevent entry of epoch less than 1 2023-11-14 21:55:36 -08:00
Matthew Russell a314581ed7 test: add test for pagination 2023-11-14 21:55:36 -08:00
Matthew Russell c437e706fe test: add test for reward history table 2023-11-14 21:55:36 -08:00
Matthew Russell f231fd0676 fix: formatting and incorrect test id prop 2023-11-14 21:55:36 -08:00
Matthew Russell 1acff681d3 chore: update tests 2023-11-14 21:55:36 -08:00
Matthew Russell 064ae55c1f chore: don't show redeem button unless rewards are available 2023-11-14 21:55:36 -08:00
Matthew Russell 654ca50a22 feat: updates for disconnected state 2023-11-14 21:55:36 -08:00
Matthew Russell 5b24d884c0 feat: pagination improvements 2023-11-14 21:55:36 -08:00
Matthew Russell d40be3ceab feat: add epoch pagination 2023-11-14 21:55:36 -08:00
Matthew Russell 15e535dc7b feat: filter rewards by account 2023-11-14 21:55:36 -08:00
Matthew Russell 285bf6b523 chore: add file for rewards history table 2023-11-14 21:55:36 -08:00
Matthew Russell 451887f3bd feat: dont skip query if not connected 2023-11-14 21:55:36 -08:00
Matthew Russell 68333d8f4a feat: add toggle for switching between users rewards and all rewarsd 2023-11-14 21:55:36 -08:00
Matthew Russell 5449fa44dd chore: move rewards to own page 2023-11-14 21:55:36 -08:00
Matthew Russell c92007f26d chore: update tests to align with formatting 2023-11-14 21:55:36 -08:00
Matthew Russell 0b830626a7 feat: remove trailing zeroes 2023-11-14 21:55:36 -08:00
Matthew Russell 62b81fd94d feat: add highlight option to card component 2023-11-14 21:55:36 -08:00
Matthew Russell a0235d7a8a feat: update rewards pot to handle different assets 2023-11-14 21:55:36 -08:00
Matthew Russell 00db11af9a chore: use generated type for vesting balances 2023-11-14 21:55:36 -08:00
Matthew Russell 16538f790b feat: use vesting balances api for vega reward pot 2023-11-14 21:55:36 -08:00
Matthew Russell b0c8b41174 feat: add rewards history table for all rewards 2023-11-14 21:55:36 -08:00
Matthew Russell c0a5418815 feat: add lock icon 2023-11-14 21:55:36 -08:00
Matthew Russell 389772f4e1 feat: update reward pot card 2023-11-14 21:55:36 -08:00
Matthew Russell 6ea9258574 feat: add calculations for rewards pot card 2023-11-14 21:55:36 -08:00
Matthew Russell 0f4ba64a54 chore: replace FeeCard with generic Card component 2023-11-14 21:55:35 -08:00
Matthew Russell bcd576a6a5 chore: add basic reward container with layout 2023-11-14 21:55:35 -08:00
Bartłomiej GłowniaandMatthew Russell a070504d2e feat(trading): i18n (#5126)
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
2023-11-14 21:10:06 -08:00
Matthew Russell f891caf08b chore(trading,governance,explorer): disable prettier tailwind plugin (#5258) 2023-11-14 18:09:29 -08:00
143 changed files with 5098 additions and 2092 deletions
+4 -1
View File
@@ -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 (
+10 -9
View File
@@ -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>;
};
+6 -3
View File
@@ -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
View File
@@ -0,0 +1 @@
../../../../libs/i18n/src/locales
+26 -14
View File
@@ -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,
},
+4 -10
View File
@@ -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;
+14
View File
@@ -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>
);
};
+102
View File
@@ -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)} />
);
};
+1
View File
@@ -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');
+23 -18
View File
@@ -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);
};
+81
View File
@@ -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;
+2
View File
@@ -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 -2
View File
@@ -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];
+11
View File
@@ -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: (
+1
View File
@@ -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,
});
};
+7 -5
View File
@@ -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,
+4 -2
View File
@@ -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);
+4 -3
View File
@@ -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
+20 -9
View File
@@ -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"
+16 -10
View File
@@ -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}
+12 -6
View File
@@ -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 (
<>
+3
View File
@@ -0,0 +1,3 @@
import { useTranslation } from 'react-i18next';
export const ns = 'accounts';
export const useT = () => useTranslation(ns).t;
+15
View File
@@ -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;
+1 -1
View File
@@ -10,4 +10,4 @@
]
],
"plugins": []
}
}
+5 -4
View File
@@ -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(
+202 -177
View File
@@ -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),
+5 -3
View File
@@ -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 (
+2 -5
View File
@@ -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[]> = {
+1
View File
@@ -7,3 +7,4 @@ export * from './asset-option';
export * from './assets-data-provider';
export * from './constants';
export * from './use-balances-store';
export * from './utils';
+3
View File
@@ -0,0 +1,3 @@
import { useTranslation } from 'react-i18next';
export const useT = () => useTranslation('assets').t;
+15
View File
@@ -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');
});
});
+5
View File
@@ -0,0 +1,5 @@
import { toBigNum } from '@vegaprotocol/utils';
export const getQuantumValue = (value: string, quantum: string) => {
return toBigNum(value, 0).dividedBy(toBigNum(quantum, 0));
};
+3 -2
View File
@@ -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,
+6 -2
View File
@@ -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>
}
+2
View File
@@ -0,0 +1,2 @@
import { useTranslation } from 'react-i18next';
export const useT = () => useTranslation('candles-chart').t;
+15
View File
@@ -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;
+2 -1
View File
@@ -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();
+10 -7
View File
@@ -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 (
+2
View File
@@ -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">
+24 -67
View File
@@ -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',
+3
View File
@@ -0,0 +1,3 @@
import { useTranslation } from 'react-i18next';
export const ns = 'deal-ticket';
export const useT = () => useTranslation(ns).t;
-1
View File
@@ -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;
+14
View File
@@ -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;
},
});
+27 -17
View File
@@ -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>}
</>
+24 -17
View File
@@ -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;
+17 -12
View File
@@ -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>
}
+18 -12
View File
@@ -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 && (
+6 -5
View File
@@ -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 || '' });
};
+2
View File
@@ -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