feat(): fractional orders ()

* feat: add positionDecimalPlaces prop to market query and regenerate types

* feat: add stepper to order amount input

* feat: convert fractions back to integers when submitting the order

* refactor: move order transformations into hooks

* fix: formatting

* fix: simplify toDecimal calculation

* fix: remove redundant function for size calculation

* fix: add new prop to e2e test mock generator

* feat: add tests for order validation

* fix: lint

* chore: add validation to the simple trading app order form

* fix: lint
This commit is contained in:
botond 2022-05-31 23:20:01 +01:00 committed by GitHub
parent 707ccc0136
commit bf07dac445
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 765 additions and 548 deletions

View File

@ -1,40 +1,54 @@
import * as React from 'react';
import type { FormEvent } from 'react';
import { useForm, Controller } from 'react-hook-form';
import Box from '@mui/material/Box';
import { Stepper } from '../stepper';
import type { Order, DealTicketQuery_market } from '@vegaprotocol/deal-ticket';
import type { DealTicketQuery_market, Order } from '@vegaprotocol/deal-ticket';
import { Button, InputError } from '@vegaprotocol/ui-toolkit';
import {
ExpirySelector,
SideSelector,
SubmitButton,
TimeInForceSelector,
TypeSelector,
useOrderState,
getDefaultOrder,
useOrderValidation,
useOrderSubmit,
DealTicketLimitForm,
DealTicketMarketForm,
DealTicketAmount,
} from '@vegaprotocol/deal-ticket';
import {
OrderSide,
OrderTimeInForce,
OrderType,
VegaTxStatus,
} from '@vegaprotocol/wallet';
import { addDecimal } from '@vegaprotocol/react-helpers';
import { t, addDecimal, toDecimal } from '@vegaprotocol/react-helpers';
interface DealTicketMarketProps {
market: DealTicketQuery_market;
}
const DEFAULT_ORDER: Order = {
type: OrderType.Market,
side: OrderSide.Buy,
size: '1',
timeInForce: OrderTimeInForce.IOC,
};
export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
const [order, updateOrder] = useOrderState(DEFAULT_ORDER);
const {
register,
control,
handleSubmit,
watch,
formState: { errors },
} = useForm<Order>({
mode: 'onChange',
defaultValues: getDefaultOrder(market),
});
const step = toDecimal(market.positionDecimalPlaces);
const orderType = watch('type');
const orderTimeInForce = watch('timeInForce');
const invalidText = useOrderValidation({
step,
market,
orderType,
orderTimeInForce,
fieldErrors: errors,
});
const { submit, transaction } = useOrderSubmit(market);
const transactionStatus =
@ -43,39 +57,14 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
? 'pending'
: 'default';
let ticket = null;
if (order.type === OrderType.Market) {
ticket = (
<DealTicketMarketForm
size={order.size}
onSizeChange={(size) => updateOrder({ size })}
price={
market.depth.lastTrade
? addDecimal(market.depth.lastTrade.price, market.decimalPlaces)
: undefined
}
quoteName={market.tradableInstrument.instrument.product.quoteName}
/>
);
} else if (order.type === OrderType.Limit) {
ticket = (
<DealTicketLimitForm
price={order.price}
size={order.size}
quoteName={market.tradableInstrument.instrument.product.quoteName}
onSizeChange={(size) => updateOrder({ size })}
onPriceChange={(price) => updateOrder({ price })}
/>
);
} else {
throw new Error('Invalid ticket type');
}
const handleSubmit = (e: FormEvent<HTMLFormElement>): Promise<void> => {
e.preventDefault();
return submit(order);
};
const onSubmit = React.useCallback(
(order: Order) => {
if (transactionStatus !== 'pending') {
submit(order);
}
},
[transactionStatus, submit]
);
const steps = [
{
@ -87,9 +76,12 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
label: 'Select Order Type',
description: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.`,
component: (
<TypeSelector
order={order}
onSelect={(type) => updateOrder({ type })}
<Controller
name="type"
control={control}
render={({ field }) => (
<TypeSelector value={field.value} onSelect={field.onChange} />
)}
/>
),
},
@ -97,9 +89,12 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
label: 'Select Market Position',
description: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.`,
component: (
<SideSelector
order={order}
onSelect={(side) => updateOrder({ side })}
<Controller
name="side"
control={control}
render={({ field }) => (
<SideSelector value={field.value} onSelect={field.onChange} />
)}
/>
),
},
@ -107,27 +102,49 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
label: 'Select Order Size',
description:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
component: ticket,
component: (
<DealTicketAmount
orderType={orderType}
step={0.02}
register={register}
price={
market.depth.lastTrade
? addDecimal(market.depth.lastTrade.price, market.decimalPlaces)
: undefined
}
quoteName={market.tradableInstrument.instrument.product.quoteName}
/>
),
},
{
label: 'Select Time In Force',
description: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.`,
component: (
<>
<TimeInForceSelector
order={order}
onSelect={(timeInForce) => updateOrder({ timeInForce })}
<Controller
name="timeInForce"
control={control}
render={({ field }) => (
<TimeInForceSelector
value={field.value}
orderType={orderType}
onSelect={field.onChange}
/>
)}
/>
{order.timeInForce === OrderTimeInForce.GTT && (
<ExpirySelector
order={order}
onSelect={(date) => {
if (date) {
updateOrder({ expiration: date });
}
}}
/>
)}
{orderType === OrderType.Limit &&
orderTimeInForce === OrderTimeInForce.GTT && (
<Controller
name="expiration"
control={control}
render={({ field }) => (
<ExpirySelector
value={field.value}
onSelect={field.onChange}
/>
)}
/>
)}
</>
),
},
@ -136,11 +153,22 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
description: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.`,
component: (
<Box sx={{ mb: 2 }}>
<SubmitButton
transactionStatus={transactionStatus}
market={market}
order={order}
/>
{invalidText && (
<InputError className="mb-8" data-testid="dealticket-error-message">
{invalidText}
</InputError>
)}
<Button
className="w-full mb-8"
variant="primary"
type="submit"
disabled={transactionStatus === 'pending' || !!invalidText}
data-testid="place-order"
>
{transactionStatus === 'pending'
? t('Pending...')
: t('Place order')}
</Button>
</Box>
),
disabled: true,
@ -148,7 +176,7 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
];
return (
<form onSubmit={handleSubmit} className="px-4 py-8">
<form onSubmit={handleSubmit(onSubmit)} className="px-4 py-8">
<Stepper steps={steps} />
</form>
);

View File

@ -10,6 +10,7 @@ export const generateDealTicketQuery = (
market: {
id: 'market-id',
decimalPlaces: 2,
positionDecimalPlaces: 1,
state: MarketState.Active,
tradingMode: MarketTradingMode.Continuous,
tradableInstrument: {

View File

@ -72,6 +72,12 @@ export interface DealTicketQuery_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;
/**
* Current state of the market
*/

View File

@ -0,0 +1,33 @@
import type { UseFormRegister } from 'react-hook-form';
import { OrderType } from '@vegaprotocol/wallet';
import type { Order } from '../utils/get-default-order';
import { DealTicketMarketAmount } from './deal-ticket-market-amount';
import { DealTicketLimitAmount } from './deal-ticket-limit-amount';
export interface DealTicketAmountProps {
orderType: OrderType;
step: number;
register: UseFormRegister<Order>;
quoteName: string;
price?: string;
}
const getAmountComponent = (type: OrderType) => {
switch (type) {
case OrderType.Market:
return DealTicketMarketAmount;
case OrderType.Limit:
return DealTicketLimitAmount;
default: {
throw new Error('Invalid ticket type');
}
}
};
export const DealTicketAmount = ({
orderType,
...props
}: DealTicketAmountProps) => {
const AmountComponent = getAmountComponent(orderType);
return <AmountComponent {...props} />;
};

View File

@ -4,7 +4,7 @@ import { DealTicketManager } from './deal-ticket-manager';
import type {
DealTicketQuery,
DealTicketQuery_market,
} from './__generated__/DealTicketQuery';
} from '../__generated__/DealTicketQuery';
import { t } from '@vegaprotocol/react-helpers';
const DEAL_TICKET_QUERY = gql`
@ -12,6 +12,7 @@ const DEAL_TICKET_QUERY = gql`
market(id: $marketId) {
id
decimalPlaces
positionDecimalPlaces
state
tradingMode
tradableInstrument {

View File

@ -1,30 +1,32 @@
import { FormGroup, Input } from '@vegaprotocol/ui-toolkit';
import { validateSize } from '../utils/validate-size';
import type { DealTicketAmountProps } from './deal-ticket-amount';
export interface DealTicketLimitFormProps {
quoteName: string;
price?: string;
size: string;
onSizeChange: (size: string) => void;
onPriceChange: (price: string) => void;
}
export type DealTicketLimitAmountProps = Omit<
DealTicketAmountProps,
'orderType'
>;
export const DealTicketLimitForm = ({
size,
price,
onSizeChange,
onPriceChange,
export const DealTicketLimitAmount = ({
register,
step,
quoteName,
}: DealTicketLimitFormProps) => {
}: DealTicketLimitAmountProps) => {
return (
<div className="flex items-center gap-8">
<div className="flex-1">
<FormGroup label="Amount">
<Input
value={size}
onChange={(e) => onSizeChange(e.target.value)}
className="w-full"
type="number"
step={step}
min={step}
data-testid="order-size"
{...register('size', {
required: true,
min: step,
validate: validateSize(step),
})}
/>
</FormGroup>
</div>
@ -32,11 +34,12 @@ export const DealTicketLimitForm = ({
<div className="flex-1">
<FormGroup label={`Price (${quoteName})`} labelAlign="right">
<Input
value={price}
onChange={(e) => onPriceChange(e.target.value)}
className="w-full"
type="number"
step={step}
defaultValue={0}
data-testid="order-price"
{...register('price', { required: true, min: 0 })}
/>
</FormGroup>
</div>

View File

@ -4,9 +4,9 @@ import { Dialog, Intent } from '@vegaprotocol/ui-toolkit';
import { OrderStatus } from '@vegaprotocol/types';
import { VegaTxStatus } from '@vegaprotocol/wallet';
import { DealTicket } from './deal-ticket';
import { useOrderSubmit } from './use-order-submit';
import { OrderDialog } from './order-dialog';
import type { DealTicketQuery_market } from './__generated__/DealTicketQuery';
import { useOrderSubmit } from '../hooks/use-order-submit';
import type { DealTicketQuery_market } from '../__generated__/DealTicketQuery';
export interface DealTicketManagerProps {
market: DealTicketQuery_market;

View File

@ -1,28 +1,33 @@
import { FormGroup, Input } from '@vegaprotocol/ui-toolkit';
import { validateSize } from '../utils/validate-size';
import type { DealTicketAmountProps } from './deal-ticket-amount';
export interface DealTicketMarketFormProps {
quoteName?: string;
price?: string;
size: string;
onSizeChange: (size: string) => void;
}
export type DealTicketMarketAmountProps = Omit<
DealTicketAmountProps,
'orderType'
>;
export const DealTicketMarketForm = ({
size,
onSizeChange,
export const DealTicketMarketAmount = ({
register,
price,
step,
quoteName,
}: DealTicketMarketFormProps) => {
}: DealTicketMarketAmountProps) => {
return (
<div className="flex items-center gap-8">
<div className="flex-1">
<FormGroup label="Amount">
<Input
value={size}
onChange={(e) => onSizeChange(e.target.value)}
className="w-full"
type="number"
step={step}
min={step}
data-testid="order-size"
{...register('size', {
required: true,
min: step,
validate: validateSize(step),
})}
/>
</FormGroup>
</div>

View File

@ -4,22 +4,17 @@ import {
OrderType,
} from '@vegaprotocol/wallet';
import { addDecimal } from '@vegaprotocol/react-helpers';
import { fireEvent, render, screen } from '@testing-library/react';
import { fireEvent, render, screen, act } from '@testing-library/react';
import { DealTicket } from './deal-ticket';
import type { Order } from './use-order-state';
import type { DealTicketQuery_market } from './__generated__/DealTicketQuery';
import type { DealTicketQuery_market } from '../__generated__/DealTicketQuery';
import type { Order } from '../utils/get-default-order';
import { MarketState, MarketTradingMode } from '@vegaprotocol/types';
const order: Order = {
type: OrderType.Market,
size: '100',
timeInForce: OrderTimeInForce.FOK,
side: null,
};
const market: DealTicketQuery_market = {
__typename: 'Market',
id: 'market-id',
decimalPlaces: 2,
positionDecimalPlaces: 1,
tradingMode: MarketTradingMode.Continuous,
state: MarketState.Active,
tradableInstrument: {
@ -43,7 +38,7 @@ const market: DealTicketQuery_market = {
const submit = jest.fn();
const transactionStatus = 'default';
function generateJsx() {
function generateJsx(order?: Order) {
return (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
<VegaWalletContext.Provider value={{} as any}>
@ -57,21 +52,23 @@ function generateJsx() {
);
}
it('Deal ticket defaults', () => {
it('Displays ticket defaults', () => {
render(generateJsx());
// Assert defaults are used
expect(
screen.getByTestId(`order-type-${order.type}-selected`)
screen.getByTestId(`order-type-${OrderType.Market}-selected`)
).toBeInTheDocument();
expect(
screen.queryByTestId('order-side-SIDE_BUY-selected')
).not.toBeInTheDocument();
).toBeInTheDocument();
expect(
screen.queryByTestId('order-side-SIDE_SELL-selected')
).not.toBeInTheDocument();
expect(screen.getByTestId('order-size')).toHaveDisplayValue(order.size);
expect(screen.getByTestId('order-tif')).toHaveValue(order.timeInForce);
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
String(1 / Math.pow(10, market.positionDecimalPlaces))
);
expect(screen.getByTestId('order-tif')).toHaveValue(OrderTimeInForce.IOC);
// Assert last price is shown
expect(screen.getByTestId('last-price')).toHaveTextContent(
@ -82,18 +79,18 @@ it('Deal ticket defaults', () => {
);
});
it('Can edit deal ticket', () => {
it('Can edit deal ticket', async () => {
render(generateJsx());
// Asssert changing values
fireEvent.click(screen.getByTestId('order-side-SIDE_BUY'));
expect(
screen.getByTestId('order-side-SIDE_BUY-selected')
).toBeInTheDocument();
// BUY is selected by default
screen.getByTestId('order-side-SIDE_BUY-selected');
fireEvent.change(screen.getByTestId('order-size'), {
target: { value: '200' },
await act(async () => {
fireEvent.change(screen.getByTestId('order-size'), {
target: { value: '200' },
});
});
expect(screen.getByTestId('order-size')).toHaveDisplayValue('200');
fireEvent.change(screen.getByTestId('order-tif'), {

View File

@ -0,0 +1,126 @@
import { useCallback } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { OrderType, OrderTimeInForce } from '@vegaprotocol/wallet';
import { t, addDecimal, toDecimal } from '@vegaprotocol/react-helpers';
import { Button, InputError } from '@vegaprotocol/ui-toolkit';
import { TypeSelector } from './type-selector';
import { SideSelector } from './side-selector';
import { DealTicketAmount } from './deal-ticket-amount';
import { TimeInForceSelector } from './time-in-force-selector';
import { ExpirySelector } from './expiry-selector';
import type { DealTicketQuery_market } from '../__generated__/DealTicketQuery';
import type { Order } from '../utils/get-default-order';
import { getDefaultOrder } from '../utils/get-default-order';
import { useOrderValidation } from '../hooks/use-order-validation';
export type TransactionStatus = 'default' | 'pending';
export interface DealTicketProps {
market: DealTicketQuery_market;
submit: (order: Order) => void;
transactionStatus: TransactionStatus;
defaultOrder?: Order;
}
export const DealTicket = ({
market,
submit,
transactionStatus,
}: DealTicketProps) => {
const {
register,
control,
handleSubmit,
watch,
formState: { errors },
} = useForm<Order>({
mode: 'onChange',
defaultValues: getDefaultOrder(market),
});
const step = toDecimal(market.positionDecimalPlaces);
const orderType = watch('type');
const orderTimeInForce = watch('timeInForce');
const invalidText = useOrderValidation({
step,
market,
orderType,
orderTimeInForce,
fieldErrors: errors,
});
const isDisabled = transactionStatus === 'pending' || Boolean(invalidText);
const onSubmit = useCallback(
(order: Order) => {
if (!isDisabled && !invalidText) {
submit(order);
}
},
[isDisabled, invalidText, submit]
);
return (
<form onSubmit={handleSubmit(onSubmit)} className="px-4 py-8" noValidate>
<Controller
name="type"
control={control}
render={({ field }) => (
<TypeSelector value={field.value} onSelect={field.onChange} />
)}
/>
<Controller
name="side"
control={control}
render={({ field }) => (
<SideSelector value={field.value} onSelect={field.onChange} />
)}
/>
<DealTicketAmount
orderType={orderType}
step={0.02}
register={register}
price={
market.depth.lastTrade
? addDecimal(market.depth.lastTrade.price, market.decimalPlaces)
: undefined
}
quoteName={market.tradableInstrument.instrument.product.quoteName}
/>
<Controller
name="timeInForce"
control={control}
render={({ field }) => (
<TimeInForceSelector
value={field.value}
orderType={orderType}
onSelect={field.onChange}
/>
)}
/>
{orderType === OrderType.Limit &&
orderTimeInForce === OrderTimeInForce.GTT && (
<Controller
name="expiration"
control={control}
render={({ field }) => (
<ExpirySelector value={field.value} onSelect={field.onChange} />
)}
/>
)}
<Button
className="w-full mb-8"
variant="primary"
type="submit"
disabled={isDisabled}
data-testid="place-order"
>
{transactionStatus === 'pending' ? t('Pending...') : t('Place order')}
</Button>
{invalidText && (
<InputError className="mb-8" data-testid="dealticket-error-message">
{invalidText}
</InputError>
)}
</form>
);
};

View File

@ -1,14 +1,13 @@
import { FormGroup, Input } from '@vegaprotocol/ui-toolkit';
import type { Order } from './use-order-state';
import { formatForInput } from '@vegaprotocol/react-helpers';
interface ExpirySelectorProps {
order: Order;
value?: Date;
onSelect: (expiration: Date | null) => void;
}
export const ExpirySelector = ({ order, onSelect }: ExpirySelectorProps) => {
const date = order.expiration ? new Date(order.expiration) : new Date();
export const ExpirySelector = ({ value, onSelect }: ExpirySelectorProps) => {
const date = value ? new Date(value) : new Date();
const dateFormatted = formatForInput(date);
const minDate = formatForInput(date);
return (

View File

@ -1,6 +1,6 @@
import { Icon, Loader } from '@vegaprotocol/ui-toolkit';
import type { ReactNode } from 'react';
import type { OrderEvent_busEvents_event_Order } from './__generated__/OrderEvent';
import type { OrderEvent_busEvents_event_Order } from '../__generated__/OrderEvent';
import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers';
import type { VegaTxState } from '@vegaprotocol/wallet';
import { VegaTxStatus } from '@vegaprotocol/wallet';

View File

@ -1,14 +1,13 @@
import { FormGroup } from '@vegaprotocol/ui-toolkit';
import { OrderSide } from '@vegaprotocol/wallet';
import { Toggle } from '@vegaprotocol/ui-toolkit';
import type { Order } from './use-order-state';
interface SideSelectorProps {
order: Order;
value: OrderSide;
onSelect: (side: OrderSide) => void;
}
export const SideSelector = ({ order, onSelect }: SideSelectorProps) => {
export const SideSelector = ({ value, onSelect }: SideSelectorProps) => {
const toggles = Object.entries(OrderSide).map(([label, value]) => ({
label,
value,
@ -19,7 +18,7 @@ export const SideSelector = ({ order, onSelect }: SideSelectorProps) => {
<Toggle
name="order-side"
toggles={toggles}
checkedValue={order.side}
checkedValue={value}
onChange={(e) => onSelect(e.target.value as OrderSide)}
/>
</FormGroup>

View File

@ -1,28 +1,30 @@
import { FormGroup, Select } from '@vegaprotocol/ui-toolkit';
import { OrderTimeInForce, OrderType } from '@vegaprotocol/wallet';
import type { Order } from './use-order-state';
interface TimeInForceSelectorProps {
order: Order;
value: OrderTimeInForce;
orderType: OrderType;
onSelect: (tif: OrderTimeInForce) => void;
}
export const TimeInForceSelector = ({
order,
value,
orderType,
onSelect,
}: TimeInForceSelectorProps) => {
const options =
order.type === OrderType.Limit
orderType === OrderType.Limit
? Object.entries(OrderTimeInForce)
: Object.entries(OrderTimeInForce).filter(
([_, value]) =>
value === OrderTimeInForce.FOK || value === OrderTimeInForce.IOC
([_, timeInForce]) =>
timeInForce === OrderTimeInForce.FOK ||
timeInForce === OrderTimeInForce.IOC
);
return (
<FormGroup label="Time in force">
<Select
value={order.timeInForce}
value={value}
onChange={(e) => onSelect(e.target.value as OrderTimeInForce)}
className="w-full"
data-testid="order-tif"

View File

@ -1,25 +1,24 @@
import { FormGroup } from '@vegaprotocol/ui-toolkit';
import { OrderType } from '@vegaprotocol/wallet';
import { Toggle } from '@vegaprotocol/ui-toolkit';
import type { Order } from './use-order-state';
interface TypeSelectorProps {
order: Order;
value: OrderType;
onSelect: (type: OrderType) => void;
}
export const TypeSelector = ({ order, onSelect }: TypeSelectorProps) => {
const toggles = Object.entries(OrderType).map(([label, value]) => ({
label,
value,
}));
const toggles = Object.entries(OrderType).map(([label, value]) => ({
label,
value,
}));
export const TypeSelector = ({ value, onSelect }: TypeSelectorProps) => {
return (
<FormGroup label="Order type">
<Toggle
name="order-type"
toggles={toggles}
checkedValue={order.type}
checkedValue={value}
onChange={(e) => onSelect(e.target.value as OrderType)}
/>
</FormGroup>

View File

@ -1,57 +0,0 @@
import { OrderTimeInForce } from '@vegaprotocol/wallet';
import type { TransactionStatus } from './deal-ticket';
import { ExpirySelector } from './expiry-selector';
import { SideSelector } from './side-selector';
import { SubmitButton } from './submit-button';
import { DealTicketLimitForm } from './deal-ticket-limit-form';
import { TimeInForceSelector } from './time-in-force-selector';
import { TypeSelector } from './type-selector';
import type { Order } from './use-order-state';
import type { DealTicketQuery_market } from './__generated__/DealTicketQuery';
interface DealTicketLimitProps {
order: Order;
updateOrder: (order: Partial<Order>) => void;
transactionStatus: TransactionStatus;
market: DealTicketQuery_market;
}
export const DealTicketLimit = ({
order,
updateOrder,
transactionStatus,
market,
}: DealTicketLimitProps) => {
return (
<>
<TypeSelector order={order} onSelect={(type) => updateOrder({ type })} />
<SideSelector order={order} onSelect={(side) => updateOrder({ side })} />
<DealTicketLimitForm
price={order.price}
size={order.size}
quoteName={market.tradableInstrument.instrument.product.quoteName}
onSizeChange={(size) => updateOrder({ size })}
onPriceChange={(price) => updateOrder({ price })}
/>
<TimeInForceSelector
order={order}
onSelect={(timeInForce) => updateOrder({ timeInForce })}
/>
{order.timeInForce === OrderTimeInForce.GTT && (
<ExpirySelector
order={order}
onSelect={(date) => {
if (date) {
updateOrder({ expiration: date });
}
}}
/>
)}
<SubmitButton
transactionStatus={transactionStatus}
market={market}
order={order}
/>
</>
);
};

View File

@ -1,49 +0,0 @@
import type { TransactionStatus } from './deal-ticket';
import { SideSelector } from './side-selector';
import { DealTicketMarketForm } from './deal-ticket-market-form';
import { SubmitButton } from './submit-button';
import { TimeInForceSelector } from './time-in-force-selector';
import { TypeSelector } from './type-selector';
import type { Order } from './use-order-state';
import type { DealTicketQuery_market } from './__generated__/DealTicketQuery';
import { addDecimal } from '@vegaprotocol/react-helpers';
interface DealTicketMarketProps {
order: Order;
updateOrder: (order: Partial<Order>) => void;
transactionStatus: TransactionStatus;
market: DealTicketQuery_market;
}
export const DealTicketMarket = ({
order,
updateOrder,
transactionStatus,
market,
}: DealTicketMarketProps) => {
return (
<>
<TypeSelector order={order} onSelect={(type) => updateOrder({ type })} />
<SideSelector order={order} onSelect={(side) => updateOrder({ side })} />
<DealTicketMarketForm
size={order.size}
onSizeChange={(size) => updateOrder({ size })}
price={
market.depth.lastTrade
? addDecimal(market.depth.lastTrade.price, market.decimalPlaces)
: undefined
}
quoteName={market.tradableInstrument.instrument.product.quoteName}
/>
<TimeInForceSelector
order={order}
onSelect={(timeInForce) => updateOrder({ timeInForce })}
/>
<SubmitButton
transactionStatus={transactionStatus}
market={market}
order={order}
/>
</>
);
};

View File

@ -1,67 +0,0 @@
import type { FormEvent } from 'react';
import { OrderSide, OrderTimeInForce, OrderType } from '@vegaprotocol/wallet';
import type { Order } from './use-order-state';
import { useOrderState } from './use-order-state';
import { DealTicketMarket } from './deal-ticket-market';
import { DealTicketLimit } from './deal-ticket-limit';
import type { DealTicketQuery_market } from './__generated__/DealTicketQuery';
const DEFAULT_ORDER: Order = {
type: OrderType.Market,
side: OrderSide.Buy,
size: '1',
timeInForce: OrderTimeInForce.IOC,
};
export type TransactionStatus = 'default' | 'pending';
export interface DealTicketProps {
market: DealTicketQuery_market;
submit: (order: Order) => void;
transactionStatus: TransactionStatus;
defaultOrder?: Order;
}
export const DealTicket = ({
market,
submit,
transactionStatus,
defaultOrder = DEFAULT_ORDER,
}: DealTicketProps) => {
const [order, updateOrder] = useOrderState(defaultOrder);
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
submit(order);
};
let ticket = null;
if (order.type === OrderType.Market) {
ticket = (
<DealTicketMarket
order={order}
updateOrder={updateOrder}
transactionStatus={transactionStatus}
market={market}
/>
);
} else if (order.type === OrderType.Limit) {
ticket = (
<DealTicketLimit
order={order}
updateOrder={updateOrder}
transactionStatus={transactionStatus}
market={market}
/>
);
} else {
throw new Error('Invalid ticket type');
}
return (
<form onSubmit={handleSubmit} className="px-4 py-8">
{ticket}
</form>
);
};

View File

@ -1,14 +1,42 @@
import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook } from '@testing-library/react-hooks';
import type { Order } from './use-order-state';
import type { Order } from '../utils/get-default-order';
import type {
VegaKeyExtended,
VegaWalletContextShape,
} from '@vegaprotocol/wallet';
import { VegaTxStatus, VegaWalletContext } from '@vegaprotocol/wallet';
import { OrderSide, OrderTimeInForce, OrderType } from '@vegaprotocol/wallet';
import { MarketState, MarketTradingMode } from '@vegaprotocol/types';
import type { ReactNode } from 'react';
import { useOrderSubmit } from './use-order-submit';
import type { DealTicketQuery_market } from '../__generated__/DealTicketQuery';
const defaultMarket: DealTicketQuery_market = {
__typename: 'Market',
id: 'market-id',
decimalPlaces: 2,
positionDecimalPlaces: 1,
tradingMode: MarketTradingMode.Continuous,
state: MarketState.Active,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
product: {
__typename: 'Future',
quoteName: 'quote-name',
},
},
},
depth: {
__typename: 'MarketDepth',
lastTrade: {
__typename: 'Trade',
price: '100',
},
},
};
const defaultWalletContext = {
keypair: null,
@ -22,7 +50,7 @@ const defaultWalletContext = {
function setup(
context?: Partial<VegaWalletContextShape>,
market = { id: 'market-id', decimalPlaces: 2 }
market = defaultMarket
) {
const wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider>
@ -111,20 +139,16 @@ it('Should submit a correctly formatted order', async () => {
const keypair = {
pub: '0x123',
} as VegaKeyExtended;
const market = {
id: 'market-id',
decimalPlaces: 2,
};
const { result } = setup(
{
sendTx: mockSendTx,
keypairs: [keypair],
keypair,
},
market
defaultMarket
);
const order = {
const order: Order = {
type: OrderType.Limit,
size: '10',
timeInForce: OrderTimeInForce.GTT,
@ -141,12 +165,12 @@ it('Should submit a correctly formatted order', async () => {
propagate: true,
orderSubmission: {
type: OrderType.Limit,
marketId: market.id, // Market provided from hook arugment
size: '10',
marketId: defaultMarket.id, // Market provided from hook arugment
size: '100', // size adjusted based on positionDecimalPlaces
side: OrderSide.Buy,
timeInForce: OrderTimeInForce.GTT,
price: '123456789', // Decimal removed
expiresAt: order.expiration.getTime() + '000000', // Nanoseconds appened
expiresAt: order.expiration?.getTime() + '000000', // Nanoseconds appened
},
});
});

View File

@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from 'react';
import { gql, useSubscription } from '@apollo/client';
import type { Order } from './use-order-state';
import type { Order } from '../utils/get-default-order';
import { OrderType, useVegaWallet } from '@vegaprotocol/wallet';
import { determineId, removeDecimal } from '@vegaprotocol/react-helpers';
import { useVegaTransaction } from '@vegaprotocol/wallet';
@ -8,7 +8,8 @@ import type {
OrderEvent,
OrderEventVariables,
OrderEvent_busEvents_event_Order,
} from './__generated__/OrderEvent';
} from '../__generated__/OrderEvent';
import type { DealTicketQuery_market } from '../__generated__/DealTicketQuery';
const ORDER_EVENT_SUB = gql`
subscription OrderEvent($partyId: ID!) {
@ -35,12 +36,7 @@ const ORDER_EVENT_SUB = gql`
}
`;
interface UseOrderSubmitMarket {
id: string;
decimalPlaces: number;
}
export const useOrderSubmit = (market: UseOrderSubmitMarket) => {
export const useOrderSubmit = (market: DealTicketQuery_market) => {
const { keypair } = useVegaWallet();
const { send, transaction, reset: resetTransaction } = useVegaTransaction();
const [id, setId] = useState('');
@ -97,7 +93,7 @@ export const useOrderSubmit = (market: UseOrderSubmitMarket) => {
order.type === OrderType.Limit && order.price
? removeDecimal(order.price, market.decimalPlaces)
: undefined,
size: order.size,
size: removeDecimal(order.size, market.positionDecimalPlaces),
type: order.type,
side: order.side,
timeInForce: order.timeInForce,

View File

@ -0,0 +1,196 @@
import { renderHook } from '@testing-library/react-hooks';
import {
OrderTimeInForce,
OrderType,
useVegaWallet,
} from '@vegaprotocol/wallet';
import type {
VegaWalletContextShape,
VegaKeyExtended,
} from '@vegaprotocol/wallet';
import { MarketState, MarketTradingMode } from '@vegaprotocol/types';
import type { ValidationProps } from './use-order-validation';
import { useOrderValidation } from './use-order-validation';
import type { DealTicketQuery_market } from '../__generated__/DealTicketQuery';
import { ERROR_SIZE_DECIMAL } from '../utils/validate-size';
jest.mock('@vegaprotocol/wallet');
const market: DealTicketQuery_market = {
__typename: 'Market',
id: 'market-id',
decimalPlaces: 2,
positionDecimalPlaces: 1,
tradingMode: MarketTradingMode.Continuous,
state: MarketState.Active,
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {
__typename: 'Instrument',
product: {
__typename: 'Future',
quoteName: 'quote-name',
},
},
},
depth: {
__typename: 'MarketDepth',
lastTrade: {
__typename: 'Trade',
price: '100',
},
},
};
const defaultWalletContext = {
keypair: {
name: 'keypair0',
tainted: false,
pub: '111111__111111',
} as VegaKeyExtended,
keypairs: [],
sendTx: jest.fn().mockReturnValue(Promise.resolve(null)),
connect: jest.fn(),
disconnect: jest.fn(),
selectPublicKey: jest.fn(),
connector: null,
};
const defaultOrder = {
market,
step: 0.1,
orderType: OrderType.Market,
orderTimeInForce: OrderTimeInForce.FOK,
};
const ERROR = {
KEY_MISSING: 'No public key selected',
KEY_TAINTED: 'Selected public key has been tainted',
MARKET_SUSPENDED: 'Market is currently suspended',
MARKET_INACTIVE: 'Market is no longer active',
MARKET_WAITING: 'Market is not active yet',
MARKET_CONTINUOUS_LIMIT:
'Only limit orders are permitted when market is in auction',
MARKET_COUNTINUOUS_TIF:
'Only GTT, GTC and GFA are permitted when market is in auction',
FIELD_SIZE_REQ: 'An amount needs to be provided',
FIELD_SIZE_MIN: `The amount cannot be lower than "${defaultOrder.step}"`,
FIELD_PRICE_REQ: 'A price needs to be provided',
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`,
};
function setup(
props?: Partial<ValidationProps>,
context?: Partial<VegaWalletContextShape>
) {
const mockUseVegaWallet = useVegaWallet as jest.Mock;
mockUseVegaWallet.mockReturnValue({ ...defaultWalletContext, context });
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 },
});
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: OrderType.Market,
});
expect(result.current).toEqual(errorMessage);
}
);
it.each`
tradingMode | orderTimeInForce | errorMessage
${MarketTradingMode.BatchAuction} | ${OrderTimeInForce.FOK} | ${ERROR.MARKET_COUNTINUOUS_TIF}
${MarketTradingMode.MonitoringAuction} | ${OrderTimeInForce.FOK} | ${ERROR.MARKET_COUNTINUOUS_TIF}
${MarketTradingMode.OpeningAuction} | ${OrderTimeInForce.FOK} | ${ERROR.MARKET_COUNTINUOUS_TIF}
${MarketTradingMode.BatchAuction} | ${OrderTimeInForce.IOC} | ${ERROR.MARKET_COUNTINUOUS_TIF}
${MarketTradingMode.MonitoringAuction} | ${OrderTimeInForce.IOC} | ${ERROR.MARKET_COUNTINUOUS_TIF}
${MarketTradingMode.OpeningAuction} | ${OrderTimeInForce.IOC} | ${ERROR.MARKET_COUNTINUOUS_TIF}
${MarketTradingMode.BatchAuction} | ${OrderTimeInForce.GFN} | ${ERROR.MARKET_COUNTINUOUS_TIF}
${MarketTradingMode.MonitoringAuction} | ${OrderTimeInForce.GFN} | ${ERROR.MARKET_COUNTINUOUS_TIF}
${MarketTradingMode.OpeningAuction} | ${OrderTimeInForce.GFN} | ${ERROR.MARKET_COUNTINUOUS_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: OrderType.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 () => {
const { result } = setup({
fieldErrors: { size: { type: 'validate', message: ERROR_SIZE_DECIMAL } },
});
expect(result.current).toEqual(ERROR.FIELD_PRICE_STEP_DECIMAL);
});

View File

@ -0,0 +1,114 @@
import type { FieldErrors } from 'react-hook-form';
import { useMemo } from 'react';
import { t } from '@vegaprotocol/react-helpers';
import {
useVegaWallet,
OrderTimeInForce,
OrderType,
} from '@vegaprotocol/wallet';
import { MarketState, MarketTradingMode } from '@vegaprotocol/types';
import type { Order } from '../utils/get-default-order';
import type { DealTicketQuery_market } from '../__generated__/DealTicketQuery';
import { ERROR_SIZE_DECIMAL } from '../utils/validate-size';
export type ValidationProps = {
step: number;
market: DealTicketQuery_market;
orderType: OrderType;
orderTimeInForce: OrderTimeInForce;
fieldErrors?: FieldErrors<Order>;
};
export const useOrderValidation = ({
step,
market,
fieldErrors = {},
orderType,
orderTimeInForce,
}: ValidationProps) => {
const { keypair } = useVegaWallet();
const invalidText = useMemo(() => {
if (!keypair) {
return t('No public key selected');
}
if (keypair.tainted) {
return t('Selected public key has been tainted');
}
if (market.state !== MarketState.Active) {
if (market.state === MarketState.Suspended) {
return t('Market is currently suspended');
}
if (
market.state === MarketState.Proposed ||
market.state === MarketState.Pending
) {
return t('Market is not active yet');
}
return t('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');
}
if (
[
OrderTimeInForce.FOK,
OrderTimeInForce.IOC,
OrderTimeInForce.GFN,
].includes(orderTimeInForce)
) {
return 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');
}
if (fieldErrors?.size?.type === 'min') {
return t(`The amount cannot be lower than "${step}"`);
}
if (fieldErrors?.price?.type === 'required') {
return t('A price needs to be provided');
}
if (fieldErrors?.price?.type === 'min') {
return t(`The price cannot be negative`);
}
if (
fieldErrors?.size?.type === 'validate' &&
fieldErrors?.size?.message === ERROR_SIZE_DECIMAL
) {
if (market.positionDecimalPlaces === 0) {
return t('No decimal amounts allowed for this order');
}
return t(
`The amount field only takes up to ${market.positionDecimalPlaces} decimals`
);
}
return '';
}, [
keypair,
step,
market,
fieldErrors?.size?.type,
fieldErrors?.size?.message,
fieldErrors?.price?.type,
orderType,
orderTimeInForce,
]);
return invalidText;
};

View File

@ -1,15 +1,16 @@
export * from './expiry-selector';
export * from './type-selector';
export * from './time-in-force-selector';
export * from './submit-button';
export * from './side-selector';
export * from './deal-ticket';
export * from './deal-ticket-limit-form';
export * from './deal-ticket-market-form';
export * from './deal-ticket-manager';
export * from './order-dialog';
export * from './use-order-state';
export * from './use-order-submit';
export * from './deal-ticket-container';
export * from './components/expiry-selector';
export * from './components/type-selector';
export * from './components/time-in-force-selector';
export * from './components/side-selector';
export * from './components/deal-ticket';
export * from './components/deal-ticket-amount';
export * from './components/deal-ticket-limit-amount';
export * from './components/deal-ticket-market-amount';
export * from './components/deal-ticket-manager';
export * from './components/order-dialog';
export * from './components/deal-ticket-container';
export * from './__generated__/DealTicketQuery';
export * from './__generated__/OrderEvent';
export * from './utils/get-default-order';
export * from './hooks/use-order-submit';
export * from './hooks/use-order-validation';

View File

@ -1,89 +0,0 @@
import { Button, InputError } from '@vegaprotocol/ui-toolkit';
import { OrderTimeInForce, OrderType } from '@vegaprotocol/wallet';
import { useMemo } from 'react';
import type { Order } from './use-order-state';
import { useVegaWallet } from '@vegaprotocol/wallet';
import type { TransactionStatus } from './deal-ticket';
import type { DealTicketQuery_market } from './__generated__/DealTicketQuery';
import { MarketState, MarketTradingMode } from '@vegaprotocol/types';
import { t } from '@vegaprotocol/react-helpers';
interface SubmitButtonProps {
transactionStatus: TransactionStatus;
market: DealTicketQuery_market;
order: Order;
}
export const SubmitButton = ({
market,
transactionStatus,
order,
}: SubmitButtonProps) => {
const { keypair } = useVegaWallet();
const invalidText = useMemo(() => {
if (!keypair) {
return t('No public key selected');
}
if (keypair.tainted) {
return t('Selected public key has been tainted');
}
if (market.state !== MarketState.Active) {
if (market.state === MarketState.Suspended) {
return t('Market is currently suspended');
}
if (
market.state === MarketState.Proposed ||
market.state === MarketState.Pending
) {
return t('Market is not active yet');
}
return t('Market is no longer active');
}
if (market.tradingMode !== MarketTradingMode.Continuous) {
if (order.type === OrderType.Market) {
return t('Only limit orders are permitted when market is in auction');
}
if (
[
OrderTimeInForce.FOK,
OrderTimeInForce.IOC,
OrderTimeInForce.GFN,
].includes(order.timeInForce)
) {
return t(
'Only GTT, GTC and GFA are permitted when market is in auction'
);
}
}
return '';
}, [keypair, market, order]);
const disabled = transactionStatus === 'pending' || Boolean(invalidText);
return (
<>
<Button
className="w-full mb-8"
variant="primary"
type="submit"
disabled={disabled}
data-testid="place-order"
>
{transactionStatus === 'pending' ? t('Pending...') : t('Place order')}
</Button>
{invalidText && (
<InputError className="mb-8" data-testid="dealticket-error-message">
{invalidText}
</InputError>
)}
</>
);
};

View File

@ -1,79 +0,0 @@
import type { OrderSide } from '@vegaprotocol/wallet';
import { OrderTimeInForce, OrderType } from '@vegaprotocol/wallet';
import { useState, useCallback } from 'react';
export interface Order {
size: string;
type: OrderType;
timeInForce: OrderTimeInForce;
side: OrderSide | null;
price?: string;
expiration?: Date;
}
export type UpdateOrder = (order: Partial<Order>) => void;
export const useOrderState = (defaultOrder: Order): [Order, UpdateOrder] => {
const [order, setOrder] = useState<Order>(defaultOrder);
const updateOrder = useCallback((orderUpdate: Partial<Order>) => {
setOrder((curr) => {
// Type is switching to market so return new market order object with correct defaults
if (
orderUpdate.type === OrderType.Market &&
curr.type !== OrderType.Market
) {
// Check if provided TIF or current TIF is valid for a market order and default
// to IOC if its not
const isTifValid = (tif: OrderTimeInForce) => {
return tif === OrderTimeInForce.FOK || tif === OrderTimeInForce.IOC;
};
// Default
let timeInForce = OrderTimeInForce.IOC;
if (orderUpdate.timeInForce) {
if (isTifValid(orderUpdate.timeInForce)) {
timeInForce = orderUpdate.timeInForce;
}
} else {
if (isTifValid(curr.timeInForce)) {
timeInForce = curr.timeInForce;
}
}
return {
type: orderUpdate.type,
size: orderUpdate.size || curr.size,
side: orderUpdate.side || curr.side,
timeInForce,
price: undefined,
expiration: undefined,
};
}
// Type is switching to limit so return new order object with correct defaults
if (
orderUpdate.type === OrderType.Limit &&
curr.type !== OrderType.Limit
) {
return {
type: orderUpdate.type,
size: orderUpdate.size || curr.size,
side: orderUpdate.side || curr.side,
timeInForce: orderUpdate.timeInForce || curr.timeInForce,
price: orderUpdate.price || '0',
expiration: orderUpdate.expiration || undefined,
};
}
return {
...curr,
...orderUpdate,
};
});
}, []);
return [order, updateOrder];
};

View File

@ -0,0 +1,28 @@
import { OrderTimeInForce, OrderType, OrderSide } from '@vegaprotocol/wallet';
import type { DealTicketQuery_market } from '../__generated__/DealTicketQuery';
import { toDecimal } from '@vegaprotocol/react-helpers';
export type Order =
| {
size: string;
type: OrderType.Market;
timeInForce: OrderTimeInForce;
side: OrderSide;
price?: never;
expiration?: never;
}
| {
size: string;
type: OrderType.Limit;
timeInForce: OrderTimeInForce;
side: OrderSide;
price?: string;
expiration?: Date;
};
export const getDefaultOrder = (market: DealTicketQuery_market): Order => ({
type: OrderType.Market,
side: OrderSide.Buy,
timeInForce: OrderTimeInForce.IOC,
size: String(toDecimal(market.positionDecimalPlaces)),
});

View File

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

View File

@ -3,7 +3,7 @@
// @generated
// This file was automatically generated and should not be edited.
import { Interval, MarketState, MarketTradingMode } from "@vegaprotocol/types";
import { Interval } from "@vegaprotocol/types";
// ====================================================
// GraphQL query operation: MarketList
@ -15,14 +15,6 @@ export interface MarketList_markets_data_market {
* Market ID
*/
id: string;
/**
* Current state of the market
*/
state: MarketState;
/**
* Current mode of execution of the market
*/
tradingMode: MarketTradingMode;
}
export interface MarketList_markets_data {
@ -31,14 +23,6 @@ export interface MarketList_markets_data {
* market id of the associated mark price
*/
market: MarketList_markets_data_market;
/**
* the highest price level on an order book for buy orders.
*/
bestBidPrice: string;
/**
* the lowest price level on an order book for offer orders.
*/
bestOfferPrice: string;
/**
* the mark price (actually an unsigned int)
*/
@ -66,7 +50,7 @@ export interface MarketList_markets_tradableInstrument_instrument {
/**
* Metadata for this instrument
*/
metadata?: MarketList_markets_tradableInstrument_instrument_metadata;
metadata: MarketList_markets_tradableInstrument_instrument_metadata;
}
export interface MarketList_markets_tradableInstrument {
@ -110,14 +94,14 @@ export interface MarketList_markets {
/**
* decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct
* number denominated in the currency of the Market. (uint64)
*
*
* Examples:
* Currency Balance decimalPlaces Real Balance
* GBP 100 0 GBP 100
* GBP 100 2 GBP 1.00
* GBP 100 4 GBP 0.01
* GBP 1 4 GBP 0.0001 ( 0.01p )
*
*
* GBX (pence) 100 0 GBP 1.00 (100p )
* GBX (pence) 100 2 GBP 0.01 ( 1p )
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )

View File

@ -2,6 +2,10 @@ import { BigNumber } from 'bignumber.js';
import memoize from 'lodash/memoize';
import { getUserLocale } from './utils';
export function toDecimal(numberOfDecimals: number) {
return Math.pow(10, -numberOfDecimals);
}
export function addDecimal(
value: string | number,
decimals: number,