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:
parent
d70bc0fdd5
commit
3077a634d1
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
@ -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,
|
||||
};
|
||||
}
|
@ -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('_')
|
||||
|
@ -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'
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -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';
|
||||
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
@ -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;
|
||||
};
|
||||
};
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -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 = ({
|
||||
|
@ -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
|
||||
|
@ -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;
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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"> </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> </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>
|
||||
);
|
||||
};
|
||||
|
@ -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
|
||||
}`
|
||||
);
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
);
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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';
|
||||
|
@ -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',
|
||||
}
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
};
|
@ -1,2 +1,4 @@
|
||||
export * from './components';
|
||||
export * from './hooks';
|
||||
export * from './utils';
|
||||
export * from './constants';
|
||||
|
8
libs/deal-ticket/src/utils/index.ts
Normal file
8
libs/deal-ticket/src/utils/index.ts
Normal 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';
|
10
libs/deal-ticket/src/utils/is-market-in-auction.ts
Normal file
10
libs/deal-ticket/src/utils/is-market-in-auction.ts
Normal 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);
|
||||
};
|
16
libs/deal-ticket/src/utils/validate-amount.ts
Normal file
16
libs/deal-ticket/src/utils/validate-amount.ts
Normal 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;
|
||||
};
|
||||
};
|
@ -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;
|
||||
};
|
42
libs/deal-ticket/src/utils/validate-market-state.ts
Normal file
42
libs/deal-ticket/src/utils/validate-market-state.ts
Normal 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();
|
||||
}
|
||||
};
|
12
libs/deal-ticket/src/utils/validate-market-trading-mode.ts
Normal file
12
libs/deal-ticket/src/utils/validate-market-trading-mode.ts
Normal 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;
|
||||
};
|
38
libs/deal-ticket/src/utils/validate-time-in-force.ts
Normal file
38
libs/deal-ticket/src/utils/validate-time-in-force.ts
Normal 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;
|
||||
};
|
||||
};
|
31
libs/deal-ticket/src/utils/validate-type.ts
Normal file
31
libs/deal-ticket/src/utils/validate-type.ts
Normal 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;
|
||||
};
|
||||
};
|
@ -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`}
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user