feat(deal-ticket): show maximum number of active stop orders limit warning (#4687)

Co-authored-by: Maciek <maciek@vegaprotocol.io>
Co-authored-by: Edd <edd@vega.xyz>
Co-authored-by: Joe Tsang <30622993+jtsang586@users.noreply.github.com>
Co-authored-by: Sam Keen <samuel.kleinmann@gmail.com>
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
Co-authored-by: m.ray <16125548+MadalinaRaicu@users.noreply.github.com>
Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
This commit is contained in:
Bartłomiej Głownia 2023-09-07 11:25:26 +02:00 committed by GitHub
parent c540d4b17f
commit 47a84b4dac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 93 additions and 11 deletions

View File

@ -39,7 +39,11 @@ export const MarketsPage = () => {
id="proposed-markets"
name={t('Proposed markets')}
menu={
<TradingAnchorButton size="extra-small" href={externalLink}>
<TradingAnchorButton
size="extra-small"
data-testid="propose-new-market"
href={externalLink}
>
{t('Propose a new market')}
</TradingAnchorButton>
}

View File

@ -78,8 +78,18 @@ const triggerPriceWarningMessage = 'stop-order-warning-message-trigger-price';
const triggerTrailingPercentOffsetErrorMessage =
'stop-order-error-message-trigger-trailing-percent-offset';
const numberOfActiveOrdersLimit = 'stop-order-warning-limit';
const ocoPostfix = (id: string, postfix = true) => (postfix ? `${id}-oco` : id);
const mockDataProvider = jest.fn((...args) => ({
data: Array(0),
}));
jest.mock('@vegaprotocol/data-provider', () => ({
...jest.requireActual('@vegaprotocol/data-provider'),
useDataProvider: jest.fn((...args) => mockDataProvider(...args)),
}));
describe('StopOrder', () => {
beforeEach(() => {
localStorage.clear();
@ -460,4 +470,26 @@ describe('StopOrder', () => {
new Date(screen.getByTestId<HTMLInputElement>(datePicker).value).getTime()
).toEqual(now);
});
it('shows limit of active stop orders number', async () => {
mockDataProvider.mockReturnValue({
data: Array(4),
});
render(generateJsx());
expect(mockDataProvider.mock.lastCall?.[0].skip).toBe(true);
await userEvent.type(screen.getByTestId(sizeInput), '0.01');
expect(mockDataProvider.mock.lastCall?.[0].skip).toBe(false);
expect(screen.getByTestId(numberOfActiveOrdersLimit)).toBeInTheDocument();
});
it('counts oco as two orders', async () => {
mockDataProvider.mockReturnValue({
data: Array(3),
});
render(generateJsx());
await userEvent.type(screen.getByTestId(sizeInput), '0.01');
expect(screen.queryByTestId(numberOfActiveOrdersLimit)).toBeNull();
await userEvent.click(screen.getByTestId(oco));
expect(screen.getByTestId(numberOfActiveOrdersLimit)).toBeInTheDocument();
});
});

View File

@ -26,6 +26,7 @@ import {
TradingButton as Button,
Pill,
Intent,
Notification,
} from '@vegaprotocol/ui-toolkit';
import { getDerivedPrice } from '@vegaprotocol/markets';
import type { Market } from '@vegaprotocol/markets';
@ -53,6 +54,8 @@ import { DealTicketFeeDetails } from './deal-ticket-fee-details';
import { validateExpiration } from '../../utils';
import { NOTIONAL_SIZE_TOOLTIP_TEXT } from '../../constants';
import { KeyValue } from './key-value';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { stopOrdersProvider } from '@vegaprotocol/orders';
export interface StopOrderProps {
market: Market;
@ -60,6 +63,8 @@ export interface StopOrderProps {
submit: (order: StopOrdersSubmission) => void;
}
const MAX_NUMBER_OF_ACTIVE_STOP_ORDERS = 4;
const POLLING_TIME = 2000;
const trailingPercentOffsetStep = '0.1';
const getDefaultValues = (
@ -802,6 +807,27 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
const triggerTrailingPercentOffset = watch('triggerTrailingPercentOffset');
const triggerType = watch('triggerType');
const { data: activeStopOrders, reload } = useDataProvider({
dataProvider: stopOrdersProvider,
variables: {
filter: {
parties: pubKey ? [pubKey] : [],
markets: [market.id],
liveOnly: true,
},
},
skip: !(pubKey && (formState.isDirty || formState.submitCount)),
});
useEffect(() => {
const interval = setInterval(() => {
reload();
}, POLLING_TIME);
return () => {
clearInterval(interval);
};
}, [reload]);
useEffect(() => {
const storedSize = storedFormValues?.[dealTicketType]?.size;
if (storedSize && size !== storedSize) {
@ -864,7 +890,6 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
{errors.type.message}
</InputError>
)}
<Controller
name="side"
control={control}
@ -1095,6 +1120,20 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
</>
)}
<NoWalletWarning isReadOnly={isReadOnly} />
{(activeStopOrders?.length ?? 0) + (oco ? 2 : 1) >
MAX_NUMBER_OF_ACTIVE_STOP_ORDERS ? (
<div className="mb-2">
<Notification
intent={Intent.Warning}
testId={'stop-order-warning-limit'}
message={t(
'There is a limit of %s active stop orders per market. Orders submitted above the limit will be immediately rejected.',
[MAX_NUMBER_OF_ACTIVE_STOP_ORDERS.toString()]
)}
/>
</div>
) : null}
<SubmitButton
assetUnit={assetUnit}
market={market}

View File

@ -144,8 +144,8 @@ fragment StopOrderFields on StopOrder {
}
}
query StopOrders($partyId: ID!) {
stopOrders(filter: { parties: [$partyId] }) {
query StopOrders($filter: StopOrderFilter) {
stopOrders(filter: $filter) {
edges {
node {
...StopOrderFields

View File

@ -37,7 +37,7 @@ export type OrderSubmissionFieldsFragment = { __typename?: 'OrderSubmission', ma
export type StopOrderFieldsFragment = { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, order?: { __typename?: 'Order', id: string, type?: Types.OrderType | null, side: Types.Side, size: string, status: Types.OrderStatus, rejectionReason?: Types.OrderRejectionReason | null, price: string, timeInForce: Types.OrderTimeInForce, remaining: string, expiresAt?: any | null, createdAt: any, updatedAt?: any | null, postOnly?: boolean | null, reduceOnly?: boolean | null, market: { __typename?: 'Market', id: string }, liquidityProvision?: { __typename: 'LiquidityProvision' } | null, peggedOrder?: { __typename: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null, icebergOrder?: { __typename: 'IcebergOrder', peakSize: string, minimumVisibleSize: string, reservedRemaining: string } | null } | null, trigger: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string }, submission: { __typename?: 'OrderSubmission', marketId: string, price: string, size: string, side: Types.Side, timeInForce: Types.OrderTimeInForce, expiresAt: any, type: Types.OrderType, reference?: string | null, postOnly?: boolean | null, reduceOnly?: boolean | null, peggedOrder?: { __typename?: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null } };
export type StopOrdersQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
filter?: Types.InputMaybe<Types.StopOrderFilter>;
}>;
@ -283,8 +283,8 @@ export function useOrdersUpdateSubscription(baseOptions: Apollo.SubscriptionHook
export type OrdersUpdateSubscriptionHookResult = ReturnType<typeof useOrdersUpdateSubscription>;
export type OrdersUpdateSubscriptionResult = Apollo.SubscriptionResult<OrdersUpdateSubscription>;
export const StopOrdersDocument = gql`
query StopOrders($partyId: ID!) {
stopOrders(filter: {parties: [$partyId]}) {
query StopOrders($filter: StopOrderFilter) {
stopOrders(filter: $filter) {
edges {
node {
...StopOrderFields
@ -306,11 +306,11 @@ export const StopOrdersDocument = gql`
* @example
* const { data, loading, error } = useStopOrdersQuery({
* variables: {
* partyId: // value for 'partyId'
* filter: // value for 'filter'
* },
* });
*/
export function useStopOrdersQuery(baseOptions: Apollo.QueryHookOptions<StopOrdersQuery, StopOrdersQueryVariables>) {
export function useStopOrdersQuery(baseOptions?: Apollo.QueryHookOptions<StopOrdersQuery, StopOrdersQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<StopOrdersQuery, StopOrdersQueryVariables>(StopOrdersDocument, options);
}

View File

@ -1,2 +1,3 @@
export * from './__generated__/Orders';
export * from './order-data-provider';
export * from './stop-orders-data-provider';

View File

@ -7,7 +7,7 @@ import type { StopOrder } from '../order-data-provider/stop-orders-data-provider
import { useDataProvider } from '@vegaprotocol/data-provider';
import { stopOrdersWithMarketProvider } from '../order-data-provider/stop-orders-data-provider';
import { OrderViewDialog } from '../order-list/order-view-dialog';
import type { Order } from '../order-data-provider';
import type { Order, StopOrdersQueryVariables } from '../order-data-provider';
export interface StopOrdersManagerProps {
partyId: string;
@ -26,7 +26,11 @@ export const StopOrdersManager = ({
}: StopOrdersManagerProps) => {
const create = useVegaTransactionStore((state) => state.create);
const [viewOrder, setViewOrder] = useState<Order | null>(null);
const variables = { partyId };
const variables: StopOrdersQueryVariables = {
filter: {
parties: [partyId],
},
};
const { data, error, reload } = useDataProvider({
dataProvider: stopOrdersWithMarketProvider,

View File

@ -132,6 +132,7 @@ export const TradingAnchorButton = forwardRef<
children,
className,
subLabel,
...props
},
ref
) => (
@ -139,6 +140,7 @@ export const TradingAnchorButton = forwardRef<
ref={ref}
href={href}
className={getClassName({ size, subLabel, intent }, className)}
{...props}
>
<Content icon={icon} subLabel={subLabel} children={children} />
</a>