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 commit c9ab55c98a.

* Revert "fix: remove market state check from markets.cy.ts"

This reverts commit be60e56d8a.

* 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:
m.ray 2022-07-16 15:31:47 +02:00 committed by GitHub
parent 00e786d596
commit 5c038690c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 406 additions and 202 deletions

View File

@ -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);
});
}
});

View File

@ -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 {

View File

@ -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
*/

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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;
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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')}

View File

@ -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);

View File

@ -21,6 +21,7 @@ const MARKET_DATA_FRAGMENT = gql`
bestBidPrice
bestOfferPrice
markPrice
trigger
}
`;

View File

@ -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']
);

View File

@ -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);
});

View File

@ -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 };
};

View File

@ -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());

View File

@ -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),

View File

@ -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,

View File

@ -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>
);

View File

@ -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>