fix: filter rejected markets, order when suspended, remove market state from header, show continuous trading (#761)
* fix: #603 filter out rejected markets & dialog lg width * fix: #609 show trading mode Continuous Trading and hide market state * fix: #656 modify order validation to trade when suspended * fix: #656 fix use order validation tests * fix: #656 format volume no * fix: format volume with positionDecimalPlaces * fix: tests don't need to be async * fix: md:w-[720px] to prevent dialog overflow * fix: add market state translations * fix: imprt type validation props * Update libs/orders/src/lib/order-hooks/use-order-validation.tsx Co-authored-by: candida-d <62548908+candida-d@users.noreply.github.com> * Update libs/orders/src/lib/order-hooks/use-order-validation.tsx Co-authored-by: candida-d <62548908+candida-d@users.noreply.github.com> * Update libs/orders/src/lib/order-hooks/use-order-validation.tsx Co-authored-by: candida-d <62548908+candida-d@users.noreply.github.com> * Update libs/orders/src/lib/order-hooks/use-order-validation.tsx Co-authored-by: candida-d <62548908+candida-d@users.noreply.github.com> * Update libs/orders/src/lib/order-hooks/use-order-validation.tsx Co-authored-by: candida-d <62548908+candida-d@users.noreply.github.com> * Update libs/orders/src/lib/order-hooks/use-order-validation.spec.tsx Co-authored-by: candida-d <62548908+candida-d@users.noreply.github.com> * Update apps/trading/pages/markets/__generated__/Market.ts Co-authored-by: candida-d <62548908+candida-d@users.noreply.github.com> * fix: fix warning messages based on feedback * fix: capitalize trading mode * fix: capitalize trading mode * fix: remove line 72 on markets.cy.ts * fix: don't show trigger if unspecified * fix: format last price and shrink 0 on warning icon * fix: order sizes must be whole numbers for this market and input warning size 20 * fix: order sizes must be whole numbers for this market and input warning size 20 * fix: format market list * fix: remove market state check from markets.cy.ts * fix: remove market state check from markets.cy.ts * fix: remove market state check from markets.cy.ts * Revert "fix: remove market state check from markets.cy.ts" This reverts commitc9ab55c98a
. * Revert "fix: remove market state check from markets.cy.ts" This reverts commitbe60e56d8a
. * fix: fix markets.cy.ts failing * fix: fix markets.cy.ts failing * fix: fix markets.cy.ts failing * fix: remove extra test from markets.cy.ts * fix: update extra test from markets.cy.ts * fix: update extra test from markets.cy.ts Co-authored-by: candida-d <62548908+candida-d@users.noreply.github.com>
This commit is contained in:
parent
00e786d596
commit
5c038690c3
@ -25,7 +25,7 @@ describe('markets table', () => {
|
||||
const expectedMarketHeaders = [
|
||||
'Market',
|
||||
'Settlement asset',
|
||||
'State',
|
||||
'Trading mode',
|
||||
'Best bid',
|
||||
'Best offer',
|
||||
'Mark price',
|
||||
@ -64,43 +64,22 @@ describe('markets table', () => {
|
||||
mockTradingPage(req, MarketState.Active);
|
||||
});
|
||||
|
||||
// click on active market
|
||||
// click on market
|
||||
cy.get('[role="gridcell"][col-id=data]').should('be.visible');
|
||||
cy.get('[role="gridcell"][col-id=data]').contains('Active').click();
|
||||
cy.get('[role="gridcell"][col-id=name]').contains('ACTIVE MARKET').click();
|
||||
|
||||
cy.wait('@Market');
|
||||
cy.contains('ACTIVE MARKET');
|
||||
cy.url().should('include', '/markets/market-0');
|
||||
|
||||
verifyMarketSummaryDisplayed('Active');
|
||||
verifyMarketSummaryDisplayed();
|
||||
});
|
||||
|
||||
it('can select a suspended market', () => {
|
||||
cy.wait('@Markets');
|
||||
cy.get('.ag-root-wrapper').should('be.visible');
|
||||
|
||||
cy.mockGQL((req) => {
|
||||
mockTradingPage(req, MarketState.Suspended);
|
||||
});
|
||||
|
||||
// click on active market
|
||||
cy.get('[role="gridcell"][col-id=data]').should('be.visible');
|
||||
cy.get('[role="gridcell"][col-id=data]').contains('Suspended').click();
|
||||
|
||||
cy.wait('@Market');
|
||||
cy.contains('SUSPENDED MARKET');
|
||||
cy.url().should('include', '/markets/market-1');
|
||||
|
||||
verifyMarketSummaryDisplayed('Suspended');
|
||||
});
|
||||
|
||||
function verifyMarketSummaryDisplayed(expectedMarketState: string) {
|
||||
function verifyMarketSummaryDisplayed() {
|
||||
const marketSummaryBlock = 'market-summary';
|
||||
const percentageValue = 'price-change-percentage';
|
||||
const priceChangeValue = 'price-change';
|
||||
const tradingVolume = 'trading-volume';
|
||||
const tradingMode = 'trading-mode';
|
||||
const marketState = 'market-state';
|
||||
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.contains('Change (24h)');
|
||||
@ -110,8 +89,6 @@ describe('markets table', () => {
|
||||
cy.getByTestId(tradingVolume).should('not.be.empty');
|
||||
cy.contains('Trading mode');
|
||||
cy.getByTestId(tradingMode).should('not.be.empty');
|
||||
cy.contains('State');
|
||||
cy.getByTestId(marketState).should('have.text', expectedMarketState);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -20,6 +20,7 @@ const MARKET_QUERY = gql`
|
||||
tradingMode
|
||||
state
|
||||
decimalPlaces
|
||||
positionDecimalPlaces
|
||||
data {
|
||||
market {
|
||||
id
|
||||
@ -31,6 +32,7 @@ const MARKET_QUERY = gql`
|
||||
bestStaticBidVolume
|
||||
bestStaticOfferVolume
|
||||
indicativeVolume
|
||||
trigger
|
||||
}
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
|
12
apps/trading/pages/markets/__generated__/Market.ts
generated
12
apps/trading/pages/markets/__generated__/Market.ts
generated
@ -3,7 +3,7 @@
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { Interval, MarketTradingMode, MarketState } from "@vegaprotocol/types";
|
||||
import { Interval, MarketTradingMode, MarketState, AuctionTrigger } from "@vegaprotocol/types";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: Market
|
||||
@ -47,6 +47,10 @@ export interface Market_market_data {
|
||||
* the aggregated volume being offered at the best static offer price, excluding pegged orders.
|
||||
*/
|
||||
bestStaticOfferVolume: string;
|
||||
/**
|
||||
* what triggered an auction (if an auction was started)
|
||||
*/
|
||||
trigger: AuctionTrigger;
|
||||
}
|
||||
|
||||
export interface Market_market_tradableInstrument_instrument_metadata {
|
||||
@ -144,6 +148,12 @@ export interface Market_market {
|
||||
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
|
||||
*/
|
||||
decimalPlaces: number;
|
||||
/**
|
||||
* positionDecimalPlaces indicated the number of decimal places that an integer must be shifted in order to get a correct size (uint64).
|
||||
* i.e. 0 means there are no fractional orders for the market, and order sizes are always whole sizes.
|
||||
* 2 means sizes given as 10^2 * desired size, e.g. a desired size of 1.23 is represented as 123 in this market.
|
||||
*/
|
||||
positionDecimalPlaces: number;
|
||||
/**
|
||||
* marketData for the given market
|
||||
*/
|
||||
|
@ -11,7 +11,11 @@ import { TradesContainer } from '@vegaprotocol/trades';
|
||||
import { PositionsContainer } from '@vegaprotocol/positions';
|
||||
import { OrderbookContainer } from '@vegaprotocol/market-depth';
|
||||
import type { Market_market } from './__generated__/Market';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatLabel,
|
||||
t,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { AccountsContainer } from '@vegaprotocol/accounts';
|
||||
import { DepthChartContainer } from '@vegaprotocol/market-depth';
|
||||
import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
|
||||
@ -23,6 +27,8 @@ import {
|
||||
PriceCellChange,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import type { CandleClose } from '@vegaprotocol/types';
|
||||
import { AuctionTrigger } from '@vegaprotocol/types';
|
||||
import { MarketTradingMode } from '@vegaprotocol/types';
|
||||
|
||||
const TradingViews = {
|
||||
Candles: CandlesChartContainer,
|
||||
@ -55,7 +61,7 @@ export const TradeMarketHeader = ({
|
||||
const itemClassName =
|
||||
'font-sans font-normal mb-0 text-black-60 dark:text-white-80 text-ui-small';
|
||||
const itemValueClassName =
|
||||
'capitalize font-sans tracking-tighter text-black dark:text-white text-ui';
|
||||
'font-sans tracking-tighter text-black dark:text-white text-ui';
|
||||
const headerClassName = classNames(
|
||||
'w-full p-8 bg-white dark:bg-black',
|
||||
className
|
||||
@ -87,20 +93,23 @@ export const TradeMarketHeader = ({
|
||||
<span className={itemClassName}>Volume</span>
|
||||
<span data-testid="trading-volume" className={itemValueClassName}>
|
||||
{market.data && market.data.indicativeVolume !== '0'
|
||||
? market.data.indicativeVolume
|
||||
? addDecimalsFormatNumber(
|
||||
market.data.indicativeVolume,
|
||||
market.positionDecimalPlaces
|
||||
)
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className={headerItemClassName}>
|
||||
<span className={itemClassName}>Trading mode</span>
|
||||
<span data-testid="trading-mode" className={itemValueClassName}>
|
||||
{market.tradingMode}
|
||||
</span>
|
||||
</div>
|
||||
<div className={headerItemClassName}>
|
||||
<span className={itemClassName}>State</span>
|
||||
<span data-testid="market-state" className={itemValueClassName}>
|
||||
{market.state}
|
||||
{market.tradingMode === MarketTradingMode.MonitoringAuction &&
|
||||
market.data?.trigger &&
|
||||
market.data.trigger !== AuctionTrigger.Unspecified
|
||||
? `${formatLabel(
|
||||
market.tradingMode
|
||||
)} - ${market.data?.trigger.toLowerCase()}`
|
||||
: formatLabel(market.tradingMode)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,7 +4,11 @@ import {
|
||||
VegaWalletOrderType,
|
||||
VegaWalletOrderTimeInForce,
|
||||
} from '@vegaprotocol/wallet';
|
||||
import { t, addDecimal, toDecimal } from '@vegaprotocol/react-helpers';
|
||||
import {
|
||||
t,
|
||||
toDecimal,
|
||||
addDecimalsFormatNumber,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { Button, InputError } from '@vegaprotocol/ui-toolkit';
|
||||
import { TypeSelector } from './type-selector';
|
||||
import { SideSelector } from './side-selector';
|
||||
@ -43,22 +47,22 @@ export const DealTicket = ({
|
||||
const step = toDecimal(market.positionDecimalPlaces);
|
||||
const orderType = watch('type');
|
||||
const orderTimeInForce = watch('timeInForce');
|
||||
const invalidText = useOrderValidation({
|
||||
const { message, isDisabled: disabled } = useOrderValidation({
|
||||
step,
|
||||
market,
|
||||
orderType,
|
||||
orderTimeInForce,
|
||||
fieldErrors: errors,
|
||||
});
|
||||
const isDisabled = transactionStatus === 'pending' || Boolean(invalidText);
|
||||
const isDisabled = transactionStatus === 'pending' || disabled;
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(order: Order) => {
|
||||
if (!isDisabled && !invalidText) {
|
||||
if (!isDisabled) {
|
||||
submit(order);
|
||||
}
|
||||
},
|
||||
[isDisabled, invalidText, submit]
|
||||
[isDisabled, submit]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -83,7 +87,10 @@ export const DealTicket = ({
|
||||
register={register}
|
||||
price={
|
||||
market.depth.lastTrade
|
||||
? addDecimal(market.depth.lastTrade.price, market.decimalPlaces)
|
||||
? addDecimalsFormatNumber(
|
||||
market.depth.lastTrade.price,
|
||||
market.decimalPlaces
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
quoteName={market.tradableInstrument.instrument.product.quoteName}
|
||||
@ -118,9 +125,12 @@ export const DealTicket = ({
|
||||
>
|
||||
{transactionStatus === 'pending' ? t('Pending...') : t('Place order')}
|
||||
</Button>
|
||||
{invalidText && (
|
||||
<InputError className="mb-8" data-testid="dealticket-error-message">
|
||||
{invalidText}
|
||||
{message && (
|
||||
<InputError
|
||||
className="mt-12 mb-12"
|
||||
data-testid="dealticket-error-message"
|
||||
>
|
||||
{message}
|
||||
</InputError>
|
||||
)}
|
||||
</form>
|
||||
|
@ -29,7 +29,10 @@ export const LandingDialog = ({ open, setOpen }: LandingDialogProps) => {
|
||||
intent={Intent.Primary}
|
||||
open={open}
|
||||
onChange={setClose}
|
||||
titleClassNames="font-bold font-sans text-3xl tracking-tight mb-0 pl-8"
|
||||
titleClassNames={
|
||||
'font-bold font-sans text-3xl tracking-tight mb-0 pl-8'
|
||||
}
|
||||
contentClassNames={'md:w-[520px] lg:w-[520px] w-full'}
|
||||
>
|
||||
<SelectMarketList data={data} onSelect={setClose} />
|
||||
</Dialog>
|
||||
|
@ -3,7 +3,7 @@
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { MarketState, MarketTradingMode } from "@vegaprotocol/types";
|
||||
import { MarketState, MarketTradingMode, AuctionTrigger } from "@vegaprotocol/types";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL fragment: MarketDataFields
|
||||
@ -43,4 +43,8 @@ export interface MarketDataFields {
|
||||
* the mark price (actually an unsigned int)
|
||||
*/
|
||||
markPrice: string;
|
||||
/**
|
||||
* what triggered an auction (if an auction was started)
|
||||
*/
|
||||
trigger: AuctionTrigger;
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { MarketState, MarketTradingMode } from "@vegaprotocol/types";
|
||||
import { MarketState, MarketTradingMode, AuctionTrigger } from "@vegaprotocol/types";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL subscription operation: MarketDataSub
|
||||
@ -43,6 +43,10 @@ export interface MarketDataSub_marketData {
|
||||
* the mark price (actually an unsigned int)
|
||||
*/
|
||||
markPrice: string;
|
||||
/**
|
||||
* what triggered an auction (if an auction was started)
|
||||
*/
|
||||
trigger: AuctionTrigger;
|
||||
}
|
||||
|
||||
export interface MarketDataSub {
|
||||
|
@ -3,7 +3,7 @@
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { MarketState, MarketTradingMode } from "@vegaprotocol/types";
|
||||
import { MarketState, MarketTradingMode, AuctionTrigger } from "@vegaprotocol/types";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: Markets
|
||||
@ -43,6 +43,10 @@ export interface Markets_markets_data {
|
||||
* the mark price (actually an unsigned int)
|
||||
*/
|
||||
markPrice: string;
|
||||
/**
|
||||
* what triggered an auction (if an auction was started)
|
||||
*/
|
||||
trigger: AuctionTrigger;
|
||||
}
|
||||
|
||||
export interface Markets_markets_tradableInstrument_instrument_product_settlementAsset {
|
||||
|
@ -4,11 +4,13 @@ import {
|
||||
PriceFlashCell,
|
||||
addDecimalsFormatNumber,
|
||||
t,
|
||||
formatLabel,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import type { Markets_markets } from '../__generated__/Markets';
|
||||
import { MarketTradingMode, AuctionTrigger } from '@vegaprotocol/types';
|
||||
|
||||
interface MarketListTableProps {
|
||||
datasource: IDatasource;
|
||||
@ -44,13 +46,19 @@ export const MarketListTable = forwardRef<AgGridReact, MarketListTableProps>(
|
||||
field="tradableInstrument.instrument.product.settlementAsset.symbol"
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('State')}
|
||||
headerName={t('Trading mode')}
|
||||
field="data"
|
||||
valueFormatter={({ value }: ValueFormatterParams) =>
|
||||
value === undefined
|
||||
? value
|
||||
: `${value.market.state} (${value.market.tradingMode})`
|
||||
}
|
||||
minWidth={200}
|
||||
valueFormatter={({ value }: ValueFormatterParams) => {
|
||||
if (!value) return value;
|
||||
const { market, trigger } = value;
|
||||
return market &&
|
||||
market.tradingMode === MarketTradingMode.MonitoringAuction &&
|
||||
trigger &&
|
||||
trigger !== AuctionTrigger.Unspecified
|
||||
? `${formatLabel(market.tradingMode)} - ${trigger.toLowerCase()}`
|
||||
: formatLabel(market?.tradingMode);
|
||||
}}
|
||||
/>
|
||||
<AgGridColumn
|
||||
headerName={t('Best bid')}
|
||||
|
@ -10,6 +10,7 @@ import type {
|
||||
Markets_markets_data,
|
||||
} from '../../components/__generated__/Markets';
|
||||
import { marketsDataProvider as dataProvider } from './markets-data-provider';
|
||||
import { MarketState } from '@vegaprotocol/types';
|
||||
|
||||
export const MarketsContainer = () => {
|
||||
const { push } = useRouter();
|
||||
@ -28,14 +29,15 @@ export const MarketsContainer = () => {
|
||||
Markets_markets_data
|
||||
>({ dataProvider, update });
|
||||
dataRef.current = data;
|
||||
|
||||
const getRows = async ({
|
||||
successCallback,
|
||||
startRow,
|
||||
endRow,
|
||||
}: IGetRowsParams) => {
|
||||
const rowsThisBlock = dataRef.current
|
||||
? dataRef.current.slice(startRow, endRow)
|
||||
? dataRef.current
|
||||
.slice(startRow, endRow)
|
||||
.filter((m) => m.data?.market.state !== MarketState.Rejected)
|
||||
: [];
|
||||
const lastRow = dataRef.current?.length ?? -1;
|
||||
successCallback(rowsThisBlock, lastRow);
|
||||
|
@ -21,6 +21,7 @@ const MARKET_DATA_FRAGMENT = gql`
|
||||
bestBidPrice
|
||||
bestOfferPrice
|
||||
markPrice
|
||||
trigger
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { MarketState } from '@vegaprotocol/types';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import type {
|
||||
MarketList,
|
||||
@ -11,22 +12,24 @@ export const lastPrice = ({ candles }: MarketList_markets) =>
|
||||
|
||||
export const mapDataToMarketList = ({ markets }: MarketList) =>
|
||||
orderBy(
|
||||
markets?.map((m) => {
|
||||
return {
|
||||
id: m.id,
|
||||
decimalPlaces: m.decimalPlaces,
|
||||
marketName: m.tradableInstrument.instrument?.code,
|
||||
lastPrice: lastPrice(m) ?? m.data?.markPrice,
|
||||
candles: (m.candles || []).filter((c) => c),
|
||||
open: m.marketTimestamps.open
|
||||
? new Date(m.marketTimestamps.open).getTime()
|
||||
: null,
|
||||
close: m.marketTimestamps.close
|
||||
? new Date(m.marketTimestamps.close).getTime()
|
||||
: null,
|
||||
state: m.state,
|
||||
};
|
||||
}) || [],
|
||||
markets
|
||||
?.filter((m) => m.state !== MarketState.Rejected)
|
||||
.map((m) => {
|
||||
return {
|
||||
id: m.id,
|
||||
decimalPlaces: m.decimalPlaces,
|
||||
marketName: m.tradableInstrument.instrument?.code,
|
||||
lastPrice: lastPrice(m) ?? m.data?.markPrice,
|
||||
candles: (m.candles || []).filter((c) => c),
|
||||
open: m.marketTimestamps.open
|
||||
? new Date(m.marketTimestamps.open).getTime()
|
||||
: null,
|
||||
close: m.marketTimestamps.close
|
||||
? new Date(m.marketTimestamps.close).getTime()
|
||||
: null,
|
||||
state: m.state,
|
||||
};
|
||||
}) || [],
|
||||
['state', 'open', 'id'],
|
||||
['asc', 'asc', 'asc']
|
||||
);
|
||||
|
@ -10,6 +10,7 @@ import type {
|
||||
} from '@vegaprotocol/wallet';
|
||||
import { MarketState, MarketTradingMode } from '@vegaprotocol/types';
|
||||
import type { ValidationProps } from './use-order-validation';
|
||||
import { marketTranslations } from './use-order-validation';
|
||||
import { useOrderValidation } from './use-order-validation';
|
||||
import { ERROR_SIZE_DECIMAL } from '../utils/validate-size';
|
||||
import type { Market } from '../market';
|
||||
@ -73,12 +74,12 @@ const ERROR = {
|
||||
'Only limit orders are permitted when market is in auction',
|
||||
MARKET_CONTINUOUS_TIF:
|
||||
'Only GTT, GTC and GFA are permitted when market is in auction',
|
||||
FIELD_SIZE_REQ: 'An amount needs to be provided',
|
||||
FIELD_SIZE_REQ: 'You need to provide an amount',
|
||||
FIELD_SIZE_MIN: `The amount cannot be lower than "${defaultOrder.step}"`,
|
||||
FIELD_PRICE_REQ: 'A price needs to be provided',
|
||||
FIELD_PRICE_REQ: 'You need to provide a price',
|
||||
FIELD_PRICE_MIN: 'The price cannot be negative',
|
||||
FIELD_PRICE_STEP_NULL: 'No decimal amounts allowed for this order',
|
||||
FIELD_PRICE_STEP_DECIMAL: `The amount field only takes up to ${market.positionDecimalPlaces} decimals`,
|
||||
FIELD_PRICE_STEP_NULL: 'Order sizes must be in whole numbers for this market',
|
||||
FIELD_PRICE_STEP_DECIMAL: `The amount field accepts up to ${market.positionDecimalPlaces} decimal places`,
|
||||
};
|
||||
|
||||
function setup(
|
||||
@ -90,107 +91,148 @@ function setup(
|
||||
return renderHook(() => useOrderValidation({ ...defaultOrder, ...props }));
|
||||
}
|
||||
|
||||
it('Returns empty string when given valid data', () => {
|
||||
const { result } = setup();
|
||||
expect(result.current).toEqual('');
|
||||
});
|
||||
|
||||
it('Returns an error message when no keypair found', async () => {
|
||||
const { result } = setup(defaultOrder, { keypair: null });
|
||||
expect(result.current).toEqual('');
|
||||
});
|
||||
|
||||
it('Returns an error message when the keypair is tainted', async () => {
|
||||
const { result } = setup(defaultOrder, {
|
||||
keypair: { ...defaultWalletContext.keypair, tainted: true },
|
||||
describe('useOrderValidation', () => {
|
||||
it('Returns empty string when given valid data', () => {
|
||||
const { result } = setup();
|
||||
expect(result.current).toStrictEqual({ isDisabled: false, message: `` });
|
||||
});
|
||||
expect(result.current).toEqual('');
|
||||
});
|
||||
|
||||
it.each`
|
||||
state | errorMessage
|
||||
${MarketState.Cancelled} | ${ERROR.MARKET_INACTIVE}
|
||||
${MarketState.Closed} | ${ERROR.MARKET_INACTIVE}
|
||||
${MarketState.Rejected} | ${ERROR.MARKET_INACTIVE}
|
||||
${MarketState.Settled} | ${ERROR.MARKET_INACTIVE}
|
||||
${MarketState.TradingTerminated} | ${ERROR.MARKET_INACTIVE}
|
||||
${MarketState.Suspended} | ${ERROR.MARKET_SUSPENDED}
|
||||
${MarketState.Pending} | ${ERROR.MARKET_WAITING}
|
||||
${MarketState.Proposed} | ${ERROR.MARKET_WAITING}
|
||||
`(
|
||||
'Returns an error message for "$marketState" market',
|
||||
async ({ state, errorMessage }) => {
|
||||
const { result } = setup({ market: { ...defaultOrder.market, state } });
|
||||
expect(result.current).toEqual(errorMessage);
|
||||
}
|
||||
);
|
||||
|
||||
it.each`
|
||||
tradingMode | errorMessage
|
||||
${MarketTradingMode.BatchAuction} | ${ERROR.MARKET_CONTINUOUS_LIMIT}
|
||||
${MarketTradingMode.MonitoringAuction} | ${ERROR.MARKET_CONTINUOUS_LIMIT}
|
||||
${MarketTradingMode.OpeningAuction} | ${ERROR.MARKET_CONTINUOUS_LIMIT}
|
||||
`(
|
||||
'Returns an error message when trying to submit a non-limit order for a "$tradingMode" market',
|
||||
async ({ tradingMode, errorMessage }) => {
|
||||
const { result } = setup({
|
||||
market: { ...defaultOrder.market, tradingMode },
|
||||
orderType: VegaWalletOrderType.Market,
|
||||
});
|
||||
expect(result.current).toEqual(errorMessage);
|
||||
}
|
||||
);
|
||||
|
||||
it.each`
|
||||
tradingMode | orderTimeInForce | errorMessage
|
||||
${MarketTradingMode.BatchAuction} | ${VegaWalletOrderTimeInForce.FOK} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
${MarketTradingMode.MonitoringAuction} | ${VegaWalletOrderTimeInForce.FOK} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
${MarketTradingMode.OpeningAuction} | ${VegaWalletOrderTimeInForce.FOK} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
${MarketTradingMode.BatchAuction} | ${VegaWalletOrderTimeInForce.IOC} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
${MarketTradingMode.MonitoringAuction} | ${VegaWalletOrderTimeInForce.IOC} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
${MarketTradingMode.OpeningAuction} | ${VegaWalletOrderTimeInForce.IOC} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
${MarketTradingMode.BatchAuction} | ${VegaWalletOrderTimeInForce.GFN} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
${MarketTradingMode.MonitoringAuction} | ${VegaWalletOrderTimeInForce.GFN} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
${MarketTradingMode.OpeningAuction} | ${VegaWalletOrderTimeInForce.GFN} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
`(
|
||||
'Returns an error message when submitting a limit order with a "$orderTimeInForce" value to a "$tradingMode" market',
|
||||
async ({ tradingMode, orderTimeInForce, errorMessage }) => {
|
||||
const { result } = setup({
|
||||
market: { ...defaultOrder.market, tradingMode },
|
||||
orderType: VegaWalletOrderType.Limit,
|
||||
orderTimeInForce,
|
||||
});
|
||||
expect(result.current).toEqual(errorMessage);
|
||||
}
|
||||
);
|
||||
|
||||
it.each`
|
||||
fieldName | errorType | errorMessage
|
||||
${'size'} | ${'required'} | ${ERROR.FIELD_SIZE_REQ}
|
||||
${'size'} | ${'min'} | ${ERROR.FIELD_SIZE_MIN}
|
||||
${'price'} | ${'required'} | ${ERROR.FIELD_PRICE_REQ}
|
||||
${'price'} | ${'min'} | ${ERROR.FIELD_PRICE_MIN}
|
||||
`(
|
||||
'Returns an error message when the order $fieldName "$errorType" validation fails',
|
||||
async ({ fieldName, errorType, errorMessage }) => {
|
||||
const { result } = setup({
|
||||
fieldErrors: { [fieldName]: { type: errorType } },
|
||||
});
|
||||
expect(result.current).toEqual(errorMessage);
|
||||
}
|
||||
);
|
||||
|
||||
it('Returns an error message when the order size incorrectly has decimal values', async () => {
|
||||
const { result } = setup({
|
||||
market: { ...market, positionDecimalPlaces: 0 },
|
||||
fieldErrors: { size: { type: 'validate', message: ERROR_SIZE_DECIMAL } },
|
||||
it('Returns an error message when no keypair found', () => {
|
||||
const { result } = setup(defaultOrder, { keypair: null });
|
||||
expect(result.current).toStrictEqual({ isDisabled: false, message: `` });
|
||||
});
|
||||
expect(result.current).toEqual(ERROR.FIELD_PRICE_STEP_NULL);
|
||||
});
|
||||
|
||||
it('Returns an error message when the order size has more decimals then allowed', async () => {
|
||||
const { result } = setup({
|
||||
fieldErrors: { size: { type: 'validate', message: ERROR_SIZE_DECIMAL } },
|
||||
it('Returns an error message when the keypair is tainted', () => {
|
||||
const { result } = setup(defaultOrder, {
|
||||
keypair: { ...defaultWalletContext.keypair, tainted: true },
|
||||
});
|
||||
expect(result.current).toStrictEqual({ isDisabled: false, message: `` });
|
||||
});
|
||||
|
||||
it.each`
|
||||
state
|
||||
${MarketState.Settled}
|
||||
${MarketState.Rejected}
|
||||
${MarketState.TradingTerminated}
|
||||
`(
|
||||
'Returns an error message for market state when not accepting orders',
|
||||
({ state }) => {
|
||||
const { result } = setup({ market: { ...defaultOrder.market, state } });
|
||||
expect(result.current).toStrictEqual({
|
||||
isDisabled: true,
|
||||
message: `This market is ${marketTranslations(
|
||||
state
|
||||
)} and not accepting orders`,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it.each`
|
||||
state
|
||||
${MarketState.Suspended}
|
||||
${MarketState.Pending}
|
||||
${MarketState.Cancelled}
|
||||
${MarketState.Proposed}
|
||||
${MarketState.Closed}
|
||||
`(
|
||||
'Returns an error message for market state suspended or pending',
|
||||
({ state }) => {
|
||||
const { result } = setup({
|
||||
market: {
|
||||
...defaultOrder.market,
|
||||
state,
|
||||
tradingMode: MarketTradingMode.BatchAuction,
|
||||
},
|
||||
});
|
||||
expect(result.current).toStrictEqual({
|
||||
isDisabled: false,
|
||||
message: `This market is ${state.toLowerCase()} and only accepting liquidity commitment orders`,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it.each`
|
||||
tradingMode | errorMessage
|
||||
${MarketTradingMode.BatchAuction} | ${ERROR.MARKET_CONTINUOUS_LIMIT}
|
||||
${MarketTradingMode.MonitoringAuction} | ${ERROR.MARKET_CONTINUOUS_LIMIT}
|
||||
${MarketTradingMode.OpeningAuction} | ${ERROR.MARKET_CONTINUOUS_LIMIT}
|
||||
`(
|
||||
`Returns an error message when trying to submit a non-limit order for a "$tradingMode" market`,
|
||||
({ tradingMode, errorMessage }) => {
|
||||
const { result } = setup({
|
||||
market: { ...defaultOrder.market, tradingMode },
|
||||
orderType: VegaWalletOrderType.Market,
|
||||
});
|
||||
expect(result.current).toStrictEqual({
|
||||
isDisabled: true,
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it.each`
|
||||
tradingMode | orderTimeInForce | errorMessage
|
||||
${MarketTradingMode.BatchAuction} | ${VegaWalletOrderTimeInForce.FOK} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
${MarketTradingMode.MonitoringAuction} | ${VegaWalletOrderTimeInForce.FOK} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
${MarketTradingMode.OpeningAuction} | ${VegaWalletOrderTimeInForce.FOK} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
${MarketTradingMode.BatchAuction} | ${VegaWalletOrderTimeInForce.IOC} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
${MarketTradingMode.MonitoringAuction} | ${VegaWalletOrderTimeInForce.IOC} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
${MarketTradingMode.OpeningAuction} | ${VegaWalletOrderTimeInForce.IOC} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
${MarketTradingMode.BatchAuction} | ${VegaWalletOrderTimeInForce.GFN} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
${MarketTradingMode.MonitoringAuction} | ${VegaWalletOrderTimeInForce.GFN} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
${MarketTradingMode.OpeningAuction} | ${VegaWalletOrderTimeInForce.GFN} | ${ERROR.MARKET_CONTINUOUS_TIF}
|
||||
`(
|
||||
`Returns an error message when submitting a limit order with a "$orderTimeInForce" value to a "$tradingMode" market`,
|
||||
({ tradingMode, orderTimeInForce, errorMessage }) => {
|
||||
const { result } = setup({
|
||||
market: { ...defaultOrder.market, tradingMode },
|
||||
orderType: VegaWalletOrderType.Limit,
|
||||
orderTimeInForce,
|
||||
});
|
||||
expect(result.current).toStrictEqual({
|
||||
isDisabled: true,
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it.each`
|
||||
fieldName | errorType | errorMessage
|
||||
${`size`} | ${`required`} | ${ERROR.FIELD_SIZE_REQ}
|
||||
${`size`} | ${`min`} | ${ERROR.FIELD_SIZE_MIN}
|
||||
${`price`} | ${`required`} | ${ERROR.FIELD_PRICE_REQ}
|
||||
${`price`} | ${`min`} | ${ERROR.FIELD_PRICE_MIN}
|
||||
`(
|
||||
`Returns an error message when the order $fieldName "$errorType" validation fails`,
|
||||
({ fieldName, errorType, errorMessage }) => {
|
||||
const { result } = setup({
|
||||
fieldErrors: { [fieldName]: { type: errorType } },
|
||||
});
|
||||
expect(result.current).toStrictEqual({
|
||||
isDisabled: true,
|
||||
message: errorMessage,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
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 } },
|
||||
});
|
||||
expect(result.current).toStrictEqual({
|
||||
isDisabled: true,
|
||||
message: ERROR.FIELD_PRICE_STEP_NULL,
|
||||
});
|
||||
});
|
||||
|
||||
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 } },
|
||||
});
|
||||
expect(result.current).toStrictEqual({
|
||||
isDisabled: true,
|
||||
message: ERROR.FIELD_PRICE_STEP_DECIMAL,
|
||||
});
|
||||
});
|
||||
expect(result.current).toEqual(ERROR.FIELD_PRICE_STEP_DECIMAL);
|
||||
});
|
||||
|
@ -19,6 +19,15 @@ export type ValidationProps = {
|
||||
fieldErrors?: FieldErrors<Order>;
|
||||
};
|
||||
|
||||
export const marketTranslations = (marketState: MarketState) => {
|
||||
switch (marketState) {
|
||||
case MarketState.TradingTerminated:
|
||||
return t('terminated');
|
||||
default:
|
||||
return t(marketState).toLowerCase();
|
||||
}
|
||||
};
|
||||
|
||||
export const useOrderValidation = ({
|
||||
step,
|
||||
market,
|
||||
@ -28,33 +37,120 @@ export const useOrderValidation = ({
|
||||
}: ValidationProps) => {
|
||||
const { keypair } = useVegaWallet();
|
||||
|
||||
const invalidText = useMemo(() => {
|
||||
const { message, isDisabled } = useMemo(() => {
|
||||
if (!keypair) {
|
||||
return t('No public key selected');
|
||||
return { message: t('No public key selected'), isDisabled: true };
|
||||
}
|
||||
|
||||
if (keypair.tainted) {
|
||||
return t('Selected public key has been tainted');
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: t('Selected public key has been tainted'),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
MarketState.Settled,
|
||||
MarketState.Rejected,
|
||||
MarketState.TradingTerminated,
|
||||
].includes(market.state)
|
||||
) {
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: t(
|
||||
`This market is ${marketTranslations(
|
||||
market.state
|
||||
)} and not accepting orders`
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
MarketState.Suspended,
|
||||
MarketState.Pending,
|
||||
MarketState.Proposed,
|
||||
MarketState.Cancelled,
|
||||
MarketState.Closed,
|
||||
].includes(market.state)
|
||||
) {
|
||||
return {
|
||||
isDisabled: false,
|
||||
message: t(
|
||||
`This market is ${marketTranslations(
|
||||
market.state
|
||||
)} and only accepting liquidity commitment orders`
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (market.state !== MarketState.Active) {
|
||||
if (market.state === MarketState.Suspended) {
|
||||
return t('Market is currently suspended');
|
||||
if (market.tradingMode === MarketTradingMode.Continuous) {
|
||||
if (orderType !== OrderType.Limit) {
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: t(
|
||||
'Only limit orders are permitted when market is in auction'
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
OrderTimeInForce.FOK,
|
||||
OrderTimeInForce.IOC,
|
||||
OrderTimeInForce.GFN,
|
||||
].includes(orderTimeInForce)
|
||||
) {
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: t(
|
||||
'Only GTT, GTC and GFA are permitted when market is in auction'
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isDisabled: false,
|
||||
message: t(
|
||||
`This market is ${marketTranslations(
|
||||
market.state
|
||||
)} and only accepting liquidity commitment orders`
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
market.state === MarketState.Proposed ||
|
||||
market.state === MarketState.Pending
|
||||
) {
|
||||
return t('Market is not active yet');
|
||||
return {
|
||||
isDisabled: false,
|
||||
message: t(
|
||||
`This market is ${marketTranslations(
|
||||
market.state
|
||||
)} and only accepting liquidity commitment orders`
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return t('Market is no longer active');
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: t('This market is no longer active.'),
|
||||
};
|
||||
}
|
||||
|
||||
if (market.tradingMode !== MarketTradingMode.Continuous) {
|
||||
if (orderType !== OrderType.Limit) {
|
||||
return t('Only limit orders are permitted when market is in auction');
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: t(
|
||||
'Only limit orders are permitted when market is in auction'
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
@ -64,26 +160,41 @@ export const useOrderValidation = ({
|
||||
OrderTimeInForce.GFN,
|
||||
].includes(orderTimeInForce)
|
||||
) {
|
||||
return t(
|
||||
'Only GTT, GTC and GFA are permitted when market is in auction'
|
||||
);
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: t(
|
||||
'Only GTT, GTC and GFA are permitted when market is in auction'
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldErrors?.size?.type === 'required') {
|
||||
return t('An amount needs to be provided');
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: t('You need to provide an amount'),
|
||||
};
|
||||
}
|
||||
|
||||
if (fieldErrors?.size?.type === 'min') {
|
||||
return t(`The amount cannot be lower than "${step}"`);
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: t(`The amount cannot be lower than "${step}"`),
|
||||
};
|
||||
}
|
||||
|
||||
if (fieldErrors?.price?.type === 'required') {
|
||||
return t('A price needs to be provided');
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: t('You need to provide a price'),
|
||||
};
|
||||
}
|
||||
|
||||
if (fieldErrors?.price?.type === 'min') {
|
||||
return t(`The price cannot be negative`);
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: t(`The price cannot be negative`),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
@ -91,14 +202,20 @@ export const useOrderValidation = ({
|
||||
fieldErrors?.size?.message === ERROR_SIZE_DECIMAL
|
||||
) {
|
||||
if (market.positionDecimalPlaces === 0) {
|
||||
return t('No decimal amounts allowed for this order');
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: t('Order sizes must be in whole numbers for this market'),
|
||||
};
|
||||
}
|
||||
return t(
|
||||
`The amount field only takes up to ${market.positionDecimalPlaces} decimals`
|
||||
);
|
||||
return {
|
||||
isDisabled: true,
|
||||
message: t(
|
||||
`The amount field accepts up to ${market.positionDecimalPlaces} decimal places`
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return '';
|
||||
return { isDisabled: false, message: '' };
|
||||
}, [
|
||||
keypair,
|
||||
step,
|
||||
@ -110,5 +227,5 @@ export const useOrderValidation = ({
|
||||
orderTimeInForce,
|
||||
]);
|
||||
|
||||
return invalidText;
|
||||
return { message, isDisabled };
|
||||
};
|
||||
|
@ -1,4 +1,10 @@
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import startCase from 'lodash/startCase';
|
||||
|
||||
export const getUserLocale = () => 'default';
|
||||
|
||||
export const splitAt = (index: number) => (x: string) =>
|
||||
[x.slice(0, index), x.slice(index)];
|
||||
|
||||
export const formatLabel = (str: string) =>
|
||||
capitalize(startCase(str).toLowerCase());
|
||||
|
@ -26,7 +26,7 @@ export function Dialog({
|
||||
}: DialogProps) {
|
||||
const contentClasses = classNames(
|
||||
// Positions the modal in the center of screen
|
||||
'z-20 fixed w-full md:w-[520px] lg:w-[940px] px-28 py-24 top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]',
|
||||
'z-20 fixed w-full md:w-[720px] lg:w-[940px] px-28 py-24 top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]',
|
||||
// Need to apply background and text colors again as content is rendered in a portal
|
||||
'dark:bg-black dark:text-white-95 bg-white text-black-95',
|
||||
getIntentShadow(intent),
|
||||
|
@ -17,6 +17,7 @@ export const Icon = ({ size = 16, name, className, ariaLabel }: IconProps) => {
|
||||
'fill-current',
|
||||
'align-text-bottom',
|
||||
'fill-current',
|
||||
'shrink-0',
|
||||
{
|
||||
'w-20': size === 20,
|
||||
'h-20': size === 20,
|
||||
|
@ -21,7 +21,6 @@ export const InputError = ({
|
||||
'flex',
|
||||
'items-center',
|
||||
'box-border',
|
||||
'h-28',
|
||||
'border-l-4',
|
||||
'text-black-95 dark:text-white-95',
|
||||
'text-ui',
|
||||
@ -44,7 +43,7 @@ export const InputError = ({
|
||||
{...props}
|
||||
role="alert"
|
||||
>
|
||||
<Icon name="warning-sign" className={iconClassName} />
|
||||
<Icon name="warning-sign" size={20} className={iconClassName} />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
@ -8,6 +8,7 @@ import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { OrderType } from '@vegaprotocol/types';
|
||||
import type { Order } from '../wallet-types';
|
||||
import get from 'lodash/get';
|
||||
|
||||
export interface VegaTransactionDialogProps {
|
||||
orderDialogOpen: boolean;
|
||||
@ -117,7 +118,8 @@ export const VegaDialog = ({
|
||||
>
|
||||
{transaction.error && (
|
||||
<pre className="text-ui break-all whitespace-pre-wrap">
|
||||
{JSON.stringify(transaction.error, null, 2)}
|
||||
{get(transaction.error, 'error') ??
|
||||
JSON.stringify(transaction.error, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</OrderDialogWrapper>
|
||||
|
Loading…
Reference in New Issue
Block a user