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 = [ const expectedMarketHeaders = [
'Market', 'Market',
'Settlement asset', 'Settlement asset',
'State', 'Trading mode',
'Best bid', 'Best bid',
'Best offer', 'Best offer',
'Mark price', 'Mark price',
@ -64,43 +64,22 @@ describe('markets table', () => {
mockTradingPage(req, MarketState.Active); 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]').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.wait('@Market');
cy.contains('ACTIVE MARKET'); cy.contains('ACTIVE MARKET');
cy.url().should('include', '/markets/market-0'); cy.url().should('include', '/markets/market-0');
verifyMarketSummaryDisplayed();
verifyMarketSummaryDisplayed('Active');
}); });
it('can select a suspended market', () => { function verifyMarketSummaryDisplayed() {
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) {
const marketSummaryBlock = 'market-summary'; const marketSummaryBlock = 'market-summary';
const percentageValue = 'price-change-percentage'; const percentageValue = 'price-change-percentage';
const priceChangeValue = 'price-change'; const priceChangeValue = 'price-change';
const tradingVolume = 'trading-volume'; const tradingVolume = 'trading-volume';
const tradingMode = 'trading-mode'; const tradingMode = 'trading-mode';
const marketState = 'market-state';
cy.getByTestId(marketSummaryBlock).within(() => { cy.getByTestId(marketSummaryBlock).within(() => {
cy.contains('Change (24h)'); cy.contains('Change (24h)');
@ -110,8 +89,6 @@ describe('markets table', () => {
cy.getByTestId(tradingVolume).should('not.be.empty'); cy.getByTestId(tradingVolume).should('not.be.empty');
cy.contains('Trading mode'); cy.contains('Trading mode');
cy.getByTestId(tradingMode).should('not.be.empty'); 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 tradingMode
state state
decimalPlaces decimalPlaces
positionDecimalPlaces
data { data {
market { market {
id id
@ -31,6 +32,7 @@ const MARKET_QUERY = gql`
bestStaticBidVolume bestStaticBidVolume
bestStaticOfferVolume bestStaticOfferVolume
indicativeVolume indicativeVolume
trigger
} }
tradableInstrument { tradableInstrument {
instrument { instrument {

View File

@ -3,7 +3,7 @@
// @generated // @generated
// This file was automatically generated and should not be edited. // 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 // 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. * the aggregated volume being offered at the best static offer price, excluding pegged orders.
*/ */
bestStaticOfferVolume: string; bestStaticOfferVolume: string;
/**
* what triggered an auction (if an auction was started)
*/
trigger: AuctionTrigger;
} }
export interface Market_market_tradableInstrument_instrument_metadata { export interface Market_market_tradableInstrument_instrument_metadata {
@ -144,6 +148,12 @@ export interface Market_market {
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p) * GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
*/ */
decimalPlaces: number; 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 * marketData for the given market
*/ */

View File

@ -11,7 +11,11 @@ import { TradesContainer } from '@vegaprotocol/trades';
import { PositionsContainer } from '@vegaprotocol/positions'; import { PositionsContainer } from '@vegaprotocol/positions';
import { OrderbookContainer } from '@vegaprotocol/market-depth'; import { OrderbookContainer } from '@vegaprotocol/market-depth';
import type { Market_market } from './__generated__/Market'; 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 { AccountsContainer } from '@vegaprotocol/accounts';
import { DepthChartContainer } from '@vegaprotocol/market-depth'; import { DepthChartContainer } from '@vegaprotocol/market-depth';
import { CandlesChartContainer } from '@vegaprotocol/candles-chart'; import { CandlesChartContainer } from '@vegaprotocol/candles-chart';
@ -23,6 +27,8 @@ import {
PriceCellChange, PriceCellChange,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import type { CandleClose } from '@vegaprotocol/types'; import type { CandleClose } from '@vegaprotocol/types';
import { AuctionTrigger } from '@vegaprotocol/types';
import { MarketTradingMode } from '@vegaprotocol/types';
const TradingViews = { const TradingViews = {
Candles: CandlesChartContainer, Candles: CandlesChartContainer,
@ -55,7 +61,7 @@ export const TradeMarketHeader = ({
const itemClassName = const itemClassName =
'font-sans font-normal mb-0 text-black-60 dark:text-white-80 text-ui-small'; 'font-sans font-normal mb-0 text-black-60 dark:text-white-80 text-ui-small';
const itemValueClassName = 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( const headerClassName = classNames(
'w-full p-8 bg-white dark:bg-black', 'w-full p-8 bg-white dark:bg-black',
className className
@ -87,20 +93,23 @@ export const TradeMarketHeader = ({
<span className={itemClassName}>Volume</span> <span className={itemClassName}>Volume</span>
<span data-testid="trading-volume" className={itemValueClassName}> <span data-testid="trading-volume" className={itemValueClassName}>
{market.data && market.data.indicativeVolume !== '0' {market.data && market.data.indicativeVolume !== '0'
? market.data.indicativeVolume ? addDecimalsFormatNumber(
market.data.indicativeVolume,
market.positionDecimalPlaces
)
: '-'} : '-'}
</span> </span>
</div> </div>
<div className={headerItemClassName}> <div className={headerItemClassName}>
<span className={itemClassName}>Trading mode</span> <span className={itemClassName}>Trading mode</span>
<span data-testid="trading-mode" className={itemValueClassName}> <span data-testid="trading-mode" className={itemValueClassName}>
{market.tradingMode} {market.tradingMode === MarketTradingMode.MonitoringAuction &&
</span> market.data?.trigger &&
</div> market.data.trigger !== AuctionTrigger.Unspecified
<div className={headerItemClassName}> ? `${formatLabel(
<span className={itemClassName}>State</span> market.tradingMode
<span data-testid="market-state" className={itemValueClassName}> )} - ${market.data?.trigger.toLowerCase()}`
{market.state} : formatLabel(market.tradingMode)}
</span> </span>
</div> </div>
</div> </div>

View File

@ -4,7 +4,11 @@ import {
VegaWalletOrderType, VegaWalletOrderType,
VegaWalletOrderTimeInForce, VegaWalletOrderTimeInForce,
} from '@vegaprotocol/wallet'; } 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 { Button, InputError } from '@vegaprotocol/ui-toolkit';
import { TypeSelector } from './type-selector'; import { TypeSelector } from './type-selector';
import { SideSelector } from './side-selector'; import { SideSelector } from './side-selector';
@ -43,22 +47,22 @@ export const DealTicket = ({
const step = toDecimal(market.positionDecimalPlaces); const step = toDecimal(market.positionDecimalPlaces);
const orderType = watch('type'); const orderType = watch('type');
const orderTimeInForce = watch('timeInForce'); const orderTimeInForce = watch('timeInForce');
const invalidText = useOrderValidation({ const { message, isDisabled: disabled } = useOrderValidation({
step, step,
market, market,
orderType, orderType,
orderTimeInForce, orderTimeInForce,
fieldErrors: errors, fieldErrors: errors,
}); });
const isDisabled = transactionStatus === 'pending' || Boolean(invalidText); const isDisabled = transactionStatus === 'pending' || disabled;
const onSubmit = useCallback( const onSubmit = useCallback(
(order: Order) => { (order: Order) => {
if (!isDisabled && !invalidText) { if (!isDisabled) {
submit(order); submit(order);
} }
}, },
[isDisabled, invalidText, submit] [isDisabled, submit]
); );
return ( return (
@ -83,7 +87,10 @@ export const DealTicket = ({
register={register} register={register}
price={ price={
market.depth.lastTrade market.depth.lastTrade
? addDecimal(market.depth.lastTrade.price, market.decimalPlaces) ? addDecimalsFormatNumber(
market.depth.lastTrade.price,
market.decimalPlaces
)
: undefined : undefined
} }
quoteName={market.tradableInstrument.instrument.product.quoteName} quoteName={market.tradableInstrument.instrument.product.quoteName}
@ -118,9 +125,12 @@ export const DealTicket = ({
> >
{transactionStatus === 'pending' ? t('Pending...') : t('Place order')} {transactionStatus === 'pending' ? t('Pending...') : t('Place order')}
</Button> </Button>
{invalidText && ( {message && (
<InputError className="mb-8" data-testid="dealticket-error-message"> <InputError
{invalidText} className="mt-12 mb-12"
data-testid="dealticket-error-message"
>
{message}
</InputError> </InputError>
)} )}
</form> </form>

View File

@ -29,7 +29,10 @@ export const LandingDialog = ({ open, setOpen }: LandingDialogProps) => {
intent={Intent.Primary} intent={Intent.Primary}
open={open} open={open}
onChange={setClose} 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} /> <SelectMarketList data={data} onSelect={setClose} />
</Dialog> </Dialog>

View File

@ -3,7 +3,7 @@
// @generated // @generated
// This file was automatically generated and should not be edited. // 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 // GraphQL fragment: MarketDataFields
@ -43,4 +43,8 @@ export interface MarketDataFields {
* the mark price (actually an unsigned int) * the mark price (actually an unsigned int)
*/ */
markPrice: string; markPrice: string;
/**
* what triggered an auction (if an auction was started)
*/
trigger: AuctionTrigger;
} }

View File

@ -3,7 +3,7 @@
// @generated // @generated
// This file was automatically generated and should not be edited. // 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 // GraphQL subscription operation: MarketDataSub
@ -43,6 +43,10 @@ export interface MarketDataSub_marketData {
* the mark price (actually an unsigned int) * the mark price (actually an unsigned int)
*/ */
markPrice: string; markPrice: string;
/**
* what triggered an auction (if an auction was started)
*/
trigger: AuctionTrigger;
} }
export interface MarketDataSub { export interface MarketDataSub {

View File

@ -3,7 +3,7 @@
// @generated // @generated
// This file was automatically generated and should not be edited. // 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 // GraphQL query operation: Markets
@ -43,6 +43,10 @@ export interface Markets_markets_data {
* the mark price (actually an unsigned int) * the mark price (actually an unsigned int)
*/ */
markPrice: string; markPrice: string;
/**
* what triggered an auction (if an auction was started)
*/
trigger: AuctionTrigger;
} }
export interface Markets_markets_tradableInstrument_instrument_product_settlementAsset { export interface Markets_markets_tradableInstrument_instrument_product_settlementAsset {

View File

@ -4,11 +4,13 @@ import {
PriceFlashCell, PriceFlashCell,
addDecimalsFormatNumber, addDecimalsFormatNumber,
t, t,
formatLabel,
} from '@vegaprotocol/react-helpers'; } from '@vegaprotocol/react-helpers';
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit'; import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
import { AgGridColumn } from 'ag-grid-react'; import { AgGridColumn } from 'ag-grid-react';
import type { AgGridReact } from 'ag-grid-react'; import type { AgGridReact } from 'ag-grid-react';
import type { Markets_markets } from '../__generated__/Markets'; import type { Markets_markets } from '../__generated__/Markets';
import { MarketTradingMode, AuctionTrigger } from '@vegaprotocol/types';
interface MarketListTableProps { interface MarketListTableProps {
datasource: IDatasource; datasource: IDatasource;
@ -44,13 +46,19 @@ export const MarketListTable = forwardRef<AgGridReact, MarketListTableProps>(
field="tradableInstrument.instrument.product.settlementAsset.symbol" field="tradableInstrument.instrument.product.settlementAsset.symbol"
/> />
<AgGridColumn <AgGridColumn
headerName={t('State')} headerName={t('Trading mode')}
field="data" field="data"
valueFormatter={({ value }: ValueFormatterParams) => minWidth={200}
value === undefined valueFormatter={({ value }: ValueFormatterParams) => {
? value if (!value) return value;
: `${value.market.state} (${value.market.tradingMode})` const { market, trigger } = value;
} return market &&
market.tradingMode === MarketTradingMode.MonitoringAuction &&
trigger &&
trigger !== AuctionTrigger.Unspecified
? `${formatLabel(market.tradingMode)} - ${trigger.toLowerCase()}`
: formatLabel(market?.tradingMode);
}}
/> />
<AgGridColumn <AgGridColumn
headerName={t('Best bid')} headerName={t('Best bid')}

View File

@ -10,6 +10,7 @@ import type {
Markets_markets_data, Markets_markets_data,
} from '../../components/__generated__/Markets'; } from '../../components/__generated__/Markets';
import { marketsDataProvider as dataProvider } from './markets-data-provider'; import { marketsDataProvider as dataProvider } from './markets-data-provider';
import { MarketState } from '@vegaprotocol/types';
export const MarketsContainer = () => { export const MarketsContainer = () => {
const { push } = useRouter(); const { push } = useRouter();
@ -28,14 +29,15 @@ export const MarketsContainer = () => {
Markets_markets_data Markets_markets_data
>({ dataProvider, update }); >({ dataProvider, update });
dataRef.current = data; dataRef.current = data;
const getRows = async ({ const getRows = async ({
successCallback, successCallback,
startRow, startRow,
endRow, endRow,
}: IGetRowsParams) => { }: IGetRowsParams) => {
const rowsThisBlock = dataRef.current 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; const lastRow = dataRef.current?.length ?? -1;
successCallback(rowsThisBlock, lastRow); successCallback(rowsThisBlock, lastRow);

View File

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

View File

@ -1,3 +1,4 @@
import { MarketState } from '@vegaprotocol/types';
import orderBy from 'lodash/orderBy'; import orderBy from 'lodash/orderBy';
import type { import type {
MarketList, MarketList,
@ -11,22 +12,24 @@ export const lastPrice = ({ candles }: MarketList_markets) =>
export const mapDataToMarketList = ({ markets }: MarketList) => export const mapDataToMarketList = ({ markets }: MarketList) =>
orderBy( orderBy(
markets?.map((m) => { markets
return { ?.filter((m) => m.state !== MarketState.Rejected)
id: m.id, .map((m) => {
decimalPlaces: m.decimalPlaces, return {
marketName: m.tradableInstrument.instrument?.code, id: m.id,
lastPrice: lastPrice(m) ?? m.data?.markPrice, decimalPlaces: m.decimalPlaces,
candles: (m.candles || []).filter((c) => c), marketName: m.tradableInstrument.instrument?.code,
open: m.marketTimestamps.open lastPrice: lastPrice(m) ?? m.data?.markPrice,
? new Date(m.marketTimestamps.open).getTime() candles: (m.candles || []).filter((c) => c),
: null, open: m.marketTimestamps.open
close: m.marketTimestamps.close ? new Date(m.marketTimestamps.open).getTime()
? new Date(m.marketTimestamps.close).getTime() : null,
: null, close: m.marketTimestamps.close
state: m.state, ? new Date(m.marketTimestamps.close).getTime()
}; : null,
}) || [], state: m.state,
};
}) || [],
['state', 'open', 'id'], ['state', 'open', 'id'],
['asc', 'asc', 'asc'] ['asc', 'asc', 'asc']
); );

View File

@ -10,6 +10,7 @@ import type {
} from '@vegaprotocol/wallet'; } from '@vegaprotocol/wallet';
import { MarketState, MarketTradingMode } from '@vegaprotocol/types'; import { MarketState, MarketTradingMode } from '@vegaprotocol/types';
import type { ValidationProps } from './use-order-validation'; import type { ValidationProps } from './use-order-validation';
import { marketTranslations } from './use-order-validation';
import { useOrderValidation } from './use-order-validation'; import { useOrderValidation } from './use-order-validation';
import { ERROR_SIZE_DECIMAL } from '../utils/validate-size'; import { ERROR_SIZE_DECIMAL } from '../utils/validate-size';
import type { Market } from '../market'; import type { Market } from '../market';
@ -73,12 +74,12 @@ const ERROR = {
'Only limit orders are permitted when market is in auction', 'Only limit orders are permitted when market is in auction',
MARKET_CONTINUOUS_TIF: MARKET_CONTINUOUS_TIF:
'Only GTT, GTC and GFA are permitted when market is in auction', '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_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_MIN: 'The price cannot be negative',
FIELD_PRICE_STEP_NULL: 'No decimal amounts allowed for this order', FIELD_PRICE_STEP_NULL: 'Order sizes must be in whole numbers for this market',
FIELD_PRICE_STEP_DECIMAL: `The amount field only takes up to ${market.positionDecimalPlaces} decimals`, FIELD_PRICE_STEP_DECIMAL: `The amount field accepts up to ${market.positionDecimalPlaces} decimal places`,
}; };
function setup( function setup(
@ -90,107 +91,148 @@ function setup(
return renderHook(() => useOrderValidation({ ...defaultOrder, ...props })); return renderHook(() => useOrderValidation({ ...defaultOrder, ...props }));
} }
it('Returns empty string when given valid data', () => { describe('useOrderValidation', () => {
const { result } = setup(); it('Returns empty string when given valid data', () => {
expect(result.current).toEqual(''); const { result } = setup();
}); expect(result.current).toStrictEqual({ isDisabled: false, message: `` });
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 },
}); });
expect(result.current).toEqual('');
});
it.each` it('Returns an error message when no keypair found', () => {
state | errorMessage const { result } = setup(defaultOrder, { keypair: null });
${MarketState.Cancelled} | ${ERROR.MARKET_INACTIVE} expect(result.current).toStrictEqual({ isDisabled: false, message: `` });
${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 } },
}); });
expect(result.current).toEqual(ERROR.FIELD_PRICE_STEP_NULL);
});
it('Returns an error message when the order size has more decimals then allowed', async () => { it('Returns an error message when the keypair is tainted', () => {
const { result } = setup({ const { result } = setup(defaultOrder, {
fieldErrors: { size: { type: 'validate', message: ERROR_SIZE_DECIMAL } }, 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>; fieldErrors?: FieldErrors<Order>;
}; };
export const marketTranslations = (marketState: MarketState) => {
switch (marketState) {
case MarketState.TradingTerminated:
return t('terminated');
default:
return t(marketState).toLowerCase();
}
};
export const useOrderValidation = ({ export const useOrderValidation = ({
step, step,
market, market,
@ -28,33 +37,120 @@ export const useOrderValidation = ({
}: ValidationProps) => { }: ValidationProps) => {
const { keypair } = useVegaWallet(); const { keypair } = useVegaWallet();
const invalidText = useMemo(() => { const { message, isDisabled } = useMemo(() => {
if (!keypair) { if (!keypair) {
return t('No public key selected'); return { message: t('No public key selected'), isDisabled: true };
} }
if (keypair.tainted) { 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.Active) {
if (market.state === MarketState.Suspended) { 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 ( if (
market.state === MarketState.Proposed || market.state === MarketState.Proposed ||
market.state === MarketState.Pending 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 (market.tradingMode !== MarketTradingMode.Continuous) {
if (orderType !== OrderType.Limit) { 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 ( if (
@ -64,26 +160,41 @@ export const useOrderValidation = ({
OrderTimeInForce.GFN, OrderTimeInForce.GFN,
].includes(orderTimeInForce) ].includes(orderTimeInForce)
) { ) {
return t( return {
'Only GTT, GTC and GFA are permitted when market is in auction' isDisabled: true,
); message: t(
'Only GTT, GTC and GFA are permitted when market is in auction'
),
};
} }
} }
if (fieldErrors?.size?.type === 'required') { 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') { 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') { 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') { if (fieldErrors?.price?.type === 'min') {
return t(`The price cannot be negative`); return {
isDisabled: true,
message: t(`The price cannot be negative`),
};
} }
if ( if (
@ -91,14 +202,20 @@ export const useOrderValidation = ({
fieldErrors?.size?.message === ERROR_SIZE_DECIMAL fieldErrors?.size?.message === ERROR_SIZE_DECIMAL
) { ) {
if (market.positionDecimalPlaces === 0) { 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( return {
`The amount field only takes up to ${market.positionDecimalPlaces} decimals` isDisabled: true,
); message: t(
`The amount field accepts up to ${market.positionDecimalPlaces} decimal places`
),
};
} }
return ''; return { isDisabled: false, message: '' };
}, [ }, [
keypair, keypair,
step, step,
@ -110,5 +227,5 @@ export const useOrderValidation = ({
orderTimeInForce, 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 getUserLocale = () => 'default';
export const splitAt = (index: number) => (x: string) => export const splitAt = (index: number) => (x: string) =>
[x.slice(0, index), x.slice(index)]; [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) { }: DialogProps) {
const contentClasses = classNames( const contentClasses = classNames(
// Positions the modal in the center of screen // 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 // 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', 'dark:bg-black dark:text-white-95 bg-white text-black-95',
getIntentShadow(intent), getIntentShadow(intent),

View File

@ -17,6 +17,7 @@ export const Icon = ({ size = 16, name, className, ariaLabel }: IconProps) => {
'fill-current', 'fill-current',
'align-text-bottom', 'align-text-bottom',
'fill-current', 'fill-current',
'shrink-0',
{ {
'w-20': size === 20, 'w-20': size === 20,
'h-20': size === 20, 'h-20': size === 20,

View File

@ -21,7 +21,6 @@ export const InputError = ({
'flex', 'flex',
'items-center', 'items-center',
'box-border', 'box-border',
'h-28',
'border-l-4', 'border-l-4',
'text-black-95 dark:text-white-95', 'text-black-95 dark:text-white-95',
'text-ui', 'text-ui',
@ -44,7 +43,7 @@ export const InputError = ({
{...props} {...props}
role="alert" role="alert"
> >
<Icon name="warning-sign" className={iconClassName} /> <Icon name="warning-sign" size={20} className={iconClassName} />
{children} {children}
</div> </div>
); );

View File

@ -8,6 +8,7 @@ import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers';
import { useEnvironment } from '@vegaprotocol/environment'; import { useEnvironment } from '@vegaprotocol/environment';
import { OrderType } from '@vegaprotocol/types'; import { OrderType } from '@vegaprotocol/types';
import type { Order } from '../wallet-types'; import type { Order } from '../wallet-types';
import get from 'lodash/get';
export interface VegaTransactionDialogProps { export interface VegaTransactionDialogProps {
orderDialogOpen: boolean; orderDialogOpen: boolean;
@ -117,7 +118,8 @@ export const VegaDialog = ({
> >
{transaction.error && ( {transaction.error && (
<pre className="text-ui break-all whitespace-pre-wrap"> <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> </pre>
)} )}
</OrderDialogWrapper> </OrderDialogWrapper>