fix(#1691,#2040): deal ticket validation (#2045)

* feat: update validation for size and price so they are reported simultaneously

* feat: adjust styles so error messages for price/size are rendered on one line

* fix: missing key in map error

* feat: split validation out on a field by field basis

* refactor: remove unnecessary price setting and just use props to calc market price

* refactor: rename setOrder func and add a comment

* chore: add type for form fields to allow for summary field

* fix: layout of market order size and price

* fix: casing of size/price in validation message

* refactor: fix type errors for summary react nodes and for tif and type validation

* feat: break out functions for testing market.state and market.tradingmode on submit

* fix: deal ticket test for last price value

* chore: remove unused files and move hooks and utils to correct dirs

* chore: tidy up constants

* fix: const before enum invalid syntax

* feat: dont block submission if not enough margin but do if zero balance

* chore: remove duplicated margin warning message

* feat: dont allow margin warning and error message to render at the same time

* feat: make trading mode check just a warning rather than error message

* test: make markets active so they are tradable, renaming describe blocks for clarity

* test: adjust test ids and disabled state and fix tests

* test: include AC codes in tests

* chore: remove click of in test as form is already dirty

* fix: lint errors, only show margin warning if you have balance

* chore: add ts ignore

* chore: re add use-order-validation hook for console-lite specifically

* chore: update use-order-validation test after consumed margin check hook changed

* fix: circular deps issue in console-lite

* chore: update use order validation hook to use Schema rather than direct import
This commit is contained in:
Matthew Russell 2022-11-15 18:00:14 -06:00 committed by GitHub
parent d70bc0fdd5
commit 3077a634d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 798 additions and 412 deletions

View File

@ -11,8 +11,8 @@ import { InputSetter } from '../../components/input-setter';
import { IconNames } from '@blueprintjs/icons';
import {
DataTitle,
EST_SLIPPAGE,
ValueTooltipRow,
EST_SLIPPAGE,
} from '@vegaprotocol/deal-ticket';
interface DealTicketSlippageProps {

View File

@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom';
import { useForm, Controller } from 'react-hook-form';
import { Stepper } from '../stepper';
import type { DealTicketMarketFragment } from '@vegaprotocol/deal-ticket';
import { useOrderValidation } from './use-order-validation';
import {
useOrderCloseOut,
useOrderMargin,
@ -10,11 +11,7 @@ import {
useMaximumPositionSize,
useCalculateSlippage,
} from '@vegaprotocol/deal-ticket';
import {
getDefaultOrder,
useOrderValidation,
validateSize,
} from '@vegaprotocol/deal-ticket';
import { getDefaultOrder, validateAmount } from '@vegaprotocol/deal-ticket';
import { InputError } from '@vegaprotocol/ui-toolkit';
import { BigNumber } from 'bignumber.js';
import { MarketSelector } from '@vegaprotocol/deal-ticket';
@ -180,7 +177,8 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
const newVal = new BigNumber(value)
.decimalPlaces(market.positionDecimalPlaces)
.toString();
const isValid = validateSize(step)(newVal);
// @ts-ignore validateAmount ts problem here
const isValid = validateAmount(step)(newVal);
if (isValid !== 'step') {
setValue('size', newVal);
}

View File

@ -6,12 +6,27 @@ import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { MarketStateMapping, Schema } from '@vegaprotocol/types';
import type { ValidationProps } from './use-order-validation';
import { marketTranslations, useOrderValidation } from './use-order-validation';
import type { DealTicketMarketFragment } from '../deal-ticket/__generated__/DealTicket';
import * as OrderMarginValidation from './use-order-margin-validation';
import { ValidateMargin } from './validate-margin';
import { ERROR_SIZE_DECIMAL } from '../constants';
import type { DealTicketMarketFragment } from '@vegaprotocol/deal-ticket';
import * as DealTicket from '@vegaprotocol/deal-ticket';
import BigNumber from 'bignumber.js';
jest.mock('@vegaprotocol/wallet');
jest.mock('@vegaprotocol/deal-ticket', () => {
return {
...jest.requireActual('@vegaprotocol/deal-ticket'),
useOrderMarginValidation: jest.fn(),
};
});
type SettlementAsset =
DealTicketMarketFragment['tradableInstrument']['instrument']['product']['settlementAsset'];
const asset: SettlementAsset = {
__typename: 'Asset',
id: 'asset-id',
symbol: 'asset-symbol',
name: 'asset-name',
decimals: 2,
};
const market: DealTicketMarketFragment = {
id: 'market-id',
@ -28,13 +43,7 @@ const market: DealTicketMarketFragment = {
product: {
__typename: 'Future',
quoteName: 'quote-name',
settlementAsset: {
__typename: 'Asset',
id: 'asset-id',
symbol: 'asset-symbol',
name: 'asset-name',
decimals: 2,
},
settlementAsset: asset,
},
},
},
@ -117,9 +126,11 @@ describe('useOrderValidation', () => {
});
it('Returns empty string when given valid data', () => {
jest
.spyOn(OrderMarginValidation, 'useOrderMarginValidation')
.mockReturnValue(false);
jest.spyOn(DealTicket, 'useOrderMarginValidation').mockReturnValue({
balance: new BigNumber(0),
margin: new BigNumber(100),
asset,
});
const { result } = setup();
expect(result.current).toStrictEqual({
@ -130,9 +141,11 @@ describe('useOrderValidation', () => {
});
it('Returns an error message when no keypair found', () => {
jest
.spyOn(OrderMarginValidation, 'useOrderMarginValidation')
.mockReturnValue(false);
jest.spyOn(DealTicket, 'useOrderMarginValidation').mockReturnValue({
balance: new BigNumber(0),
margin: new BigNumber(100),
asset,
});
const { result } = setup(defaultOrder, { pubKey: null });
expect(result.current).toStrictEqual({
isDisabled: false,
@ -169,9 +182,11 @@ describe('useOrderValidation', () => {
`(
'Returns an error message for market state suspended or pending',
({ state }) => {
jest
.spyOn(OrderMarginValidation, 'useOrderMarginValidation')
.mockReturnValue(false);
jest.spyOn(DealTicket, 'useOrderMarginValidation').mockReturnValue({
balance: new BigNumber(0),
margin: new BigNumber(100),
asset,
});
const { result } = setup({
market: {
...defaultOrder.market,
@ -259,7 +274,9 @@ describe('useOrderValidation', () => {
it('Returns an error message when the order size incorrectly has decimal values', () => {
const { result } = setup({
market: { ...market, positionDecimalPlaces: 0 },
fieldErrors: { size: { type: `validate`, message: ERROR_SIZE_DECIMAL } },
fieldErrors: {
size: { type: `validate`, message: DealTicket.ERROR_SIZE_DECIMAL },
},
});
expect(result.current).toStrictEqual({
isDisabled: true,
@ -270,7 +287,9 @@ describe('useOrderValidation', () => {
it('Returns an error message when the order size has more decimals than allowed', () => {
const { result } = setup({
fieldErrors: { size: { type: `validate`, message: ERROR_SIZE_DECIMAL } },
fieldErrors: {
size: { type: `validate`, message: DealTicket.ERROR_SIZE_DECIMAL },
},
});
expect(result.current).toStrictEqual({
isDisabled: true,
@ -281,20 +300,26 @@ describe('useOrderValidation', () => {
it('Returns an error message when the estimated margin is higher than collateral', async () => {
const invalidatedMockValue = {
balance: '0000000,1',
margin: '000,1',
id: 'instrument-id',
symbol: 'asset-symbol',
decimals: 5,
balance: new BigNumber(100),
margin: new BigNumber(200),
asset,
};
jest
.spyOn(OrderMarginValidation, 'useOrderMarginValidation')
.spyOn(DealTicket, 'useOrderMarginValidation')
.mockReturnValue(invalidatedMockValue);
const { result } = setup({});
expect(result.current.isDisabled).toBe(true);
expect(result.current.isDisabled).toBe(false);
const testElement = <ValidateMargin {...invalidatedMockValue} />;
const testElement = (
<DealTicket.MarginWarning
margin={invalidatedMockValue.margin.toString()}
balance={invalidatedMockValue.balance.toString()}
asset={invalidatedMockValue.asset}
/>
);
expect((result.current.message as React.ReactElement)?.props).toEqual(
testElement.props
);
@ -312,7 +337,7 @@ describe('useOrderValidation', () => {
({ state }) => {
const { result } = setup({
fieldErrors: {
size: { type: `validate`, message: ERROR_SIZE_DECIMAL },
size: { type: `validate`, message: DealTicket.ERROR_SIZE_DECIMAL },
},
market: {
...market,

View File

@ -6,23 +6,30 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
import { MarketStateMapping, Schema } from '@vegaprotocol/types';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import { MarketDataGrid } from '../trading-mode-tooltip';
import { compileGridData } from '../trading-mode-tooltip/compile-grid-data';
import type { DealTicketMarketFragment } from '../deal-ticket/__generated__/DealTicket';
import { ValidateMargin } from './validate-margin';
import type { OrderMargin } from '../../hooks/use-order-margin';
import { useOrderMarginValidation } from './use-order-margin-validation';
import { ERROR_EXPIRATION_IN_THE_PAST } from './validate-expiration';
import { DEAL_TICKET_SECTION, ERROR_SIZE_DECIMAL } from '../constants';
import type {
DealTicketMarketFragment,
OrderMargin,
} from '@vegaprotocol/deal-ticket';
import {
MarketDataGrid,
compileGridData,
MarginWarning,
isMarketInAuction,
ERROR_SIZE_DECIMAL,
useOrderMarginValidation,
} from '@vegaprotocol/deal-ticket';
export const isMarketInAuction = (market: DealTicketMarketFragment) => {
return [
Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION,
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
].includes(market.tradingMode);
export const DEAL_TICKET_SECTION = {
TYPE: 'sec-type',
SIZE: 'sec-size',
PRICE: 'sec-price',
FORCE: 'sec-force',
EXPIRY: 'sec-expiry',
SUMMARY: 'sec-summary',
};
export const ERROR_EXPIRATION_IN_THE_PAST = 'ERROR_EXPIRATION_IN_THE_PAST';
export type ValidationProps = {
step?: number;
market: DealTicketMarketFragment;
@ -336,10 +343,19 @@ export const useOrderValidation = ({
return fieldErrorChecking;
}
if (isInvalidOrderMargin) {
if (
isInvalidOrderMargin.balance.isGreaterThan(0) &&
isInvalidOrderMargin.balance.isLessThan(isInvalidOrderMargin.margin)
) {
return {
isDisabled: true,
message: <ValidateMargin {...isInvalidOrderMargin} />,
isDisabled: false,
message: (
<MarginWarning
margin={isInvalidOrderMargin.margin.toString()}
balance={isInvalidOrderMargin.balance.toString()}
asset={isInvalidOrderMargin.asset}
/>
),
section: DEAL_TICKET_SECTION.PRICE,
};
}

View File

@ -235,7 +235,7 @@ describe('market states', { tags: '@smoke' }, function () {
//7002-/SORD-/061 no state displayed
it('must display that market is not accepting orders', function () {
cy.getByTestId('place-order').click();
cy.getByTestId('dealticket-error-message').should(
cy.getByTestId('dealticket-error-message-summary').should(
'have.text',
`This market is ${marketState
.split('_')

View File

@ -235,11 +235,6 @@ describe('must submit order', { tags: '@smoke' }, () => {
new Date(order.expiresAt as string).getTime().toString() + '000000',
});
});
it.skip('must not allow to place an order if balance is 0 (no collateral)', function () {
//7002-/SORD-/003
// it will be covered in https://github.com/vegaprotocol/frontend-monorepo/issues/1660
});
});
describe(
@ -277,7 +272,7 @@ describe(
side: 'SIDE_SELL',
size: '100',
price: '50000',
timeInForce: 'TIME_IN_FORCE_GFN',
timeInForce: 'TIME_IN_FORCE_GTC',
};
testOrder(order, { price: '5000000000' });
});
@ -302,7 +297,7 @@ describe(
);
describe(
'must submit order for market in batch auction',
'must submit order for market in opening auction',
{ tags: '@regression' },
() => {
before(() => {
@ -336,7 +331,7 @@ describe(
side: 'SIDE_SELL',
size: '100',
price: '50000',
timeInForce: 'TIME_IN_FORCE_GFN',
timeInForce: 'TIME_IN_FORCE_GTC',
};
testOrder(order, { price: '5000000000' });
});
@ -361,7 +356,7 @@ describe(
);
describe(
'must submit order for market in batch auction',
'must submit order for market in monitoring auction',
{ tags: '@regression' },
() => {
before(() => {
@ -395,7 +390,7 @@ describe(
side: 'SIDE_SELL',
size: '100',
price: '50000',
timeInForce: 'TIME_IN_FORCE_GFN',
timeInForce: 'TIME_IN_FORCE_GTC',
};
testOrder(order, { price: '5000000000' });
});
@ -474,6 +469,7 @@ describe('deal ticket size validation', { tags: '@smoke' }, function () {
cy.wait('@Market');
connectVegaWallet();
});
it('must warn if order size input has too many digits after the decimal place', function () {
//7002-SORD-016
cy.getByTestId('order-type-TYPE_MARKET').click();
@ -481,9 +477,9 @@ describe('deal ticket size validation', { tags: '@smoke' }, function () {
cy.getByTestId(placeOrderBtn).should('not.be.disabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId(placeOrderBtn).should('be.disabled');
cy.getByTestId('dealticket-error-message-price-market').should(
cy.getByTestId('dealticket-error-message-size-market').should(
'have.text',
'Order sizes must be in whole numbers for this market'
'Size must be whole numbers for this market'
);
});
@ -493,9 +489,9 @@ describe('deal ticket size validation', { tags: '@smoke' }, function () {
cy.getByTestId(placeOrderBtn).should('not.be.disabled');
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId(placeOrderBtn).should('be.disabled');
cy.getByTestId('dealticket-error-message-price-market').should(
cy.getByTestId('dealticket-error-message-size-market').should(
'have.text',
'Size cannot be lower than "1"'
'Size cannot be lower than 1'
);
});
});
@ -513,7 +509,7 @@ describe('limit order validations', { tags: '@smoke' }, () => {
//7002-SORD-018
cy.getByTestId(orderPriceField)
.siblings('label')
.should('have.text', 'Price (BTC)');
.should('have.text', 'Price (tBTC)');
});
it('must see warning when placing an order with expiry date in past', function () {
@ -529,15 +525,22 @@ describe('limit order validations', { tags: '@smoke' }, () => {
cy.getByTestId(placeOrderBtn).click();
cy.getByTestId('dealticket-error-message-force').should(
cy.getByTestId('dealticket-error-message-expiry').should(
'have.text',
'The expiry date that you have entered appears to be in the past'
);
});
it.skip('must receive warning if price has too many digits after decimal place', function () {
//7002/-SORD-/059
// Skipped until https://github.com/vegaprotocol/frontend-monorepo/issues/1686 resolved
it('must see warning if price has too many digits after decimal place', function () {
//7002-SORD-059
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderTIFDropDown).select('TIME_IN_FORCE_GTC');
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(orderPriceField).clear().type('1.123456');
cy.getByTestId('dealticket-error-message-price-limit').should(
'have.text',
'Price accepts up to 5 decimal places'
);
});
describe('time in force validations', function () {
@ -651,7 +654,7 @@ describe('suspended market validation', { tags: '@regression' }, () => {
cy.getByTestId(orderPriceField).clear().type('0.1');
cy.getByTestId(orderSizeField).clear().type('1');
cy.getByTestId(placeOrderBtn).should('be.enabled');
cy.getByTestId(errorMessage).should(
cy.getByTestId('dealticket-warning-auction').should(
'have.text',
'Any orders placed now will not trade until the auction ends'
);
@ -663,15 +666,15 @@ describe('suspended market validation', { tags: '@regression' }, () => {
TIFlist.filter((item) => item.code === 'FOK')[0].value
);
cy.getByTestId(placeOrderBtn).should('be.disabled');
cy.getByTestId('dealticket-error-message-force').should(
cy.getByTestId('dealticket-error-message-tif').should(
'have.text',
'This market is in auction until it reaches sufficient liquidity. Until the auction ends, you can only place GFA, GTT, or GTC limit orders'
);
});
});
describe('margin required validation', { tags: '@regression' }, () => {
before(() => {
describe('account validation', { tags: '@regression' }, () => {
beforeEach(() => {
cy.mockTradingPage();
cy.mockGQL((req) => {
aliasQuery(
@ -689,15 +692,25 @@ describe('margin required validation', { tags: '@regression' }, () => {
cy.wait('@Market');
});
it('should display info and button for deposit', () => {
it('should show an error if your balance is zero', () => {
cy.getByTestId('place-order').should('not.be.disabled');
cy.getByTestId('place-order').click();
cy.getByTestId('place-order').should('be.disabled');
cy.getByTestId('deal-ticket-margin-invalidated').should(
'contain.text',
"You don't have enough margin available to open this position"
//7002-SORD-003
cy.getByTestId('dealticket-error-message-zero-balance').should(
'have.text',
'Insufficient balance. Deposit ' + 'tBTC'
);
cy.getByTestId('deal-ticket-margin-invalidated').should(
cy.getByTestId('deal-ticket-deposit-dialog-button').should('exist');
});
it('should display info and button for deposit', () => {
// warning should show immediately
cy.getByTestId('dealticket-warning-margin').should(
'contain.text',
'You may not have enough margin available to open this position'
);
cy.getByTestId('dealticket-warning-margin').should(
'contain.text',
'0.01 tBTC currently required, 0.001 tBTC available'
);

View File

@ -3,7 +3,7 @@ import type { ReactNode } from 'react';
import { t } from '@vegaprotocol/react-helpers';
import { Icon, Tooltip, TrafficLight } from '@vegaprotocol/ui-toolkit';
import { IconNames } from '@blueprintjs/icons';
import * as constants from './constants';
import * as constants from '../constants';
interface DealTicketEstimatesProps {
quoteName?: string;

View File

@ -1,5 +1 @@
export * from './get-default-order';
export * from './use-order-validation';
export * from './validate-size';
export * from './use-persisted-order';
export * from './validate-expiration';
export * from './margin-warning';

View File

@ -1,52 +1,48 @@
import { normalizeFormatNumber, t } from '@vegaprotocol/react-helpers';
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
import React from 'react';
import { useState } from 'react';
import { DepositDialog } from '@vegaprotocol/deposits';
interface Props {
margin: string;
symbol: string;
id: string;
balance: string;
decimals: number;
asset: {
id: string;
symbol: string;
decimals: number;
};
}
export const ValidateMargin = ({
margin,
symbol,
id,
balance,
decimals,
}: Props) => {
export const MarginWarning = ({ margin, balance, asset }: Props) => {
const [depositDialog, setDepositDialog] = useState(false);
return (
<>
<div
className="flex flex-col center pb-3"
data-testid="deal-ticket-margin-invalidated"
className="text-sm text-vega-orange mb-4"
data-testid="dealticket-warning-margin"
>
<p className="mb-2">
{t("You don't have enough margin available to open this position.")}{' '}
{t('You may not have enough margin available to open this position.')}{' '}
<ButtonLink
data-testid="deal-ticket-deposit-dialog-button"
onClick={() => setDepositDialog(true)}
>
{t(`Deposit ${symbol}`)}
{t(`Deposit ${asset.symbol}`)}
</ButtonLink>
</p>
<p>
{`${normalizeFormatNumber(margin, decimals)} ${symbol} ${t(
'currently required'
)}, ${normalizeFormatNumber(balance, decimals)} ${symbol} ${t(
'available'
)}`}
{`${normalizeFormatNumber(margin, asset.decimals)} ${
asset.symbol
} ${t('currently required')}, ${normalizeFormatNumber(
balance,
asset.decimals
)} ${asset.symbol} ${t('available')}`}
</p>
</div>
<DepositDialog
depositDialog={depositDialog}
setDepositDialog={setDepositDialog}
assetId={id}
assetId={asset.id}
/>
</>
);

View File

@ -1,12 +0,0 @@
import { ERROR_SIZE_DECIMAL } from '../constants';
export const validateSize = (step: number) => {
const [, stepDecimals = ''] = String(step).split('.');
return (value: string) => {
const [, valueDecimals = ''] = value.split('.');
if (stepDecimals.length < valueDecimals.length) {
return ERROR_SIZE_DECIMAL;
}
return true;
};
};

View File

@ -0,0 +1,35 @@
import { t } from '@vegaprotocol/react-helpers';
import { ButtonLink, InputError } from '@vegaprotocol/ui-toolkit';
import { useState } from 'react';
import { DepositDialog } from '@vegaprotocol/deposits';
interface ZeroBalanceErrorProps {
asset: {
id: string;
symbol: string;
};
}
export const ZeroBalanceError = ({ asset }: ZeroBalanceErrorProps) => {
const [depositDialog, setDepositDialog] = useState(false);
return (
<>
<InputError data-testid="dealticket-error-message-zero-balance">
<p className="mb-2">
{t('Insufficient balance. ')}
<ButtonLink
data-testid="deal-ticket-deposit-dialog-button"
onClick={() => setDepositDialog(true)}
>
{t(`Deposit ${asset.symbol}`)}
</ButtonLink>
</p>
</InputError>
<DepositDialog
depositDialog={depositDialog}
setDepositDialog={setDepositDialog}
assetId={asset.id}
/>
</>
);
};

View File

@ -1,18 +1,16 @@
import type { UseFormRegister } from 'react-hook-form';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { DealTicketMarketAmount } from './deal-ticket-market-amount';
import { DealTicketLimitAmount } from './deal-ticket-limit-amount';
import type { DealTicketMarketFragment } from './__generated__/DealTicket';
import { Schema } from '@vegaprotocol/types';
import type { DealTicketErrorMessage } from './deal-ticket-error';
import type { DealTicketFormFields } from './deal-ticket';
export interface DealTicketAmountProps {
orderType: Schema.OrderType;
market: DealTicketMarketFragment;
register: UseFormRegister<OrderSubmissionBody['orderSubmission']>;
quoteName: string;
price?: string;
errorMessage?: DealTicketErrorMessage;
register: UseFormRegister<DealTicketFormFields>;
sizeError?: string;
priceError?: string;
}
export const DealTicketAmount = ({

View File

@ -2,41 +2,28 @@ import { t } from '@vegaprotocol/react-helpers';
import { Button } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { DEAL_TICKET_SECTION } from '../constants';
import { DealTicketError } from './deal-ticket-error';
import type { DealTicketErrorMessage } from './deal-ticket-error';
interface Props {
transactionStatus: 'default' | 'pending';
isDisabled: boolean;
errorMessage?: DealTicketErrorMessage;
disabled: boolean;
}
export const DealTicketButton = ({
transactionStatus,
errorMessage,
isDisabled,
}: Props) => {
export const DealTicketButton = ({ transactionStatus, disabled }: Props) => {
const { pubKey } = useVegaWallet();
const { openVegaWalletDialog } = useVegaWalletDialogStore((store) => ({
openVegaWalletDialog: store.openVegaWalletDialog,
}));
const isPending = transactionStatus === 'pending';
return pubKey ? (
<div className="mb-6">
<div className="mb-4">
<Button
variant="primary"
fill
type="submit"
disabled={isDisabled}
disabled={disabled || isPending}
data-testid="place-order"
>
{transactionStatus === 'pending' ? t('Pending...') : t('Place order')}
{isPending ? t('Pending...') : t('Place order')}
</Button>
<DealTicketError
errorMessage={errorMessage}
data-testid="dealticket-error-message"
section={DEAL_TICKET_SECTION.SUMMARY}
/>
</div>
) : (
<Button

View File

@ -1,34 +0,0 @@
import type { ReactNode } from 'react';
import { InputError } from '@vegaprotocol/ui-toolkit';
import type { DealTicketSection } from '../deal-ticket-validation';
export interface DealTicketErrorMessage {
message: ReactNode | string;
isDisabled: boolean;
errorSection: DealTicketSection;
}
interface Props {
errorMessage?: DealTicketErrorMessage;
'data-testid'?: string;
section: DealTicketSection | DealTicketSection[];
}
export const DealTicketError = ({
errorMessage,
'data-testid': dataTestId = 'deal-ticket-error-message',
section,
}: Props) =>
errorMessage &&
(Array.isArray(section) ? section : [section]).includes(
errorMessage.errorSection
) ? (
<div className="-mt-1">
<InputError
intent={errorMessage.isDisabled ? 'danger' : 'warning'}
data-testid={dataTestId}
>
{errorMessage.message}
</InputError>
</div>
) : null;

View File

@ -1,9 +1,7 @@
import { FormGroup, Input } from '@vegaprotocol/ui-toolkit';
import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit';
import { t, toDecimal } from '@vegaprotocol/react-helpers';
import type { DealTicketAmountProps } from './deal-ticket-amount';
import { validateSize } from '../deal-ticket-validation';
import { DealTicketError } from './deal-ticket-error';
import { DEAL_TICKET_SECTION } from '../constants';
import { validateAmount } from '../../utils';
export type DealTicketLimitAmountProps = Omit<
DealTicketAmountProps,
@ -13,11 +11,33 @@ export type DealTicketLimitAmountProps = Omit<
export const DealTicketLimitAmount = ({
register,
market,
quoteName,
errorMessage,
sizeError,
priceError,
}: DealTicketLimitAmountProps) => {
const priceStep = toDecimal(market?.decimalPlaces);
const sizeStep = toDecimal(market?.positionDecimalPlaces);
const quoteName =
market.tradableInstrument.instrument.product.settlementAsset.symbol;
const renderError = () => {
if (sizeError) {
return (
<InputError data-testid="dealticket-error-message-size-limit">
{sizeError}
</InputError>
);
}
if (priceError) {
return (
<InputError data-testid="dealticket-error-message-price-limit">
{priceError}
</InputError>
);
}
return null;
};
return (
<div className="mb-6">
@ -37,9 +57,12 @@ export const DealTicketLimitAmount = ({
data-testid="order-size"
onWheel={(e) => e.currentTarget.blur()}
{...register('size', {
required: true,
min: sizeStep,
validate: validateSize(sizeStep),
required: t('You need to provide a size'),
min: {
value: sizeStep,
message: t('Size cannot be lower than ' + sizeStep),
},
validate: validateAmount(sizeStep, 'Size'),
})}
/>
</FormGroup>
@ -63,18 +86,19 @@ export const DealTicketLimitAmount = ({
data-testid="order-price"
onWheel={(e) => e.currentTarget.blur()}
{...register('price', {
required: true,
min: 0,
required: t('You need provide a price'),
min: {
value: priceStep,
message: t('Price cannot be lower than ' + priceStep),
},
// @ts-ignore this fulfills the interface but still errors
validate: validateAmount(priceStep, 'Price'),
})}
/>
</FormGroup>
</div>
</div>
<DealTicketError
errorMessage={errorMessage}
data-testid="dealticket-error-message-price-limit"
section={[DEAL_TICKET_SECTION.SIZE, DEAL_TICKET_SECTION.PRICE]}
/>
{renderError()}
</div>
);
};

View File

@ -1,10 +1,10 @@
import { formatNumber, t, toDecimal } from '@vegaprotocol/react-helpers';
import { FormGroup, Input, Tooltip } from '@vegaprotocol/ui-toolkit';
import { DEAL_TICKET_SECTION } from '../constants';
import { isMarketInAuction } from '../deal-ticket-validation/use-order-validation';
import { validateSize } from '../deal-ticket-validation/validate-size';
import { DealTicketError } from './deal-ticket-error';
import {
addDecimalsFormatNumber,
t,
toDecimal,
} from '@vegaprotocol/react-helpers';
import { Input, InputError, Tooltip } from '@vegaprotocol/ui-toolkit';
import { isMarketInAuction, validateAmount } from '../../utils';
import type { DealTicketAmountProps } from './deal-ticket-amount';
@ -15,71 +15,88 @@ export type DealTicketMarketAmountProps = Omit<
export const DealTicketMarketAmount = ({
register,
price,
market,
quoteName,
errorMessage,
sizeError,
}: DealTicketMarketAmountProps) => {
const quoteName =
market.tradableInstrument.instrument.product.settlementAsset.symbol;
const sizeStep = toDecimal(market?.positionDecimalPlaces);
let price;
if (isMarketInAuction(market)) {
// 0 can never be a valid uncrossing price
// as it would require there being orders on the book at that price.
if (
market.data?.indicativePrice &&
market.data.indicativePrice !== '0' &&
BigInt(market.data?.indicativePrice) !== BigInt(0)
) {
price = market.data.indicativePrice;
}
} else {
price = market.depth.lastTrade?.price;
}
const priceFormatted = price
? addDecimalsFormatNumber(price, market.decimalPlaces)
: undefined;
return (
<div className="mb-6">
<div className="flex items-center gap-4 relative">
<div className="flex-1">
<FormGroup
label={t('Size')}
labelFor="input-order-size-market"
className="!mb-1"
>
<Input
id="input-order-size-market"
className="w-full"
type="number"
step={sizeStep}
min={sizeStep}
onWheel={(e) => e.currentTarget.blur()}
data-testid="order-size"
{...register('size', {
required: true,
min: sizeStep,
validate: validateSize(sizeStep),
})}
/>
</FormGroup>
</div>
<div className="flex-0 items-center">
<div className="flex">&nbsp;</div>
<div className="flex">@</div>
</div>
<div className="flex-1" data-testid="last-price">
{isMarketInAuction(market) ? (
<div className="flex items-end gap-4 mb-2">
<div className="flex-1 text-sm">Size</div>
<div />
<div className="flex-1 text-sm text-right">
{isMarketInAuction(market) && (
<Tooltip
description={t(
'This market is in auction. The uncrossing price is an indication of what the price is expected to be when the auction ends.'
)}
>
<div className="absolute top-0 right-0 text-xs">
{t(`Estimated uncrossing price`)}
</div>
<div>{t(`Estimated uncrossing price`)}</div>
</Tooltip>
) : (
<div>&nbsp;</div>
)}
<div className="text-sm text-right">
{price && quoteName ? (
<>
~{formatNumber(price, market.decimalPlaces)} {quoteName}
</>
) : (
'-'
)}
</div>
</div>
</div>
<DealTicketError
errorMessage={errorMessage}
data-testid="dealticket-error-message-price-market"
section={[DEAL_TICKET_SECTION.SIZE, DEAL_TICKET_SECTION.PRICE]}
/>
<div className="flex items-center gap-4">
<div className="flex-1">
<Input
id="input-order-size-market"
className="w-full"
type="number"
step={sizeStep}
min={sizeStep}
onWheel={(e) => e.currentTarget.blur()}
data-testid="order-size"
{...register('size', {
required: t('You need to provide a size'),
min: {
value: sizeStep,
message: t('Size cannot be lower than ' + sizeStep),
},
validate: validateAmount(sizeStep, 'Size'),
})}
/>
</div>
<div>@</div>
<div className="flex-1 text-sm text-right" data-testid="last-price">
{priceFormatted && quoteName ? (
<>
~{priceFormatted} {quoteName}
</>
) : (
'-'
)}
</div>
</div>
{sizeError && (
<InputError
intent="danger"
data-testid="dealticket-error-message-size-market"
>
{sizeError}
</InputError>
)}
</div>
);
};

View File

@ -110,7 +110,7 @@ describe('DealTicket', () => {
expect(screen.getByTestId('last-price')).toHaveTextContent(
// eslint-disable-next-line
`~${addDecimal(market.depth.lastTrade!.price, market.decimalPlaces)} ${
market.tradableInstrument.instrument.product.quoteName
market.tradableInstrument.instrument.product.settlementAsset.symbol
}`
);
});

View File

@ -1,17 +1,12 @@
import { addDecimal, removeDecimal } from '@vegaprotocol/react-helpers';
import { removeDecimal, t } from '@vegaprotocol/react-helpers';
import { Schema } from '@vegaprotocol/types';
import { useCallback, useEffect, useState } from 'react';
import { memo, useCallback, useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form';
import {
getFeeDetailsValues,
useFeeDealTicketDetails,
} from '../../hooks/use-fee-deal-ticket-details';
import { getDefaultOrder, usePersistedOrder } from '../deal-ticket-validation';
import {
isMarketInAuction,
useOrderValidation,
} from '../deal-ticket-validation/use-order-validation';
import { DealTicketAmount } from './deal-ticket-amount';
import { DealTicketButton } from './deal-ticket-button';
import { DealTicketFeeDetails } from './deal-ticket-fee-details';
@ -22,8 +17,21 @@ import { TypeSelector } from './type-selector';
import type { DealTicketMarketFragment } from './__generated__/DealTicket';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import type { DealTicketErrorMessage } from './deal-ticket-error';
import { DEAL_TICKET_SECTION } from '../constants';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { InputError } from '@vegaprotocol/ui-toolkit';
import { useOrderMarginValidation } from '../../hooks/use-order-margin-validation';
import { MarginWarning } from '../deal-ticket-validation/margin-warning';
import { usePersistedOrder } from '../../hooks/use-persisted-order';
import {
getDefaultOrder,
validateMarketState,
validateMarketTradingMode,
validateTimeInForce,
validateType,
} from '../../utils';
import { ZeroBalanceError } from '../deal-ticket-validation/zero-balance-error';
import { AccountValidationType } from '../../constants';
import type BigNumber from 'bignumber.js';
export type TransactionStatus = 'default' | 'pending';
@ -34,116 +42,104 @@ export interface DealTicketProps {
defaultOrder?: OrderSubmissionBody['orderSubmission'];
}
export type DealTicketFormFields = OrderSubmissionBody['orderSubmission'] & {
// This is not a field used in the form but allows us to set a
// summary error message
summary: string;
};
export const DealTicket = ({
market,
submit,
transactionStatus,
}: DealTicketProps) => {
const [errorMessage, setErrorMessage] = useState<
DealTicketErrorMessage | undefined
>(undefined);
const [persistedOrder, setOrder] = usePersistedOrder(market);
const { pubKey } = useVegaWallet();
const [persistedOrder, setPersistedOrder] = usePersistedOrder(market);
const {
register,
control,
handleSubmit,
watch,
setValue,
clearErrors,
setError,
formState: { errors, isSubmitted },
} = useForm<OrderSubmissionBody['orderSubmission']>({
mode: 'onChange',
formState: { errors },
} = useForm<DealTicketFormFields>({
defaultValues: persistedOrder || getDefaultOrder(market),
});
const order = watch();
const order = watch();
const feeDetails = useFeeDealTicketDetails(order, market);
const details = getFeeDetailsValues(feeDetails);
useEffect(() => setOrder(order), [order, setOrder]);
// When order state changes persist it in local storage
useEffect(() => setPersistedOrder(order), [order, setPersistedOrder]);
const {
isDisabled: disabled,
message,
section: errorSection,
} = useOrderValidation({
const accountData = useOrderMarginValidation({
market,
orderType: order.type,
orderTimeInForce: order.timeInForce,
fieldErrors: errors,
estMargin: feeDetails.estMargin,
});
useEffect(() => {
if (disabled) {
setError('marketId', {});
} else {
clearErrors('marketId');
}
}, [disabled, setError, clearErrors]);
useEffect(() => {
if (isSubmitted || errorSection === DEAL_TICKET_SECTION.SUMMARY) {
setErrorMessage({ message, isDisabled: disabled, errorSection });
} else {
setErrorMessage(undefined);
}
}, [disabled, message, errorSection, isSubmitted]);
const isDisabled = transactionStatus === 'pending' || disabled;
const onSubmit = useCallback(
(order: OrderSubmissionBody['orderSubmission']) => {
if (!isDisabled) {
submit({
...order,
price:
order.price && removeDecimal(order.price, market.decimalPlaces),
size: removeDecimal(order.size, market.positionDecimalPlaces),
expiresAt:
order.timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
? order.expiresAt
: undefined,
});
if (!pubKey) {
setError('summary', { message: t('No public key selected') });
return;
}
},
[isDisabled, submit, market.decimalPlaces, market.positionDecimalPlaces]
);
const getEstimatedMarketPrice = () => {
if (isMarketInAuction(market)) {
// 0 can never be a valid uncrossing price
// as it would require there being orders on the book at that price.
if (
market.data?.indicativePrice &&
BigInt(market.data?.indicativePrice) !== BigInt(0)
) {
return market.data.indicativePrice;
const marketStateError = validateMarketState(market.state);
if (marketStateError !== true) {
setError('summary', { message: marketStateError });
return;
}
return undefined;
}
return market.depth.lastTrade?.price;
};
const marketPrice = getEstimatedMarketPrice();
const marketPriceFormatted =
marketPrice && addDecimal(marketPrice, market.decimalPlaces);
useEffect(() => {
if (marketPriceFormatted && order.type === Schema.OrderType.TYPE_MARKET) {
setValue('price', marketPriceFormatted);
}
}, [marketPriceFormatted, order.type, setValue]);
if (accountData.balance.isZero()) {
setError('summary', { message: AccountValidationType.NoCollateral });
return;
}
const marketTradingModeError = validateMarketTradingMode(
market.tradingMode
);
if (marketTradingModeError !== true) {
setError('summary', { message: marketTradingModeError });
return;
}
submit({
...order,
price: order.price && removeDecimal(order.price, market.decimalPlaces),
size: removeDecimal(order.size, market.positionDecimalPlaces),
expiresAt:
order.timeInForce === Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
? order.expiresAt
: undefined,
});
},
[
submit,
pubKey,
accountData,
market.positionDecimalPlaces,
market.decimalPlaces,
market.state,
market.tradingMode,
setError,
]
);
return (
<form onSubmit={handleSubmit(onSubmit)} className="p-4" noValidate>
<Controller
name="type"
control={control}
rules={{
validate: validateType(market),
}}
render={({ field }) => (
<TypeSelector
value={field.value}
onSelect={field.onChange}
errorMessage={errorMessage}
market={market}
errorMessage={errors.type?.message}
/>
)}
/>
@ -158,19 +154,22 @@ export const DealTicket = ({
orderType={order.type}
market={market}
register={register}
price={order.price}
quoteName={market.tradableInstrument.instrument.product.quoteName}
errorMessage={errorMessage}
sizeError={errors.size?.message}
priceError={errors.price?.message}
/>
<Controller
name="timeInForce"
control={control}
rules={{
validate: validateTimeInForce(market),
}}
render={({ field }) => (
<TimeInForceSelector
value={field.value}
orderType={order.type}
onSelect={field.onChange}
errorMessage={errorMessage}
market={market}
errorMessage={errors.timeInForce?.message}
/>
)}
/>
@ -183,18 +182,103 @@ export const DealTicket = ({
<ExpirySelector
value={field.value}
onSelect={field.onChange}
errorMessage={errorMessage}
errorMessage={errors.expiresAt?.message}
register={register}
/>
)}
/>
)}
<DealTicketButton
disabled={Object.keys(errors).length >= 1}
transactionStatus={transactionStatus}
isDisabled={isSubmitted && isDisabled}
errorMessage={errorMessage}
/>
<SummaryMessage
errorMessage={errors.summary?.message}
market={market}
accountData={accountData}
/>
<DealTicketFeeDetails details={details} />
</form>
);
};
/**
* Renders an error message if errors.summary is present otherwise
* renders warnings about current state of the market
*/
interface SummaryMessageProps {
errorMessage?: string;
market: DealTicketMarketFragment;
accountData: {
balance: BigNumber;
margin: BigNumber;
asset: {
id: string;
symbol: string;
decimals: number;
name: string;
};
};
}
const SummaryMessage = memo(
({ errorMessage, market, accountData }: SummaryMessageProps) => {
// Specific error UI for if balance is so we can
// render a deposit dialog
if (errorMessage === AccountValidationType.NoCollateral) {
return (
<ZeroBalanceError
asset={market.tradableInstrument.instrument.product.settlementAsset}
/>
);
}
// If we have any other full error which prevents
// submission render that first
if (errorMessage) {
return (
<div className="mb-4">
<InputError data-testid="dealticket-error-message-summary">
{errorMessage}
</InputError>
</div>
);
}
// If there is no blocking error but user doesn't have enough
// balance render the margin warning, but still allow submission
if (
accountData.balance.isGreaterThan(0) &&
accountData.balance.isLessThan(accountData.margin)
) {
return (
<MarginWarning
balance={accountData.balance.toString()}
margin={accountData.margin.toString()}
asset={accountData.asset}
/>
);
}
// Show auction mode warning
if (
[
Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION,
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
].includes(market.tradingMode)
) {
return (
<div
className="text-sm text-vega-orange mb-4"
data-testid="dealticket-warning-auction"
>
<p>
{t('Any orders placed now will not trade until the auction ends')}
</p>
</div>
);
}
return null;
}
);

View File

@ -1,18 +1,15 @@
import { FormGroup, Input } from '@vegaprotocol/ui-toolkit';
import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit';
import { formatForInput } from '@vegaprotocol/react-helpers';
import { t } from '@vegaprotocol/react-helpers';
import type { UseFormRegister } from 'react-hook-form';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { validateExpiration } from '../deal-ticket-validation/validate-expiration';
import type { DealTicketErrorMessage } from './deal-ticket-error';
import { DealTicketError } from './deal-ticket-error';
import { DEAL_TICKET_SECTION } from '../constants';
import { validateExpiration } from '../../utils/validate-expiration';
import type { DealTicketFormFields } from '.';
interface ExpirySelectorProps {
value?: string;
onSelect: (expiration: string | null) => void;
errorMessage?: DealTicketErrorMessage;
register?: UseFormRegister<OrderSubmissionBody['orderSubmission']>;
errorMessage?: string;
register?: UseFormRegister<DealTicketFormFields>;
}
export const ExpirySelector = ({
@ -37,11 +34,11 @@ export const ExpirySelector = ({
validate: validateExpiration,
})}
/>
<DealTicketError
errorMessage={errorMessage}
data-testid="dealticket-error-message-force"
section={DEAL_TICKET_SECTION.EXPIRY}
/>
{errorMessage && (
<InputError data-testid="dealticket-error-message-expiry">
{errorMessage}
</InputError>
)}
</FormGroup>
);
};

View File

@ -1,17 +1,23 @@
import { useEffect, useState } from 'react';
import { FormGroup, Select } from '@vegaprotocol/ui-toolkit';
import {
FormGroup,
InputError,
Select,
Tooltip,
} from '@vegaprotocol/ui-toolkit';
import { Schema } from '@vegaprotocol/types';
import { t } from '@vegaprotocol/react-helpers';
import { timeInForceLabel } from '@vegaprotocol/orders';
import type { DealTicketErrorMessage } from './deal-ticket-error';
import { DealTicketError } from './deal-ticket-error';
import { DEAL_TICKET_SECTION } from '../constants';
import type { DealTicketMarketFragment } from './__generated__/DealTicket';
import { compileGridData, MarketDataGrid } from '../trading-mode-tooltip';
import { MarketModeValidationType } from '../../constants';
interface TimeInForceSelectorProps {
value: Schema.OrderTimeInForce;
orderType: Schema.OrderType;
onSelect: (tif: Schema.OrderTimeInForce) => void;
errorMessage?: DealTicketErrorMessage;
market: DealTicketMarketFragment;
errorMessage?: string;
}
type OrderType = Schema.OrderType.TYPE_MARKET | Schema.OrderType.TYPE_LIMIT;
@ -27,6 +33,7 @@ export const TimeInForceSelector = ({
value,
orderType,
onSelect,
market,
errorMessage,
}: TimeInForceSelectorProps) => {
const options =
@ -60,6 +67,50 @@ export const TimeInForceSelector = ({
setPreviousOrderType,
]);
const renderError = (errorType: string) => {
if (errorType === MarketModeValidationType.Auction) {
return t(
`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={<MarketDataGrid grid={compileGridData(market)} />}
>
<span>{t('sufficient liquidity')}</span>
</Tooltip>
{'. '}
{t(
`Until the auction ends, you can only place GFA, GTT, or GTC limit orders`
)}
</span>
);
}
if (errorType === MarketModeValidationType.PriceMonitoringAuction) {
return (
<span>
{t('This market is in auction due to')}{' '}
<Tooltip
description={<MarketDataGrid grid={compileGridData(market)} />}
>
<span>{t('high price volatility')}</span>
</Tooltip>
{'. '}
{t(
`Until the auction ends, you can only place GFA, GTT, or GTC limit orders`
)}
</span>
);
}
return null;
};
return (
<FormGroup label={t('Time in force')} labelFor="select-time-in-force">
<Select
@ -81,11 +132,11 @@ export const TimeInForceSelector = ({
</option>
))}
</Select>
<DealTicketError
errorMessage={errorMessage}
data-testid="dealticket-error-message-force"
section={DEAL_TICKET_SECTION.FORCE}
/>
{errorMessage && (
<InputError data-testid="dealticket-error-message-tif">
{renderError(errorMessage)}
</InputError>
)}
</FormGroup>
);
};

View File

@ -1,15 +1,16 @@
import { FormGroup } from '@vegaprotocol/ui-toolkit';
import { FormGroup, InputError, Tooltip } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/react-helpers';
import { Schema } from '@vegaprotocol/types';
import { Toggle } from '@vegaprotocol/ui-toolkit';
import type { DealTicketErrorMessage } from './deal-ticket-error';
import { DealTicketError } from './deal-ticket-error';
import { DEAL_TICKET_SECTION } from '../constants';
import { compileGridData, MarketDataGrid } from '../trading-mode-tooltip';
import type { DealTicketMarketFragment } from './__generated__/DealTicket';
import { MarketModeValidationType } from '../../constants';
interface TypeSelectorProps {
value: Schema.OrderType;
onSelect: (type: Schema.OrderType) => void;
errorMessage?: DealTicketErrorMessage;
market: DealTicketMarketFragment;
errorMessage?: string;
}
const toggles = [
@ -20,8 +21,47 @@ const toggles = [
export const TypeSelector = ({
value,
onSelect,
market,
errorMessage,
}: TypeSelectorProps) => {
const renderError = (errorType: MarketModeValidationType) => {
if (errorType === MarketModeValidationType.Auction) {
return t('Only limit orders are permitted when market is in auction');
}
if (errorType === MarketModeValidationType.LiquidityMonitoringAuction) {
return (
<span>
{t('This market is in auction until it reaches')}{' '}
<Tooltip
description={<MarketDataGrid grid={compileGridData(market)} />}
>
<span>{t('sufficient liquidity')}</span>
</Tooltip>
{'. '}
{t('Only limit orders are permitted when market is in auction')}
</span>
);
}
if (errorType === MarketModeValidationType.PriceMonitoringAuction) {
return (
<span>
{t('This market is in auction due to')}{' '}
<Tooltip
description={<MarketDataGrid grid={compileGridData(market)} />}
>
<span>{t('high price volatility')}</span>
</Tooltip>
{'. '}
{t('Only limit orders are permitted when market is in auction')}
</span>
);
}
return null;
};
return (
<FormGroup label={t('Order type')} labelFor="order-type">
<Toggle
@ -31,11 +71,11 @@ export const TypeSelector = ({
checkedValue={value}
onChange={(e) => onSelect(e.target.value as Schema.OrderType)}
/>
<DealTicketError
errorMessage={errorMessage}
data-testid="dealticket-error-message-type"
section={DEAL_TICKET_SECTION.TYPE}
/>
{errorMessage && (
<InputError data-testid="dealticket-error-message-type">
{renderError(errorMessage as MarketModeValidationType)}
</InputError>
)}
</FormGroup>
);
};

View File

@ -2,4 +2,3 @@ export * from './deal-ticket';
export * from './deal-ticket-validation';
export * from './trading-mode-tooltip';
export * from './deal-ticket-estimates';
export * from './constants';

View File

@ -20,13 +20,16 @@ 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 DEAL_TICKET_SECTION = {
TYPE: 'sec-type',
SIZE: 'sec-size',
PRICE: 'sec-price',
FORCE: 'sec-force',
EXPIRY: 'sec-expiry',
SUMMARY: 'sec-summary',
};
export const ERROR_SIZE_DECIMAL = t(
'The size field accepts up to X decimal places'
);
export const ERROR_SIZE_DECIMAL = 'step';
export enum MarketModeValidationType {
PriceMonitoringAuction = 'PriceMonitoringAuction',
LiquidityMonitoringAuction = 'LiquidityMonitoringAuction',
Auction = 'Auction',
}
export enum AccountValidationType {
NoCollateral = 'NoCollateral',
}

View File

@ -10,4 +10,5 @@ export * from './use-market-positions';
export * from './use-maximum-position-size';
export * from './use-order-closeout';
export * from './use-order-margin';
export * from './use-order-margin-validation';
export * from './use-settlement-account';

View File

@ -13,7 +13,7 @@ import {
EST_CLOSEOUT_TOOLTIP_TEXT,
EST_MARGIN_TOOLTIP_TEXT,
NOTIONAL_SIZE_TOOLTIP_TEXT,
} from '../components/constants';
} from '../constants';
import { usePartyBalanceQuery } from './__generated__/PartyBalance';
import { useCalculateSlippage } from './use-calculate-slippage';
import { useOrderCloseOut } from './use-order-closeout';

View File

@ -2,9 +2,10 @@ import { useMemo } from 'react';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { Schema } from '@vegaprotocol/types';
import { toBigNum } from '@vegaprotocol/react-helpers';
import type { DealTicketMarketFragment } from '../deal-ticket/__generated__/DealTicket';
import type { OrderMargin } from '../../hooks/use-order-margin';
import { usePartyBalanceQuery, useSettlementAccount } from '../../hooks';
import type { DealTicketMarketFragment } from '../components/deal-ticket/__generated__/DealTicket';
import type { OrderMargin } from './use-order-margin';
import { usePartyBalanceQuery } from './__generated__/PartyBalance';
import { useSettlementAccount } from './use-settlement-account';
interface Props {
market: DealTicketMarketFragment;
@ -34,22 +35,15 @@ export const useOrderMarginValidation = ({ market, estMargin }: Props) => {
)
: toBigNum('0', assetDecimals);
const margin = toBigNum(estMargin?.margin || 0, assetDecimals);
const { id, symbol, decimals } =
market.tradableInstrument.instrument.product.settlementAsset;
const balanceString = balance.toString();
const marginString = margin.toString();
const asset = market.tradableInstrument.instrument.product.settlementAsset;
const memoizedValue = useMemo(() => {
return {
balance: balanceString,
margin: marginString,
id,
symbol,
decimals,
balance,
margin,
asset,
};
}, [balanceString, marginString, id, symbol, decimals]);
}, [balance, margin, asset]);
if (balance.isZero() || balance.isLessThan(margin)) {
return memoizedValue;
}
return false;
return memoizedValue;
};

View File

@ -1,2 +1,4 @@
export * from './components';
export * from './hooks';
export * from './utils';
export * from './constants';

View File

@ -0,0 +1,8 @@
export * from './get-default-order';
export * from './is-market-in-auction';
export * from './validate-amount';
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';

View File

@ -0,0 +1,10 @@
import { Schema } from '@vegaprotocol/types';
import type { DealTicketMarketFragment } from '../components';
export const isMarketInAuction = (market: DealTicketMarketFragment) => {
return [
Schema.MarketTradingMode.TRADING_MODE_BATCH_AUCTION,
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION,
Schema.MarketTradingMode.TRADING_MODE_OPENING_AUCTION,
].includes(market.tradingMode);
};

View File

@ -0,0 +1,16 @@
import { t } from '@vegaprotocol/react-helpers';
export const validateAmount = (step: number, field: string) => {
const [, stepDecimals = ''] = String(step).split('.');
return (value: string) => {
const [, valueDecimals = ''] = value.split('.');
if (stepDecimals.length < valueDecimals.length) {
if (stepDecimals === '') {
return t(`${field} must be whole numbers for this market`);
}
return t(`${field} accepts up to ${stepDecimals.length} decimal places`);
}
return true;
};
};

View File

@ -1,14 +1,13 @@
import { t } from '@vegaprotocol/react-helpers';
import type { Validate } from 'react-hook-form';
export const ERROR_EXPIRATION_IN_THE_PAST = 'ERROR_EXPIRATION_IN_THE_PAST';
export const validateExpiration: Validate<string | undefined> = (
value?: string
) => {
const now = new Date();
const valueAsDate = value ? new Date(value) : now;
if (now > valueAsDate) {
return ERROR_EXPIRATION_IN_THE_PAST;
return t('The expiry date that you have entered appears to be in the past');
}
return true;
};

View File

@ -0,0 +1,42 @@
import { t } from '@vegaprotocol/react-helpers';
import { MarketStateMapping, Schema } from '@vegaprotocol/types';
export const validateMarketState = (state: Schema.MarketState) => {
if (
[
Schema.MarketState.STATE_SETTLED,
Schema.MarketState.STATE_REJECTED,
Schema.MarketState.STATE_TRADING_TERMINATED,
Schema.MarketState.STATE_CANCELLED,
Schema.MarketState.STATE_CLOSED,
].includes(state)
) {
return t(
`This market is ${marketTranslations(state)} and not accepting orders`
);
}
if (
[
Schema.MarketState.STATE_PROPOSED,
Schema.MarketState.STATE_PENDING,
].includes(state)
) {
return t(
`This market is ${marketTranslations(
state
)} and only accepting liquidity commitment orders`
);
}
return true;
};
const marketTranslations = (marketState: Schema.MarketState) => {
switch (marketState) {
case Schema.MarketState.STATE_TRADING_TERMINATED:
return t('terminated');
default:
return t(MarketStateMapping[marketState]).toLowerCase();
}
};

View File

@ -0,0 +1,12 @@
import { t } from '@vegaprotocol/react-helpers';
import { Schema } from '@vegaprotocol/types';
export const validateMarketTradingMode = (
tradingMode: Schema.MarketTradingMode
) => {
if (tradingMode === Schema.MarketTradingMode.TRADING_MODE_NO_TRADING) {
return t('Trading terminated');
}
return true;
};

View File

@ -0,0 +1,38 @@
import { Schema } from '@vegaprotocol/types';
import type { DealTicketMarketFragment } from '../components';
import { MarketModeValidationType } from '../constants';
import { isMarketInAuction } from './is-market-in-auction';
export const validateTimeInForce = (market: DealTicketMarketFragment) => {
return (value: Schema.OrderTimeInForce) => {
const isMonitoringAuction =
market.tradingMode ===
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION;
const isPriceTrigger =
market.data?.trigger === Schema.AuctionTrigger.AUCTION_TRIGGER_PRICE;
const isLiquidityTrigger =
market.data?.trigger === Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY;
if (isMarketInAuction(market)) {
if (
[
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
Schema.OrderTimeInForce.TIME_IN_FORCE_GFN,
].includes(value)
) {
if (isMonitoringAuction && isLiquidityTrigger) {
return MarketModeValidationType.LiquidityMonitoringAuction;
}
if (isMonitoringAuction && isPriceTrigger) {
return MarketModeValidationType.PriceMonitoringAuction;
}
return MarketModeValidationType.Auction;
}
}
return true;
};
};

View File

@ -0,0 +1,31 @@
import { Schema } from '@vegaprotocol/types';
import type { DealTicketMarketFragment } from '../components';
import { MarketModeValidationType } from '../constants';
import { isMarketInAuction } from './is-market-in-auction';
export const validateType = (market: DealTicketMarketFragment) => {
return (value: Schema.OrderType) => {
if (isMarketInAuction(market) && value === Schema.OrderType.TYPE_MARKET) {
const isMonitoringAuction =
market.tradingMode ===
Schema.MarketTradingMode.TRADING_MODE_MONITORING_AUCTION;
const isPriceTrigger =
market.data?.trigger === Schema.AuctionTrigger.AUCTION_TRIGGER_PRICE;
const isLiquidityTrigger =
market.data?.trigger ===
Schema.AuctionTrigger.AUCTION_TRIGGER_LIQUIDITY;
if (isMonitoringAuction && isPriceTrigger) {
return MarketModeValidationType.PriceMonitoringAuction;
}
if (isMonitoringAuction && isLiquidityTrigger) {
return MarketModeValidationType.LiquidityMonitoringAuction;
}
return MarketModeValidationType.Auction;
}
return true;
};
};

View File

@ -49,7 +49,7 @@ export const DepositLimits = ({
return (
<KeyValueTable>
{limits.map(({ key, label, rawValue, value }) => (
<KeyValueTableRow>
<KeyValueTableRow key={key}>
<div data-testid={`${key}_label`}>{label}</div>
<div
data-testid={`${key}_value`}

View File

@ -14,7 +14,7 @@ export const InputError = ({
...props
}: InputErrorProps) => {
const effectiveClassName = classNames(
'text-sm flex items-center',
'text-sm flex items-center first-letter:uppercase',
'mt-2',
{
'border-danger': intent === 'danger',