feat(deal-ticket): improve stop order rejection handling (#6084)
This commit is contained in:
parent
1e15032c06
commit
a77bb06b2f
@ -1,15 +1,8 @@
|
|||||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
import { useActiveOrders } from '@vegaprotocol/orders';
|
||||||
import { activeOrdersProvider } from '@vegaprotocol/orders';
|
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet-react';
|
import { useVegaWallet } from '@vegaprotocol/wallet-react';
|
||||||
|
|
||||||
export const PartyActiveOrdersHandler = () => {
|
export const PartyActiveOrdersHandler = () => {
|
||||||
const { pubKey } = useVegaWallet();
|
const { pubKey } = useVegaWallet();
|
||||||
const variables = { partyId: pubKey || '' };
|
useActiveOrders(pubKey);
|
||||||
const skip = !pubKey;
|
|
||||||
useDataProvider({
|
|
||||||
dataProvider: activeOrdersProvider,
|
|
||||||
variables,
|
|
||||||
skip,
|
|
||||||
});
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { generateMarket } from '../../test-helpers';
|
import { generateMarket } from '../../test-helpers';
|
||||||
import { StopOrder } from './deal-ticket-stop-order';
|
import { NoOpenVolumeWarning, StopOrder } from './deal-ticket-stop-order';
|
||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
import {
|
import {
|
||||||
@ -84,6 +84,22 @@ jest.mock('@vegaprotocol/data-provider', () => ({
|
|||||||
useDataProvider: jest.fn((...args) => mockDataProvider(...args)),
|
useDataProvider: jest.fn((...args) => mockDataProvider(...args)),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const mockUseOpenVolume = jest.fn(() => ({
|
||||||
|
openVolume: '0',
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@vegaprotocol/positions', () => ({
|
||||||
|
...jest.requireActual('@vegaprotocol/positions'),
|
||||||
|
useOpenVolume: jest.fn(() => mockUseOpenVolume()),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockActiveOrders = jest.fn(() => ({}));
|
||||||
|
|
||||||
|
jest.mock('@vegaprotocol/orders', () => ({
|
||||||
|
...jest.requireActual('@vegaprotocol/orders'),
|
||||||
|
useActiveOrders: jest.fn(() => mockActiveOrders()),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('StopOrder', () => {
|
describe('StopOrder', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
@ -576,3 +592,57 @@ describe('StopOrder', () => {
|
|||||||
expect(screen.getByTestId(numberOfActiveOrdersLimit)).toBeInTheDocument();
|
expect(screen.getByTestId(numberOfActiveOrdersLimit)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('NoOpenVolumeWarning', () => {
|
||||||
|
const testId = 'stop-order-warning-position';
|
||||||
|
|
||||||
|
it('shows warning if there is no possible position to reduce', () => {
|
||||||
|
const activeOrders = [
|
||||||
|
{
|
||||||
|
side: Schema.Side.SIDE_BUY,
|
||||||
|
remaining: '8',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
side: Schema.Side.SIDE_BUY,
|
||||||
|
remaining: '2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
side: Schema.Side.SIDE_SELL,
|
||||||
|
remaining: '7',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
side: Schema.Side.SIDE_SELL,
|
||||||
|
remaining: '3',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
mockActiveOrders.mockReturnValue({ data: activeOrders });
|
||||||
|
|
||||||
|
mockUseOpenVolume.mockReturnValue({ openVolume: '10' });
|
||||||
|
const result = render(
|
||||||
|
<NoOpenVolumeWarning side={Schema.Side.SIDE_BUY} marketId="" />
|
||||||
|
);
|
||||||
|
// side buy, volume + remaining 20
|
||||||
|
expect(screen.getByTestId(testId)).toBeInTheDocument();
|
||||||
|
|
||||||
|
result.rerender(
|
||||||
|
<NoOpenVolumeWarning side={Schema.Side.SIDE_SELL} marketId="" />
|
||||||
|
);
|
||||||
|
// side sell, volume - remaining = 0
|
||||||
|
expect(screen.queryByTestId(testId)).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
result.rerender(
|
||||||
|
<NoOpenVolumeWarning side={Schema.Side.SIDE_BUY} marketId="" />
|
||||||
|
);
|
||||||
|
// side sell, volume - remaining = 0
|
||||||
|
expect(screen.queryByTestId(testId)).toBeInTheDocument();
|
||||||
|
|
||||||
|
mockUseOpenVolume.mockReturnValue({ openVolume: '2' });
|
||||||
|
render(<NoOpenVolumeWarning side={Schema.Side.SIDE_SELL} marketId="" />);
|
||||||
|
// side buy, volume + remaining = 12
|
||||||
|
expect(screen.queryByTestId(testId)).toBeInTheDocument();
|
||||||
|
|
||||||
|
render(<NoOpenVolumeWarning side={Schema.Side.SIDE_SELL} marketId="" />);
|
||||||
|
// side sell, volume - remaining = -8
|
||||||
|
expect(screen.getByTestId(testId)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -56,9 +56,10 @@ import { validateExpiration } from '../../utils';
|
|||||||
import { NOTIONAL_SIZE_TOOLTIP_TEXT } from '../../constants';
|
import { NOTIONAL_SIZE_TOOLTIP_TEXT } from '../../constants';
|
||||||
import { KeyValue } from './key-value';
|
import { KeyValue } from './key-value';
|
||||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||||
import { stopOrdersProvider } from '@vegaprotocol/orders';
|
import { useActiveOrders, stopOrdersProvider } from '@vegaprotocol/orders';
|
||||||
import { useT } from '../../use-t';
|
import { useT } from '../../use-t';
|
||||||
import { determinePriceStep, determineSizeStep } from '@vegaprotocol/utils';
|
import { determinePriceStep, determineSizeStep } from '@vegaprotocol/utils';
|
||||||
|
import { useOpenVolume } from '@vegaprotocol/positions';
|
||||||
|
|
||||||
export interface StopOrderProps {
|
export interface StopOrderProps {
|
||||||
market: Market;
|
market: Market;
|
||||||
@ -453,6 +454,47 @@ const Price = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const NoOpenVolumeWarning = ({
|
||||||
|
side,
|
||||||
|
partyId,
|
||||||
|
marketId,
|
||||||
|
}: {
|
||||||
|
side: Schema.Side;
|
||||||
|
partyId?: string;
|
||||||
|
marketId: string;
|
||||||
|
}) => {
|
||||||
|
const { data: activeOrders } = useActiveOrders(partyId, marketId);
|
||||||
|
const t = useT();
|
||||||
|
const { openVolume } = useOpenVolume(partyId, marketId) || {};
|
||||||
|
const volume = BigInt(openVolume || 0);
|
||||||
|
const remaining = activeOrders
|
||||||
|
? activeOrders.reduce((size, order) => {
|
||||||
|
if (side !== order.side) {
|
||||||
|
size += BigInt(order.remaining);
|
||||||
|
}
|
||||||
|
return size;
|
||||||
|
}, BigInt(0))
|
||||||
|
: BigInt(0);
|
||||||
|
|
||||||
|
if (
|
||||||
|
(side === Schema.Side.SIDE_BUY && volume - remaining < BigInt(0)) ||
|
||||||
|
(side === Schema.Side.SIDE_SELL && volume + remaining > BigInt(0))
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="mb-2">
|
||||||
|
<Notification
|
||||||
|
intent={Intent.Warning}
|
||||||
|
testId={'stop-order-warning-position'}
|
||||||
|
message={t(
|
||||||
|
'Stop orders are reduce only and this order would increase your position.'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const TimeInForce = ({
|
const TimeInForce = ({
|
||||||
control,
|
control,
|
||||||
oco,
|
oco,
|
||||||
@ -1188,8 +1230,13 @@ export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : (
|
||||||
|
<NoOpenVolumeWarning
|
||||||
|
side={side}
|
||||||
|
partyId={pubKey}
|
||||||
|
marketId={market.id}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
assetUnit={assetUnit}
|
assetUnit={assetUnit}
|
||||||
market={market}
|
market={market}
|
||||||
|
@ -31,6 +31,8 @@ import {
|
|||||||
jest.mock('zustand');
|
jest.mock('zustand');
|
||||||
jest.mock('./deal-ticket-fee-details', () => ({
|
jest.mock('./deal-ticket-fee-details', () => ({
|
||||||
DealTicketFeeDetails: () => <div data-testid="deal-ticket-fee-details" />,
|
DealTicketFeeDetails: () => <div data-testid="deal-ticket-fee-details" />,
|
||||||
|
}));
|
||||||
|
jest.mock('./deal-ticket-margin-details', () => ({
|
||||||
DealTicketMarginDetails: () => (
|
DealTicketMarginDetails: () => (
|
||||||
<div data-testid="deal-ticket-margin-details" />
|
<div data-testid="deal-ticket-margin-details" />
|
||||||
),
|
),
|
||||||
|
@ -36,7 +36,7 @@ import {
|
|||||||
formatForInput,
|
formatForInput,
|
||||||
formatValue,
|
formatValue,
|
||||||
} from '@vegaprotocol/utils';
|
} from '@vegaprotocol/utils';
|
||||||
import { activeOrdersProvider } from '@vegaprotocol/orders';
|
import { useActiveOrders } from '@vegaprotocol/orders';
|
||||||
import {
|
import {
|
||||||
getAsset,
|
getAsset,
|
||||||
getDerivedPrice,
|
getDerivedPrice,
|
||||||
@ -252,11 +252,7 @@ export const DealTicket = ({
|
|||||||
market.positionDecimalPlaces
|
market.positionDecimalPlaces
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: activeOrders } = useDataProvider({
|
const { data: activeOrders } = useActiveOrders(pubKey, market.id);
|
||||||
dataProvider: activeOrdersProvider,
|
|
||||||
variables: { partyId: pubKey || '', marketId: market.id },
|
|
||||||
skip: !pubKey,
|
|
||||||
});
|
|
||||||
const { data: margin } = useDataProvider({
|
const { data: margin } = useDataProvider({
|
||||||
dataProvider: marginModeDataProvider,
|
dataProvider: marginModeDataProvider,
|
||||||
variables: { partyId: pubKey || '', marketId: market.id },
|
variables: { partyId: pubKey || '', marketId: market.id },
|
||||||
|
@ -25,7 +25,7 @@ import {
|
|||||||
useMarginAccountBalance,
|
useMarginAccountBalance,
|
||||||
} from '@vegaprotocol/accounts';
|
} from '@vegaprotocol/accounts';
|
||||||
import { useMaxLeverage, useOpenVolume } from '@vegaprotocol/positions';
|
import { useMaxLeverage, useOpenVolume } from '@vegaprotocol/positions';
|
||||||
import { activeOrdersProvider } from '@vegaprotocol/orders';
|
import { useActiveOrders } from '@vegaprotocol/orders';
|
||||||
import { usePositionEstimate } from '../../hooks/use-position-estimate';
|
import { usePositionEstimate } from '../../hooks/use-position-estimate';
|
||||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||||
import { getAsset, useMarket } from '@vegaprotocol/markets';
|
import { getAsset, useMarket } from '@vegaprotocol/markets';
|
||||||
@ -64,10 +64,7 @@ export const MarginChange = ({
|
|||||||
openVolume: '0',
|
openVolume: '0',
|
||||||
averageEntryPrice: '0',
|
averageEntryPrice: '0',
|
||||||
};
|
};
|
||||||
const { data: activeOrders } = useDataProvider({
|
const { data: activeOrders } = useActiveOrders(partyId, marketId);
|
||||||
dataProvider: activeOrdersProvider,
|
|
||||||
variables: { partyId: partyId || '', marketId },
|
|
||||||
});
|
|
||||||
const orders = activeOrders
|
const orders = activeOrders
|
||||||
? activeOrders.map<Schema.OrderInfo>((order) => ({
|
? activeOrders.map<Schema.OrderInfo>((order) => ({
|
||||||
isMarketOrder: order.type === Schema.OrderType.TYPE_MARKET,
|
isMarketOrder: order.type === Schema.OrderType.TYPE_MARKET,
|
||||||
|
@ -106,6 +106,7 @@
|
|||||||
"Stop Limit": "Stop Limit",
|
"Stop Limit": "Stop Limit",
|
||||||
"Stop Market": "Stop Market",
|
"Stop Market": "Stop Market",
|
||||||
"Stop order will be triggered immediately": "Stop order will be triggered immediately",
|
"Stop order will be triggered immediately": "Stop order will be triggered immediately",
|
||||||
|
"Stop orders are reduce only and this order would increase your position.": "Stop orders are reduce only and this order would increase your position.",
|
||||||
"Strategy": "Strategy",
|
"Strategy": "Strategy",
|
||||||
"Submit": "Submit",
|
"Submit": "Submit",
|
||||||
"Subtotal": "Subtotal",
|
"Subtotal": "Subtotal",
|
||||||
|
@ -4,6 +4,7 @@ import {
|
|||||||
makeDataProvider,
|
makeDataProvider,
|
||||||
makeDerivedDataProvider,
|
makeDerivedDataProvider,
|
||||||
defaultAppend as append,
|
defaultAppend as append,
|
||||||
|
useDataProvider,
|
||||||
} from '@vegaprotocol/data-provider';
|
} from '@vegaprotocol/data-provider';
|
||||||
import { type Market } from '@vegaprotocol/markets';
|
import { type Market } from '@vegaprotocol/markets';
|
||||||
import { marketsMapProvider } from '@vegaprotocol/markets';
|
import { marketsMapProvider } from '@vegaprotocol/markets';
|
||||||
@ -208,6 +209,18 @@ export const activeOrdersProvider = makeDerivedDataProvider<
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const useActiveOrders = (
|
||||||
|
partyId: string | undefined,
|
||||||
|
marketId?: string
|
||||||
|
) =>
|
||||||
|
useDataProvider({
|
||||||
|
dataProvider: activeOrdersProvider,
|
||||||
|
variables: marketId
|
||||||
|
? { partyId: partyId || '', marketId }
|
||||||
|
: { partyId: partyId || '' },
|
||||||
|
skip: !partyId,
|
||||||
|
});
|
||||||
|
|
||||||
export const ordersWithMarketProvider = makeDerivedDataProvider<
|
export const ordersWithMarketProvider = makeDerivedDataProvider<
|
||||||
(Order & Cursor)[],
|
(Order & Cursor)[],
|
||||||
never,
|
never,
|
||||||
|
@ -10,7 +10,10 @@ import {
|
|||||||
type TransferStatus,
|
type TransferStatus,
|
||||||
MarketUpdateType,
|
MarketUpdateType,
|
||||||
} from './__generated__/types';
|
} from './__generated__/types';
|
||||||
import type { AccountType } from './__generated__/types';
|
import type {
|
||||||
|
AccountType,
|
||||||
|
StopOrderRejectionReason,
|
||||||
|
} from './__generated__/types';
|
||||||
import type {
|
import type {
|
||||||
AuctionTrigger,
|
AuctionTrigger,
|
||||||
DataSourceSpecStatus,
|
DataSourceSpecStatus,
|
||||||
@ -285,6 +288,29 @@ export const StopOrderStatusMapping: {
|
|||||||
STATUS_UNSPECIFIED: 'Unspecified',
|
STATUS_UNSPECIFIED: 'Unspecified',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop order rejection reason mappings.
|
||||||
|
*/
|
||||||
|
export const StopOrderRejectionReasonMapping: {
|
||||||
|
[T in StopOrderRejectionReason]: string;
|
||||||
|
} = {
|
||||||
|
REJECTION_REASON_TRADING_NOT_ALLOWED: 'Trading is not allowed yet',
|
||||||
|
REJECTION_REASON_EXPIRY_IN_THE_PAST:
|
||||||
|
'Expiry of the stop order is in the past',
|
||||||
|
REJECTION_REASON_MUST_BE_REDUCE_ONLY:
|
||||||
|
'Stop orders submission must be reduce only',
|
||||||
|
REJECTION_REASON_MAX_STOP_ORDERS_PER_PARTY_REACHED:
|
||||||
|
'Party has reached the maximum stop orders allowed for this market',
|
||||||
|
REJECTION_REASON_STOP_ORDER_NOT_ALLOWED_WITHOUT_A_POSITION:
|
||||||
|
'Stop orders are not allowed without a position',
|
||||||
|
REJECTION_REASON_STOP_ORDER_NOT_CLOSING_THE_POSITION:
|
||||||
|
'This stop order does not close the position',
|
||||||
|
REJECTION_REASON_STOP_ORDER_NOT_ALLOWED_DURING_OPENING_AUCTION:
|
||||||
|
'Stop orders are not allowed during the opening auction',
|
||||||
|
REJECTION_REASON_STOP_ORDER_CANNOT_MATCH_OCO_EXPIRY_TIMES:
|
||||||
|
'Stop order cannot have matching OCO expiry times',
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Valid order types, these determine what happens when an order is added to the book
|
* Valid order types, these determine what happens when an order is added to the book
|
||||||
*/
|
*/
|
||||||
|
@ -42,6 +42,7 @@ query StopOrderById($stopOrderId: ID!) {
|
|||||||
expiryStrategy
|
expiryStrategy
|
||||||
triggerDirection
|
triggerDirection
|
||||||
status
|
status
|
||||||
|
rejectionReason
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
partyId
|
partyId
|
||||||
|
3
libs/web3/src/lib/__generated__/Orders.ts
generated
3
libs/web3/src/lib/__generated__/Orders.ts
generated
@ -15,7 +15,7 @@ export type StopOrderByIdQueryVariables = Types.Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type StopOrderByIdQuery = { __typename?: 'Query', stopOrder?: { __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, 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 } } | null };
|
export type StopOrderByIdQuery = { __typename?: 'Query', stopOrder?: { __typename?: 'StopOrder', id: string, ocoLinkId?: string | null, expiresAt?: any | null, expiryStrategy?: Types.StopOrderExpiryStrategy | null, triggerDirection: Types.StopOrderTriggerDirection, status: Types.StopOrderStatus, rejectionReason?: Types.StopOrderRejectionReason | null, createdAt: any, updatedAt?: any | null, partyId: string, marketId: string, 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 } } | null };
|
||||||
|
|
||||||
|
|
||||||
export const OrderByIdDocument = gql`
|
export const OrderByIdDocument = gql`
|
||||||
@ -92,6 +92,7 @@ export const StopOrderByIdDocument = gql`
|
|||||||
expiryStrategy
|
expiryStrategy
|
||||||
triggerDirection
|
triggerDirection
|
||||||
status
|
status
|
||||||
|
rejectionReason
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
partyId
|
partyId
|
||||||
|
@ -47,6 +47,7 @@ export interface VegaTransactionStore {
|
|||||||
) => void;
|
) => void;
|
||||||
dismiss: (index: number) => void;
|
dismiss: (index: number) => void;
|
||||||
delete: (index: number) => void;
|
delete: (index: number) => void;
|
||||||
|
getTransaction: (txHash: string) => VegaStoredTxState | undefined;
|
||||||
updateWithdrawal: (
|
updateWithdrawal: (
|
||||||
withdrawal: NonNullable<VegaStoredTxState['withdrawal']>,
|
withdrawal: NonNullable<VegaStoredTxState['withdrawal']>,
|
||||||
withdrawalApproval: NonNullable<VegaStoredTxState['withdrawalApproval']>
|
withdrawalApproval: NonNullable<VegaStoredTxState['withdrawalApproval']>
|
||||||
@ -60,6 +61,12 @@ export interface VegaTransactionStore {
|
|||||||
export const useVegaTransactionStore = create<VegaTransactionStore>()(
|
export const useVegaTransactionStore = create<VegaTransactionStore>()(
|
||||||
subscribeWithSelector((set, get) => ({
|
subscribeWithSelector((set, get) => ({
|
||||||
transactions: [] as (VegaStoredTxState | undefined)[],
|
transactions: [] as (VegaStoredTxState | undefined)[],
|
||||||
|
getTransaction: (txHash: string) => {
|
||||||
|
return get().transactions.find(
|
||||||
|
(transaction) =>
|
||||||
|
transaction?.txHash && transaction.txHash.toLowerCase() === txHash
|
||||||
|
);
|
||||||
|
},
|
||||||
create: (body: Transaction, order?: OrderTxUpdateFieldsFragment) => {
|
create: (body: Transaction, order?: OrderTxUpdateFieldsFragment) => {
|
||||||
const transactions = get().transactions;
|
const transactions = get().transactions;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
@ -23,6 +23,9 @@ import {
|
|||||||
OrderTimeInForce,
|
OrderTimeInForce,
|
||||||
OrderType,
|
OrderType,
|
||||||
Side,
|
Side,
|
||||||
|
StopOrderRejectionReason,
|
||||||
|
StopOrderRejectionReasonMapping,
|
||||||
|
StopOrderStatus,
|
||||||
WithdrawalStatus,
|
WithdrawalStatus,
|
||||||
} from '@vegaprotocol/types';
|
} from '@vegaprotocol/types';
|
||||||
|
|
||||||
@ -48,14 +51,22 @@ jest.mock('./wait-for-withdrawal-approval', () => ({
|
|||||||
waitForWithdrawalApproval: () => mockWaitForWithdrawalApproval(),
|
waitForWithdrawalApproval: () => mockWaitForWithdrawalApproval(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const mockWaitForStopOrder = jest.fn();
|
||||||
|
|
||||||
|
jest.mock('./wait-for-stop-order', () => ({
|
||||||
|
waitForStopOrder: () => mockWaitForStopOrder(),
|
||||||
|
}));
|
||||||
|
|
||||||
const updateWithdrawal = jest.fn();
|
const updateWithdrawal = jest.fn();
|
||||||
const updateOrder = jest.fn();
|
const updateOrder = jest.fn();
|
||||||
const updateTransactionResult = jest.fn();
|
const updateTransactionResult = jest.fn();
|
||||||
|
const getTransaction = jest.fn();
|
||||||
|
|
||||||
const defaultState: Partial<VegaTransactionStore> = {
|
const defaultState: Partial<VegaTransactionStore> = {
|
||||||
updateWithdrawal,
|
updateWithdrawal,
|
||||||
updateOrder,
|
updateOrder,
|
||||||
updateTransactionResult,
|
updateTransactionResult,
|
||||||
|
getTransaction,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockTransactionStoreState = jest.fn<Partial<VegaTransactionStore>, []>();
|
const mockTransactionStoreState = jest.fn<Partial<VegaTransactionStore>, []>();
|
||||||
@ -180,6 +191,29 @@ describe('useVegaTransactionManager', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('waits for stop order and sets error if rejected', async () => {
|
||||||
|
getTransaction.mockReturnValueOnce({
|
||||||
|
signature: 'signature',
|
||||||
|
body: {
|
||||||
|
stopOrdersSubmission: {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const rejectionReason =
|
||||||
|
StopOrderRejectionReason.REJECTION_REASON_STOP_ORDER_NOT_CLOSING_THE_POSITION;
|
||||||
|
mockTransactionStoreState.mockReturnValue(defaultState);
|
||||||
|
mockWaitForStopOrder.mockResolvedValueOnce({
|
||||||
|
status: StopOrderStatus.STATUS_REJECTED,
|
||||||
|
rejectionReason,
|
||||||
|
});
|
||||||
|
render([mockedTransactionResultBusEvent]);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(updateTransactionResult).toHaveBeenCalledWith({
|
||||||
|
...transactionResultBusEvent,
|
||||||
|
error: StopOrderRejectionReasonMapping[rejectionReason],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('updates withdrawal on WithdrawalBusEvents', async () => {
|
it('updates withdrawal on WithdrawalBusEvents', async () => {
|
||||||
mockTransactionStoreState.mockReturnValue(defaultState);
|
mockTransactionStoreState.mockReturnValue(defaultState);
|
||||||
const erc20WithdrawalApproval = {};
|
const erc20WithdrawalApproval = {};
|
||||||
|
@ -7,6 +7,16 @@ import {
|
|||||||
} from './__generated__/TransactionResult';
|
} from './__generated__/TransactionResult';
|
||||||
import { useVegaTransactionStore } from './use-vega-transaction-store';
|
import { useVegaTransactionStore } from './use-vega-transaction-store';
|
||||||
import { waitForWithdrawalApproval } from './wait-for-withdrawal-approval';
|
import { waitForWithdrawalApproval } from './wait-for-withdrawal-approval';
|
||||||
|
import {
|
||||||
|
determineId,
|
||||||
|
isStopOrdersSubmissionTransaction,
|
||||||
|
} from '@vegaprotocol/wallet';
|
||||||
|
import { waitForStopOrder } from './wait-for-stop-order';
|
||||||
|
import {
|
||||||
|
StopOrderRejectionReasonMapping,
|
||||||
|
StopOrderStatus,
|
||||||
|
StopOrderStatusMapping,
|
||||||
|
} from '@vegaprotocol/types';
|
||||||
|
|
||||||
export const useVegaTransactionUpdater = () => {
|
export const useVegaTransactionUpdater = () => {
|
||||||
const client = useApolloClient();
|
const client = useApolloClient();
|
||||||
@ -17,6 +27,9 @@ export const useVegaTransactionUpdater = () => {
|
|||||||
const updateTransaction = useVegaTransactionStore(
|
const updateTransaction = useVegaTransactionStore(
|
||||||
(state) => state.updateTransactionResult
|
(state) => state.updateTransactionResult
|
||||||
);
|
);
|
||||||
|
const getTransaction = useVegaTransactionStore(
|
||||||
|
(state) => state.getTransaction
|
||||||
|
);
|
||||||
|
|
||||||
const { pubKey } = useVegaWallet();
|
const { pubKey } = useVegaWallet();
|
||||||
const variables = { partyId: pubKey || '' };
|
const variables = { partyId: pubKey || '' };
|
||||||
@ -51,11 +64,43 @@ export const useVegaTransactionUpdater = () => {
|
|||||||
variables,
|
variables,
|
||||||
skip,
|
skip,
|
||||||
fetchPolicy: 'no-cache',
|
fetchPolicy: 'no-cache',
|
||||||
onData: ({ data: result }) =>
|
onData: ({ data: result }) => {
|
||||||
result.data?.busEvents?.forEach((event) => {
|
result.data?.busEvents?.forEach(({ event }) => {
|
||||||
if (event.event.__typename === 'TransactionResult') {
|
if (event.__typename === 'TransactionResult') {
|
||||||
updateTransaction(event.event);
|
let updateImmediately = true;
|
||||||
|
if (event.status && !event.error) {
|
||||||
|
const transaction = getTransaction(event.hash.toLocaleLowerCase());
|
||||||
|
if (
|
||||||
|
transaction &&
|
||||||
|
transaction.signature &&
|
||||||
|
isStopOrdersSubmissionTransaction(transaction.body)
|
||||||
|
) {
|
||||||
|
waitForStopOrder(determineId(transaction.signature), client).then(
|
||||||
|
(stopOrder) => {
|
||||||
|
updateTransaction(
|
||||||
|
stopOrder &&
|
||||||
|
stopOrder.status === StopOrderStatus.STATUS_REJECTED
|
||||||
|
? {
|
||||||
|
...event,
|
||||||
|
error:
|
||||||
|
(stopOrder.rejectionReason &&
|
||||||
|
StopOrderRejectionReasonMapping[
|
||||||
|
stopOrder.rejectionReason
|
||||||
|
]) ||
|
||||||
|
StopOrderStatusMapping[stopOrder.status],
|
||||||
|
}
|
||||||
|
: event
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
updateImmediately = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updateImmediately) {
|
||||||
|
updateTransaction(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}),
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
72
libs/web3/src/lib/wait-for-stop-order.spec.tsx
Normal file
72
libs/web3/src/lib/wait-for-stop-order.spec.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { ApolloClient, InMemoryCache } from '@apollo/client';
|
||||||
|
import { MockLink } from '@apollo/client/testing';
|
||||||
|
import type { StopOrderByIdQuery } from './__generated__/Orders';
|
||||||
|
import { StopOrderByIdDocument } from './__generated__/Orders';
|
||||||
|
import type { MockedResponse } from '@apollo/client/testing';
|
||||||
|
import { waitForStopOrder } from './wait-for-stop-order';
|
||||||
|
import {
|
||||||
|
OrderTimeInForce,
|
||||||
|
OrderType,
|
||||||
|
Side,
|
||||||
|
StopOrderStatus,
|
||||||
|
StopOrderTriggerDirection,
|
||||||
|
} from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
const stopOrderId =
|
||||||
|
'ad427c4f5cb599e73ffb6f0ae371d1e0fcba89b6be2401a06e61cab982668d63';
|
||||||
|
|
||||||
|
const stopOrder: StopOrderByIdQuery['stopOrder'] = {
|
||||||
|
__typename: 'StopOrder',
|
||||||
|
id: stopOrderId,
|
||||||
|
ocoLinkId: null,
|
||||||
|
expiresAt: null,
|
||||||
|
expiryStrategy: null,
|
||||||
|
triggerDirection: StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE,
|
||||||
|
status: StopOrderStatus.STATUS_PENDING,
|
||||||
|
rejectionReason: null,
|
||||||
|
createdAt: '2024-03-25T10:18:48.946943Z',
|
||||||
|
updatedAt: null,
|
||||||
|
partyId: '02eceaba4df2bef76ea10caf728d8a099a2aa846cced25737cccaa9812342f65',
|
||||||
|
marketId: '00788b763b999ef555ac5da17de155ff4237dd14aa6671a303d1285f27f094f0',
|
||||||
|
trigger: {
|
||||||
|
__typename: 'StopOrderPrice',
|
||||||
|
price: '700000',
|
||||||
|
},
|
||||||
|
submission: {
|
||||||
|
__typename: 'OrderSubmission',
|
||||||
|
marketId:
|
||||||
|
'00788b763b999ef555ac5da17de155ff4237dd14aa6671a303d1285f27f094f0',
|
||||||
|
price: '0',
|
||||||
|
size: '1',
|
||||||
|
side: Side.SIDE_BUY,
|
||||||
|
timeInForce: OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||||
|
expiresAt: null,
|
||||||
|
type: OrderType.TYPE_MARKET,
|
||||||
|
reference: '',
|
||||||
|
peggedOrder: null,
|
||||||
|
postOnly: false,
|
||||||
|
reduceOnly: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockedStopOrderById: MockedResponse<StopOrderByIdQuery> = {
|
||||||
|
request: {
|
||||||
|
query: StopOrderByIdDocument,
|
||||||
|
variables: { stopOrderId },
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
data: {
|
||||||
|
stopOrder,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('waitForStopOrder', () => {
|
||||||
|
it('resolves with matching stopOrder', async () => {
|
||||||
|
const client = new ApolloClient({
|
||||||
|
cache: new InMemoryCache(),
|
||||||
|
link: new MockLink([mockedStopOrderById]),
|
||||||
|
});
|
||||||
|
expect(await waitForStopOrder(stopOrderId, client)).toEqual(stopOrder);
|
||||||
|
});
|
||||||
|
});
|
32
libs/web3/src/lib/wait-for-stop-order.tsx
Normal file
32
libs/web3/src/lib/wait-for-stop-order.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { type ApolloClient } from '@apollo/client';
|
||||||
|
import {
|
||||||
|
StopOrderByIdDocument,
|
||||||
|
type StopOrderByIdQuery,
|
||||||
|
type StopOrderByIdQueryVariables,
|
||||||
|
} from './__generated__/Orders';
|
||||||
|
|
||||||
|
export const waitForStopOrder = (
|
||||||
|
stopOrderId: string,
|
||||||
|
client: ApolloClient<object>
|
||||||
|
) =>
|
||||||
|
new Promise<StopOrderByIdQuery['stopOrder']>((resolve) => {
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const res = await client.query<
|
||||||
|
StopOrderByIdQuery,
|
||||||
|
StopOrderByIdQueryVariables
|
||||||
|
>({
|
||||||
|
query: StopOrderByIdDocument,
|
||||||
|
variables: { stopOrderId },
|
||||||
|
fetchPolicy: 'network-only',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.data) {
|
||||||
|
clearInterval(interval);
|
||||||
|
resolve(res.data.stopOrder);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// no op as the query will error until the approval is created
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user