Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19850e95df | ||
|
|
374890dc08 | ||
|
|
7f95ba577e | ||
|
|
2f7e53e282 | ||
|
|
568dcd5305 | ||
|
|
9605b0e7fa | ||
|
|
0776332ba7 | ||
|
|
f780013846 | ||
|
|
73f37c2477 | ||
|
|
607ad06971 | ||
|
|
7aee2a3a7b | ||
|
|
1fdf519780 |
@@ -1,2 +1,4 @@
|
||||
* @vegaprotocol/frontend
|
||||
apps/ @vegaprotocol/frontend-qa
|
||||
libs/ @vegaprotocol/frontend-qa
|
||||
*.graphql @vegaprotocol/core
|
||||
|
||||
@@ -8,7 +8,7 @@ jest.mock('@vegaprotocol/accounts', () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../../components/welcome-dialog/get-started.ts', () => ({
|
||||
jest.mock('../../components/welcome-dialog/get-started', () => ({
|
||||
GetStarted: () => <div>GetStarted</div>,
|
||||
}));
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ jest.mock('../../components/withdraw-container', () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../../components/welcome-dialog/get-started.ts', () => ({
|
||||
jest.mock('../../components/welcome-dialog/get-started', () => ({
|
||||
GetStarted: () => <div>GetStarted</div>,
|
||||
}));
|
||||
|
||||
|
||||
@@ -36,6 +36,22 @@ query Fees(
|
||||
}
|
||||
}
|
||||
}
|
||||
referrer: referralSets(referrer: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
referrer
|
||||
}
|
||||
}
|
||||
}
|
||||
referee: referralSets(referee: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
referrer
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetReferees(referee: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
|
||||
+17
-1
@@ -15,7 +15,7 @@ export type FeesQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type FeesQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string }, volumeDiscountStats: { __typename?: 'VolumeDiscountStatsConnection', edges: Array<{ __typename?: 'VolumeDiscountStatsEdge', node: { __typename?: 'VolumeDiscountStats', atEpoch: number, discountFactor: string, runningVolume: string } } | null> }, referralSetReferees: { __typename?: 'ReferralSetRefereeConnection', edges: Array<{ __typename?: 'ReferralSetRefereeEdge', node: { __typename?: 'ReferralSetReferee', atEpoch: number } } | null> }, referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, discountFactor: string, referralSetRunningNotionalTakerVolume: string } } | null> } };
|
||||
export type FeesQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string }, volumeDiscountStats: { __typename?: 'VolumeDiscountStatsConnection', edges: Array<{ __typename?: 'VolumeDiscountStatsEdge', node: { __typename?: 'VolumeDiscountStats', atEpoch: number, discountFactor: string, runningVolume: string } } | null> }, referrer: { __typename?: 'ReferralSetConnection', edges: Array<{ __typename?: 'ReferralSetEdge', node: { __typename?: 'ReferralSet', id: string, referrer: string } } | null> }, referee: { __typename?: 'ReferralSetConnection', edges: Array<{ __typename?: 'ReferralSetEdge', node: { __typename?: 'ReferralSet', id: string, referrer: string } } | null> }, referralSetReferees: { __typename?: 'ReferralSetRefereeConnection', edges: Array<{ __typename?: 'ReferralSetRefereeEdge', node: { __typename?: 'ReferralSetReferee', atEpoch: number } } | null> }, referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, discountFactor: string, referralSetRunningNotionalTakerVolume: string } } | null> } };
|
||||
|
||||
|
||||
export const DiscountProgramsDocument = gql`
|
||||
@@ -81,6 +81,22 @@ export const FeesDocument = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
referrer: referralSets(referrer: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
referrer
|
||||
}
|
||||
}
|
||||
}
|
||||
referee: referralSets(referee: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
referrer
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetReferees(referee: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
|
||||
@@ -17,6 +17,14 @@ import { useReferralStats } from './use-referral-stats';
|
||||
import { formatPercentage, getAdjustedFee } from './utils';
|
||||
import { Table, Td, Th, THead, Tr } from './table';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { Links } from '../../lib/links';
|
||||
import { Link } from 'react-router-dom';
|
||||
import {
|
||||
Tooltip,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
truncateMiddle,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const FeesContainer = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
@@ -56,16 +64,25 @@ export const FeesContainer = () => {
|
||||
referralTierIndex,
|
||||
referralTiers,
|
||||
epochsInSet,
|
||||
code,
|
||||
isReferrer,
|
||||
} = useReferralStats(
|
||||
feesData?.referralSetStats,
|
||||
feesData?.referralSetReferees,
|
||||
programData?.currentReferralProgram,
|
||||
feesData?.epoch
|
||||
feesData?.epoch,
|
||||
feesData?.referrer,
|
||||
feesData?.referee
|
||||
);
|
||||
|
||||
const loading = paramsLoading || feesLoading || programLoading;
|
||||
const isConnected = Boolean(pubKey);
|
||||
|
||||
const isReferralProgramRunning = Boolean(programData?.currentReferralProgram);
|
||||
const isVolumeDiscountProgramRunning = Boolean(
|
||||
programData?.currentVolumeDiscountProgram
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid auto-rows-min grid-cols-4 gap-3">
|
||||
{isConnected && (
|
||||
@@ -90,6 +107,8 @@ export const FeesContainer = () => {
|
||||
<TotalDiscount
|
||||
referralDiscount={referralDiscount}
|
||||
volumeDiscount={volumeDiscount}
|
||||
isReferralProgramRunning={isReferralProgramRunning}
|
||||
isVolumeDiscountProgramRunning={isVolumeDiscountProgramRunning}
|
||||
/>
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
@@ -97,23 +116,37 @@ export const FeesContainer = () => {
|
||||
className="sm:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<CurrentVolume
|
||||
tiers={volumeTiers}
|
||||
tierIndex={volumeTierIndex}
|
||||
windowLengthVolume={volumeInWindow}
|
||||
windowLength={volumeDiscountWindowLength}
|
||||
/>
|
||||
{isVolumeDiscountProgramRunning ? (
|
||||
<CurrentVolume
|
||||
tiers={volumeTiers}
|
||||
tierIndex={volumeTierIndex}
|
||||
windowLengthVolume={volumeInWindow}
|
||||
windowLength={volumeDiscountWindowLength}
|
||||
/>
|
||||
) : (
|
||||
<p className="pt-3 text-sm text-muted">
|
||||
{t('No volume discount program active')}
|
||||
</p>
|
||||
)}
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
title={t('Referral benefits')}
|
||||
className="sm:col-span-2"
|
||||
loading={loading}
|
||||
>
|
||||
<ReferralBenefits
|
||||
setRunningNotionalTakerVolume={referralVolumeInWindow}
|
||||
epochsInSet={epochsInSet}
|
||||
epochs={referralDiscountWindowLength}
|
||||
/>
|
||||
{isReferrer ? (
|
||||
<ReferrerInfo code={code} />
|
||||
) : isReferralProgramRunning ? (
|
||||
<ReferralBenefits
|
||||
setRunningNotionalTakerVolume={referralVolumeInWindow}
|
||||
epochsInSet={epochsInSet}
|
||||
epochs={referralDiscountWindowLength}
|
||||
/>
|
||||
) : (
|
||||
<p className="pt-3 text-sm text-muted">
|
||||
{t('No referral program active')}
|
||||
</p>
|
||||
)}
|
||||
</FeeCard>
|
||||
</>
|
||||
)}
|
||||
@@ -142,7 +175,7 @@ export const FeesContainer = () => {
|
||||
/>
|
||||
</FeeCard>
|
||||
<FeeCard
|
||||
title={t('Liquidity fees')}
|
||||
title={t('Fees by market')}
|
||||
className="lg:col-span-full"
|
||||
loading={marketsLoading}
|
||||
>
|
||||
@@ -325,26 +358,64 @@ const ReferralBenefits = ({
|
||||
const TotalDiscount = ({
|
||||
referralDiscount,
|
||||
volumeDiscount,
|
||||
isReferralProgramRunning,
|
||||
isVolumeDiscountProgramRunning,
|
||||
}: {
|
||||
referralDiscount: number;
|
||||
volumeDiscount: number;
|
||||
isReferralProgramRunning: boolean;
|
||||
isVolumeDiscountProgramRunning: boolean;
|
||||
}) => {
|
||||
const totalDiscount = 1 - (1 - volumeDiscount) * (1 - referralDiscount);
|
||||
const totalDiscountDescription = t(
|
||||
'The total discount is calculated according to the following formula: '
|
||||
);
|
||||
const formula = (
|
||||
<span className="italic">
|
||||
1 - (1 - d<sub>volume</sub>) ⋇ (1 - d<sub>referral</sub>)
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Stat
|
||||
value={formatPercentage(referralDiscount + volumeDiscount) + '%'}
|
||||
description={
|
||||
<>
|
||||
{totalDiscountDescription}
|
||||
{formula}
|
||||
</>
|
||||
}
|
||||
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)}%</td>
|
||||
<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>
|
||||
@@ -491,3 +562,31 @@ const YourTier = () => {
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const ReferrerInfo = ({ code }: { code?: string }) => (
|
||||
<div className="pt-3 text-sm text-vega-clight-200 dark:vega-cdark-200">
|
||||
<p className="mb-1">
|
||||
{t('Connected key is owner of the referral set')}
|
||||
{code && (
|
||||
<>
|
||||
{' '}
|
||||
<span className="text-transparent bg-rainbow bg-clip-text">
|
||||
{truncateMiddle(code)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{'. '}
|
||||
{t('As owner, it is eligible for commission not fee discounts.')}
|
||||
</p>
|
||||
<p>
|
||||
{t('See')}{' '}
|
||||
<Link
|
||||
className="underline text-black dark:text-white"
|
||||
to={Links.REFERRALS()}
|
||||
>
|
||||
{t('Referrals')}
|
||||
</Link>{' '}
|
||||
{t('for more information.')}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
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">
|
||||
<span
|
||||
className={classNames('inline-block text-3xl leading-none', {
|
||||
'text-transparent bg-rainbow bg-clip-text': highlight,
|
||||
})}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
{description ? <Tooltip description={description}>{val}</Tooltip> : val}
|
||||
{text && (
|
||||
<small className="block mt-0.5 text-xs text-muted">{text}</small>
|
||||
)}
|
||||
|
||||
@@ -73,6 +73,8 @@ describe('useReferralStats', () => {
|
||||
referralTierIndex: -1,
|
||||
referralTiers: [],
|
||||
epochsInSet: 0,
|
||||
code: undefined,
|
||||
isReferrer: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -93,6 +95,8 @@ describe('useReferralStats', () => {
|
||||
referralTierIndex: 1,
|
||||
referralTiers: program.benefitTiers,
|
||||
epochsInSet: Number(epoch.id) - set.atEpoch,
|
||||
code: undefined,
|
||||
isReferrer: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,12 +2,15 @@ import compact from 'lodash/compact';
|
||||
import maxBy from 'lodash/maxBy';
|
||||
import { getReferralBenefitTier } from './utils';
|
||||
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
|
||||
import { first } from 'lodash';
|
||||
|
||||
export const useReferralStats = (
|
||||
setStats?: FeesQuery['referralSetStats'],
|
||||
setReferees?: FeesQuery['referralSetReferees'],
|
||||
program?: DiscountProgramsQuery['currentReferralProgram'],
|
||||
epoch?: FeesQuery['epoch']
|
||||
epoch?: FeesQuery['epoch'],
|
||||
setIfReferrer?: FeesQuery['referrer'],
|
||||
setIfReferee?: FeesQuery['referee']
|
||||
) => {
|
||||
const referralTiers = program?.benefitTiers || [];
|
||||
|
||||
@@ -18,9 +21,18 @@ export const useReferralStats = (
|
||||
referralTierIndex: -1,
|
||||
referralTiers,
|
||||
epochsInSet: 0,
|
||||
code: undefined,
|
||||
isReferrer: false,
|
||||
};
|
||||
}
|
||||
|
||||
const setIfReferrerData = first(
|
||||
compact(setIfReferrer?.edges).map((e) => e.node)
|
||||
);
|
||||
const setIfRefereeData = first(
|
||||
compact(setIfReferee?.edges).map((e) => e.node)
|
||||
);
|
||||
|
||||
const referralSetsStats = compact(setStats.edges).map((e) => e.node);
|
||||
const referralSets = compact(setReferees.edges).map((e) => e.node);
|
||||
|
||||
@@ -48,5 +60,7 @@ export const useReferralStats = (
|
||||
referralTierIndex,
|
||||
referralTiers,
|
||||
epochsInSet,
|
||||
code: (setIfReferrerData || setIfRefereeData)?.id,
|
||||
isReferrer: Boolean(setIfReferrerData),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('getAdjustedFee', () => {
|
||||
new BigNumber(referralDiscount),
|
||||
];
|
||||
|
||||
// 1 - 0.5 - 0.5
|
||||
// 1 - 0.5 = 0.5
|
||||
const v = new BigNumber(1).minus(new BigNumber(volumeDiscount));
|
||||
|
||||
// 1 - 0.5 = 0.5
|
||||
@@ -34,13 +34,15 @@ describe('getAdjustedFee', () => {
|
||||
// 0.1 + 0.1 + 0.1 = 0.3
|
||||
const totalFees = fees.reduce((sum, x) => sum.plus(x), new BigNumber(0));
|
||||
|
||||
// 0.3 * 0.75 = 0.225
|
||||
const expected = new BigNumber(totalFees).times(factor).toNumber();
|
||||
// (1 - 0.3) * 0.75 = 0.525
|
||||
const expected = new BigNumber(totalFees)
|
||||
.times(new BigNumber(1).minus(factor))
|
||||
.toNumber();
|
||||
|
||||
expect(getAdjustedFee(fees, discounts)).toBe(expected);
|
||||
});
|
||||
|
||||
it('combines discount factors multiplicativly', () => {
|
||||
it('combines discount factors multiplicatively', () => {
|
||||
const volumeDiscount = 0.4;
|
||||
const referralDiscount = 0.1;
|
||||
|
||||
@@ -67,7 +69,9 @@ describe('getAdjustedFee', () => {
|
||||
// summed fees
|
||||
const totalFees = fees.reduce((sum, x) => sum.plus(x), new BigNumber(0));
|
||||
|
||||
const expected = new BigNumber(totalFees).times(factor).toNumber();
|
||||
const expected = new BigNumber(totalFees)
|
||||
.times(new BigNumber(1).minus(factor))
|
||||
.toNumber();
|
||||
|
||||
expect(getAdjustedFee(fees, discounts)).toBe(expected);
|
||||
});
|
||||
|
||||
@@ -12,7 +12,9 @@ export const formatPercentage = (num: number) => {
|
||||
const pct = new BigNumber(num).times(100);
|
||||
const dps = pct.decimalPlaces();
|
||||
const formatter = new Intl.NumberFormat(getUserLocale(), {
|
||||
minimumFractionDigits: dps || 0,
|
||||
// set to 0 in order to remove the "trailing zeroes" for numbers such as:
|
||||
// 0.123456789 -non-zero-min-> 12.3456800% -zero-min-> 12.34568%
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: dps || 0,
|
||||
});
|
||||
return formatter.format(parseFloat(pct.toFixed(5)));
|
||||
@@ -101,5 +103,7 @@ export const getAdjustedFee = (fees: BigNumber[], discounts: BigNumber[]) => {
|
||||
|
||||
const totalFactor = new BigNumber(1).minus(combinedFactors);
|
||||
|
||||
return totalFee.times(BigNumber.max(0, totalFactor)).toNumber();
|
||||
return totalFee
|
||||
.times(new BigNumber(1).minus(BigNumber.max(0, totalFactor)))
|
||||
.toNumber();
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/bin/bash -e
|
||||
yarn --pure-lockfile
|
||||
app=${1:-trading}
|
||||
envCmd="envCmd="yarn env-cmd -f ./apps/${app}/.env.${2:-mainnet}"
|
||||
envCmd="envCmd="yarn -f ./apps/${app}/.env.${2:-mainnet}"
|
||||
yarn install
|
||||
if [ "${app}" = "trading" ]; then
|
||||
$envCmd yarn nx export trading
|
||||
DIST_LOCATION=dist/apps/trading/exported
|
||||
DIST_LOCATION=dist/apps/trading/exported/
|
||||
else
|
||||
$envCmd yarn nx build ${app}
|
||||
DIST_LOCATION=dist/apps/${app}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import { truncateByChars } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
NetworkParams,
|
||||
useNetworkParam,
|
||||
useNetworkParams,
|
||||
} from '@vegaprotocol/network-parameters';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import type { Transfer } from '@vegaprotocol/wallet';
|
||||
@@ -21,7 +22,11 @@ export const ALLOWED_ACCOUNTS = [
|
||||
|
||||
export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
||||
const { pubKey, pubKeys } = useVegaWallet();
|
||||
const { param } = useNetworkParam(NetworkParams.transfer_fee_factor);
|
||||
const { params } = useNetworkParams([
|
||||
NetworkParams.transfer_fee_factor,
|
||||
NetworkParams.transfer_minTransferQuantumMultiple,
|
||||
]);
|
||||
|
||||
const { data } = useDataProvider({
|
||||
dataProvider: accountsDataProvider,
|
||||
variables: { partyId: pubKey || '' },
|
||||
@@ -40,6 +45,7 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
||||
const accounts = data
|
||||
? data.filter((account) => ALLOWED_ACCOUNTS.includes(account.type))
|
||||
: [];
|
||||
const sortedAccounts = sortBy(accounts, (a) => a.asset.symbol.toLowerCase());
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -59,9 +65,10 @@ export const TransferContainer = ({ assetId }: { assetId?: string }) => {
|
||||
pubKey={pubKey}
|
||||
pubKeys={pubKeys ? pubKeys?.map((pk) => pk.publicKey) : null}
|
||||
assetId={assetId}
|
||||
feeFactor={param}
|
||||
feeFactor={params.transfer_fee_factor}
|
||||
minQuantumMultiple={params.transfer_minTransferQuantumMultiple}
|
||||
submitTransfer={transfer}
|
||||
accounts={accounts}
|
||||
accounts={sortedAccounts}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -9,18 +9,10 @@ import {
|
||||
} from './transfer-form';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import { removeDecimal } from '@vegaprotocol/utils';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
|
||||
describe('TransferForm', () => {
|
||||
const renderComponent = (props: TransferFormProps) => {
|
||||
return render(
|
||||
// Wrap with mock provider as the form will make queries to fetch the selected
|
||||
// toVegaKey accounts. We don't test this for now but we need to wrap so that
|
||||
// the component has access to the client
|
||||
<MockedProvider>
|
||||
<TransferForm {...props} />
|
||||
</MockedProvider>
|
||||
);
|
||||
return render(<TransferForm {...props} />);
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
@@ -54,6 +46,7 @@ describe('TransferForm', () => {
|
||||
symbol: '€',
|
||||
name: 'EUR',
|
||||
decimals: 2,
|
||||
quantum: '1',
|
||||
};
|
||||
const props = {
|
||||
pubKey,
|
||||
@@ -72,9 +65,10 @@ describe('TransferForm', () => {
|
||||
{
|
||||
type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
asset,
|
||||
balance: '100000',
|
||||
balance: '10000',
|
||||
},
|
||||
],
|
||||
minQuantumMultiple: '1',
|
||||
};
|
||||
|
||||
it('form tooltips correctly displayed', async () => {
|
||||
@@ -132,7 +126,7 @@ describe('TransferForm', () => {
|
||||
// 1003-TRAN-004
|
||||
renderComponent(props);
|
||||
await submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3); // pubkey is set as default value
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey is set as default value
|
||||
const toggle = screen.getByText('Enter manually');
|
||||
await userEvent.click(toggle);
|
||||
// has switched to input
|
||||
@@ -145,12 +139,12 @@ describe('TransferForm', () => {
|
||||
screen.getByLabelText('To Vega key'),
|
||||
'invalid-address'
|
||||
);
|
||||
expect(screen.getAllByTestId('input-error-text')[0]).toHaveTextContent(
|
||||
expect(screen.getAllByTestId('input-error-text')[1]).toHaveTextContent(
|
||||
'Invalid Vega key'
|
||||
);
|
||||
});
|
||||
|
||||
it('validates fields and submits', async () => {
|
||||
it('sends transfer from general accounts', async () => {
|
||||
// 1003-TRAN-002
|
||||
// 1003-TRAN-003
|
||||
// 1002-WITH-010
|
||||
@@ -168,7 +162,7 @@ describe('TransferForm', () => {
|
||||
]);
|
||||
|
||||
await submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3); // pubkey is set as default value
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey is set as default value
|
||||
|
||||
// Select a pubkey
|
||||
await userEvent.selectOptions(
|
||||
@@ -181,15 +175,20 @@ describe('TransferForm', () => {
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('From account'),
|
||||
AccountType.ACCOUNT_TYPE_VESTED_REWARDS
|
||||
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
|
||||
);
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
|
||||
// Test use max button
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
|
||||
expect(amountInput).toHaveValue('1000');
|
||||
|
||||
// Test amount validation
|
||||
await userEvent.type(amountInput, '0.00000001');
|
||||
await userEvent.clear(amountInput);
|
||||
await userEvent.type(amountInput, '0.001'); // Below quantum multiple amount
|
||||
expect(
|
||||
await screen.findByText('Value is below minimum')
|
||||
await screen.findByText(/Amount below minimum requirement/)
|
||||
).toBeInTheDocument();
|
||||
|
||||
await userEvent.clear(amountInput);
|
||||
@@ -210,7 +209,7 @@ describe('TransferForm', () => {
|
||||
await waitFor(() => {
|
||||
expect(props.submitTransfer).toHaveBeenCalledTimes(1);
|
||||
expect(props.submitTransfer).toHaveBeenCalledWith({
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
to: props.pubKeys[1],
|
||||
asset: asset.id,
|
||||
@@ -220,6 +219,83 @@ describe('TransferForm', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('sends transfer from vested accounts', async () => {
|
||||
const mockSubmit = jest.fn();
|
||||
renderComponent({
|
||||
...props,
|
||||
submitTransfer: mockSubmit,
|
||||
minQuantumMultiple: '100000',
|
||||
});
|
||||
|
||||
// check current pubkey not shown
|
||||
const keySelect: HTMLSelectElement = screen.getByLabelText('To Vega key');
|
||||
const pubKeyOptions = ['', pubKey, props.pubKeys[1]];
|
||||
expect(keySelect.children).toHaveLength(pubKeyOptions.length);
|
||||
expect(Array.from(keySelect.options).map((o) => o.value)).toEqual(
|
||||
pubKeyOptions
|
||||
);
|
||||
|
||||
await submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value
|
||||
|
||||
// Select a pubkey
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('To Vega key'),
|
||||
props.pubKeys[1] // Use not current pubkey so we can check it switches to current pubkey later
|
||||
);
|
||||
|
||||
// Select asset
|
||||
await selectAsset(asset);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('From account'),
|
||||
`${AccountType.ACCOUNT_TYPE_VESTED_REWARDS}-${asset.id}`
|
||||
);
|
||||
|
||||
// Check switch back to connected key
|
||||
expect(screen.getByLabelText('To Vega key')).toHaveValue(props.pubKey);
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
|
||||
const checkbox = screen.getByTestId('include-transfer-fee');
|
||||
expect(checkbox).not.toBeChecked();
|
||||
|
||||
await userEvent.clear(amountInput);
|
||||
await userEvent.type(amountInput, '50');
|
||||
|
||||
expect(await screen.findByText(/Use max to bypass/)).toBeInTheDocument();
|
||||
|
||||
// Test use max button
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
|
||||
expect(amountInput).toHaveValue('100');
|
||||
|
||||
// If transfering from a vested account 'include fees' checkbox should
|
||||
// be disabled and fees should be 0
|
||||
expect(checkbox).not.toBeChecked();
|
||||
expect(checkbox).toBeDisabled();
|
||||
const expectedFee = '0';
|
||||
const total = new BigNumber(amount).plus(expectedFee).toFixed();
|
||||
|
||||
expect(screen.getByTestId('transfer-fee')).toHaveTextContent(expectedFee);
|
||||
expect(screen.getByTestId('transfer-amount')).toHaveTextContent(amount);
|
||||
expect(screen.getByTestId('total-transfer-fee')).toHaveTextContent(total);
|
||||
|
||||
await submit();
|
||||
|
||||
await waitFor(() => {
|
||||
// 1003-TRAN-023
|
||||
expect(mockSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(mockSubmit).toHaveBeenCalledWith({
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
to: props.pubKey,
|
||||
asset: asset.id,
|
||||
amount: removeDecimal(amount, asset.decimals),
|
||||
oneOff: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('IncludeFeesCheckbox', () => {
|
||||
it('validates fields and submits when checkbox is checked', async () => {
|
||||
const mockSubmit = jest.fn();
|
||||
@@ -234,7 +310,7 @@ describe('TransferForm', () => {
|
||||
);
|
||||
|
||||
await submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3); // pubkey set as default value
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value
|
||||
|
||||
// Select a pubkey
|
||||
await userEvent.selectOptions(
|
||||
@@ -247,7 +323,7 @@ describe('TransferForm', () => {
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('From account'),
|
||||
AccountType.ACCOUNT_TYPE_VESTED_REWARDS
|
||||
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
|
||||
);
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
@@ -281,7 +357,7 @@ describe('TransferForm', () => {
|
||||
// 1003-TRAN-023
|
||||
expect(mockSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(mockSubmit).toHaveBeenCalledWith({
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
to: props.pubKeys[1],
|
||||
asset: asset.id,
|
||||
@@ -303,7 +379,7 @@ describe('TransferForm', () => {
|
||||
);
|
||||
|
||||
await submit();
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(3); // pubkey set as default value
|
||||
expect(await screen.findAllByText('Required')).toHaveLength(2); // pubkey set as default value
|
||||
|
||||
// Select a pubkey
|
||||
await userEvent.selectOptions(
|
||||
@@ -314,6 +390,11 @@ describe('TransferForm', () => {
|
||||
// Select asset
|
||||
await selectAsset(asset);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('From account'),
|
||||
`${AccountType.ACCOUNT_TYPE_GENERAL}-${asset.id}`
|
||||
);
|
||||
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
const checkbox = screen.getByTestId('include-transfer-fee');
|
||||
expect(checkbox).not.toBeChecked();
|
||||
@@ -333,26 +414,28 @@ describe('TransferForm', () => {
|
||||
|
||||
describe('AddressField', () => {
|
||||
const props = {
|
||||
mode: 'select' as const,
|
||||
select: <div>select</div>,
|
||||
input: <div>input</div>,
|
||||
onChange: jest.fn(),
|
||||
};
|
||||
|
||||
it('toggles content and calls onChange', async () => {
|
||||
it('renders correct content by mode prop and calls onChange', async () => {
|
||||
const mockOnChange = jest.fn();
|
||||
render(<AddressField {...props} onChange={mockOnChange} />);
|
||||
const { rerender } = render(
|
||||
<AddressField {...props} onChange={mockOnChange} />
|
||||
);
|
||||
|
||||
// select should be shown by default
|
||||
expect(screen.getByText('select')).toBeInTheDocument();
|
||||
expect(screen.queryByText('input')).not.toBeInTheDocument();
|
||||
await userEvent.click(screen.getByText('Enter manually'));
|
||||
expect(mockOnChange).toHaveBeenCalled();
|
||||
|
||||
rerender(<AddressField {...props} mode="input" />);
|
||||
|
||||
expect(screen.queryByText('select')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('input')).toBeInTheDocument();
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(1);
|
||||
await userEvent.click(screen.getByText('Select from wallet'));
|
||||
expect(screen.getByText('select')).toBeInTheDocument();
|
||||
expect(screen.queryByText('input')).not.toBeInTheDocument();
|
||||
expect(mockOnChange).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import {
|
||||
minSafe,
|
||||
maxSafe,
|
||||
required,
|
||||
vegaPublicKey,
|
||||
addDecimal,
|
||||
formatNumber,
|
||||
addDecimalsFormatNumber,
|
||||
toBigNum,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
@@ -27,14 +27,20 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { AssetOption, Balance } from '@vegaprotocol/assets';
|
||||
import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { accountsDataProvider } from './accounts-data-provider';
|
||||
|
||||
interface FormFields {
|
||||
toVegaKey: string;
|
||||
asset: string;
|
||||
asset: string; // This is used to simply filter the from account list, the fromAccount type should be used in the tx
|
||||
amount: string;
|
||||
fromAccount: AccountType;
|
||||
fromAccount: string; // AccountType-AssetId
|
||||
}
|
||||
|
||||
interface Asset {
|
||||
id: string;
|
||||
symbol: string;
|
||||
name: string;
|
||||
decimals: number;
|
||||
quantum: string;
|
||||
}
|
||||
|
||||
export interface TransferFormProps {
|
||||
@@ -43,10 +49,11 @@ export interface TransferFormProps {
|
||||
accounts: Array<{
|
||||
type: AccountType;
|
||||
balance: string;
|
||||
asset: { id: string; symbol: string; name: string; decimals: number };
|
||||
asset: Asset;
|
||||
}>;
|
||||
assetId?: string;
|
||||
feeFactor: string | null;
|
||||
minQuantumMultiple: string | null;
|
||||
submitTransfer: (transfer: Transfer) => void;
|
||||
}
|
||||
|
||||
@@ -57,6 +64,7 @@ export const TransferForm = ({
|
||||
feeFactor,
|
||||
submitTransfer,
|
||||
accounts,
|
||||
minQuantumMultiple,
|
||||
}: TransferFormProps) => {
|
||||
const {
|
||||
control,
|
||||
@@ -72,6 +80,8 @@ export const TransferForm = ({
|
||||
},
|
||||
});
|
||||
|
||||
const [toVegaKeyMode, setToVegaKeyMode] = useState<ToVegaKeyMode>('select');
|
||||
|
||||
const assets = sortBy(
|
||||
accounts
|
||||
.filter(
|
||||
@@ -99,48 +109,29 @@ export const TransferForm = ({
|
||||
...account.asset,
|
||||
balance: addDecimal(account.balance, account.asset.decimals),
|
||||
})),
|
||||
'name'
|
||||
(a) => a.symbol.toLowerCase()
|
||||
);
|
||||
|
||||
const selectedPubKey = watch('toVegaKey');
|
||||
const amount = watch('amount');
|
||||
const fromAccount = watch('fromAccount');
|
||||
const assetId = watch('asset');
|
||||
const selectedAssetId = watch('asset');
|
||||
|
||||
const asset = assets.find((a) => a.id === assetId);
|
||||
|
||||
const { data: toAccounts } = useDataProvider({
|
||||
dataProvider: accountsDataProvider,
|
||||
variables: {
|
||||
partyId: selectedPubKey,
|
||||
},
|
||||
skip: !selectedPubKey,
|
||||
});
|
||||
// Convert the account type (Type-AssetId) into separate values
|
||||
const [accountType, accountAssetId] = fromAccount
|
||||
? parseFromAccount(fromAccount)
|
||||
: [undefined, undefined];
|
||||
const fromVested = accountType === AccountType.ACCOUNT_TYPE_VESTED_REWARDS;
|
||||
const asset = assets.find((a) => a.id === accountAssetId);
|
||||
|
||||
const account = accounts.find(
|
||||
(a) => a.asset.id === assetId && a.type === fromAccount
|
||||
(a) => a.asset.id === accountAssetId && a.type === accountType
|
||||
);
|
||||
const accountBalance =
|
||||
account && addDecimal(account.balance, account.asset.decimals);
|
||||
|
||||
// The general account of the selected pubkey. You can only transfer
|
||||
// to general accounts, either when redeeming vested rewards or just
|
||||
// during normal general -> general transfers
|
||||
const toGeneralAccount =
|
||||
toAccounts &&
|
||||
toAccounts.find((a) => {
|
||||
return (
|
||||
a.asset.id === assetId && a.type === AccountType.ACCOUNT_TYPE_GENERAL
|
||||
);
|
||||
});
|
||||
|
||||
const [includeFee, setIncludeFee] = useState(false);
|
||||
|
||||
// Min viable amount given asset decimals EG for WEI 0.000000000000000001
|
||||
const min = asset
|
||||
? new BigNumber(addDecimal('1', asset.decimals))
|
||||
: new BigNumber(0);
|
||||
|
||||
// Max amount given selected asset and from account
|
||||
const max = accountBalance ? new BigNumber(accountBalance) : new BigNumber(0);
|
||||
|
||||
@@ -164,16 +155,21 @@ export const TransferForm = ({
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(fields: FormFields) => {
|
||||
if (!asset) {
|
||||
throw new Error('Submitted transfer with no asset selected');
|
||||
}
|
||||
if (!transferAmount) {
|
||||
throw new Error('Submitted transfer with no amount selected');
|
||||
}
|
||||
|
||||
const [type, assetId] = parseFromAccount(fields.fromAccount);
|
||||
const asset = assets.find((a) => a.id === assetId);
|
||||
|
||||
if (!asset) {
|
||||
throw new Error('Submitted transfer with no asset selected');
|
||||
}
|
||||
|
||||
const transfer = normalizeTransfer(
|
||||
fields.toVegaKey,
|
||||
transferAmount,
|
||||
fields.fromAccount,
|
||||
type,
|
||||
AccountType.ACCOUNT_TYPE_GENERAL, // field is readonly in the form
|
||||
{
|
||||
id: asset.id,
|
||||
@@ -182,7 +178,7 @@ export const TransferForm = ({
|
||||
);
|
||||
submitTransfer(transfer);
|
||||
},
|
||||
[asset, submitTransfer, transferAmount]
|
||||
[submitTransfer, transferAmount, assets]
|
||||
);
|
||||
|
||||
// reset for placeholder workaround https://github.com/radix-ui/primitives/issues/1569
|
||||
@@ -198,55 +194,10 @@ export const TransferForm = ({
|
||||
className="text-sm"
|
||||
data-testid="transfer-form"
|
||||
>
|
||||
<TradingFormGroup label="To Vega key" labelFor="toVegaKey">
|
||||
<AddressField
|
||||
onChange={() => setValue('toVegaKey', '')}
|
||||
select={
|
||||
<TradingSelect {...register('toVegaKey')} id="toVegaKey">
|
||||
<option value="" disabled={true}>
|
||||
{t('Please select')}
|
||||
</option>
|
||||
{pubKeys?.map((pk) => {
|
||||
const text = pk === pubKey ? t('Current key: ') + pk : pk;
|
||||
|
||||
return (
|
||||
<option key={pk} value={pk}>
|
||||
{text}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</TradingSelect>
|
||||
}
|
||||
input={
|
||||
<TradingInput
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={true} // focus input immediately after is shown
|
||||
id="toVegaKey"
|
||||
type="text"
|
||||
{...register('toVegaKey', {
|
||||
validate: {
|
||||
required,
|
||||
vegaPublicKey,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{errors.toVegaKey?.message && (
|
||||
<TradingInputError forInput="toVegaKey">
|
||||
{errors.toVegaKey.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup label={t('Asset')} labelFor="asset">
|
||||
<Controller
|
||||
control={control}
|
||||
name="asset"
|
||||
rules={{
|
||||
validate: {
|
||||
required,
|
||||
},
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TradingRichSelect
|
||||
data-testid="select-asset"
|
||||
@@ -254,6 +205,7 @@ export const TransferForm = ({
|
||||
name={field.name}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
setValue('fromAccount', '');
|
||||
}}
|
||||
placeholder={t('Please select an asset')}
|
||||
value={field.value}
|
||||
@@ -280,10 +232,10 @@ export const TransferForm = ({
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup label={t('From account')} labelFor="fromAccount">
|
||||
<TradingSelect
|
||||
id="fromAccount"
|
||||
defaultValue=""
|
||||
{...register('fromAccount', {
|
||||
<Controller
|
||||
control={control}
|
||||
name="fromAccount"
|
||||
rules={{
|
||||
validate: {
|
||||
required,
|
||||
sameAccount: (value) => {
|
||||
@@ -298,50 +250,106 @@ export const TransferForm = ({
|
||||
return true;
|
||||
},
|
||||
},
|
||||
})}
|
||||
>
|
||||
<option value="" disabled={true}>
|
||||
{t('Please select')}
|
||||
</option>
|
||||
{accounts
|
||||
.filter((a) => {
|
||||
if (!assetId) return true;
|
||||
return assetId === a.asset.id;
|
||||
})
|
||||
.map((a) => {
|
||||
return (
|
||||
<option value={a.type} key={`${a.type}-${a.asset.id}`}>
|
||||
{AccountTypeMapping[a.type]} (
|
||||
{addDecimalsFormatNumber(a.balance, a.asset.decimals)}{' '}
|
||||
{a.asset.symbol})
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</TradingSelect>
|
||||
}}
|
||||
render={({ field }) => (
|
||||
<TradingSelect
|
||||
id="fromAccount"
|
||||
defaultValue=""
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
field.onChange(e);
|
||||
|
||||
const [type] = parseFromAccount(e.target.value);
|
||||
|
||||
// Enforce that if transferring from a vested rewards account it must go to
|
||||
// the current connected general account
|
||||
if (
|
||||
type === AccountType.ACCOUNT_TYPE_VESTED_REWARDS &&
|
||||
pubKey
|
||||
) {
|
||||
setValue('toVegaKey', pubKey);
|
||||
setToVegaKeyMode('select');
|
||||
setIncludeFee(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="" disabled={true}>
|
||||
{t('Please select')}
|
||||
</option>
|
||||
{accounts
|
||||
.filter((a) => {
|
||||
if (!selectedAssetId) return true;
|
||||
return selectedAssetId === a.asset.id;
|
||||
})
|
||||
.map((a) => {
|
||||
const id = `${a.type}-${a.asset.id}`;
|
||||
return (
|
||||
<option value={id} key={id}>
|
||||
{AccountTypeMapping[a.type]} (
|
||||
{addDecimalsFormatNumber(a.balance, a.asset.decimals)}{' '}
|
||||
{a.asset.symbol})
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</TradingSelect>
|
||||
)}
|
||||
/>
|
||||
{errors.fromAccount?.message && (
|
||||
<TradingInputError forInput="fromAccount">
|
||||
{errors.fromAccount.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup label={t('To account')} labelFor="toAccount">
|
||||
<TradingSelect
|
||||
id="toAccount"
|
||||
defaultValue={AccountType.ACCOUNT_TYPE_GENERAL}
|
||||
>
|
||||
<option value={AccountType.ACCOUNT_TYPE_GENERAL}>
|
||||
{toGeneralAccount
|
||||
? `${
|
||||
AccountTypeMapping[AccountType.ACCOUNT_TYPE_GENERAL]
|
||||
} (${addDecimalsFormatNumber(
|
||||
toGeneralAccount.balance,
|
||||
toGeneralAccount.asset.decimals
|
||||
)} ${toGeneralAccount.asset.symbol})`
|
||||
: `${AccountTypeMapping[AccountType.ACCOUNT_TYPE_GENERAL]} ${
|
||||
asset ? `(0 ${asset.symbol})` : ''
|
||||
}`}
|
||||
</option>
|
||||
</TradingSelect>
|
||||
<TradingFormGroup label="To Vega key" labelFor="toVegaKey">
|
||||
<AddressField
|
||||
onChange={() => {
|
||||
setValue('toVegaKey', '');
|
||||
setToVegaKeyMode((curr) => (curr === 'input' ? 'select' : 'input'));
|
||||
}}
|
||||
mode={toVegaKeyMode}
|
||||
select={
|
||||
<TradingSelect
|
||||
{...register('toVegaKey')}
|
||||
disabled={fromVested}
|
||||
id="toVegaKey"
|
||||
>
|
||||
<option value="" disabled={true}>
|
||||
{t('Please select')}
|
||||
</option>
|
||||
{pubKeys?.map((pk) => {
|
||||
const text = pk === pubKey ? t('Current key: ') + pk : pk;
|
||||
|
||||
return (
|
||||
<option key={pk} value={pk}>
|
||||
{text}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</TradingSelect>
|
||||
}
|
||||
input={
|
||||
fromVested ? null : (
|
||||
<TradingInput
|
||||
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={true} // focus input immediately after is shown
|
||||
id="toVegaKey"
|
||||
type="text"
|
||||
disabled={fromVested}
|
||||
{...register('toVegaKey', {
|
||||
validate: {
|
||||
required,
|
||||
vegaPublicKey,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{errors.toVegaKey?.message && (
|
||||
<TradingInputError forInput="toVegaKey">
|
||||
{errors.toVegaKey.message}
|
||||
</TradingInputError>
|
||||
)}
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup label="Amount" labelFor="amount">
|
||||
<TradingInput
|
||||
@@ -353,7 +361,43 @@ export const TransferForm = ({
|
||||
{...register('amount', {
|
||||
validate: {
|
||||
required,
|
||||
minSafe: (value) => minSafe(new BigNumber(min))(value),
|
||||
minSafe: (v) => {
|
||||
if (!asset || !minQuantumMultiple) return true;
|
||||
|
||||
const value = new BigNumber(v);
|
||||
|
||||
if (value.isZero()) {
|
||||
return t('Amount cannot be 0');
|
||||
}
|
||||
|
||||
const minByQuantumMultiple = toBigNum(
|
||||
minQuantumMultiple,
|
||||
asset.decimals
|
||||
);
|
||||
|
||||
if (fromVested) {
|
||||
// special conditions which let you bypass min transfer rules set by quantum multiple
|
||||
if (value.isGreaterThanOrEqualTo(max)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value.isLessThan(minByQuantumMultiple)) {
|
||||
return t(
|
||||
'Amount below minimum requirements for partial transfer. Use max to bypass'
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
if (value.isLessThan(minByQuantumMultiple)) {
|
||||
return t(
|
||||
'Amount below minimum requirement set by transfer.minTransferQuantumMultiple'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
maxSafe: (v) => {
|
||||
const value = new BigNumber(v);
|
||||
if (value.isGreaterThan(max)) {
|
||||
@@ -369,7 +413,9 @@ export const TransferForm = ({
|
||||
type="button"
|
||||
className="absolute top-0 right-0 ml-auto text-xs underline"
|
||||
onClick={() =>
|
||||
setValue('amount', parseFloat(accountBalance).toString())
|
||||
setValue('amount', parseFloat(accountBalance).toString(), {
|
||||
shouldValidate: true,
|
||||
})
|
||||
}
|
||||
>
|
||||
{t('Use max')}
|
||||
@@ -390,10 +436,10 @@ export const TransferForm = ({
|
||||
<div>
|
||||
<TradingCheckbox
|
||||
name="include-transfer-fee"
|
||||
disabled={!transferAmount}
|
||||
disabled={!transferAmount || fromVested}
|
||||
label={t('Include transfer fee')}
|
||||
checked={includeFee}
|
||||
onCheckedChange={() => setIncludeFee(!includeFee)}
|
||||
onCheckedChange={() => setIncludeFee((x) => !x)}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
@@ -403,7 +449,7 @@ export const TransferForm = ({
|
||||
amount={transferAmount}
|
||||
transferAmount={transferAmount}
|
||||
feeFactor={feeFactor}
|
||||
fee={fee}
|
||||
fee={fromVested ? '0' : fee}
|
||||
decimals={asset?.decimals}
|
||||
/>
|
||||
)}
|
||||
@@ -485,32 +531,38 @@ export const TransferFee = ({
|
||||
);
|
||||
};
|
||||
|
||||
type ToVegaKeyMode = 'input' | 'select';
|
||||
|
||||
interface AddressInputProps {
|
||||
select: ReactNode;
|
||||
input: ReactNode;
|
||||
mode: ToVegaKeyMode;
|
||||
onChange: () => void;
|
||||
}
|
||||
|
||||
export const AddressField = ({
|
||||
select,
|
||||
input,
|
||||
mode,
|
||||
onChange,
|
||||
}: AddressInputProps) => {
|
||||
const [isInput, setIsInput] = useState(false);
|
||||
|
||||
const isInput = mode === 'input';
|
||||
return (
|
||||
<>
|
||||
{isInput ? input : select}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsInput((curr) => !curr);
|
||||
onChange();
|
||||
}}
|
||||
className="absolute top-0 right-0 ml-auto text-xs underline"
|
||||
>
|
||||
{isInput ? t('Select from wallet') : t('Enter manually')}
|
||||
</button>
|
||||
{select && input && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onChange}
|
||||
className="absolute top-0 right-0 ml-auto text-xs underline"
|
||||
>
|
||||
{isInput ? t('Select from wallet') : t('Enter manually')}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const parseFromAccount = (fromAccountStr: string) => {
|
||||
return fromAccountStr.split('-') as [AccountType, string];
|
||||
};
|
||||
|
||||
@@ -72,15 +72,11 @@ export const DealTicketFeeDetails = ({
|
||||
|
||||
return (
|
||||
<KeyValue
|
||||
label={t('Fees')}
|
||||
value={
|
||||
totalDiscountedFeeAmount &&
|
||||
`~${formatValue(totalDiscountedFeeAmount, assetDecimals)}`
|
||||
}
|
||||
formattedValue={
|
||||
label={
|
||||
<>
|
||||
{t('Fees')}
|
||||
{totalDiscountFactor ? (
|
||||
<Pill size="xxs" intent={Intent.Warning} className="mr-1">
|
||||
<Pill size="xxs" intent={Intent.Info} className="ml-1">
|
||||
-
|
||||
{formatNumberPercentage(
|
||||
new BigNumber(totalDiscountFactor).multipliedBy(100),
|
||||
@@ -88,10 +84,16 @@ export const DealTicketFeeDetails = ({
|
||||
)}
|
||||
</Pill>
|
||||
) : null}
|
||||
{totalDiscountedFeeAmount &&
|
||||
`~${formatValue(totalDiscountedFeeAmount, assetDecimals, quantum)}`}
|
||||
</>
|
||||
}
|
||||
value={
|
||||
totalDiscountedFeeAmount &&
|
||||
`~${formatValue(totalDiscountedFeeAmount, assetDecimals)}`
|
||||
}
|
||||
formattedValue={
|
||||
totalDiscountedFeeAmount &&
|
||||
`~${formatValue(totalDiscountedFeeAmount, assetDecimals, quantum)}`
|
||||
}
|
||||
labelDescription={
|
||||
<div className="flex flex-col gap-2">
|
||||
<p>
|
||||
|
||||
@@ -685,7 +685,7 @@ export const DealTicket = ({
|
||||
subLabel={`${formatValue(
|
||||
normalizedOrder.size,
|
||||
market.positionDecimalPlaces
|
||||
)} ${baseQuote} @ ${
|
||||
)} ${baseQuote || ''} @ ${
|
||||
type === Schema.OrderType.TYPE_MARKET
|
||||
? 'market'
|
||||
: `${formatValue(
|
||||
|
||||
@@ -79,7 +79,11 @@ export const FeesBreakdown = ({
|
||||
volumeDiscountFactor
|
||||
);
|
||||
|
||||
const { volumeDiscount, referralDiscount } = getDiscountedFee(
|
||||
const {
|
||||
discountedFee: discountedTotalFeeAmount,
|
||||
volumeDiscount,
|
||||
referralDiscount,
|
||||
} = getDiscountedFee(
|
||||
totalFeeAmount,
|
||||
referralDiscountFactor,
|
||||
volumeDiscountFactor
|
||||
@@ -131,7 +135,7 @@ export const FeesBreakdown = ({
|
||||
<FeesBreakdownItem
|
||||
label={t('Total fees')}
|
||||
factor={feeFactors ? sumFeesFactors(feeFactors)?.toString() : undefined}
|
||||
value={totalFeeAmount}
|
||||
value={discountedTotalFeeAmount}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
|
||||
@@ -289,17 +289,30 @@ describe('FeesDiscountBreakdownTooltip', () => {
|
||||
const { container } = render(<FeesDiscountBreakdownTooltip {...props} />);
|
||||
const dt = container.querySelectorAll('dt');
|
||||
const dd = container.querySelectorAll('dd');
|
||||
const expected = [
|
||||
{ label: 'Infrastructure Fee Referral Discount', value: '0.05 BTC' },
|
||||
{ label: 'Infrastructure Fee Volume Discount', value: '0.06 BTC' },
|
||||
{ label: 'Liquidity Fee Referral Discount', value: '0.01 BTC' },
|
||||
{ label: 'Liquidity Fee Volume Discount', value: '0.02 BTC' },
|
||||
{ label: 'Maker Fee Referral Discount', value: '0.03 BTC' },
|
||||
{ label: 'Maker Fee Volume Discount', value: '0.04 BTC' },
|
||||
const expectedDt = [
|
||||
'Infrastructure Fee',
|
||||
'Referral Discount',
|
||||
'Volume Discount',
|
||||
'Liquidity Fee',
|
||||
'Referral Discount',
|
||||
'Volume Discount',
|
||||
'Maker Fee',
|
||||
'Referral Discount',
|
||||
'Volume Discount',
|
||||
];
|
||||
expected.forEach(({ label, value }, i) => {
|
||||
const expectedDD = [
|
||||
'0.05 BTC',
|
||||
'0.06 BTC',
|
||||
'0.01 BTC',
|
||||
'0.02 BTC',
|
||||
'0.03 BTC',
|
||||
'0.04 BTC',
|
||||
];
|
||||
expectedDt.forEach((label, i) => {
|
||||
expect(dt[i]).toHaveTextContent(label);
|
||||
expect(dd[i]).toHaveTextContent(value);
|
||||
});
|
||||
expectedDD.forEach((label, i) => {
|
||||
expect(dd[i]).toHaveTextContent(label);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -391,7 +391,7 @@ const FeesDiscountBreakdownTooltipItem = ({
|
||||
label: string;
|
||||
asset: ReturnType<typeof getAsset>;
|
||||
}) =>
|
||||
value ? (
|
||||
value && value !== '0' ? (
|
||||
<>
|
||||
<dt className="col-span-1">{label}</dt>
|
||||
<dd className="text-right col-span-1">
|
||||
@@ -418,34 +418,47 @@ export const FeesDiscountBreakdownTooltip = ({
|
||||
className="max-w-sm bg-vega-light-100 dark:bg-vega-dark-100 border border-vega-light-200 dark:border-vega-dark-200 px-4 py-2 z-20 rounded text-sm break-word text-black dark:text-white"
|
||||
>
|
||||
<dl className="grid grid-cols-2 gap-x-1">
|
||||
{(fees.infrastructureFeeReferralDiscount || '0') !== '0' ||
|
||||
(fees.infrastructureFeeVolumeDiscount || '0') !== '0' ? (
|
||||
<dt className="col-span-2">{t('Infrastructure Fee')}</dt>
|
||||
) : null}
|
||||
<FeesDiscountBreakdownTooltipItem
|
||||
value={fees.infrastructureFeeReferralDiscount}
|
||||
label={t('Infrastructure Fee Referral Discount')}
|
||||
label={t('Referral Discount')}
|
||||
asset={asset}
|
||||
/>
|
||||
<FeesDiscountBreakdownTooltipItem
|
||||
value={fees.infrastructureFeeVolumeDiscount}
|
||||
label={t('Infrastructure Fee Volume Discount')}
|
||||
label={t('Volume Discount')}
|
||||
asset={asset}
|
||||
/>
|
||||
{(fees.liquidityFeeReferralDiscount || '0') !== '0' ||
|
||||
(fees.liquidityFeeVolumeDiscount || '0') !== '0' ? (
|
||||
<dt className="col-span-2">{t('Liquidity Fee')}</dt>
|
||||
) : null}
|
||||
|
||||
<FeesDiscountBreakdownTooltipItem
|
||||
value={fees.liquidityFeeReferralDiscount}
|
||||
label={t('Liquidity Fee Referral Discount')}
|
||||
label={t('Referral Discount')}
|
||||
asset={asset}
|
||||
/>
|
||||
<FeesDiscountBreakdownTooltipItem
|
||||
value={fees.liquidityFeeVolumeDiscount}
|
||||
label={t('Liquidity Fee Volume Discount')}
|
||||
label={t('Volume Discount')}
|
||||
asset={asset}
|
||||
/>
|
||||
{(fees.makerFeeReferralDiscount || '0') !== '0' ||
|
||||
(fees.makerFeeVolumeDiscount || '0') !== '0' ? (
|
||||
<dt className="col-span-2">{t('Maker Fee')}</dt>
|
||||
) : null}
|
||||
<FeesDiscountBreakdownTooltipItem
|
||||
value={fees.makerFeeReferralDiscount}
|
||||
label={t('Maker Fee Referral Discount')}
|
||||
label={t('Referral Discount')}
|
||||
asset={asset}
|
||||
/>
|
||||
<FeesDiscountBreakdownTooltipItem
|
||||
value={fees.makerFeeVolumeDiscount}
|
||||
label={t('Maker Fee Volume Discount')}
|
||||
label={t('Volume Discount')}
|
||||
asset={asset}
|
||||
/>
|
||||
</dl>
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useMemo } from 'react';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
addDecimalsFormatNumberQuantum,
|
||||
formatNumberPercentage,
|
||||
getDateTimeFormat,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
@@ -29,6 +28,12 @@ import { LiquidityProvisionStatus } from '@vegaprotocol/types';
|
||||
import { LiquidityProvisionStatusMapping } from '@vegaprotocol/types';
|
||||
import type { LiquidityProvisionData } from './liquidity-data-provider';
|
||||
|
||||
const formatNumberPercentage = (value: BigNumber, decimals?: number) => {
|
||||
const decimalPlaces =
|
||||
typeof decimals === 'undefined' ? value.dp() || 0 : decimals;
|
||||
return `${value.toFixed(decimalPlaces, 1)}%`;
|
||||
};
|
||||
|
||||
const percentageFormatter = ({ value }: ValueFormatterParams) => {
|
||||
if (!value) return '-';
|
||||
return formatNumberPercentage(new BigNumber(value).times(100), 2) || '-';
|
||||
@@ -126,11 +131,11 @@ export const LiquidityTable = ({
|
||||
new BigNumber(data.sla.currentEpochFractionOfTimeOnBook).times(
|
||||
100
|
||||
),
|
||||
2
|
||||
4
|
||||
),
|
||||
formatNumberPercentage(
|
||||
new BigNumber(data.commitmentMinTimeFraction).times(100),
|
||||
2
|
||||
4
|
||||
),
|
||||
]
|
||||
);
|
||||
@@ -143,7 +148,7 @@ export const LiquidityTable = ({
|
||||
new BigNumber(data.sla.currentEpochFractionOfTimeOnBook).times(
|
||||
100
|
||||
),
|
||||
2
|
||||
4
|
||||
),
|
||||
]
|
||||
);
|
||||
|
||||
@@ -16,7 +16,7 @@ import type {
|
||||
MarketDataQueryVariables,
|
||||
} from './__generated__/market-data';
|
||||
import { getMarketPrice } from './get-price';
|
||||
import { isMarketInAuction } from './is-market-in-auction';
|
||||
import { MarketTradingMode } from '@vegaprotocol/types';
|
||||
|
||||
export type MarketData = MarketDataFieldsFragment;
|
||||
|
||||
@@ -142,7 +142,11 @@ export const fundingRateProvider = makeDerivedDataProvider<
|
||||
MarketDataQueryVariables
|
||||
>([marketDataProvider], (parts) => {
|
||||
const marketData = parts[0] as ReturnType<typeof getData>;
|
||||
return marketData && !isMarketInAuction(marketData.marketTradingMode)
|
||||
return marketData &&
|
||||
![
|
||||
MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
|
||||
MarketTradingMode.TRADING_MODE_SUSPENDED_VIA_GOVERNANCE,
|
||||
].includes(marketData.marketTradingMode)
|
||||
? marketData?.productData?.fundingRate || null
|
||||
: null;
|
||||
});
|
||||
|
||||
@@ -176,6 +176,7 @@ export const NetworkParams = {
|
||||
market_liquidity_feeCalculationTimeStep:
|
||||
'market_liquidity_feeCalculationTimeStep',
|
||||
transfer_fee_factor: 'transfer_fee_factor',
|
||||
transfer_minTransferQuantumMultiple: 'transfer_minTransferQuantumMultiple',
|
||||
network_validators_incumbentBonus: 'network_validators_incumbentBonus',
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -148,6 +148,7 @@ export const OrderListManager = ({
|
||||
expiresAt: editOrder.expiresAt,
|
||||
side: editOrder.side,
|
||||
marketId: editOrder.market.id,
|
||||
remaining: editOrder.remaining,
|
||||
};
|
||||
create({ orderAmendment }, originalOrder);
|
||||
setEditOrder(null);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
MAXGOINT64,
|
||||
addDecimalsFormatNumber,
|
||||
getDateTimeFormat,
|
||||
isNumeric,
|
||||
@@ -144,11 +145,22 @@ export const OrderListTable = memo<
|
||||
if (!data?.market || !isNumeric(data.size)) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
const prefix = data
|
||||
? data.side === Schema.Side.SIDE_BUY
|
||||
? '+'
|
||||
: '-'
|
||||
: '';
|
||||
|
||||
if (
|
||||
data.size === MAXGOINT64 &&
|
||||
data.timeInForce ===
|
||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC &&
|
||||
data.reduceOnly
|
||||
) {
|
||||
return t('MAX');
|
||||
}
|
||||
|
||||
return (
|
||||
prefix +
|
||||
addDecimalsFormatNumber(
|
||||
|
||||
@@ -27,7 +27,9 @@ jest.mock('@vegaprotocol/data-provider', () => ({
|
||||
}));
|
||||
|
||||
describe('PositionsManager', () => {
|
||||
it('should close position with max uint64', async () => {
|
||||
// TODO: temporarily disable close position
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('should close position with max uint64', async () => {
|
||||
render(<PositionsManager partyIds={['partyId']} isReadOnly={false} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ export const PositionsManager = ({
|
||||
}: PositionsManagerProps) => {
|
||||
const { pubKeys, pubKey } = useVegaWallet();
|
||||
const create = useVegaTransactionStore((store) => store.create);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const onClose = useCallback(
|
||||
({ marketId, openVolume }: { marketId: string; openVolume: string }) =>
|
||||
create({
|
||||
@@ -73,7 +74,7 @@ export const PositionsManager = ({
|
||||
pubKeys={pubKeys}
|
||||
rowData={data}
|
||||
onMarketClick={onMarketClick}
|
||||
onClose={onClose}
|
||||
// onClose={onClose}
|
||||
isReadOnly={isReadOnly}
|
||||
multipleKeys={partyIds.length > 1}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No positions')}
|
||||
|
||||
@@ -65,6 +65,7 @@ fragment OrderTxUpdateFields on OrderUpdate {
|
||||
expiresAt
|
||||
side
|
||||
marketId
|
||||
remaining
|
||||
}
|
||||
|
||||
subscription OrderTxUpdate($partyId: ID!) {
|
||||
|
||||
+3
-2
@@ -21,14 +21,14 @@ export type WithdrawalBusEventSubscriptionVariables = Types.Exact<{
|
||||
|
||||
export type WithdrawalBusEventSubscription = { __typename?: 'Subscription', busEvents?: Array<{ __typename?: 'BusEvent', event: { __typename?: 'Deposit' } | { __typename?: 'TimeUpdate' } | { __typename?: 'TransactionResult' } | { __typename?: 'Withdrawal', id: string, status: Types.WithdrawalStatus, amount: string, createdTimestamp: any, withdrawnTimestamp?: any | null, txHash?: string | null, pendingOnForeignChain: boolean, asset: { __typename?: 'Asset', id: string, name: string, symbol: string, decimals: number, status: Types.AssetStatus, source: { __typename?: 'BuiltinAsset' } | { __typename?: 'ERC20', contractAddress: string } }, details?: { __typename?: 'Erc20WithdrawalDetails', receiverAddress: string } | null } }> | null };
|
||||
|
||||
export type OrderTxUpdateFieldsFragment = { __typename?: 'OrderUpdate', type?: Types.OrderType | null, id: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, createdAt: any, size: string, price: string, timeInForce: Types.OrderTimeInForce, expiresAt?: any | null, side: Types.Side, marketId: string };
|
||||
export type OrderTxUpdateFieldsFragment = { __typename?: 'OrderUpdate', type?: Types.OrderType | null, id: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, createdAt: any, size: string, price: string, timeInForce: Types.OrderTimeInForce, expiresAt?: any | null, side: Types.Side, marketId: string, remaining: string };
|
||||
|
||||
export type OrderTxUpdateSubscriptionVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type OrderTxUpdateSubscription = { __typename?: 'Subscription', orders?: Array<{ __typename?: 'OrderUpdate', type?: Types.OrderType | null, id: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, createdAt: any, size: string, price: string, timeInForce: Types.OrderTimeInForce, expiresAt?: any | null, side: Types.Side, marketId: string }> | null };
|
||||
export type OrderTxUpdateSubscription = { __typename?: 'Subscription', orders?: Array<{ __typename?: 'OrderUpdate', type?: Types.OrderType | null, id: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, createdAt: any, size: string, price: string, timeInForce: Types.OrderTimeInForce, expiresAt?: any | null, side: Types.Side, marketId: string, remaining: string }> | null };
|
||||
|
||||
export type DepositBusEventFieldsFragment = { __typename?: 'Deposit', id: string, status: Types.DepositStatus, amount: string, createdTimestamp: any, creditedTimestamp?: any | null, txHash?: string | null, asset: { __typename?: 'Asset', id: string, symbol: string, decimals: number } };
|
||||
|
||||
@@ -88,6 +88,7 @@ export const OrderTxUpdateFieldsFragmentDoc = gql`
|
||||
expiresAt
|
||||
side
|
||||
marketId
|
||||
remaining
|
||||
}
|
||||
`;
|
||||
export const DepositBusEventFieldsFragmentDoc = gql`
|
||||
|
||||
@@ -56,6 +56,7 @@ describe('useVegaTransactionStore', () => {
|
||||
side: Side.SIDE_BUY,
|
||||
marketId:
|
||||
'3aa2a828687cc3d59e92445d294891cbbd40e2165bbfb15674158ef5d4e8848d',
|
||||
remaining: '12',
|
||||
};
|
||||
|
||||
const processedTransactionUpdate = {
|
||||
|
||||
@@ -193,6 +193,7 @@ const submitOrder: VegaStoredTxState = {
|
||||
createdAt: new Date(),
|
||||
marketId: 'market-1',
|
||||
status: OrderStatus.STATUS_ACTIVE,
|
||||
remaining: '10',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -249,6 +250,7 @@ const editOrder: VegaStoredTxState = {
|
||||
createdAt: new Date(),
|
||||
marketId: 'market-1',
|
||||
status: OrderStatus.STATUS_ACTIVE,
|
||||
remaining: '10',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -489,6 +491,7 @@ describe('getRejectionReason', () => {
|
||||
timeInForce: Types.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
side: Types.Side.SIDE_BUY,
|
||||
marketId: '',
|
||||
remaining: '',
|
||||
})
|
||||
).toBe('Insufficient asset balance');
|
||||
});
|
||||
@@ -505,6 +508,7 @@ describe('getRejectionReason', () => {
|
||||
timeInForce: Types.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||
side: Types.Side.SIDE_BUY,
|
||||
marketId: '',
|
||||
remaining: '',
|
||||
})
|
||||
).toBe(
|
||||
'Your Fill or Kill (FOK) order was not filled and it has been stopped'
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
toBigNum,
|
||||
truncateByChars,
|
||||
formatTrigger,
|
||||
MAXGOINT64,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useAssetsMapProvider } from '@vegaprotocol/assets';
|
||||
@@ -585,6 +586,23 @@ export const VegaTransactionDetails = ({ tx }: { tx: VegaStoredTxState }) => {
|
||||
<Panel>
|
||||
{t('Close position for')}{' '}
|
||||
<strong>{market.tradableInstrument.instrument.code}</strong>
|
||||
{tx.order?.remaining && (
|
||||
<p>
|
||||
{t('Filled')}{' '}
|
||||
<SizeAtPrice
|
||||
meta={{
|
||||
positionDecimalPlaces: market.positionDecimalPlaces,
|
||||
decimalPlaces: market.decimalPlaces,
|
||||
asset: getAsset(market).symbol,
|
||||
}}
|
||||
side={tx.order.side}
|
||||
size={(
|
||||
BigInt(tx.order.size) - BigInt(tx.order.remaining)
|
||||
).toString()}
|
||||
price={tx.order.price}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
@@ -953,7 +971,19 @@ export const getVegaTransactionContentIntent = (tx: VegaStoredTxState) => {
|
||||
isWithdrawTransaction(tx.body) &&
|
||||
Intent.Warning;
|
||||
|
||||
// Toast for an IOC should go green when it is stopped,
|
||||
// because stopping an IOC once all available volume has filled is the correct behaviour (it is immediate or cancel),
|
||||
// this behaviour should apply to all IOC, not just those created due to "Close position"
|
||||
const intentForClosedPosition =
|
||||
tx.order &&
|
||||
tx.order.status === Schema.OrderStatus.STATUS_STOPPED &&
|
||||
tx.order.timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_IOC &&
|
||||
tx.order.size === MAXGOINT64 &&
|
||||
// isClosePositionTransaction(tx) &&
|
||||
Intent.Success;
|
||||
|
||||
const intent =
|
||||
intentForClosedPosition ||
|
||||
intentForRejectedOrder ||
|
||||
intentForCompletedWithdrawal ||
|
||||
intentMap[tx.status];
|
||||
|
||||
@@ -76,6 +76,7 @@ const orderUpdate: OrderTxUpdateFieldsFragment = {
|
||||
createdAt: '2022-07-05T14:25:47.815283706Z',
|
||||
expiresAt: '2022-07-05T14:25:47.815283706Z',
|
||||
size: '10',
|
||||
remaining: '10',
|
||||
price: '300000',
|
||||
timeInForce: OrderTimeInForce.TIME_IN_FORCE_GTC,
|
||||
side: Side.SIDE_BUY,
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@
|
||||
"jsondiffpatch": "^0.4.1",
|
||||
"lodash": "^4.17.21",
|
||||
"next": "13.3.0",
|
||||
"pennant": "1.14.0",
|
||||
"pennant": "^1.14.1",
|
||||
"react": "18.2.0",
|
||||
"react-copy-to-clipboard": "^5.0.4",
|
||||
"react-dom": "18.2.0",
|
||||
|
||||
@@ -20500,10 +20500,10 @@ pend@~1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
|
||||
integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==
|
||||
|
||||
pennant@1.14.0:
|
||||
version "1.14.0"
|
||||
resolved "https://registry.yarnpkg.com/pennant/-/pennant-1.14.0.tgz#4100c25a6d836d6f0ff425181fb6f812f9fe5778"
|
||||
integrity sha512-9H0zWzFUSbD1BlDXnHFmKwkAxXGb1xTxjkUD+RwaMygtSwPXzQEyk2ScVyMqxdcz0RuJmI5HCVmZTOjdr1NwuA==
|
||||
pennant@^1.14.1:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/pennant/-/pennant-1.14.1.tgz#8e7a53256095e398b03397af31c831b02a56032b"
|
||||
integrity sha512-rjzo/tlFanO96OKhJiyjQtjug7sY6pjVZqVbieD3Tf4zt20bqIb6m4L2JfwOXEe0jqP/yddgCNPY+vlkVuJW1Q==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@d3fc/d3fc-technical-indicator" "^8.0.1"
|
||||
|
||||
Reference in New Issue
Block a user