feat(trading): add stop orders table and form (#4265)
Co-authored-by: Dariusz Majcherczyk <dariusz.majcherczyk@gmail.com>
This commit is contained in:
parent
1dd97a2bce
commit
375d447fa8
@ -100,7 +100,6 @@ describe('markets proposed table', { tags: '@smoke' }, () => {
|
|||||||
'VEGA_TOKEN_URL'
|
'VEGA_TOKEN_URL'
|
||||||
)}/proposals/e9ec6d5c46a7e7bcabf9ba7a893fa5a5eeeec08b731f06f7a6eb7bf0e605b829`
|
)}/proposals/e9ec6d5c46a7e7bcabf9ba7a893fa5a5eeeec08b731f06f7a6eb7bf0e605b829`
|
||||||
);
|
);
|
||||||
cy.getByTestId('proposal-actions-content').click();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 6001-MARK-060
|
// 6001-MARK-060
|
||||||
@ -214,11 +213,12 @@ describe('no markets proposed', { tags: '@smoke', testIsolation: true }, () => {
|
|||||||
aliasGQLQuery(req, 'ProposalsList', proposal);
|
aliasGQLQuery(req, 'ProposalsList', proposal);
|
||||||
});
|
});
|
||||||
cy.mockSubscription();
|
cy.mockSubscription();
|
||||||
cy.visit('/#/markets/all');
|
|
||||||
cy.get('[data-testid="Proposed markets"]').click();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('can see no markets message', () => {
|
it('can see no markets message', () => {
|
||||||
|
cy.visit('/#/markets/all');
|
||||||
|
cy.get('[data-testid="Proposed markets"]').click();
|
||||||
|
|
||||||
// 6001-MARK-061
|
// 6001-MARK-061
|
||||||
cy.getByTestId('tab-proposed-markets').should('contain.text', 'No markets');
|
cy.getByTestId('tab-proposed-markets').should('contain.text', 'No markets');
|
||||||
});
|
});
|
||||||
|
@ -28,16 +28,16 @@ describe('deal ticket basics', { tags: '@smoke' }, () => {
|
|||||||
|
|
||||||
it('must be able to select order direction - long/short', function () {
|
it('must be able to select order direction - long/short', function () {
|
||||||
// 7002-SORD-004
|
// 7002-SORD-004
|
||||||
cy.getByTestId(toggleShort).click().children('input').should('be.checked');
|
cy.getByTestId(toggleShort).click().next('input').should('be.checked');
|
||||||
cy.getByTestId(toggleLong).click().children('input').should('be.checked');
|
cy.getByTestId(toggleLong).click().next('input').should('be.checked');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('must be able to select order type - limit/market', function () {
|
it('must be able to select order type - limit/market', function () {
|
||||||
// 7002-SORD-005
|
// 7002-SORD-005
|
||||||
// 7002-SORD-006
|
// 7002-SORD-006
|
||||||
// 7002-SORD-007
|
// 7002-SORD-007
|
||||||
cy.getByTestId(toggleLimit).click().children('input').should('be.checked');
|
cy.getByTestId(toggleLimit).click().next('input').should('be.checked');
|
||||||
cy.getByTestId(toggleMarket).click().children('input').should('be.checked');
|
cy.getByTestId(toggleMarket).click().next('input').should('be.checked');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('order connect vega wallet button should connect', () => {
|
it('order connect vega wallet button should connect', () => {
|
||||||
@ -51,7 +51,7 @@ describe('deal ticket basics', { tags: '@smoke' }, () => {
|
|||||||
.click();
|
.click();
|
||||||
cy.wait('@walletReq');
|
cy.wait('@walletReq');
|
||||||
cy.getByTestId(placeOrderBtn).should('be.visible');
|
cy.getByTestId(placeOrderBtn).should('be.visible');
|
||||||
cy.getByTestId(toggleLimit).children('input').should('be.checked');
|
cy.getByTestId(toggleLimit).next('input').should('be.checked');
|
||||||
cy.getByTestId(orderPriceField).should('have.value', '101');
|
cy.getByTestId(orderPriceField).should('have.value', '101');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -35,6 +35,8 @@ describe('transfer fees', { tags: '@regression', testIsolation: true }, () => {
|
|||||||
cy.setVegaWallet();
|
cy.setVegaWallet();
|
||||||
|
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
|
cy.getByTestId(manageVegaWallet).click();
|
||||||
|
cy.getByTestId(walletTransfer).click();
|
||||||
|
|
||||||
cy.wait('@Assets');
|
cy.wait('@Assets');
|
||||||
cy.wait('@Accounts');
|
cy.wait('@Accounts');
|
||||||
@ -57,7 +59,6 @@ describe('transfer fees', { tags: '@regression', testIsolation: true }, () => {
|
|||||||
// 1003-TRAN-019
|
// 1003-TRAN-019
|
||||||
cy.getByTestId(transferForm);
|
cy.getByTestId(transferForm);
|
||||||
cy.contains('Enter manually').click();
|
cy.contains('Enter manually').click();
|
||||||
|
|
||||||
cy.getByTestId(transferForm)
|
cy.getByTestId(transferForm)
|
||||||
.find(toAddressField)
|
.find(toAddressField)
|
||||||
.type('7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535');
|
.type('7f9cf07d3a9905b1a61a1069f7a758855da428bc0f4a97de87f48644bfc25535');
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { OrderType } from '@vegaprotocol/types';
|
||||||
import type { OrderSubmission } from '@vegaprotocol/wallet';
|
import type { OrderSubmission } from '@vegaprotocol/wallet';
|
||||||
|
|
||||||
const orderSizeField = 'order-size';
|
const orderSizeField = 'order-size';
|
||||||
@ -9,7 +10,9 @@ export const createOrder = (order: OrderSubmission): void => {
|
|||||||
cy.log('Placing order', order);
|
cy.log('Placing order', order);
|
||||||
const { type, side, size, price, timeInForce, expiresAt } = order;
|
const { type, side, size, price, timeInForce, expiresAt } = order;
|
||||||
|
|
||||||
cy.getByTestId(`order-type-${type}`).click();
|
cy.getByTestId(
|
||||||
|
`order-type-${type === OrderType.TYPE_LIMIT ? 'Limit' : 'Market'}`
|
||||||
|
).click();
|
||||||
cy.getByTestId(`order-side-${side}`).click();
|
cy.getByTestId(`order-side-${side}`).click();
|
||||||
cy.getByTestId(orderSizeField).clear().type(size);
|
cy.getByTestId(orderSizeField).clear().type(size);
|
||||||
if (price) {
|
if (price) {
|
||||||
|
@ -6,8 +6,8 @@ export const orderTIFDropDown = 'order-tif';
|
|||||||
export const placeOrderBtn = 'place-order';
|
export const placeOrderBtn = 'place-order';
|
||||||
export const toggleShort = 'order-side-SIDE_SELL';
|
export const toggleShort = 'order-side-SIDE_SELL';
|
||||||
export const toggleLong = 'order-side-SIDE_BUY';
|
export const toggleLong = 'order-side-SIDE_BUY';
|
||||||
export const toggleLimit = 'order-type-TYPE_LIMIT';
|
export const toggleLimit = 'order-type-Limit';
|
||||||
export const toggleMarket = 'order-type-TYPE_MARKET';
|
export const toggleMarket = 'order-type-Market';
|
||||||
|
|
||||||
export const TIFlist = Object.values(Schema.OrderTimeInForce).map((value) => {
|
export const TIFlist = Object.values(Schema.OrderTimeInForce).map((value) => {
|
||||||
return {
|
return {
|
||||||
|
@ -16,6 +16,6 @@ NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
|
|||||||
|
|
||||||
# Cosmic elevator flags
|
# Cosmic elevator flags
|
||||||
NX_SUCCESSOR_MARKETS=true
|
NX_SUCCESSOR_MARKETS=true
|
||||||
# NX_STOP_ORDERS
|
NX_STOP_ORDERS=true
|
||||||
# NX_ICEBERG_ORDERS
|
# NX_ICEBERG_ORDERS
|
||||||
# NX_PRODUCT_PERPETUALS
|
# NX_PRODUCT_PERPETUALS
|
@ -18,6 +18,6 @@ NX_ETH_WALLET_MNEMONIC="ozone access unlock valid olympic save include omit supp
|
|||||||
|
|
||||||
# Cosmic elevator flags
|
# Cosmic elevator flags
|
||||||
NX_SUCCESSOR_MARKETS=false
|
NX_SUCCESSOR_MARKETS=false
|
||||||
# NX_STOP_ORDERS
|
NX_STOP_ORDERS=false
|
||||||
# NX_ICEBERG_ORDERS
|
# NX_ICEBERG_ORDERS
|
||||||
# NX_PRODUCT_PERPETUALS
|
# NX_PRODUCT_PERPETUALS
|
@ -16,6 +16,6 @@ NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
|
|||||||
|
|
||||||
# Cosmic elevator flags
|
# Cosmic elevator flags
|
||||||
NX_SUCCESSOR_MARKETS=true
|
NX_SUCCESSOR_MARKETS=true
|
||||||
# NX_STOP_ORDERS
|
NX_STOP_ORDERS=true
|
||||||
# NX_ICEBERG_ORDERS
|
# NX_ICEBERG_ORDERS
|
||||||
# NX_PRODUCT_PERPETUALS
|
# NX_PRODUCT_PERPETUALS
|
@ -18,6 +18,6 @@ NX_APP_VERSION=v0.20.21-core-0.71.6
|
|||||||
|
|
||||||
# Cosmic elevator flags
|
# Cosmic elevator flags
|
||||||
NX_SUCCESSOR_MARKETS=false
|
NX_SUCCESSOR_MARKETS=false
|
||||||
# NX_STOP_ORDERS
|
NX_STOP_ORDERS=false
|
||||||
# NX_ICEBERG_ORDERS
|
# NX_ICEBERG_ORDERS
|
||||||
# NX_PRODUCT_PERPETUALS
|
# NX_PRODUCT_PERPETUALS
|
@ -18,6 +18,6 @@ NX_APP_VERSION=v0.20.19-core-0.71.6
|
|||||||
|
|
||||||
# Cosmic elevator flags
|
# Cosmic elevator flags
|
||||||
NX_SUCCESSOR_MARKETS=false
|
NX_SUCCESSOR_MARKETS=false
|
||||||
# NX_STOP_ORDERS
|
NX_STOP_ORDERS=false
|
||||||
# NX_ICEBERG_ORDERS
|
# NX_ICEBERG_ORDERS
|
||||||
# NX_PRODUCT_PERPETUALS
|
# NX_PRODUCT_PERPETUALS
|
@ -16,6 +16,6 @@ NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
|
|||||||
|
|
||||||
# Cosmic elevator flags
|
# Cosmic elevator flags
|
||||||
NX_SUCCESSOR_MARKETS=true
|
NX_SUCCESSOR_MARKETS=true
|
||||||
# NX_STOP_ORDERS
|
NX_STOP_ORDERS=true
|
||||||
# NX_ICEBERG_ORDERS
|
# NX_ICEBERG_ORDERS
|
||||||
# NX_PRODUCT_PERPETUALS
|
# NX_PRODUCT_PERPETUALS
|
@ -17,6 +17,6 @@ NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
|
|||||||
|
|
||||||
# Cosmic elevator flags
|
# Cosmic elevator flags
|
||||||
NX_SUCCESSOR_MARKETS=true
|
NX_SUCCESSOR_MARKETS=true
|
||||||
# NX_STOP_ORDERS
|
NX_STOP_ORDERS=true
|
||||||
# NX_ICEBERG_ORDERS
|
# NX_ICEBERG_ORDERS
|
||||||
# NX_PRODUCT_PERPETUALS
|
# NX_PRODUCT_PERPETUALS
|
@ -17,6 +17,6 @@ NX_VEGA_CONSOLE_URL=https://trading.validators-testnet.vega.rocks
|
|||||||
|
|
||||||
# Cosmic elevator flags
|
# Cosmic elevator flags
|
||||||
NX_SUCCESSOR_MARKETS=false
|
NX_SUCCESSOR_MARKETS=false
|
||||||
# NX_STOP_ORDERS
|
NX_STOP_ORDERS=false
|
||||||
# NX_ICEBERG_ORDERS
|
# NX_ICEBERG_ORDERS
|
||||||
# NX_PRODUCT_PERPETUALS
|
# NX_PRODUCT_PERPETUALS
|
@ -130,6 +130,13 @@ const MainGrid = memo(
|
|||||||
<TradingViews.orders.component marketId={marketId} />
|
<TradingViews.orders.component marketId={marketId} />
|
||||||
</VegaWalletContainer>
|
</VegaWalletContainer>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
{FLAGS.STOP_ORDERS ? (
|
||||||
|
<Tab id="stop-orders" name={t('Stop orders')}>
|
||||||
|
<VegaWalletContainer>
|
||||||
|
<TradingViews.stopOrders.component />
|
||||||
|
</VegaWalletContainer>
|
||||||
|
</Tab>
|
||||||
|
) : null}
|
||||||
<Tab id="fills" name={t('Fills')}>
|
<Tab id="fills" name={t('Fills')}>
|
||||||
<VegaWalletContainer>
|
<VegaWalletContainer>
|
||||||
<TradingViews.fills.component
|
<TradingViews.fills.component
|
||||||
|
@ -12,6 +12,7 @@ import { AccountsContainer } from '../../components/accounts-container';
|
|||||||
import { LiquidityContainer } from '../../components/liquidity-container';
|
import { LiquidityContainer } from '../../components/liquidity-container';
|
||||||
import type { OrderContainerProps } from '../../components/orders-container';
|
import type { OrderContainerProps } from '../../components/orders-container';
|
||||||
import { OrdersContainer } from '../../components/orders-container';
|
import { OrdersContainer } from '../../components/orders-container';
|
||||||
|
import { StopOrdersContainer } from '../../components/stop-orders-container';
|
||||||
|
|
||||||
type MarketDependantView =
|
type MarketDependantView =
|
||||||
| typeof CandlesChartContainer
|
| typeof CandlesChartContainer
|
||||||
@ -74,6 +75,10 @@ export const TradingViews = {
|
|||||||
label: 'All',
|
label: 'All',
|
||||||
component: OrdersContainer,
|
component: OrdersContainer,
|
||||||
},
|
},
|
||||||
|
stopOrders: {
|
||||||
|
label: 'Stop',
|
||||||
|
component: StopOrdersContainer,
|
||||||
|
},
|
||||||
collateral: { label: 'Collateral', component: AccountsContainer },
|
collateral: { label: 'Collateral', component: AccountsContainer },
|
||||||
fills: { label: 'Fills', component: FillsContainer },
|
fills: { label: 'Fills', component: FillsContainer },
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
import { OrderbookManager } from '@vegaprotocol/market-depth';
|
import { OrderbookManager } from '@vegaprotocol/market-depth';
|
||||||
import { useCreateOrderStore } from '@vegaprotocol/orders';
|
import { useCreateOrderStore } from '@vegaprotocol/orders';
|
||||||
import { ViewType, useSidebar } from '../sidebar';
|
import { ViewType, useSidebar } from '../sidebar';
|
||||||
|
import { useStopOrderFormValues } from '@vegaprotocol/deal-ticket';
|
||||||
|
|
||||||
export const OrderbookContainer = ({ marketId }: { marketId: string }) => {
|
export const OrderbookContainer = ({ marketId }: { marketId: string }) => {
|
||||||
const useOrderStoreRef = useCreateOrderStore();
|
const useOrderStoreRef = useCreateOrderStore();
|
||||||
const updateOrder = useOrderStoreRef((store) => store.update);
|
const updateOrder = useOrderStoreRef((store) => store.update);
|
||||||
|
const updateStoredFormValues = useStopOrderFormValues(
|
||||||
|
(state) => state.update
|
||||||
|
);
|
||||||
const setView = useSidebar((store) => store.setView);
|
const setView = useSidebar((store) => store.setView);
|
||||||
return (
|
return (
|
||||||
<OrderbookManager
|
<OrderbookManager
|
||||||
@ -12,9 +16,11 @@ export const OrderbookContainer = ({ marketId }: { marketId: string }) => {
|
|||||||
onClick={({ price, size }) => {
|
onClick={({ price, size }) => {
|
||||||
if (price) {
|
if (price) {
|
||||||
updateOrder(marketId, { price });
|
updateOrder(marketId, { price });
|
||||||
|
updateStoredFormValues(marketId, { price });
|
||||||
}
|
}
|
||||||
if (size) {
|
if (size) {
|
||||||
updateOrder(marketId, { size });
|
updateOrder(marketId, { size });
|
||||||
|
updateStoredFormValues(marketId, { size });
|
||||||
}
|
}
|
||||||
setView({ type: ViewType.Order });
|
setView({ type: ViewType.Order });
|
||||||
}}
|
}}
|
||||||
|
1
apps/trading/components/stop-orders-container/index.ts
Normal file
1
apps/trading/components/stop-orders-container/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './stop-orders-container';
|
@ -0,0 +1,41 @@
|
|||||||
|
import { useDataGridEvents } from '@vegaprotocol/datagrid';
|
||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import { StopOrdersManager } from '@vegaprotocol/orders';
|
||||||
|
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
|
import { useMarketClickHandler } from '../../lib/hooks/use-market-click-handler';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
import type { DataGridSlice } from '../../stores/datagrid-store-slice';
|
||||||
|
import { createDataGridSlice } from '../../stores/datagrid-store-slice';
|
||||||
|
|
||||||
|
export const StopOrdersContainer = () => {
|
||||||
|
const { pubKey, isReadOnly } = useVegaWallet();
|
||||||
|
const onMarketClick = useMarketClickHandler(true);
|
||||||
|
|
||||||
|
const gridStore = useStopOrdersStore((store) => store.gridStore);
|
||||||
|
const updateGridStore = useStopOrdersStore((store) => store.updateGridStore);
|
||||||
|
|
||||||
|
const gridStoreCallbacks = useDataGridEvents(gridStore, (colState) => {
|
||||||
|
updateGridStore(colState);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!pubKey) {
|
||||||
|
return <Splash>{t('Please connect Vega wallet')}</Splash>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StopOrdersManager
|
||||||
|
partyId={pubKey}
|
||||||
|
onMarketClick={onMarketClick}
|
||||||
|
isReadOnly={isReadOnly}
|
||||||
|
gridProps={gridStoreCallbacks}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useStopOrdersStore = create<DataGridSlice>()(
|
||||||
|
persist(createDataGridSlice, {
|
||||||
|
name: 'vega_fills_store',
|
||||||
|
})
|
||||||
|
);
|
@ -23,7 +23,7 @@ export const useAccountBalance = (assetId?: string) => {
|
|||||||
},
|
},
|
||||||
[assetId]
|
[assetId]
|
||||||
);
|
);
|
||||||
useDataProvider({
|
const { loading, error } = useDataProvider({
|
||||||
dataProvider: accountsDataProvider,
|
dataProvider: accountsDataProvider,
|
||||||
variables,
|
variables,
|
||||||
skip: !pubKey || !assetId,
|
skip: !pubKey || !assetId,
|
||||||
@ -34,7 +34,9 @@ export const useAccountBalance = (assetId?: string) => {
|
|||||||
() => ({
|
() => ({
|
||||||
accountBalance: pubKey ? accountBalance : '',
|
accountBalance: pubKey ? accountBalance : '',
|
||||||
accountDecimals: pubKey ? accountDecimals : null,
|
accountDecimals: pubKey ? accountDecimals : null,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
}),
|
}),
|
||||||
[accountBalance, accountDecimals, pubKey]
|
[accountBalance, accountDecimals, pubKey, loading, error]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -22,7 +22,7 @@ export const useMarketAccountBalance = (marketId: string) => {
|
|||||||
},
|
},
|
||||||
[marketId]
|
[marketId]
|
||||||
);
|
);
|
||||||
useDataProvider({
|
const { loading, error } = useDataProvider({
|
||||||
dataProvider: accountsDataProvider,
|
dataProvider: accountsDataProvider,
|
||||||
variables: { partyId: pubKey || '' },
|
variables: { partyId: pubKey || '' },
|
||||||
skip: !pubKey || !marketId,
|
skip: !pubKey || !marketId,
|
||||||
@ -33,7 +33,9 @@ export const useMarketAccountBalance = (marketId: string) => {
|
|||||||
() => ({
|
() => ({
|
||||||
accountBalance: pubKey ? accountBalance : '',
|
accountBalance: pubKey ? accountBalance : '',
|
||||||
accountDecimals: pubKey ? accountDecimals : null,
|
accountDecimals: pubKey ? accountDecimals : null,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
}),
|
}),
|
||||||
[accountBalance, accountDecimals, pubKey]
|
[accountBalance, accountDecimals, pubKey, loading, error]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,7 @@ import { useCallback } from 'react';
|
|||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
|
|
||||||
interface MarketNameCellProps {
|
interface MarketNameCellProps {
|
||||||
value?: string;
|
value?: string | null;
|
||||||
data?: { id?: string; marketId?: string; market?: { id: string } };
|
data?: { id?: string; marketId?: string; market?: { id: string } };
|
||||||
idPath?: string;
|
idPath?: string;
|
||||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { Control } from 'react-hook-form';
|
import type { Control } from 'react-hook-form';
|
||||||
import type { Market, MarketData } from '@vegaprotocol/markets';
|
import type { Market, StaticMarketData } from '@vegaprotocol/markets';
|
||||||
import { DealTicketMarketAmount } from './deal-ticket-market-amount';
|
import { DealTicketMarketAmount } from './deal-ticket-market-amount';
|
||||||
import { DealTicketLimitAmount } from './deal-ticket-limit-amount';
|
import { DealTicketLimitAmount } from './deal-ticket-limit-amount';
|
||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
@ -9,7 +9,8 @@ import type { OrderFormFields } from '../../hooks/use-order-form';
|
|||||||
export interface DealTicketAmountProps {
|
export interface DealTicketAmountProps {
|
||||||
control: Control<OrderFormFields>;
|
control: Control<OrderFormFields>;
|
||||||
orderType: Schema.OrderType;
|
orderType: Schema.OrderType;
|
||||||
marketData: MarketData;
|
marketData: StaticMarketData;
|
||||||
|
marketPrice?: string;
|
||||||
market: Market;
|
market: Market;
|
||||||
sizeError?: string;
|
sizeError?: string;
|
||||||
priceError?: string;
|
priceError?: string;
|
||||||
@ -21,11 +22,18 @@ export interface DealTicketAmountProps {
|
|||||||
export const DealTicketAmount = ({
|
export const DealTicketAmount = ({
|
||||||
orderType,
|
orderType,
|
||||||
marketData,
|
marketData,
|
||||||
|
marketPrice,
|
||||||
...props
|
...props
|
||||||
}: DealTicketAmountProps) => {
|
}: DealTicketAmountProps) => {
|
||||||
switch (orderType) {
|
switch (orderType) {
|
||||||
case Schema.OrderType.TYPE_MARKET:
|
case Schema.OrderType.TYPE_MARKET:
|
||||||
return <DealTicketMarketAmount {...props} marketData={marketData} />;
|
return (
|
||||||
|
<DealTicketMarketAmount
|
||||||
|
{...props}
|
||||||
|
marketData={marketData}
|
||||||
|
marketPrice={marketPrice}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case Schema.OrderType.TYPE_LIMIT:
|
case Schema.OrderType.TYPE_LIMIT:
|
||||||
return <DealTicketLimitAmount {...props} />;
|
return <DealTicketLimitAmount {...props} />;
|
||||||
default: {
|
default: {
|
||||||
|
@ -4,9 +4,10 @@ import classNames from 'classnames';
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
side: Side;
|
side: Side;
|
||||||
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DealTicketButton = ({ side }: Props) => {
|
export const DealTicketButton = ({ side, label }: Props) => {
|
||||||
const buttonClasses = classNames(
|
const buttonClasses = classNames(
|
||||||
'px-10 py-2 uppercase rounded-md text-white w-full',
|
'px-10 py-2 uppercase rounded-md text-white w-full',
|
||||||
{
|
{
|
||||||
@ -17,7 +18,7 @@ export const DealTicketButton = ({ side }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<button type="submit" data-testid="place-order" className={buttonClasses}>
|
<button type="submit" data-testid="place-order" className={buttonClasses}>
|
||||||
{t('Place order')}
|
{label || t('Place order')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,11 +1,20 @@
|
|||||||
|
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
|
||||||
|
import {
|
||||||
|
DealTicketType,
|
||||||
|
useDealTicketTypeStore,
|
||||||
|
} from '../../hooks/use-type-store';
|
||||||
|
import { StopOrder } from './deal-ticket-stop-order';
|
||||||
|
import {
|
||||||
|
useStaticMarketData,
|
||||||
|
useMarket,
|
||||||
|
useMarketPrice,
|
||||||
|
} from '@vegaprotocol/markets';
|
||||||
import { AsyncRenderer, Splash } from '@vegaprotocol/ui-toolkit';
|
import { AsyncRenderer, Splash } from '@vegaprotocol/ui-toolkit';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { useThrottledDataProvider } from '@vegaprotocol/data-provider';
|
|
||||||
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
|
|
||||||
import { useMarket, marketDataProvider } from '@vegaprotocol/markets';
|
|
||||||
import { DealTicket } from './deal-ticket';
|
import { DealTicket } from './deal-ticket';
|
||||||
|
import { FLAGS } from '@vegaprotocol/environment';
|
||||||
|
|
||||||
export interface DealTicketContainerProps {
|
interface DealTicketContainerProps {
|
||||||
marketId: string;
|
marketId: string;
|
||||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||||
onClickCollateral?: () => void;
|
onClickCollateral?: () => void;
|
||||||
@ -14,10 +23,9 @@ export interface DealTicketContainerProps {
|
|||||||
|
|
||||||
export const DealTicketContainer = ({
|
export const DealTicketContainer = ({
|
||||||
marketId,
|
marketId,
|
||||||
onMarketClick,
|
...props
|
||||||
onClickCollateral,
|
|
||||||
onDeposit,
|
|
||||||
}: DealTicketContainerProps) => {
|
}: DealTicketContainerProps) => {
|
||||||
|
const type = useDealTicketTypeStore((state) => state.type[marketId]);
|
||||||
const {
|
const {
|
||||||
data: market,
|
data: market,
|
||||||
error: marketError,
|
error: marketError,
|
||||||
@ -29,15 +37,9 @@ export const DealTicketContainer = ({
|
|||||||
error: marketDataError,
|
error: marketDataError,
|
||||||
loading: marketDataLoading,
|
loading: marketDataLoading,
|
||||||
reload,
|
reload,
|
||||||
} = useThrottledDataProvider(
|
} = useStaticMarketData(marketId);
|
||||||
{
|
const { data: marketPrice } = useMarketPrice(market?.id);
|
||||||
dataProvider: marketDataProvider,
|
|
||||||
variables: { marketId },
|
|
||||||
},
|
|
||||||
1000
|
|
||||||
);
|
|
||||||
const create = useVegaTransactionStore((state) => state.create);
|
const create = useVegaTransactionStore((state) => state.create);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncRenderer
|
<AsyncRenderer
|
||||||
data={market && marketData}
|
data={market && marketData}
|
||||||
@ -46,14 +48,23 @@ export const DealTicketContainer = ({
|
|||||||
reload={reload}
|
reload={reload}
|
||||||
>
|
>
|
||||||
{market && marketData ? (
|
{market && marketData ? (
|
||||||
<DealTicket
|
FLAGS.STOP_ORDERS &&
|
||||||
market={market}
|
(type === DealTicketType.StopLimit ||
|
||||||
marketData={marketData}
|
type === DealTicketType.StopMarket) ? (
|
||||||
submit={(orderSubmission) => create({ orderSubmission })}
|
<StopOrder
|
||||||
onClickCollateral={onClickCollateral}
|
market={market}
|
||||||
onMarketClick={onMarketClick}
|
marketPrice={marketPrice}
|
||||||
onDeposit={onDeposit}
|
submit={(stopOrdersSubmission) => create({ stopOrdersSubmission })}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<DealTicket
|
||||||
|
{...props}
|
||||||
|
market={market}
|
||||||
|
marketPrice={marketPrice}
|
||||||
|
marketData={marketData}
|
||||||
|
submit={(orderSubmission) => create({ orderSubmission })}
|
||||||
|
/>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<Splash>
|
<Splash>
|
||||||
<p>{t('Could not load market')}</p>
|
<p>{t('Could not load market')}</p>
|
||||||
|
@ -4,11 +4,11 @@ import classnames from 'classnames';
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { FeesBreakdown } from '@vegaprotocol/markets';
|
import { FeesBreakdown } from '@vegaprotocol/markets';
|
||||||
|
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
|
|
||||||
import type { Market } from '@vegaprotocol/markets';
|
import type { Market } from '@vegaprotocol/markets';
|
||||||
import type { EstimatePositionQuery } from '@vegaprotocol/positions';
|
import type { EstimatePositionQuery } from '@vegaprotocol/positions';
|
||||||
import type { EstimateFeesQuery } from '../../hooks/__generated__/EstimateOrder';
|
|
||||||
import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
|
import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
|
||||||
|
|
||||||
import { formatRange, formatValue } from '@vegaprotocol/utils';
|
import { formatRange, formatValue } from '@vegaprotocol/utils';
|
||||||
@ -24,6 +24,7 @@ import {
|
|||||||
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
|
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
|
||||||
MARGIN_ACCOUNT_TOOLTIP_TEXT,
|
MARGIN_ACCOUNT_TOOLTIP_TEXT,
|
||||||
} from '../../constants';
|
} from '../../constants';
|
||||||
|
import { useEstimateFees } from '../../hooks';
|
||||||
|
|
||||||
const emptyValue = '-';
|
const emptyValue = '-';
|
||||||
|
|
||||||
@ -76,26 +77,82 @@ export const DealTicketFeeDetail = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface DealTicketFeeDetailsProps {
|
export interface DealTicketFeeDetailsProps {
|
||||||
|
assetSymbol: string;
|
||||||
|
order: OrderSubmissionBody['orderSubmission'];
|
||||||
|
market: Market;
|
||||||
|
notionalSize: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DealTicketFeeDetails = ({
|
||||||
|
assetSymbol,
|
||||||
|
order,
|
||||||
|
market,
|
||||||
|
notionalSize,
|
||||||
|
}: DealTicketFeeDetailsProps) => {
|
||||||
|
const feeEstimate = useEstimateFees(order);
|
||||||
|
const { settlementAsset: asset } =
|
||||||
|
market.tradableInstrument.instrument.product;
|
||||||
|
const { decimals: assetDecimals, quantum } = asset;
|
||||||
|
const marketDecimals = market.decimalPlaces;
|
||||||
|
const quoteName = market.tradableInstrument.instrument.product.quoteName;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DealTicketFeeDetail
|
||||||
|
label={t('Notional')}
|
||||||
|
value={formatValue(notionalSize, marketDecimals)}
|
||||||
|
formattedValue={formatValue(notionalSize, marketDecimals)}
|
||||||
|
symbol={quoteName}
|
||||||
|
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName)}
|
||||||
|
/>
|
||||||
|
<DealTicketFeeDetail
|
||||||
|
label={t('Fees')}
|
||||||
|
value={
|
||||||
|
feeEstimate?.totalFeeAmount &&
|
||||||
|
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`
|
||||||
|
}
|
||||||
|
formattedValue={
|
||||||
|
feeEstimate?.totalFeeAmount &&
|
||||||
|
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals, quantum)}`
|
||||||
|
}
|
||||||
|
labelDescription={
|
||||||
|
<>
|
||||||
|
<span>
|
||||||
|
{t(
|
||||||
|
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.`
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<FeesBreakdown
|
||||||
|
fees={feeEstimate?.fees}
|
||||||
|
feeFactors={market.fees.factors}
|
||||||
|
symbol={assetSymbol}
|
||||||
|
decimals={assetDecimals}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
symbol={assetSymbol}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DealTicketMarginDetailsProps {
|
||||||
generalAccountBalance?: string;
|
generalAccountBalance?: string;
|
||||||
marginAccountBalance?: string;
|
marginAccountBalance?: string;
|
||||||
market: Market;
|
market: Market;
|
||||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||||
assetSymbol: string;
|
assetSymbol: string;
|
||||||
notionalSize: string | null;
|
|
||||||
feeEstimate: EstimateFeesQuery['estimateFees'] | undefined;
|
|
||||||
positionEstimate: EstimatePositionQuery['estimatePosition'];
|
positionEstimate: EstimatePositionQuery['estimatePosition'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DealTicketFeeDetails = ({
|
export const DealTicketMarginDetails = ({
|
||||||
marginAccountBalance,
|
marginAccountBalance,
|
||||||
generalAccountBalance,
|
generalAccountBalance,
|
||||||
assetSymbol,
|
assetSymbol,
|
||||||
feeEstimate,
|
|
||||||
market,
|
market,
|
||||||
onMarketClick,
|
onMarketClick,
|
||||||
notionalSize,
|
|
||||||
positionEstimate,
|
positionEstimate,
|
||||||
}: DealTicketFeeDetailsProps) => {
|
}: DealTicketMarginDetailsProps) => {
|
||||||
const [breakdownDialog, setBreakdownDialog] = useState(false);
|
const [breakdownDialog, setBreakdownDialog] = useState(false);
|
||||||
const { pubKey: partyId } = useVegaWallet();
|
const { pubKey: partyId } = useVegaWallet();
|
||||||
const { data: currentMargins } = useDataProvider({
|
const { data: currentMargins } = useDataProvider({
|
||||||
@ -110,7 +167,6 @@ export const DealTicketFeeDetails = ({
|
|||||||
const { settlementAsset: asset } =
|
const { settlementAsset: asset } =
|
||||||
market.tradableInstrument.instrument.product;
|
market.tradableInstrument.instrument.product;
|
||||||
const { decimals: assetDecimals, quantum } = asset;
|
const { decimals: assetDecimals, quantum } = asset;
|
||||||
const marketDecimals = market.decimalPlaces;
|
|
||||||
let marginRequiredBestCase: string | undefined = undefined;
|
let marginRequiredBestCase: string | undefined = undefined;
|
||||||
let marginRequiredWorstCase: string | undefined = undefined;
|
let marginRequiredWorstCase: string | undefined = undefined;
|
||||||
if (marginEstimate) {
|
if (marginEstimate) {
|
||||||
@ -251,41 +307,7 @@ export const DealTicketFeeDetails = ({
|
|||||||
const quoteName = market.tradableInstrument.instrument.product.quoteName;
|
const quoteName = market.tradableInstrument.instrument.product.quoteName;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<DealTicketFeeDetail
|
|
||||||
label={t('Notional')}
|
|
||||||
value={formatValue(notionalSize, marketDecimals)}
|
|
||||||
formattedValue={formatValue(notionalSize, marketDecimals)}
|
|
||||||
symbol={quoteName}
|
|
||||||
labelDescription={NOTIONAL_SIZE_TOOLTIP_TEXT(quoteName)}
|
|
||||||
/>
|
|
||||||
<DealTicketFeeDetail
|
|
||||||
label={t('Fees')}
|
|
||||||
value={
|
|
||||||
feeEstimate?.totalFeeAmount &&
|
|
||||||
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`
|
|
||||||
}
|
|
||||||
formattedValue={
|
|
||||||
feeEstimate?.totalFeeAmount &&
|
|
||||||
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals, quantum)}`
|
|
||||||
}
|
|
||||||
labelDescription={
|
|
||||||
<>
|
|
||||||
<span>
|
|
||||||
{t(
|
|
||||||
`An estimate of the most you would be expected to pay in fees, in the market's settlement asset ${assetSymbol}.`
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<FeesBreakdown
|
|
||||||
fees={feeEstimate?.fees}
|
|
||||||
feeFactors={market.fees.factors}
|
|
||||||
symbol={assetSymbol}
|
|
||||||
decimals={assetDecimals}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
symbol={assetSymbol}
|
|
||||||
/>
|
|
||||||
<DealTicketFeeDetail
|
<DealTicketFeeDetail
|
||||||
label={t('Margin required')}
|
label={t('Margin required')}
|
||||||
value={formatRange(
|
value={formatRange(
|
||||||
@ -351,6 +373,6 @@ export const DealTicketFeeDetails = ({
|
|||||||
onClose={onAccountBreakdownDialogClose}
|
onClose={onAccountBreakdownDialogClose}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -44,12 +44,12 @@ export const DealTicketLimitAmount = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={t('Size')}
|
label={t('Size')}
|
||||||
labelFor="input-order-size-limit"
|
labelFor="input-order-size-limit"
|
||||||
className="!mb-1"
|
className="!mb-0"
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name="size"
|
name="size"
|
||||||
@ -78,16 +78,13 @@ export const DealTicketLimitAmount = ({
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-0 items-center">
|
<div className="pt-7 leading-10">@</div>
|
||||||
<div className="flex"> </div>
|
|
||||||
<div className="flex">@</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<FormGroup
|
<FormGroup
|
||||||
labelFor="input-price-quote"
|
labelFor="input-price-quote"
|
||||||
label={t(`Price (${quoteName})`)}
|
label={t(`Price (${quoteName})`)}
|
||||||
labelAlign="right"
|
labelAlign="right"
|
||||||
className="!mb-1"
|
className="!mb-0"
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name="price"
|
name="price"
|
||||||
|
@ -5,10 +5,10 @@ import {
|
|||||||
} from '@vegaprotocol/utils';
|
} from '@vegaprotocol/utils';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import { Input, InputError, Tooltip } from '@vegaprotocol/ui-toolkit';
|
import { Input, InputError, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||||
import { isMarketInAuction } from '../../utils';
|
import { isMarketInAuction } from '@vegaprotocol/markets';
|
||||||
import type { DealTicketAmountProps } from './deal-ticket-amount';
|
import type { DealTicketAmountProps } from './deal-ticket-amount';
|
||||||
import { getMarketPrice } from '../../utils/get-price';
|
|
||||||
import { Controller } from 'react-hook-form';
|
import { Controller } from 'react-hook-form';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
export type DealTicketMarketAmountProps = Omit<
|
export type DealTicketMarketAmountProps = Omit<
|
||||||
DealTicketAmountProps,
|
DealTicketAmountProps,
|
||||||
@ -19,37 +19,26 @@ export const DealTicketMarketAmount = ({
|
|||||||
control,
|
control,
|
||||||
market,
|
market,
|
||||||
marketData,
|
marketData,
|
||||||
|
marketPrice,
|
||||||
sizeError,
|
sizeError,
|
||||||
update,
|
update,
|
||||||
size,
|
size,
|
||||||
}: DealTicketMarketAmountProps) => {
|
}: DealTicketMarketAmountProps) => {
|
||||||
const quoteName = market.tradableInstrument.instrument.product.quoteName;
|
const quoteName = market.tradableInstrument.instrument.product.quoteName;
|
||||||
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
||||||
const price = getMarketPrice(marketData);
|
const price = marketPrice;
|
||||||
|
|
||||||
const priceFormatted = price
|
const priceFormatted = price
|
||||||
? addDecimalsFormatNumber(price, market.decimalPlaces)
|
? addDecimalsFormatNumber(price, market.decimalPlaces)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const inAuction = isMarketInAuction(marketData.marketTradingMode);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<div className="flex items-end gap-4 mb-2">
|
<div className="flex items-start gap-4">
|
||||||
<div className="flex-1 text-sm">{t('Size')}</div>
|
|
||||||
<div />
|
|
||||||
<div className="flex-2 text-sm text-right">
|
|
||||||
{isMarketInAuction(marketData.marketTradingMode) && (
|
|
||||||
<Tooltip
|
|
||||||
description={t(
|
|
||||||
'This market is in auction. The uncrossing price is an indication of what the price is expected to be when the auction ends.'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div>{t(`Indicative price`)}</div>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
|
<div className="mb-2 text-sm">{t('Size')}</div>
|
||||||
<Controller
|
<Controller
|
||||||
name="size"
|
name="size"
|
||||||
control={control}
|
control={control}
|
||||||
@ -76,15 +65,29 @@ export const DealTicketMarketAmount = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>@</div>
|
<div className="pt-7 leading-10">@</div>
|
||||||
<div className="flex-1 text-sm text-right" data-testid="last-price">
|
<div className="flex-1 text-sm text-right">
|
||||||
{priceFormatted && quoteName ? (
|
{inAuction && (
|
||||||
<>
|
<Tooltip
|
||||||
~{priceFormatted} {quoteName}
|
description={t(
|
||||||
</>
|
'This market is in auction. The uncrossing price is an indication of what the price is expected to be when the auction ends.'
|
||||||
) : (
|
)}
|
||||||
'-'
|
>
|
||||||
|
<div className="mb-2">{t(`Indicative price`)}</div>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
<div
|
||||||
|
data-testid="last-price"
|
||||||
|
className={classNames('leading-10', { 'pt-7': !inAuction })}
|
||||||
|
>
|
||||||
|
{priceFormatted && quoteName ? (
|
||||||
|
<>
|
||||||
|
~{priceFormatted} {quoteName}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{sizeError && (
|
{sizeError && (
|
||||||
|
@ -0,0 +1,314 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||||
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { generateMarket } from '../../test-helpers';
|
||||||
|
import { StopOrder } from './deal-ticket-stop-order';
|
||||||
|
import * as Schema from '@vegaprotocol/types';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import type { StopOrderFormValues } from '../../hooks/use-stop-order-form-values';
|
||||||
|
import { useStopOrderFormValues } from '../../hooks/use-stop-order-form-values';
|
||||||
|
import type { FeatureFlags } from '@vegaprotocol/environment';
|
||||||
|
|
||||||
|
jest.mock('zustand');
|
||||||
|
jest.mock('./deal-ticket-fee-details', () => ({
|
||||||
|
DealTicketFeeDetails: () => <div data-testid="deal-ticket-fee-details" />,
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@vegaprotocol/environment', () => {
|
||||||
|
const actual = jest.requireActual('@vegaprotocol/environment');
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
FLAGS: {
|
||||||
|
...actual.FLAGS,
|
||||||
|
STOP_ORDERS: true,
|
||||||
|
} as FeatureFlags,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const marketPrice = '200';
|
||||||
|
const market = generateMarket();
|
||||||
|
const submit = jest.fn();
|
||||||
|
|
||||||
|
function generateJsx(pubKey: string | null = 'pubKey', isReadOnly = false) {
|
||||||
|
return (
|
||||||
|
<MockedProvider>
|
||||||
|
<VegaWalletContext.Provider value={{ pubKey, isReadOnly } as any}>
|
||||||
|
<StopOrder market={market} marketPrice={marketPrice} submit={submit} />
|
||||||
|
</VegaWalletContext.Provider>
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitButton = 'place-order';
|
||||||
|
const sizeInput = 'order-size';
|
||||||
|
const priceInput = 'order-price';
|
||||||
|
const triggerPriceInput = 'triggerPrice';
|
||||||
|
const triggerTrailingPercentOffsetInput = 'triggerTrailingPercentOffset';
|
||||||
|
|
||||||
|
const orderTypeTrigger = 'order-type-Stop';
|
||||||
|
const orderTypeLimit = 'order-type-StopLimit';
|
||||||
|
const orderTypeMarket = 'order-type-StopMarket';
|
||||||
|
|
||||||
|
const orderSideBuy = 'order-side-SIDE_BUY';
|
||||||
|
const orderSideSell = 'order-side-SIDE_SELL';
|
||||||
|
|
||||||
|
const triggerDirectionRisesAbove = 'triggerDirection-risesAbove';
|
||||||
|
// const triggerDirectionFallsBelow = 'triggerDirection-fallsBelow';
|
||||||
|
|
||||||
|
const expiryStrategySubmit = 'expiryStrategy-submit';
|
||||||
|
const expiryStrategyCancel = 'expiryStrategy-cancel';
|
||||||
|
|
||||||
|
const triggerTypePrice = 'triggerType-price';
|
||||||
|
const triggerTypeTrailingPercentOffset = 'triggerType-trailingPercentOffset';
|
||||||
|
|
||||||
|
const expire = 'expire';
|
||||||
|
const datePicker = 'date-picker-field';
|
||||||
|
const timeInForce = 'order-tif';
|
||||||
|
|
||||||
|
const sizeErrorMessage = 'stop-order-error-message-size';
|
||||||
|
const priceErrorMessage = 'stop-order-error-message-price';
|
||||||
|
const triggerPriceErrorMessage = 'stop-order-error-message-trigger-price';
|
||||||
|
const triggerTrailingPercentOffsetErrorMessage =
|
||||||
|
'stop-order-error-message-trigger-trailing-percent-offset';
|
||||||
|
|
||||||
|
describe('StopOrder', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display ticket defaults', async () => {
|
||||||
|
render(generateJsx());
|
||||||
|
// place order button should always be enabled
|
||||||
|
expect(screen.getByTestId(submitButton)).toBeEnabled();
|
||||||
|
|
||||||
|
// Assert defaults are used
|
||||||
|
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
||||||
|
expect(screen.getByTestId(orderTypeLimit).dataset.state).toEqual('checked');
|
||||||
|
await userEvent.click(screen.getByTestId(orderTypeLimit));
|
||||||
|
expect(screen.getByTestId(orderSideBuy).dataset.state).toEqual('checked');
|
||||||
|
expect(screen.getByTestId(sizeInput)).toHaveDisplayValue('0');
|
||||||
|
expect(screen.getByTestId(timeInForce)).toHaveValue(
|
||||||
|
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByTestId(triggerDirectionRisesAbove).dataset.state
|
||||||
|
).toEqual('checked');
|
||||||
|
expect(screen.getByTestId(triggerTypePrice).dataset.state).toEqual(
|
||||||
|
'checked'
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId(expire).dataset.state).toEqual('unchecked');
|
||||||
|
await userEvent.click(screen.getByTestId(expire));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId(expiryStrategySubmit).dataset.state).toEqual(
|
||||||
|
'checked'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display trigger price as price for market type order', async () => {
|
||||||
|
render(generateJsx());
|
||||||
|
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
||||||
|
await userEvent.click(screen.getByTestId(orderTypeMarket));
|
||||||
|
await userEvent.type(screen.getByTestId(triggerPriceInput), '10');
|
||||||
|
expect(screen.getByTestId('price')).toHaveTextContent('10.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use local storage state for initial values', async () => {
|
||||||
|
const values: Partial<StopOrderFormValues> = {
|
||||||
|
type: Schema.OrderType.TYPE_LIMIT,
|
||||||
|
side: Schema.Side.SIDE_SELL,
|
||||||
|
size: '0.1',
|
||||||
|
price: '300.22',
|
||||||
|
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||||
|
expire: true,
|
||||||
|
expiryStrategy: Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS,
|
||||||
|
expiresAt: '2023-07-27T16:43:27.000',
|
||||||
|
};
|
||||||
|
|
||||||
|
useStopOrderFormValues.setState({
|
||||||
|
formValues: {
|
||||||
|
[market.id]: values,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
render(generateJsx());
|
||||||
|
// Assert correct defaults are used from store
|
||||||
|
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
||||||
|
expect(screen.queryByTestId(orderTypeLimit)).toBeChecked();
|
||||||
|
expect(screen.getByTestId(orderSideSell).dataset.state).toEqual('checked');
|
||||||
|
expect(screen.getByTestId(sizeInput)).toHaveDisplayValue(
|
||||||
|
values.size as string
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('order-tif')).toHaveValue(values.timeInForce);
|
||||||
|
expect(screen.getByTestId(priceInput)).toHaveDisplayValue(
|
||||||
|
values.price as string
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId(expire).dataset.state).toEqual('checked');
|
||||||
|
expect(screen.getByTestId(expiryStrategyCancel).dataset.state).toEqual(
|
||||||
|
'checked'
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId(datePicker)).toHaveDisplayValue(
|
||||||
|
values.expiresAt as string
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows no wallet warning and do not submit if no wallet connected', async () => {
|
||||||
|
render(generateJsx(null));
|
||||||
|
await userEvent.type(screen.getByTestId(sizeInput), '1');
|
||||||
|
await userEvent.type(screen.getByTestId(priceInput), '1');
|
||||||
|
await userEvent.type(screen.getByTestId(triggerPriceInput), '1');
|
||||||
|
await userEvent.click(screen.getByTestId(submitButton));
|
||||||
|
expect(submit).not.toBeCalled();
|
||||||
|
expect(
|
||||||
|
screen.getByTestId('deal-ticket-connect-wallet')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls submit if form is valid', async () => {
|
||||||
|
render(generateJsx());
|
||||||
|
await userEvent.type(screen.getByTestId(sizeInput), '1');
|
||||||
|
await userEvent.type(screen.getByTestId(priceInput), '1');
|
||||||
|
await userEvent.type(screen.getByTestId(triggerPriceInput), '1');
|
||||||
|
await userEvent.click(screen.getByTestId(submitButton));
|
||||||
|
expect(submit).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates size field', async () => {
|
||||||
|
render(generateJsx());
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByTestId(submitButton));
|
||||||
|
|
||||||
|
// default value should be invalid
|
||||||
|
expect(screen.getByTestId(sizeErrorMessage)).toBeInTheDocument();
|
||||||
|
// to small value should be invalid
|
||||||
|
await userEvent.type(screen.getByTestId(sizeInput), '0.01');
|
||||||
|
expect(screen.getByTestId(sizeErrorMessage)).toBeInTheDocument();
|
||||||
|
|
||||||
|
// clear and fill using valid value
|
||||||
|
await userEvent.clear(screen.getByTestId(sizeInput));
|
||||||
|
await userEvent.type(screen.getByTestId(sizeInput), '0.1');
|
||||||
|
expect(screen.queryByTestId(sizeErrorMessage)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates price field', async () => {
|
||||||
|
render(generateJsx());
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByTestId(submitButton));
|
||||||
|
// price error message should not show if size has error
|
||||||
|
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
|
||||||
|
await userEvent.type(screen.getByTestId(sizeInput), '0.1');
|
||||||
|
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
|
||||||
|
await userEvent.type(screen.getByTestId(priceInput), '0.001');
|
||||||
|
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
|
||||||
|
|
||||||
|
// switch to market order type error should disappear
|
||||||
|
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
||||||
|
await userEvent.click(screen.getByTestId(orderTypeMarket));
|
||||||
|
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
|
||||||
|
|
||||||
|
// switch back to limit type
|
||||||
|
await userEvent.click(screen.getByTestId(orderTypeTrigger));
|
||||||
|
await userEvent.click(screen.getByTestId(orderTypeLimit));
|
||||||
|
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
|
||||||
|
|
||||||
|
// to small value should be invalid
|
||||||
|
await userEvent.type(screen.getByTestId(priceInput), '0.001');
|
||||||
|
expect(screen.getByTestId(priceErrorMessage)).toBeInTheDocument();
|
||||||
|
|
||||||
|
// clear and fill using valid value
|
||||||
|
await userEvent.clear(screen.getByTestId(priceInput));
|
||||||
|
await userEvent.type(screen.getByTestId(priceInput), '0.01');
|
||||||
|
expect(screen.queryByTestId(priceErrorMessage)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates trigger price field', async () => {
|
||||||
|
render(generateJsx());
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByTestId(submitButton));
|
||||||
|
expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
|
||||||
|
|
||||||
|
// switch to trailing percentage offset trigger type
|
||||||
|
await userEvent.click(screen.getByTestId(triggerTypeTrailingPercentOffset));
|
||||||
|
expect(screen.queryByTestId(triggerPriceErrorMessage)).toBeNull();
|
||||||
|
|
||||||
|
// switch back to price trigger type
|
||||||
|
await userEvent.click(screen.getByTestId(triggerTypePrice));
|
||||||
|
expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
|
||||||
|
|
||||||
|
// to small value should be invalid
|
||||||
|
await userEvent.type(screen.getByTestId(triggerPriceInput), '0.001');
|
||||||
|
expect(screen.getByTestId(triggerPriceErrorMessage)).toBeInTheDocument();
|
||||||
|
|
||||||
|
// clear and fill using valid value
|
||||||
|
await userEvent.clear(screen.getByTestId(triggerPriceInput));
|
||||||
|
await userEvent.type(screen.getByTestId(triggerPriceInput), '0.01');
|
||||||
|
expect(screen.queryByTestId(triggerPriceErrorMessage)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates trigger trailing percentage offset field', async () => {
|
||||||
|
render(generateJsx());
|
||||||
|
|
||||||
|
// should not show error with default form values
|
||||||
|
await userEvent.click(screen.getByTestId(submitButton));
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage)
|
||||||
|
).toBeNull();
|
||||||
|
|
||||||
|
// switch to trailing percentage offset trigger type
|
||||||
|
await userEvent.click(screen.getByTestId(triggerTypeTrailingPercentOffset));
|
||||||
|
expect(
|
||||||
|
screen.getByTestId(triggerTrailingPercentOffsetErrorMessage)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
// to small value should be invalid
|
||||||
|
await userEvent.type(
|
||||||
|
screen.getByTestId(triggerTrailingPercentOffsetInput),
|
||||||
|
'0.09'
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByTestId(triggerTrailingPercentOffsetErrorMessage)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
// clear and fill using valid value
|
||||||
|
await userEvent.clear(
|
||||||
|
screen.getByTestId(triggerTrailingPercentOffsetInput)
|
||||||
|
);
|
||||||
|
await userEvent.type(
|
||||||
|
screen.getByTestId(triggerTrailingPercentOffsetInput),
|
||||||
|
'0.1'
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage)
|
||||||
|
).toBeNull();
|
||||||
|
|
||||||
|
// to big value should be invalid
|
||||||
|
await userEvent.clear(
|
||||||
|
screen.getByTestId(triggerTrailingPercentOffsetInput)
|
||||||
|
);
|
||||||
|
await userEvent.type(
|
||||||
|
screen.getByTestId(triggerTrailingPercentOffsetInput),
|
||||||
|
'99.91'
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByTestId(triggerTrailingPercentOffsetErrorMessage)
|
||||||
|
).toBeInTheDocument();
|
||||||
|
|
||||||
|
// clear and fill using valid value
|
||||||
|
await userEvent.clear(
|
||||||
|
screen.getByTestId(triggerTrailingPercentOffsetInput)
|
||||||
|
);
|
||||||
|
await userEvent.type(
|
||||||
|
screen.getByTestId(triggerTrailingPercentOffsetInput),
|
||||||
|
'99.9'
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId(triggerTrailingPercentOffsetErrorMessage)
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,590 @@
|
|||||||
|
import type { FormEventHandler } from 'react';
|
||||||
|
import { useRef, useCallback, useEffect } from 'react';
|
||||||
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
|
import type { StopOrdersSubmission } from '@vegaprotocol/wallet';
|
||||||
|
import {
|
||||||
|
formatNumber,
|
||||||
|
removeDecimal,
|
||||||
|
toDecimal,
|
||||||
|
validateAmount,
|
||||||
|
} from '@vegaprotocol/utils';
|
||||||
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
|
import * as Schema from '@vegaprotocol/types';
|
||||||
|
import {
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
Input,
|
||||||
|
Checkbox,
|
||||||
|
FormGroup,
|
||||||
|
InputError,
|
||||||
|
Select,
|
||||||
|
Tooltip,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { getDerivedPrice, type Market } from '@vegaprotocol/markets';
|
||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import { ExpirySelector } from './expiry-selector';
|
||||||
|
import { SideSelector } from './side-selector';
|
||||||
|
import { timeInForceLabel, useOrder } from '@vegaprotocol/orders';
|
||||||
|
import {
|
||||||
|
NoWalletWarning,
|
||||||
|
REDUCE_ONLY_TOOLTIP,
|
||||||
|
useNotionalSize,
|
||||||
|
} from './deal-ticket';
|
||||||
|
import { TypeToggle } from './type-selector';
|
||||||
|
import {
|
||||||
|
useStopOrderFormValues,
|
||||||
|
type StopOrderFormValues,
|
||||||
|
} from '../../hooks/use-stop-order-form-values';
|
||||||
|
import {
|
||||||
|
DealTicketType,
|
||||||
|
useDealTicketTypeStore,
|
||||||
|
} from '../../hooks/use-type-store';
|
||||||
|
import { mapFormValuesToStopOrdersSubmission } from '../../utils/map-form-values-to-stop-order-submission';
|
||||||
|
import { DealTicketButton } from './deal-ticket-button';
|
||||||
|
import { DealTicketFeeDetails } from './deal-ticket-fee-details';
|
||||||
|
import { validateExpiration } from '../../utils';
|
||||||
|
|
||||||
|
export interface StopOrderProps {
|
||||||
|
market: Market;
|
||||||
|
marketPrice?: string | null;
|
||||||
|
submit: (order: StopOrdersSubmission) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValues: Partial<StopOrderFormValues> = {
|
||||||
|
type: Schema.OrderType.TYPE_LIMIT,
|
||||||
|
side: Schema.Side.SIDE_BUY,
|
||||||
|
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||||
|
triggerType: 'price',
|
||||||
|
triggerDirection:
|
||||||
|
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE,
|
||||||
|
expiryStrategy: Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT,
|
||||||
|
size: '0',
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopSubmit: FormEventHandler = (e) => e.preventDefault();
|
||||||
|
|
||||||
|
export const StopOrder = ({ market, marketPrice, submit }: StopOrderProps) => {
|
||||||
|
const { pubKey, isReadOnly } = useVegaWallet();
|
||||||
|
const setDealTicketType = useDealTicketTypeStore((state) => state.set);
|
||||||
|
const [, updateOrder] = useOrder(market.id);
|
||||||
|
const updateStoredFormValues = useStopOrderFormValues(
|
||||||
|
(state) => state.update
|
||||||
|
);
|
||||||
|
const storedFormValues = useStopOrderFormValues(
|
||||||
|
(state) => state.formValues[market.id]
|
||||||
|
);
|
||||||
|
const { handleSubmit, setValue, watch, control, formState } =
|
||||||
|
useForm<StopOrderFormValues>({
|
||||||
|
defaultValues: { ...defaultValues, ...storedFormValues },
|
||||||
|
});
|
||||||
|
const { errors } = formState;
|
||||||
|
const lastSubmitTime = useRef(0);
|
||||||
|
const onSubmit = useCallback(
|
||||||
|
(data: StopOrderFormValues) => {
|
||||||
|
const now = new Date().getTime();
|
||||||
|
if (lastSubmitTime.current && now - lastSubmitTime.current < 1000) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
submit(
|
||||||
|
mapFormValuesToStopOrdersSubmission(
|
||||||
|
data,
|
||||||
|
market.id,
|
||||||
|
market.decimalPlaces,
|
||||||
|
market.positionDecimalPlaces
|
||||||
|
)
|
||||||
|
);
|
||||||
|
lastSubmitTime.current = now;
|
||||||
|
},
|
||||||
|
[market.id, market.decimalPlaces, market.positionDecimalPlaces, submit]
|
||||||
|
);
|
||||||
|
const side = watch('side');
|
||||||
|
const expire = watch('expire');
|
||||||
|
const triggerType = watch('triggerType');
|
||||||
|
const triggerPrice = watch('triggerPrice');
|
||||||
|
const timeInForce = watch('timeInForce');
|
||||||
|
const type = watch('type');
|
||||||
|
const rawPrice = watch('price');
|
||||||
|
const rawSize = watch('size');
|
||||||
|
|
||||||
|
if (storedFormValues?.size && rawSize !== storedFormValues?.size) {
|
||||||
|
setValue('size', storedFormValues.size);
|
||||||
|
}
|
||||||
|
if (storedFormValues?.price && rawPrice !== storedFormValues?.price) {
|
||||||
|
setValue('price', storedFormValues.price);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPriceTrigger = triggerType === 'price';
|
||||||
|
const size = removeDecimal(rawSize, market.positionDecimalPlaces);
|
||||||
|
const price =
|
||||||
|
marketPrice &&
|
||||||
|
getDerivedPrice(
|
||||||
|
{
|
||||||
|
type,
|
||||||
|
price: rawPrice && removeDecimal(rawPrice, market.decimalPlaces),
|
||||||
|
},
|
||||||
|
type === Schema.OrderType.TYPE_MARKET && isPriceTrigger && triggerPrice
|
||||||
|
? removeDecimal(triggerPrice, market.decimalPlaces)
|
||||||
|
: marketPrice
|
||||||
|
);
|
||||||
|
|
||||||
|
const notionalSize = useNotionalSize(
|
||||||
|
price,
|
||||||
|
size,
|
||||||
|
market.decimalPlaces,
|
||||||
|
market.positionDecimalPlaces
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = watch((value, { name, type }) => {
|
||||||
|
updateStoredFormValues(market.id, value);
|
||||||
|
});
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, [watch, market.id, updateStoredFormValues]);
|
||||||
|
|
||||||
|
const { quoteName, settlementAsset: asset } =
|
||||||
|
market.tradableInstrument.instrument.product;
|
||||||
|
|
||||||
|
const sizeStep = toDecimal(market?.positionDecimalPlaces);
|
||||||
|
const priceStep = toDecimal(market?.decimalPlaces);
|
||||||
|
const trailingPercentOffsetStep = '0.1';
|
||||||
|
|
||||||
|
const priceFormatted =
|
||||||
|
isPriceTrigger && triggerPrice
|
||||||
|
? formatNumber(triggerPrice, market.decimalPlaces)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={isReadOnly || !pubKey ? stopSubmit : handleSubmit(onSubmit)}
|
||||||
|
noValidate
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="type"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => {
|
||||||
|
const { value } = field;
|
||||||
|
return (
|
||||||
|
<TypeToggle
|
||||||
|
value={
|
||||||
|
value === Schema.OrderType.TYPE_LIMIT
|
||||||
|
? DealTicketType.StopLimit
|
||||||
|
: DealTicketType.StopMarket
|
||||||
|
}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const type = value as DealTicketType;
|
||||||
|
setDealTicketType(market.id, type);
|
||||||
|
if (
|
||||||
|
type === DealTicketType.Limit ||
|
||||||
|
type === DealTicketType.Market
|
||||||
|
) {
|
||||||
|
updateOrder({
|
||||||
|
type:
|
||||||
|
type === DealTicketType.Limit
|
||||||
|
? Schema.OrderType.TYPE_LIMIT
|
||||||
|
: Schema.OrderType.TYPE_MARKET,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setValue(
|
||||||
|
'type',
|
||||||
|
type === DealTicketType.StopLimit
|
||||||
|
? Schema.OrderType.TYPE_LIMIT
|
||||||
|
: Schema.OrderType.TYPE_MARKET
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.type && (
|
||||||
|
<InputError testId="stop-order-error-message-type">
|
||||||
|
{errors.type.message}
|
||||||
|
</InputError>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="side"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<SideSelector value={field.value} onValueChange={field.onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormGroup label={t('Trigger')} compact={true} labelFor="">
|
||||||
|
<Controller
|
||||||
|
name="triggerDirection"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => {
|
||||||
|
const { onChange, value } = field;
|
||||||
|
return (
|
||||||
|
<RadioGroup
|
||||||
|
name="triggerDirection"
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
orientation="horizontal"
|
||||||
|
className="mb-2"
|
||||||
|
>
|
||||||
|
<Radio
|
||||||
|
value={
|
||||||
|
Schema.StopOrderTriggerDirection
|
||||||
|
.TRIGGER_DIRECTION_RISES_ABOVE
|
||||||
|
}
|
||||||
|
id="triggerDirection-risesAbove"
|
||||||
|
label={'Rises above'}
|
||||||
|
/>
|
||||||
|
<Radio
|
||||||
|
value={
|
||||||
|
Schema.StopOrderTriggerDirection
|
||||||
|
.TRIGGER_DIRECTION_FALLS_BELOW
|
||||||
|
}
|
||||||
|
id="triggerDirection-fallsBelow"
|
||||||
|
label={'Falls below'}
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{isPriceTrigger && (
|
||||||
|
<div className="mb-2">
|
||||||
|
<Controller
|
||||||
|
name="triggerPrice"
|
||||||
|
rules={{
|
||||||
|
required: t('You need provide a price'),
|
||||||
|
min: {
|
||||||
|
value: priceStep,
|
||||||
|
message: t('Price cannot be lower than ' + priceStep),
|
||||||
|
},
|
||||||
|
validate: validateAmount(priceStep, 'Price'),
|
||||||
|
}}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => {
|
||||||
|
const { value, ...props } = field;
|
||||||
|
return (
|
||||||
|
<div className="mb-2">
|
||||||
|
<Input
|
||||||
|
data-testid="triggerPrice"
|
||||||
|
type="number"
|
||||||
|
step={priceStep}
|
||||||
|
appendElement={asset.symbol}
|
||||||
|
value={value || ''}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.triggerPrice && (
|
||||||
|
<InputError testId="stop-order-error-message-trigger-price">
|
||||||
|
{errors.triggerPrice.message}
|
||||||
|
</InputError>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isPriceTrigger && (
|
||||||
|
<div className="mb-2">
|
||||||
|
<Controller
|
||||||
|
name="triggerTrailingPercentOffset"
|
||||||
|
control={control}
|
||||||
|
rules={{
|
||||||
|
required: t('You need provide a trailing percent offset'),
|
||||||
|
min: {
|
||||||
|
value: trailingPercentOffsetStep,
|
||||||
|
message: t(
|
||||||
|
'Trailing percent offset cannot be lower than ' +
|
||||||
|
trailingPercentOffsetStep
|
||||||
|
),
|
||||||
|
},
|
||||||
|
max: {
|
||||||
|
value: '99.9',
|
||||||
|
message: t(
|
||||||
|
'Trailing percent offset cannot be higher than 99.9'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
validate: validateAmount(
|
||||||
|
trailingPercentOffsetStep,
|
||||||
|
'Trailing percentage offset'
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
render={({ field }) => {
|
||||||
|
const { value, ...props } = field;
|
||||||
|
return (
|
||||||
|
<div className="mb-2">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
step={trailingPercentOffsetStep}
|
||||||
|
appendElement="%"
|
||||||
|
data-testid="triggerTrailingPercentOffset"
|
||||||
|
value={value || ''}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{errors.triggerTrailingPercentOffset && (
|
||||||
|
<InputError testId="stop-order-error-message-trigger-trailing-percent-offset">
|
||||||
|
{errors.triggerTrailingPercentOffset.message}
|
||||||
|
</InputError>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Controller
|
||||||
|
name="triggerType"
|
||||||
|
control={control}
|
||||||
|
rules={{ deps: ['triggerTrailingPercentOffset', 'triggerPrice'] }}
|
||||||
|
render={({ field }) => {
|
||||||
|
const { onChange, value } = field;
|
||||||
|
return (
|
||||||
|
<RadioGroup
|
||||||
|
onChange={onChange}
|
||||||
|
value={value}
|
||||||
|
orientation="horizontal"
|
||||||
|
>
|
||||||
|
<Radio value="price" id="triggerType-price" label={'Price'} />
|
||||||
|
<Radio
|
||||||
|
value="trailingPercentOffset"
|
||||||
|
id="triggerType-trailingPercentOffset"
|
||||||
|
label={'Trailing Percent Offset'}
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<div className="mb-2">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<FormGroup
|
||||||
|
labelFor="input-price-quote"
|
||||||
|
label={t(`Size`)}
|
||||||
|
className="!mb-0 flex-1"
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="size"
|
||||||
|
control={control}
|
||||||
|
rules={{
|
||||||
|
required: t('You need to provide a size'),
|
||||||
|
min: {
|
||||||
|
value: sizeStep,
|
||||||
|
message: t('Size cannot be lower than ' + sizeStep),
|
||||||
|
},
|
||||||
|
validate: validateAmount(sizeStep, 'Size'),
|
||||||
|
}}
|
||||||
|
render={({ field }) => {
|
||||||
|
const { value, ...props } = field;
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
id="order-size"
|
||||||
|
className="w-full"
|
||||||
|
type="number"
|
||||||
|
step={sizeStep}
|
||||||
|
min={sizeStep}
|
||||||
|
onWheel={(e) => e.currentTarget.blur()}
|
||||||
|
data-testid="order-size"
|
||||||
|
value={value || ''}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<div className="pt-7 leading-10">@</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
{type === Schema.OrderType.TYPE_LIMIT ? (
|
||||||
|
<FormGroup
|
||||||
|
labelFor="input-price-quote"
|
||||||
|
label={t(`Price (${quoteName})`)}
|
||||||
|
labelAlign="right"
|
||||||
|
className="!mb-0"
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="price"
|
||||||
|
control={control}
|
||||||
|
rules={{
|
||||||
|
deps: 'type',
|
||||||
|
required: t('You need provide a price'),
|
||||||
|
min: {
|
||||||
|
value: priceStep,
|
||||||
|
message: t('Price cannot be lower than ' + priceStep),
|
||||||
|
},
|
||||||
|
validate: validateAmount(priceStep, 'Price'),
|
||||||
|
}}
|
||||||
|
render={({ field }) => {
|
||||||
|
const { value, ...props } = field;
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
id="input-price-quote"
|
||||||
|
className="w-full"
|
||||||
|
type="number"
|
||||||
|
step={priceStep}
|
||||||
|
data-testid="order-price"
|
||||||
|
onWheel={(e) => e.currentTarget.blur()}
|
||||||
|
value={value || ''}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="text-sm text-right pt-7 leading-10"
|
||||||
|
data-testid="price"
|
||||||
|
>
|
||||||
|
{priceFormatted && quoteName
|
||||||
|
? `~${priceFormatted} ${quoteName}`
|
||||||
|
: '-'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{errors.size && (
|
||||||
|
<InputError testId="stop-order-error-message-size">
|
||||||
|
{errors.size.message}
|
||||||
|
</InputError>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!errors.size &&
|
||||||
|
errors.price &&
|
||||||
|
type === Schema.OrderType.TYPE_LIMIT && (
|
||||||
|
<InputError testId="stop-order-error-message-price">
|
||||||
|
{errors.price.message}
|
||||||
|
</InputError>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mb-2">
|
||||||
|
<FormGroup
|
||||||
|
label={t('Time in force')}
|
||||||
|
labelFor="select-time-in-force"
|
||||||
|
compact={true}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="timeInForce"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Select
|
||||||
|
id="select-time-in-force"
|
||||||
|
className="w-full"
|
||||||
|
data-testid="order-tif"
|
||||||
|
{...field}
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
key={Schema.OrderTimeInForce.TIME_IN_FORCE_IOC}
|
||||||
|
value={Schema.OrderTimeInForce.TIME_IN_FORCE_IOC}
|
||||||
|
>
|
||||||
|
{timeInForceLabel(Schema.OrderTimeInForce.TIME_IN_FORCE_IOC)}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
key={Schema.OrderTimeInForce.TIME_IN_FORCE_FOK}
|
||||||
|
value={Schema.OrderTimeInForce.TIME_IN_FORCE_FOK}
|
||||||
|
>
|
||||||
|
{timeInForceLabel(Schema.OrderTimeInForce.TIME_IN_FORCE_FOK)}
|
||||||
|
</option>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
{errors.timeInForce && (
|
||||||
|
<InputError testId="stop-error-message-tif">
|
||||||
|
{errors.timeInForce.message}
|
||||||
|
</InputError>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 pb-2 justify-end">
|
||||||
|
<Checkbox
|
||||||
|
name="reduce-only"
|
||||||
|
checked={true}
|
||||||
|
disabled={true}
|
||||||
|
label={
|
||||||
|
<Tooltip description={<span>{t(REDUCE_ONLY_TOOLTIP)}</span>}>
|
||||||
|
<span className="text-xs">{t('Reduce only')}</span>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mb-2">
|
||||||
|
<Controller
|
||||||
|
name="expire"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => {
|
||||||
|
const { onChange: onCheckedChange, value } = field;
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
onCheckedChange={onCheckedChange}
|
||||||
|
checked={value}
|
||||||
|
name="expire"
|
||||||
|
label={'Expire'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{expire && (
|
||||||
|
<>
|
||||||
|
<FormGroup
|
||||||
|
label={t('Strategy')}
|
||||||
|
labelFor="expiryStrategy"
|
||||||
|
compact={true}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="expiryStrategy"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<RadioGroup orientation="horizontal" {...field}>
|
||||||
|
<Radio
|
||||||
|
value={
|
||||||
|
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
|
||||||
|
}
|
||||||
|
id="expiryStrategy-submit"
|
||||||
|
label={'Submit'}
|
||||||
|
/>
|
||||||
|
<Radio
|
||||||
|
value={
|
||||||
|
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS
|
||||||
|
}
|
||||||
|
id="expiryStrategy-cancel"
|
||||||
|
label={'Cancel'}
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<div className="mb-2">
|
||||||
|
<Controller
|
||||||
|
name="expiresAt"
|
||||||
|
control={control}
|
||||||
|
rules={{
|
||||||
|
validate: validateExpiration,
|
||||||
|
}}
|
||||||
|
render={({ field }) => {
|
||||||
|
const { value, onChange: onSelect } = field;
|
||||||
|
return (
|
||||||
|
<ExpirySelector
|
||||||
|
value={value}
|
||||||
|
onSelect={onSelect}
|
||||||
|
errorMessage={errors.expiresAt?.message}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<NoWalletWarning pubKey={pubKey} isReadOnly={isReadOnly} asset={asset} />
|
||||||
|
<DealTicketButton side={side} label={t('Submit Stop Order')} />
|
||||||
|
<DealTicketFeeDetails
|
||||||
|
order={{
|
||||||
|
marketId: market.id,
|
||||||
|
price: price || undefined,
|
||||||
|
side,
|
||||||
|
size,
|
||||||
|
timeInForce,
|
||||||
|
type,
|
||||||
|
}}
|
||||||
|
notionalSize={notionalSize}
|
||||||
|
assetSymbol={asset.symbol}
|
||||||
|
market={market}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
@ -22,8 +22,12 @@ import { OrdersDocument } from '@vegaprotocol/orders';
|
|||||||
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" />,
|
||||||
|
DealTicketMarginDetails: () => (
|
||||||
|
<div data-testid="deal-ticket-margin-details" />
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const marketPrice = '200';
|
||||||
const pubKey = 'pubKey';
|
const pubKey = 'pubKey';
|
||||||
const market = generateMarket();
|
const market = generateMarket();
|
||||||
const marketData = generateMarketData();
|
const marketData = generateMarketData();
|
||||||
@ -36,6 +40,7 @@ function generateJsx(mocks: MockedResponse[] = []) {
|
|||||||
<DealTicket
|
<DealTicket
|
||||||
market={market}
|
market={market}
|
||||||
marketData={marketData}
|
marketData={marketData}
|
||||||
|
marketPrice={marketPrice}
|
||||||
submit={submit}
|
submit={submit}
|
||||||
onDeposit={jest.fn()}
|
onDeposit={jest.fn()}
|
||||||
/>
|
/>
|
||||||
@ -114,30 +119,22 @@ describe('DealTicket', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should display ticket defaults', () => {
|
it('should display ticket defaults', () => {
|
||||||
const { container } = render(generateJsx());
|
render(generateJsx());
|
||||||
|
|
||||||
// place order button should always be enabled
|
// place order button should always be enabled
|
||||||
expect(screen.getByTestId('place-order')).toBeEnabled();
|
expect(screen.getByTestId('place-order')).toBeEnabled();
|
||||||
|
|
||||||
// Assert defaults are used
|
// Assert defaults are used
|
||||||
expect(
|
expect(screen.getByTestId('order-type-Market')).toBeInTheDocument();
|
||||||
screen.getByTestId(`order-type-${Schema.OrderType.TYPE_MARKET}`)
|
expect(screen.getByTestId('order-type-Limit')).toBeInTheDocument();
|
||||||
).toBeInTheDocument();
|
|
||||||
expect(
|
|
||||||
screen.getByTestId(`order-type-${Schema.OrderType.TYPE_LIMIT}`)
|
|
||||||
).toBeInTheDocument();
|
|
||||||
|
|
||||||
const oderTypeLimitToggle = container.querySelector(
|
expect(screen.getByTestId('order-type-Limit').dataset.state).toEqual(
|
||||||
`[data-testid="order-type-${Schema.OrderType.TYPE_LIMIT}"] input[type="radio"]`
|
'checked'
|
||||||
);
|
);
|
||||||
expect(oderTypeLimitToggle).toBeChecked();
|
|
||||||
|
|
||||||
expect(
|
expect(screen.getByTestId('order-side-SIDE_BUY').dataset.state).toEqual(
|
||||||
screen.queryByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
'checked'
|
||||||
).toBeChecked();
|
);
|
||||||
expect(
|
|
||||||
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
|
||||||
).not.toBeChecked();
|
|
||||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue('0');
|
expect(screen.getByTestId('order-size')).toHaveDisplayValue('0');
|
||||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||||
Schema.OrderTimeInForce.TIME_IN_FORCE_GTC
|
Schema.OrderTimeInForce.TIME_IN_FORCE_GTC
|
||||||
@ -147,12 +144,12 @@ describe('DealTicket', () => {
|
|||||||
it('should display last price for market type order', () => {
|
it('should display last price for market type order', () => {
|
||||||
render(generateJsx());
|
render(generateJsx());
|
||||||
act(() => {
|
act(() => {
|
||||||
screen.getByTestId(`order-type-${Schema.OrderType.TYPE_MARKET}`).click();
|
screen.getByTestId('order-type-Market').click();
|
||||||
});
|
});
|
||||||
// Assert last price is shown
|
// Assert last price is shown
|
||||||
expect(screen.getByTestId('last-price')).toHaveTextContent(
|
expect(screen.getByTestId('last-price')).toHaveTextContent(
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
`~${addDecimal(marketData.markPrice, market.decimalPlaces)} ${
|
`~${addDecimal(marketPrice, market.decimalPlaces)} ${
|
||||||
market.tradableInstrument.instrument.product.quoteName
|
market.tradableInstrument.instrument.product.quoteName
|
||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
@ -178,17 +175,12 @@ describe('DealTicket', () => {
|
|||||||
render(generateJsx());
|
render(generateJsx());
|
||||||
|
|
||||||
// Assert correct defaults are used from store
|
// Assert correct defaults are used from store
|
||||||
expect(
|
expect(screen.getByTestId('order-type-Limit').dataset.state).toEqual(
|
||||||
screen
|
'checked'
|
||||||
.getByTestId(`order-type-${Schema.OrderType.TYPE_LIMIT}`)
|
);
|
||||||
.querySelector('input')
|
expect(screen.getByTestId('order-side-SIDE_SELL').dataset.state).toEqual(
|
||||||
).toBeChecked();
|
'checked'
|
||||||
expect(
|
);
|
||||||
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
|
||||||
).toBeChecked();
|
|
||||||
expect(
|
|
||||||
screen.queryByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
|
||||||
).not.toBeChecked();
|
|
||||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
||||||
expectedOrder.size
|
expectedOrder.size
|
||||||
);
|
);
|
||||||
@ -221,17 +213,12 @@ describe('DealTicket', () => {
|
|||||||
render(generateJsx());
|
render(generateJsx());
|
||||||
|
|
||||||
// Assert correct defaults are used from store
|
// Assert correct defaults are used from store
|
||||||
expect(
|
expect(screen.getByTestId('order-type-Limit').dataset.state).toEqual(
|
||||||
screen
|
'checked'
|
||||||
.getByTestId(`order-type-${Schema.OrderType.TYPE_LIMIT}`)
|
);
|
||||||
.querySelector('input')
|
expect(screen.getByTestId('order-side-SIDE_SELL').dataset.state).toEqual(
|
||||||
).toBeChecked();
|
'checked'
|
||||||
expect(
|
);
|
||||||
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
|
||||||
).toBeChecked();
|
|
||||||
expect(
|
|
||||||
screen.queryByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
|
||||||
).not.toBeChecked();
|
|
||||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
||||||
expectedOrder.size
|
expectedOrder.size
|
||||||
);
|
);
|
||||||
@ -269,17 +256,12 @@ describe('DealTicket', () => {
|
|||||||
render(generateJsx());
|
render(generateJsx());
|
||||||
|
|
||||||
// Assert correct defaults are used from store
|
// Assert correct defaults are used from store
|
||||||
expect(
|
expect(screen.getByTestId('order-type-Limit').dataset.state).toEqual(
|
||||||
screen
|
'checked'
|
||||||
.getByTestId(`order-type-${Schema.OrderType.TYPE_LIMIT}`)
|
);
|
||||||
.querySelector('input')
|
expect(screen.getByTestId('order-side-SIDE_SELL').dataset.state).toEqual(
|
||||||
).toBeChecked();
|
'checked'
|
||||||
expect(
|
);
|
||||||
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
|
||||||
).toBeChecked();
|
|
||||||
expect(
|
|
||||||
screen.queryByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
|
||||||
).not.toBeChecked();
|
|
||||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
||||||
expectedOrder.size
|
expectedOrder.size
|
||||||
);
|
);
|
||||||
@ -322,17 +304,12 @@ describe('DealTicket', () => {
|
|||||||
render(generateJsx());
|
render(generateJsx());
|
||||||
|
|
||||||
// Assert correct defaults are used from store
|
// Assert correct defaults are used from store
|
||||||
expect(
|
expect(screen.getByTestId('order-type-Limit').dataset.state).toEqual(
|
||||||
screen
|
'checked'
|
||||||
.getByTestId(`order-type-${Schema.OrderType.TYPE_LIMIT}`)
|
);
|
||||||
.querySelector('input')
|
expect(screen.getByTestId('order-side-SIDE_SELL').dataset.state).toEqual(
|
||||||
).toBeChecked();
|
'checked'
|
||||||
expect(
|
);
|
||||||
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
|
||||||
).toBeChecked();
|
|
||||||
expect(
|
|
||||||
screen.queryByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
|
||||||
).not.toBeChecked();
|
|
||||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
||||||
expectedOrder.size
|
expectedOrder.size
|
||||||
);
|
);
|
||||||
@ -371,17 +348,12 @@ describe('DealTicket', () => {
|
|||||||
render(generateJsx());
|
render(generateJsx());
|
||||||
|
|
||||||
// Assert correct defaults are used from store
|
// Assert correct defaults are used from store
|
||||||
expect(
|
expect(screen.getByTestId('order-type-Limit').dataset.state).toEqual(
|
||||||
screen
|
'checked'
|
||||||
.getByTestId(`order-type-${Schema.OrderType.TYPE_LIMIT}`)
|
);
|
||||||
.querySelector('input')
|
expect(screen.getByTestId('order-side-SIDE_SELL').dataset.state).toEqual(
|
||||||
).toBeChecked();
|
'checked'
|
||||||
expect(
|
);
|
||||||
screen.queryByTestId('order-side-SIDE_SELL')?.querySelector('input')
|
|
||||||
).toBeChecked();
|
|
||||||
expect(
|
|
||||||
screen.queryByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
|
||||||
).not.toBeChecked();
|
|
||||||
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
expect(screen.getByTestId('order-size')).toHaveDisplayValue(
|
||||||
expectedOrder.size
|
expectedOrder.size
|
||||||
);
|
);
|
||||||
@ -402,7 +374,7 @@ describe('DealTicket', () => {
|
|||||||
render(generateJsx());
|
render(generateJsx());
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
screen.getByTestId(`order-type-${Schema.OrderType.TYPE_MARKET}`).click();
|
screen.getByTestId('order-type-Market').click();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Only FOK and IOC should be present for type market order
|
// Only FOK and IOC should be present for type market order
|
||||||
@ -427,7 +399,7 @@ describe('DealTicket', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Switch to type limit order -> all TIF options should be shown
|
// Switch to type limit order -> all TIF options should be shown
|
||||||
await userEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
await userEvent.click(screen.getByTestId('order-type-Limit'));
|
||||||
expect(screen.getByTestId('order-tif').children).toHaveLength(
|
expect(screen.getByTestId('order-tif').children).toHaveLength(
|
||||||
Object.keys(Schema.OrderTimeInForce).length
|
Object.keys(Schema.OrderTimeInForce).length
|
||||||
);
|
);
|
||||||
@ -447,7 +419,7 @@ describe('DealTicket', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Switch back to type market order -> FOK should be preserved from previous selection
|
// Switch back to type market order -> FOK should be preserved from previous selection
|
||||||
await userEvent.click(screen.getByTestId('order-type-TYPE_MARKET'));
|
await userEvent.click(screen.getByTestId('order-type-Market'));
|
||||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||||
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK
|
Schema.OrderTimeInForce.TIME_IN_FORCE_FOK
|
||||||
);
|
);
|
||||||
@ -462,7 +434,7 @@ describe('DealTicket', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Switch back type limit order -> GTT should be preserved
|
// Switch back type limit order -> GTT should be preserved
|
||||||
await userEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
await userEvent.click(screen.getByTestId('order-type-Limit'));
|
||||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||||
Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
|
Schema.OrderTimeInForce.TIME_IN_FORCE_GTT
|
||||||
);
|
);
|
||||||
@ -477,7 +449,7 @@ describe('DealTicket', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Switch to type market order -> IOC should be preserved
|
// Switch to type market order -> IOC should be preserved
|
||||||
await userEvent.click(screen.getByTestId('order-type-TYPE_MARKET'));
|
await userEvent.click(screen.getByTestId('order-type-Market'));
|
||||||
expect(screen.getByTestId('order-tif')).toHaveValue(
|
expect(screen.getByTestId('order-tif')).toHaveValue(
|
||||||
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
Schema.OrderTimeInForce.TIME_IN_FORCE_IOC
|
||||||
);
|
);
|
||||||
@ -487,9 +459,9 @@ describe('DealTicket', () => {
|
|||||||
render(generateJsx());
|
render(generateJsx());
|
||||||
|
|
||||||
// BUY is selected by default
|
// BUY is selected by default
|
||||||
expect(
|
expect(screen.getByTestId('order-side-SIDE_BUY').dataset.state).toEqual(
|
||||||
screen.getByTestId('order-side-SIDE_BUY')?.querySelector('input')
|
'checked'
|
||||||
).toBeChecked();
|
);
|
||||||
|
|
||||||
await userEvent.type(screen.getByTestId('order-size'), '200');
|
await userEvent.type(screen.getByTestId('order-size'), '200');
|
||||||
|
|
||||||
@ -504,7 +476,7 @@ describe('DealTicket', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Switch to limit order
|
// Switch to limit order
|
||||||
await userEvent.click(screen.getByTestId('order-type-TYPE_LIMIT'));
|
await userEvent.click(screen.getByTestId('order-type-Limit'));
|
||||||
|
|
||||||
// Check all TIF options shown
|
// Check all TIF options shown
|
||||||
expect(screen.getByTestId('order-tif').children).toHaveLength(
|
expect(screen.getByTestId('order-tif').children).toHaveLength(
|
||||||
|
@ -4,7 +4,10 @@ import { memo, useCallback, useEffect, useState, useRef, useMemo } from 'react';
|
|||||||
import { Controller } from 'react-hook-form';
|
import { Controller } from 'react-hook-form';
|
||||||
import { DealTicketAmount } from './deal-ticket-amount';
|
import { DealTicketAmount } from './deal-ticket-amount';
|
||||||
import { DealTicketButton } from './deal-ticket-button';
|
import { DealTicketButton } from './deal-ticket-button';
|
||||||
import { DealTicketFeeDetails } from './deal-ticket-fee-details';
|
import {
|
||||||
|
DealTicketFeeDetails,
|
||||||
|
DealTicketMarginDetails,
|
||||||
|
} from './deal-ticket-fee-details';
|
||||||
import { ExpirySelector } from './expiry-selector';
|
import { ExpirySelector } from './expiry-selector';
|
||||||
import { SideSelector } from './side-selector';
|
import { SideSelector } from './side-selector';
|
||||||
import { TimeInForceSelector } from './time-in-force-selector';
|
import { TimeInForceSelector } from './time-in-force-selector';
|
||||||
@ -30,8 +33,7 @@ import {
|
|||||||
} from '@vegaprotocol/positions';
|
} from '@vegaprotocol/positions';
|
||||||
import { toBigNum, removeDecimal } from '@vegaprotocol/utils';
|
import { toBigNum, removeDecimal } from '@vegaprotocol/utils';
|
||||||
import { activeOrdersProvider } from '@vegaprotocol/orders';
|
import { activeOrdersProvider } from '@vegaprotocol/orders';
|
||||||
import { useEstimateFees } from '../../hooks/use-estimate-fees';
|
import { getDerivedPrice } from '@vegaprotocol/markets';
|
||||||
import { getDerivedPrice } from '../../utils/get-price';
|
|
||||||
import type { OrderInfo } from '@vegaprotocol/types';
|
import type { OrderInfo } from '@vegaprotocol/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -43,7 +45,11 @@ import {
|
|||||||
} from '../../utils';
|
} from '../../utils';
|
||||||
import { ZeroBalanceError } from '../deal-ticket-validation/zero-balance-error';
|
import { ZeroBalanceError } from '../deal-ticket-validation/zero-balance-error';
|
||||||
import { SummaryValidationType } from '../../constants';
|
import { SummaryValidationType } from '../../constants';
|
||||||
import type { Market, MarketData } from '@vegaprotocol/markets';
|
import type {
|
||||||
|
Market,
|
||||||
|
MarketData,
|
||||||
|
StaticMarketData,
|
||||||
|
} from '@vegaprotocol/markets';
|
||||||
import { MarginWarning } from '../deal-ticket-validation/margin-warning';
|
import { MarginWarning } from '../deal-ticket-validation/margin-warning';
|
||||||
import {
|
import {
|
||||||
useMarketAccountBalance,
|
useMarketAccountBalance,
|
||||||
@ -53,26 +59,59 @@ import {
|
|||||||
import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
|
import { OrderTimeInForce, OrderType } from '@vegaprotocol/types';
|
||||||
import { useOrderForm } from '../../hooks/use-order-form';
|
import { useOrderForm } from '../../hooks/use-order-form';
|
||||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||||
|
import {
|
||||||
|
DealTicketType,
|
||||||
|
useDealTicketTypeStore,
|
||||||
|
} from '../../hooks/use-type-store';
|
||||||
|
import { useStopOrderFormValues } from '../../hooks/use-stop-order-form-values';
|
||||||
import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg';
|
import { DealTicketSizeIceberg } from './deal-ticket-size-iceberg';
|
||||||
|
import noop from 'lodash/noop';
|
||||||
|
|
||||||
|
export const REDUCE_ONLY_TOOLTIP =
|
||||||
|
'"Reduce only" will ensure that this order will not increase the size of an open position. When the order is matched, it will only trade enough volume to bring your open volume towards 0 but never change the direction of your position. If applied to a limit order that is not instantly filled, the order will be stopped.';
|
||||||
|
|
||||||
export interface DealTicketProps {
|
export interface DealTicketProps {
|
||||||
market: Market;
|
market: Market;
|
||||||
marketData: MarketData;
|
marketData: StaticMarketData;
|
||||||
|
marketPrice?: string | null;
|
||||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||||
submit: (order: OrderSubmission) => void;
|
submit: (order: OrderSubmission) => void;
|
||||||
onClickCollateral?: () => void;
|
onClickCollateral?: () => void;
|
||||||
onDeposit: (assetId: string) => void;
|
onDeposit: (assetId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useNotionalSize = (
|
||||||
|
price: string | null | undefined,
|
||||||
|
size: string | undefined,
|
||||||
|
decimalPlaces: number,
|
||||||
|
positionDecimalPlaces: number
|
||||||
|
) =>
|
||||||
|
useMemo(() => {
|
||||||
|
if (price && size) {
|
||||||
|
return removeDecimal(
|
||||||
|
toBigNum(size, positionDecimalPlaces).multipliedBy(
|
||||||
|
toBigNum(price, decimalPlaces)
|
||||||
|
),
|
||||||
|
decimalPlaces
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [price, size, decimalPlaces, positionDecimalPlaces]);
|
||||||
|
|
||||||
export const DealTicket = ({
|
export const DealTicket = ({
|
||||||
market,
|
market,
|
||||||
onMarketClick,
|
onMarketClick,
|
||||||
marketData,
|
marketData,
|
||||||
|
marketPrice,
|
||||||
submit,
|
submit,
|
||||||
onClickCollateral,
|
onClickCollateral,
|
||||||
onDeposit,
|
onDeposit,
|
||||||
}: DealTicketProps) => {
|
}: DealTicketProps) => {
|
||||||
const { pubKey, isReadOnly } = useVegaWallet();
|
const { pubKey, isReadOnly } = useVegaWallet();
|
||||||
|
const setDealTicketType = useDealTicketTypeStore((state) => state.set);
|
||||||
|
const updateStopOrderFormValues = useStopOrderFormValues(
|
||||||
|
(state) => state.update
|
||||||
|
);
|
||||||
// store last used tif for market so that when changing OrderType the previous TIF
|
// store last used tif for market so that when changing OrderType the previous TIF
|
||||||
// selection for that type is used when switching back
|
// selection for that type is used when switching back
|
||||||
|
|
||||||
@ -95,11 +134,15 @@ export const DealTicket = ({
|
|||||||
|
|
||||||
const asset = market.tradableInstrument.instrument.product.settlementAsset;
|
const asset = market.tradableInstrument.instrument.product.settlementAsset;
|
||||||
|
|
||||||
const { accountBalance: marginAccountBalance } = useMarketAccountBalance(
|
const {
|
||||||
market.id
|
accountBalance: marginAccountBalance,
|
||||||
);
|
loading: loadingMarginAccountBalance,
|
||||||
|
} = useMarketAccountBalance(market.id);
|
||||||
|
|
||||||
const { accountBalance: generalAccountBalance } = useAccountBalance(asset.id);
|
const {
|
||||||
|
accountBalance: generalAccountBalance,
|
||||||
|
loading: loadingGeneralAccountBalance,
|
||||||
|
} = useAccountBalance(asset.id);
|
||||||
|
|
||||||
const balance = (
|
const balance = (
|
||||||
BigInt(marginAccountBalance) + BigInt(generalAccountBalance)
|
BigInt(marginAccountBalance) + BigInt(generalAccountBalance)
|
||||||
@ -116,30 +159,20 @@ export const DealTicket = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const price = useMemo(() => {
|
const price = useMemo(() => {
|
||||||
return normalizedOrder && getDerivedPrice(normalizedOrder, marketData);
|
return (
|
||||||
}, [normalizedOrder, marketData]);
|
normalizedOrder &&
|
||||||
|
marketPrice &&
|
||||||
|
getDerivedPrice(normalizedOrder, marketPrice)
|
||||||
|
);
|
||||||
|
}, [normalizedOrder, marketPrice]);
|
||||||
|
|
||||||
const notionalSize = useMemo(() => {
|
const notionalSize = useNotionalSize(
|
||||||
if (price && normalizedOrder?.size) {
|
|
||||||
return removeDecimal(
|
|
||||||
toBigNum(
|
|
||||||
normalizedOrder.size,
|
|
||||||
market.positionDecimalPlaces
|
|
||||||
).multipliedBy(toBigNum(price, market.decimalPlaces)),
|
|
||||||
market.decimalPlaces
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, [
|
|
||||||
price,
|
price,
|
||||||
normalizedOrder?.size,
|
normalizedOrder?.size,
|
||||||
market.decimalPlaces,
|
market.decimalPlaces,
|
||||||
market.positionDecimalPlaces,
|
market.positionDecimalPlaces
|
||||||
]);
|
|
||||||
|
|
||||||
const feeEstimate = useEstimateFees(
|
|
||||||
normalizedOrder && { ...normalizedOrder, price }
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: activeOrders } = useDataProvider({
|
const { data: activeOrders } = useDataProvider({
|
||||||
dataProvider: activeOrdersProvider,
|
dataProvider: activeOrdersProvider,
|
||||||
variables: { partyId: pubKey || '', marketId: market.id },
|
variables: { partyId: pubKey || '', marketId: market.id },
|
||||||
@ -197,7 +230,10 @@ export const DealTicket = ({
|
|||||||
|
|
||||||
const hasNoBalance =
|
const hasNoBalance =
|
||||||
!BigInt(generalAccountBalance) && !BigInt(marginAccountBalance);
|
!BigInt(generalAccountBalance) && !BigInt(marginAccountBalance);
|
||||||
if (hasNoBalance) {
|
if (
|
||||||
|
hasNoBalance &&
|
||||||
|
!(loadingMarginAccountBalance || loadingGeneralAccountBalance)
|
||||||
|
) {
|
||||||
setError('summary', {
|
setError('summary', {
|
||||||
message: SummaryValidationType.NoCollateral,
|
message: SummaryValidationType.NoCollateral,
|
||||||
type: SummaryValidationType.NoCollateral,
|
type: SummaryValidationType.NoCollateral,
|
||||||
@ -221,6 +257,8 @@ export const DealTicket = ({
|
|||||||
marketTradingMode,
|
marketTradingMode,
|
||||||
generalAccountBalance,
|
generalAccountBalance,
|
||||||
marginAccountBalance,
|
marginAccountBalance,
|
||||||
|
loadingMarginAccountBalance,
|
||||||
|
loadingGeneralAccountBalance,
|
||||||
pubKey,
|
pubKey,
|
||||||
setError,
|
setError,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
@ -265,11 +303,13 @@ export const DealTicket = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// if an order doesn't exist one will be created by the store immediately
|
// if an order doesn't exist one will be created by the store immediately
|
||||||
if (!order || !normalizedOrder) return null;
|
if (!order || !normalizedOrder) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={isReadOnly ? undefined : handleSubmit(onSubmit)}
|
onSubmit={isReadOnly ? noop : handleSubmit(onSubmit)}
|
||||||
noValidate
|
noValidate
|
||||||
data-testid="deal-ticket-form"
|
data-testid="deal-ticket-form"
|
||||||
>
|
>
|
||||||
@ -284,9 +324,29 @@ export const DealTicket = ({
|
|||||||
}}
|
}}
|
||||||
render={() => (
|
render={() => (
|
||||||
<TypeSelector
|
<TypeSelector
|
||||||
value={order.type}
|
value={
|
||||||
onSelect={(type) => {
|
order.type === OrderType.TYPE_LIMIT
|
||||||
if (type === OrderType.TYPE_NETWORK) return;
|
? DealTicketType.Limit
|
||||||
|
: DealTicketType.Market
|
||||||
|
}
|
||||||
|
onValueChange={(dealTicketType) => {
|
||||||
|
setDealTicketType(market.id, dealTicketType);
|
||||||
|
if (
|
||||||
|
dealTicketType !== DealTicketType.Limit &&
|
||||||
|
dealTicketType !== DealTicketType.Market
|
||||||
|
) {
|
||||||
|
updateStopOrderFormValues(market.id, {
|
||||||
|
type:
|
||||||
|
dealTicketType === DealTicketType.StopLimit
|
||||||
|
? OrderType.TYPE_LIMIT
|
||||||
|
: OrderType.TYPE_MARKET,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const type =
|
||||||
|
dealTicketType === DealTicketType.Limit
|
||||||
|
? OrderType.TYPE_LIMIT
|
||||||
|
: OrderType.TYPE_MARKET;
|
||||||
update({
|
update({
|
||||||
type,
|
type,
|
||||||
// when changing type also update the TIF to what was last used of new type
|
// when changing type also update the TIF to what was last used of new type
|
||||||
@ -333,7 +393,7 @@ export const DealTicket = ({
|
|||||||
render={() => (
|
render={() => (
|
||||||
<SideSelector
|
<SideSelector
|
||||||
value={order.side}
|
value={order.side}
|
||||||
onSelect={(side) => {
|
onValueChange={(side) => {
|
||||||
update({ side });
|
update({ side });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -344,6 +404,7 @@ export const DealTicket = ({
|
|||||||
orderType={order.type}
|
orderType={order.type}
|
||||||
market={market}
|
market={market}
|
||||||
marketData={marketData}
|
marketData={marketData}
|
||||||
|
marketPrice={marketPrice || undefined}
|
||||||
sizeError={errors.size?.message}
|
sizeError={errors.size?.message}
|
||||||
priceError={errors.price?.message}
|
priceError={errors.price?.message}
|
||||||
update={update}
|
update={update}
|
||||||
@ -467,9 +528,7 @@ export const DealTicket = ({
|
|||||||
? t(
|
? t(
|
||||||
'"Reduce only" can be used only with non-persistent orders, such as "Fill or Kill" or "Immediate or Cancel".'
|
'"Reduce only" can be used only with non-persistent orders, such as "Fill or Kill" or "Immediate or Cancel".'
|
||||||
)
|
)
|
||||||
: t(
|
: t(REDUCE_ONLY_TOOLTIP)}
|
||||||
'"Reduce only" will ensure that this order will not increase the size of an open position. When the order is matched, it will only trade enough volume to bring your open volume towards 0 but never change the direction of your position. If applied to a limit order that is not instantly filled, the order will be stopped.'
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@ -541,10 +600,16 @@ export const DealTicket = ({
|
|||||||
/>
|
/>
|
||||||
<DealTicketButton side={order.side} />
|
<DealTicketButton side={order.side} />
|
||||||
<DealTicketFeeDetails
|
<DealTicketFeeDetails
|
||||||
onMarketClick={onMarketClick}
|
order={
|
||||||
feeEstimate={feeEstimate}
|
normalizedOrder && { ...normalizedOrder, price: price || undefined }
|
||||||
|
}
|
||||||
notionalSize={notionalSize}
|
notionalSize={notionalSize}
|
||||||
assetSymbol={assetSymbol}
|
assetSymbol={assetSymbol}
|
||||||
|
market={market}
|
||||||
|
/>
|
||||||
|
<DealTicketMarginDetails
|
||||||
|
onMarketClick={onMarketClick}
|
||||||
|
assetSymbol={assetSymbol}
|
||||||
marginAccountBalance={marginAccountBalance}
|
marginAccountBalance={marginAccountBalance}
|
||||||
generalAccountBalance={generalAccountBalance}
|
generalAccountBalance={generalAccountBalance}
|
||||||
positionEstimate={positionEstimate?.estimatePosition}
|
positionEstimate={positionEstimate?.estimatePosition}
|
||||||
@ -569,6 +634,55 @@ interface SummaryMessageProps {
|
|||||||
onClickCollateral?: () => void;
|
onClickCollateral?: () => void;
|
||||||
onDeposit: (assetId: string) => void;
|
onDeposit: (assetId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const NoWalletWarning = ({
|
||||||
|
isReadOnly,
|
||||||
|
pubKey,
|
||||||
|
asset,
|
||||||
|
}: Pick<SummaryMessageProps, 'isReadOnly' | 'pubKey' | 'asset'>) => {
|
||||||
|
const assetSymbol = asset.symbol;
|
||||||
|
const openVegaWalletDialog = useVegaWalletDialogStore(
|
||||||
|
(store) => store.openVegaWalletDialog
|
||||||
|
);
|
||||||
|
if (isReadOnly) {
|
||||||
|
return (
|
||||||
|
<div className="mb-2">
|
||||||
|
<InputError testId="deal-ticket-error-message-summary">
|
||||||
|
{
|
||||||
|
'You need to connect your own wallet to start trading on this market'
|
||||||
|
}
|
||||||
|
</InputError>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!pubKey) {
|
||||||
|
return (
|
||||||
|
<div className="mb-2">
|
||||||
|
<Notification
|
||||||
|
testId={'deal-ticket-connect-wallet'}
|
||||||
|
intent={Intent.Warning}
|
||||||
|
message={
|
||||||
|
<p className="text-sm pb-2">
|
||||||
|
You need a{' '}
|
||||||
|
<ExternalLink href="https://vega.xyz/wallet">
|
||||||
|
Vega wallet
|
||||||
|
</ExternalLink>{' '}
|
||||||
|
with {assetSymbol} to start trading in this market.
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
buttonProps={{
|
||||||
|
text: t('Connect wallet'),
|
||||||
|
action: openVegaWalletDialog,
|
||||||
|
dataTestId: 'order-connect-wallet',
|
||||||
|
size: 'small',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const SummaryMessage = memo(
|
const SummaryMessage = memo(
|
||||||
({
|
({
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@ -583,46 +697,16 @@ const SummaryMessage = memo(
|
|||||||
}: SummaryMessageProps) => {
|
}: SummaryMessageProps) => {
|
||||||
// Specific error UI for if balance is so we can
|
// Specific error UI for if balance is so we can
|
||||||
// render a deposit dialog
|
// render a deposit dialog
|
||||||
const assetSymbol = asset.symbol;
|
if (isReadOnly || !pubKey) {
|
||||||
const openVegaWalletDialog = useVegaWalletDialogStore(
|
|
||||||
(store) => store.openVegaWalletDialog
|
|
||||||
);
|
|
||||||
if (isReadOnly) {
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<NoWalletWarning
|
||||||
<InputError testId="deal-ticket-error-message-summary">
|
isReadOnly={isReadOnly}
|
||||||
{
|
asset={asset}
|
||||||
'You need to connect your own wallet to start trading on this market'
|
pubKey={pubKey}
|
||||||
}
|
/>
|
||||||
</InputError>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!pubKey) {
|
|
||||||
return (
|
|
||||||
<div className="mb-2">
|
|
||||||
<Notification
|
|
||||||
testId={'deal-ticket-connect-wallet'}
|
|
||||||
intent={Intent.Warning}
|
|
||||||
message={
|
|
||||||
<p className="text-sm pb-2">
|
|
||||||
You need a{' '}
|
|
||||||
<ExternalLink href="https://vega.xyz/wallet">
|
|
||||||
Vega wallet
|
|
||||||
</ExternalLink>{' '}
|
|
||||||
with {assetSymbol} to start trading in this market.
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
buttonProps={{
|
|
||||||
text: t('Connect wallet'),
|
|
||||||
action: openVegaWalletDialog,
|
|
||||||
dataTestId: 'order-connect-wallet',
|
|
||||||
size: 'small',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errorMessage === SummaryValidationType.NoCollateral) {
|
if (errorMessage === SummaryValidationType.NoCollateral) {
|
||||||
return (
|
return (
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit';
|
import { FormGroup, Input, InputError } from '@vegaprotocol/ui-toolkit';
|
||||||
import { formatForInput } from '@vegaprotocol/utils';
|
import { formatForInput } from '@vegaprotocol/utils';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
interface ExpirySelectorProps {
|
interface ExpirySelectorProps {
|
||||||
value?: string;
|
value?: string;
|
||||||
@ -13,7 +14,8 @@ export const ExpirySelector = ({
|
|||||||
onSelect,
|
onSelect,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
}: ExpirySelectorProps) => {
|
}: ExpirySelectorProps) => {
|
||||||
const date = value ? new Date(value) : new Date();
|
const now = useRef(new Date());
|
||||||
|
const date = value ? new Date(value) : now.current;
|
||||||
const dateFormatted = formatForInput(date);
|
const dateFormatted = formatForInput(date);
|
||||||
const minDate = formatForInput(date);
|
const minDate = formatForInput(date);
|
||||||
return (
|
return (
|
||||||
|
@ -3,6 +3,8 @@ export * from './deal-ticket-container';
|
|||||||
export * from './deal-ticket-limit-amount';
|
export * from './deal-ticket-limit-amount';
|
||||||
export * from './deal-ticket-market-amount';
|
export * from './deal-ticket-market-amount';
|
||||||
export * from './deal-ticket';
|
export * from './deal-ticket';
|
||||||
|
export * from './deal-ticket-stop-order';
|
||||||
|
export * from './deal-ticket-container';
|
||||||
export * from './expiry-selector';
|
export * from './expiry-selector';
|
||||||
export * from './side-selector';
|
export * from './side-selector';
|
||||||
export * from './time-in-force-selector';
|
export * from './time-in-force-selector';
|
||||||
|
@ -1,46 +1,41 @@
|
|||||||
import { FormGroup } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { Toggle } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
|
import * as RadioGroup from '@radix-ui/react-radio-group';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
interface SideSelectorProps {
|
interface SideSelectorProps {
|
||||||
value: Schema.Side;
|
value: Schema.Side;
|
||||||
onSelect: (side: Schema.Side) => void;
|
onValueChange: (side: Schema.Side) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SideSelector = ({ value, onSelect }: SideSelectorProps) => {
|
const toggles = [
|
||||||
const toggles = [
|
{ label: t('Long'), value: Schema.Side.SIDE_BUY },
|
||||||
{ label: t('Long'), value: Schema.Side.SIDE_BUY },
|
{ label: t('Short'), value: Schema.Side.SIDE_SELL },
|
||||||
{ label: t('Short'), value: Schema.Side.SIDE_SELL },
|
];
|
||||||
];
|
|
||||||
|
|
||||||
const toggleType = (e: Schema.Side) => {
|
|
||||||
switch (e) {
|
|
||||||
case Schema.Side.SIDE_BUY:
|
|
||||||
return 'buy';
|
|
||||||
case Schema.Side.SIDE_SELL:
|
|
||||||
return 'sell';
|
|
||||||
default:
|
|
||||||
return 'primary';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
export const SideSelector = (props: SideSelectorProps) => {
|
||||||
return (
|
return (
|
||||||
<FormGroup
|
<RadioGroup.Root
|
||||||
label={t('Direction')}
|
name="order-side"
|
||||||
labelFor="order-side-toggle"
|
className="mb-2 flex h-10 leading-10"
|
||||||
compact={true}
|
{...props}
|
||||||
>
|
>
|
||||||
<Toggle
|
{toggles.map(({ label, value }) => (
|
||||||
id="order-side-toggle"
|
<RadioGroup.Item value={value} key={value} id={`side-${value}`} asChild>
|
||||||
name="order-side"
|
<button
|
||||||
toggles={toggles}
|
className="flex-1 relative font-alpha text-sm"
|
||||||
checkedValue={value}
|
data-testid={`order-side-${value}`}
|
||||||
type={toggleType(value)}
|
>
|
||||||
onChange={(e) => {
|
{label}
|
||||||
onSelect(e.target.value as Schema.Side);
|
<RadioGroup.Indicator
|
||||||
}}
|
className={classNames('absolute bottom-0 left-0 right-0 h-0.5', {
|
||||||
/>
|
'bg-market-red': props.value === Schema.Side.SIDE_SELL,
|
||||||
</FormGroup>
|
'bg-market-green-550': props.value === Schema.Side.SIDE_BUY,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</RadioGroup.Item>
|
||||||
|
))}
|
||||||
|
</RadioGroup.Root>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,32 +1,127 @@
|
|||||||
import {
|
import {
|
||||||
FormGroup,
|
Icon,
|
||||||
InputError,
|
InputError,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
TradingDropdown,
|
||||||
|
TradingDropdownContent,
|
||||||
|
TradingDropdownItemIndicator,
|
||||||
|
TradingDropdownPortal,
|
||||||
|
TradingDropdownRadioGroup,
|
||||||
|
TradingDropdownRadioItem,
|
||||||
|
TradingDropdownTrigger,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import * as Schema from '@vegaprotocol/types';
|
import type { Market, StaticMarketData } from '@vegaprotocol/markets';
|
||||||
import { Toggle } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import type { Market, MarketData } from '@vegaprotocol/markets';
|
|
||||||
import { compileGridData } from '../trading-mode-tooltip';
|
import { compileGridData } from '../trading-mode-tooltip';
|
||||||
import { MarketModeValidationType } from '../../constants';
|
import { MarketModeValidationType } from '../../constants';
|
||||||
|
import { DealTicketType } from '../../hooks/use-type-store';
|
||||||
|
import * as RadioGroup from '@radix-ui/react-radio-group';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { FLAGS } from '@vegaprotocol/environment';
|
||||||
|
|
||||||
interface TypeSelectorProps {
|
interface TypeSelectorProps {
|
||||||
value: Schema.OrderType;
|
value: DealTicketType;
|
||||||
onSelect: (type: Schema.OrderType) => void;
|
onValueChange: (type: DealTicketType) => void;
|
||||||
market: Market;
|
market: Market;
|
||||||
marketData: MarketData;
|
marketData: StaticMarketData;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggles = [
|
const toggles = [
|
||||||
{ label: t('Limit'), value: Schema.OrderType.TYPE_LIMIT },
|
{ label: t('Limit'), value: DealTicketType.Limit },
|
||||||
{ label: t('Market'), value: Schema.OrderType.TYPE_MARKET },
|
{ label: t('Market'), value: DealTicketType.Market },
|
||||||
];
|
];
|
||||||
|
const options = [
|
||||||
|
{ label: t('Stop Limit'), value: DealTicketType.StopLimit },
|
||||||
|
{ label: t('Stop Market'), value: DealTicketType.StopMarket },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TypeToggle = ({
|
||||||
|
value,
|
||||||
|
onValueChange,
|
||||||
|
}: Pick<TypeSelectorProps, 'onValueChange' | 'value'>) => {
|
||||||
|
const selectedOption = options.find((t) => t.value === value);
|
||||||
|
return (
|
||||||
|
<RadioGroup.Root
|
||||||
|
name="order-type"
|
||||||
|
className={classNames('mb-2 grid h-8 leading-8 font-alpha text-sm', {
|
||||||
|
'grid-cols-3': FLAGS.STOP_ORDERS,
|
||||||
|
'grid-cols-2': !FLAGS.STOP_ORDERS,
|
||||||
|
})}
|
||||||
|
value={value}
|
||||||
|
onValueChange={onValueChange}
|
||||||
|
>
|
||||||
|
{toggles.map(({ label, value: itemValue }) => (
|
||||||
|
<RadioGroup.Item
|
||||||
|
value={itemValue}
|
||||||
|
key={itemValue}
|
||||||
|
id={`order-type-${itemValue}`}
|
||||||
|
data-testid={`order-type-${itemValue}`}
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className={classNames('rounded', {
|
||||||
|
'bg-vega-clight-500 dark:bg-vega-cdark-500': value === itemValue,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
</RadioGroup.Item>
|
||||||
|
))}
|
||||||
|
{FLAGS.STOP_ORDERS && (
|
||||||
|
<TradingDropdown
|
||||||
|
trigger={
|
||||||
|
<TradingDropdownTrigger
|
||||||
|
data-testid="order-type-Stop"
|
||||||
|
className={classNames(
|
||||||
|
'rounded px-3 flex flex-nowrap items-center justify-center',
|
||||||
|
{
|
||||||
|
'bg-vega-clight-500 dark:bg-vega-cdark-500': selectedOption,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<button>
|
||||||
|
<span className="text-ellipsis whitespace-nowrap shrink overflow-hidden">
|
||||||
|
{t(selectedOption ? selectedOption.label : 'Stop')}
|
||||||
|
</span>
|
||||||
|
<Icon name="chevron-down" className="ml-1" />
|
||||||
|
</button>
|
||||||
|
</TradingDropdownTrigger>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TradingDropdownPortal>
|
||||||
|
<TradingDropdownContent>
|
||||||
|
<TradingDropdownRadioGroup
|
||||||
|
onValueChange={(value) =>
|
||||||
|
onValueChange(value as DealTicketType)
|
||||||
|
}
|
||||||
|
value={value}
|
||||||
|
>
|
||||||
|
{options.map(({ label, value: itemValue }) => (
|
||||||
|
<TradingDropdownRadioItem
|
||||||
|
key={itemValue}
|
||||||
|
value={itemValue}
|
||||||
|
textValue={itemValue}
|
||||||
|
id={`order-type-${itemValue}`}
|
||||||
|
data-testid={`order-type-${itemValue}`}
|
||||||
|
>
|
||||||
|
{t(label)}
|
||||||
|
<TradingDropdownItemIndicator />
|
||||||
|
</TradingDropdownRadioItem>
|
||||||
|
))}
|
||||||
|
</TradingDropdownRadioGroup>
|
||||||
|
</TradingDropdownContent>
|
||||||
|
</TradingDropdownPortal>
|
||||||
|
</TradingDropdown>
|
||||||
|
)}
|
||||||
|
</RadioGroup.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const TypeSelector = ({
|
export const TypeSelector = ({
|
||||||
value,
|
value,
|
||||||
onSelect,
|
onValueChange,
|
||||||
market,
|
market,
|
||||||
marketData,
|
marketData,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@ -74,19 +169,18 @@ export const TypeSelector = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup label={t('Order type')} labelFor="order-type" compact={true}>
|
<>
|
||||||
<Toggle
|
<TypeToggle
|
||||||
id="order-type"
|
onValueChange={(value) => {
|
||||||
name="order-type"
|
onValueChange(value as DealTicketType);
|
||||||
toggles={toggles}
|
}}
|
||||||
checkedValue={value}
|
value={value}
|
||||||
onChange={(e) => onSelect(e.target.value as Schema.OrderType)}
|
|
||||||
/>
|
/>
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<InputError testId="deal-ticket-error-message-type">
|
<InputError testId="deal-ticket-error-message-type">
|
||||||
{renderError(errorMessage as MarketModeValidationType)}
|
{renderError(errorMessage as MarketModeValidationType)}
|
||||||
</InputError>
|
</InputError>
|
||||||
)}
|
)}
|
||||||
</FormGroup>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
export * from './__generated__/EstimateOrder';
|
export * from './__generated__/EstimateOrder';
|
||||||
export * from './use-estimate-fees';
|
export * from './use-estimate-fees';
|
||||||
|
export * from './use-type-store';
|
||||||
|
export * from './use-stop-order-form-values';
|
||||||
|
62
libs/deal-ticket/src/hooks/use-stop-order-form-values.ts
Normal file
62
libs/deal-ticket/src/hooks/use-stop-order-form-values.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist, subscribeWithSelector } from 'zustand/middleware';
|
||||||
|
import type { OrderTimeInForce, Side, OrderType } from '@vegaprotocol/types';
|
||||||
|
import type * as Schema from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
export interface StopOrderFormValues {
|
||||||
|
side: Side;
|
||||||
|
|
||||||
|
triggerDirection: Schema.StopOrderTriggerDirection;
|
||||||
|
|
||||||
|
triggerType: 'price' | 'trailingPercentOffset';
|
||||||
|
triggerPrice: string;
|
||||||
|
triggerTrailingPercentOffset: string;
|
||||||
|
|
||||||
|
type: OrderType;
|
||||||
|
size: string;
|
||||||
|
timeInForce: OrderTimeInForce;
|
||||||
|
price?: string;
|
||||||
|
|
||||||
|
expire: boolean;
|
||||||
|
expiryStrategy?: Schema.StopOrderExpiryStrategy;
|
||||||
|
expiresAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type StopOrderFormValuesMap = {
|
||||||
|
[marketId: string]: Partial<StopOrderFormValues> | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Update = (
|
||||||
|
marketId: string,
|
||||||
|
formValues: Partial<StopOrderFormValues>,
|
||||||
|
persist?: boolean
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
interface Store {
|
||||||
|
formValues: StopOrderFormValuesMap;
|
||||||
|
update: Update;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useStopOrderFormValues = create<Store>()(
|
||||||
|
persist(
|
||||||
|
subscribeWithSelector((set) => ({
|
||||||
|
formValues: {},
|
||||||
|
update: (marketId, formValues, persist = true) => {
|
||||||
|
set((state) => {
|
||||||
|
return {
|
||||||
|
formValues: {
|
||||||
|
...state.formValues,
|
||||||
|
[marketId]: {
|
||||||
|
...state.formValues[marketId],
|
||||||
|
...formValues,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
name: 'vega_stop_order_store',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
28
libs/deal-ticket/src/hooks/use-type-store.ts
Normal file
28
libs/deal-ticket/src/hooks/use-type-store.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist, subscribeWithSelector } from 'zustand/middleware';
|
||||||
|
|
||||||
|
export enum DealTicketType {
|
||||||
|
Limit = 'Limit',
|
||||||
|
Market = 'Market',
|
||||||
|
StopLimit = 'StopLimit',
|
||||||
|
StopMarket = 'StopMarket',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDealTicketTypeStore = create<{
|
||||||
|
set: (marketId: string, type: DealTicketType) => void;
|
||||||
|
type: Record<string, DealTicketType>;
|
||||||
|
}>()(
|
||||||
|
persist(
|
||||||
|
subscribeWithSelector((set) => ({
|
||||||
|
type: {},
|
||||||
|
set: (marketId: string, type: DealTicketType) =>
|
||||||
|
set((state) => ({
|
||||||
|
...state,
|
||||||
|
type: { ...state.type, [marketId]: type },
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
name: 'deal_ticket_type',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
@ -1,4 +1,4 @@
|
|||||||
import type { Market, MarketData } from '@vegaprotocol/markets';
|
import type { Market, StaticMarketData } from '@vegaprotocol/markets';
|
||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
import type { PartialDeep } from 'type-fest';
|
import type { PartialDeep } from 'type-fest';
|
||||||
@ -85,33 +85,15 @@ export function generateMarket(override?: PartialDeep<Market>): Market {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function generateMarketData(
|
export function generateMarketData(
|
||||||
override?: PartialDeep<MarketData>
|
override?: PartialDeep<StaticMarketData>
|
||||||
): MarketData {
|
): StaticMarketData {
|
||||||
const defaultMarketData: MarketData = {
|
const defaultMarketData: StaticMarketData = {
|
||||||
__typename: 'MarketData',
|
|
||||||
market: {
|
|
||||||
id: 'market-id',
|
|
||||||
__typename: 'Market',
|
|
||||||
},
|
|
||||||
auctionEnd: '2022-06-21T17:18:43.484055236Z',
|
auctionEnd: '2022-06-21T17:18:43.484055236Z',
|
||||||
auctionStart: '2022-06-21T17:18:43.484055236Z',
|
auctionStart: '2022-06-21T17:18:43.484055236Z',
|
||||||
bestBidPrice: '0',
|
|
||||||
bestBidVolume: '0',
|
|
||||||
bestOfferPrice: '0',
|
|
||||||
bestOfferVolume: '0',
|
|
||||||
bestStaticBidPrice: '0',
|
|
||||||
bestStaticBidVolume: '0',
|
|
||||||
bestStaticOfferPrice: '0',
|
|
||||||
bestStaticOfferVolume: '0',
|
|
||||||
indicativePrice: '100',
|
indicativePrice: '100',
|
||||||
indicativeVolume: '10',
|
indicativeVolume: '10',
|
||||||
marketState: Schema.MarketState.STATE_ACTIVE,
|
marketState: Schema.MarketState.STATE_ACTIVE,
|
||||||
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
marketTradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||||
marketValueProxy: '',
|
|
||||||
markPrice: '200',
|
|
||||||
midPrice: '0',
|
|
||||||
openInterest: '',
|
|
||||||
staticMidPrice: '0',
|
|
||||||
suppliedStake: '1000',
|
suppliedStake: '1000',
|
||||||
targetStake: '1000000',
|
targetStake: '1000000',
|
||||||
trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_BATCH,
|
trigger: Schema.AuctionTrigger.AUCTION_TRIGGER_BATCH,
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
export * from './get-default-order';
|
export * from './get-default-order';
|
||||||
export * from './is-market-in-auction';
|
|
||||||
export * from './validate-expiration';
|
export * from './validate-expiration';
|
||||||
export * from './validate-market-state';
|
export * from './validate-market-state';
|
||||||
export * from './validate-market-trading-mode';
|
export * from './validate-market-trading-mode';
|
||||||
|
@ -0,0 +1,71 @@
|
|||||||
|
import type {
|
||||||
|
StopOrderSetup,
|
||||||
|
StopOrdersSubmission,
|
||||||
|
} from '@vegaprotocol/wallet';
|
||||||
|
import { normalizeOrderSubmission } from '@vegaprotocol/wallet';
|
||||||
|
import type { StopOrderFormValues } from '../hooks/use-stop-order-form-values';
|
||||||
|
import * as Schema from '@vegaprotocol/types';
|
||||||
|
import { removeDecimal, toNanoSeconds } from '@vegaprotocol/utils';
|
||||||
|
|
||||||
|
export const mapFormValuesToStopOrdersSubmission = (
|
||||||
|
data: StopOrderFormValues,
|
||||||
|
marketId: string,
|
||||||
|
decimalPlaces: number,
|
||||||
|
positionDecimalPlaces: number
|
||||||
|
): StopOrdersSubmission => {
|
||||||
|
const submission: StopOrdersSubmission = {};
|
||||||
|
const stopOrderSetup: StopOrderSetup = {
|
||||||
|
orderSubmission: normalizeOrderSubmission(
|
||||||
|
{
|
||||||
|
marketId,
|
||||||
|
type: data.type,
|
||||||
|
side: data.side,
|
||||||
|
size: data.size,
|
||||||
|
timeInForce: data.timeInForce,
|
||||||
|
price: data.price,
|
||||||
|
reduceOnly: true,
|
||||||
|
},
|
||||||
|
decimalPlaces,
|
||||||
|
positionDecimalPlaces
|
||||||
|
),
|
||||||
|
};
|
||||||
|
if (data.triggerType === 'price') {
|
||||||
|
stopOrderSetup.price = removeDecimal(data.triggerPrice, decimalPlaces);
|
||||||
|
} else if (data.triggerType === 'trailingPercentOffset') {
|
||||||
|
stopOrderSetup.trailingPercentOffset = (
|
||||||
|
Number(data.triggerTrailingPercentOffset) / 100
|
||||||
|
).toFixed(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.expire) {
|
||||||
|
stopOrderSetup.expiresAt = data.expiresAt && toNanoSeconds(data.expiresAt);
|
||||||
|
if (
|
||||||
|
data.expiryStrategy ===
|
||||||
|
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS
|
||||||
|
) {
|
||||||
|
stopOrderSetup.expiryStrategy =
|
||||||
|
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS;
|
||||||
|
} else if (
|
||||||
|
data.expiryStrategy ===
|
||||||
|
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
|
||||||
|
) {
|
||||||
|
stopOrderSetup.expiryStrategy =
|
||||||
|
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
data.triggerDirection ===
|
||||||
|
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE
|
||||||
|
) {
|
||||||
|
submission.risesAbove = stopOrderSetup;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
data.triggerDirection ===
|
||||||
|
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
|
||||||
|
) {
|
||||||
|
submission.fallsBelow = stopOrderSetup;
|
||||||
|
}
|
||||||
|
|
||||||
|
return submission;
|
||||||
|
};
|
@ -1,6 +1,6 @@
|
|||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
import { MarketModeValidationType } from '../constants';
|
import { MarketModeValidationType } from '../constants';
|
||||||
import { isMarketInAuction } from './is-market-in-auction';
|
import { isMarketInAuction } from '@vegaprotocol/markets';
|
||||||
|
|
||||||
export const validateTimeInForce = (
|
export const validateTimeInForce = (
|
||||||
marketTradingMode: Schema.MarketTradingMode,
|
marketTradingMode: Schema.MarketTradingMode,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
import { MarketModeValidationType } from '../constants';
|
import { MarketModeValidationType } from '../constants';
|
||||||
import { isMarketInAuction } from './is-market-in-auction';
|
import { isMarketInAuction } from '@vegaprotocol/markets';
|
||||||
|
|
||||||
export const validateType = (
|
export const validateType = (
|
||||||
marketTradingMode: Schema.MarketTradingMode,
|
marketTradingMode: Schema.MarketTradingMode,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
import { isMarketInAuction } from './is-market-in-auction';
|
import { isMarketInAuction } from './is-market-in-auction';
|
||||||
import type { MarketData } from '@vegaprotocol/markets';
|
import type { MarketData } from './market-data-provider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the market price based on market mode (auction or not auction)
|
* Get the market price based on market mode (auction or not auction)
|
||||||
@ -33,19 +33,16 @@ export const getDerivedPrice = (
|
|||||||
type: Schema.OrderType;
|
type: Schema.OrderType;
|
||||||
price?: string | undefined;
|
price?: string | undefined;
|
||||||
},
|
},
|
||||||
marketData: MarketData
|
marketPrice: string
|
||||||
) => {
|
) => {
|
||||||
// If order type is market we should use either the mark price
|
// If order type is market we should use either the mark price
|
||||||
// or the uncrossing price. If order type is limit use the price
|
// or the uncrossing price. If order type is limit use the price
|
||||||
// the user has input
|
// the user has input
|
||||||
|
|
||||||
// Use the market price if order is a market order
|
// Use the market price if order is a market order
|
||||||
let price;
|
const price =
|
||||||
if (order.type === Schema.OrderType.TYPE_LIMIT && order.price) {
|
order.type === Schema.OrderType.TYPE_LIMIT && order.price
|
||||||
price = order.price;
|
? order.price
|
||||||
} else {
|
: marketPrice;
|
||||||
price = getMarketPrice(marketData);
|
|
||||||
}
|
|
||||||
|
|
||||||
return price === '0' ? undefined : price;
|
return price === '0' ? undefined : price;
|
||||||
};
|
};
|
@ -6,6 +6,8 @@ export * from './hooks';
|
|||||||
export * from './market-utils';
|
export * from './market-utils';
|
||||||
export { marketCandlesProvider } from './market-candles-provider';
|
export { marketCandlesProvider } from './market-candles-provider';
|
||||||
export type { Candle } from './market-candles-provider';
|
export type { Candle } from './market-candles-provider';
|
||||||
|
export * from './get-price';
|
||||||
|
export * from './is-market-in-auction';
|
||||||
export * from './market-data-provider';
|
export * from './market-data-provider';
|
||||||
export * from './markets-candles-provider';
|
export * from './markets-candles-provider';
|
||||||
export * from './markets-data-provider';
|
export * from './markets-data-provider';
|
||||||
|
@ -15,6 +15,7 @@ import type {
|
|||||||
MarketDataUpdateFieldsFragment,
|
MarketDataUpdateFieldsFragment,
|
||||||
MarketDataQueryVariables,
|
MarketDataQueryVariables,
|
||||||
} from './__generated__/market-data';
|
} from './__generated__/market-data';
|
||||||
|
import { getMarketPrice } from './get-price';
|
||||||
|
|
||||||
export type MarketData = MarketDataFieldsFragment;
|
export type MarketData = MarketDataFieldsFragment;
|
||||||
|
|
||||||
@ -58,6 +59,21 @@ export const markPriceProvider = makeDerivedDataProvider<
|
|||||||
MarketDataQueryVariables
|
MarketDataQueryVariables
|
||||||
>([marketDataProvider], ([marketData]) => (marketData as MarketData).markPrice);
|
>([marketDataProvider], ([marketData]) => (marketData as MarketData).markPrice);
|
||||||
|
|
||||||
|
export const marketPriceProvider = makeDerivedDataProvider<
|
||||||
|
string | undefined,
|
||||||
|
never,
|
||||||
|
MarketDataQueryVariables
|
||||||
|
>([marketDataProvider], ([marketData]) =>
|
||||||
|
getMarketPrice(marketData as MarketData)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useMarketPrice = (marketId?: string, skip?: boolean) =>
|
||||||
|
useDataProvider({
|
||||||
|
dataProvider: marketPriceProvider,
|
||||||
|
variables: { marketId: marketId || '' },
|
||||||
|
skip: skip || !marketId,
|
||||||
|
});
|
||||||
|
|
||||||
export type StaticMarketData = Pick<
|
export type StaticMarketData = Pick<
|
||||||
MarketData,
|
MarketData,
|
||||||
| 'marketTradingMode'
|
| 'marketTradingMode'
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export * from './order-data-provider';
|
export * from './order-data-provider';
|
||||||
export * from './order-list';
|
export * from './order-list';
|
||||||
export * from './order-list-manager';
|
export * from './order-list-manager';
|
||||||
|
export * from './stop-orders-manager';
|
||||||
export * from './mocks/generate-orders';
|
export * from './mocks/generate-orders';
|
||||||
|
113
libs/orders/src/lib/components/mocks/generate-stop-orders.ts
Normal file
113
libs/orders/src/lib/components/mocks/generate-stop-orders.ts
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import merge from 'lodash/merge';
|
||||||
|
import * as Schema from '@vegaprotocol/types';
|
||||||
|
import type { StopOrder } from '../order-data-provider/stop-orders-data-provider';
|
||||||
|
import type { PartialDeep } from 'type-fest';
|
||||||
|
|
||||||
|
export const generateStopOrder = (
|
||||||
|
partialStopOrder?: PartialDeep<StopOrder>
|
||||||
|
) => {
|
||||||
|
const stopOrder: StopOrder = {
|
||||||
|
__typename: 'StopOrder',
|
||||||
|
id: 'stop-order-id',
|
||||||
|
marketId: 'market-id',
|
||||||
|
partyId: 'party-id',
|
||||||
|
triggerDirection:
|
||||||
|
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE,
|
||||||
|
trigger: {
|
||||||
|
trailingPercentOffset: '5',
|
||||||
|
},
|
||||||
|
ocoLinkId: undefined,
|
||||||
|
market: {
|
||||||
|
__typename: 'Market',
|
||||||
|
id: 'market-id',
|
||||||
|
decimalPlaces: 1,
|
||||||
|
fees: {
|
||||||
|
__typename: 'Fees',
|
||||||
|
factors: {
|
||||||
|
__typename: 'FeeFactors',
|
||||||
|
infrastructureFee: '0.1',
|
||||||
|
liquidityFee: '0.1',
|
||||||
|
makerFee: '0.1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
marketTimestamps: {
|
||||||
|
__typename: 'MarketTimestamps',
|
||||||
|
close: '',
|
||||||
|
open: '',
|
||||||
|
},
|
||||||
|
positionDecimalPlaces: 2,
|
||||||
|
state: Schema.MarketState.STATE_ACTIVE,
|
||||||
|
tradableInstrument: {
|
||||||
|
__typename: 'TradableInstrument',
|
||||||
|
instrument: {
|
||||||
|
__typename: 'Instrument',
|
||||||
|
code: 'XYZ',
|
||||||
|
id: 'XYZ',
|
||||||
|
metadata: {
|
||||||
|
__typename: 'InstrumentMetadata',
|
||||||
|
tags: ['xyz asset'],
|
||||||
|
},
|
||||||
|
name: 'XYZ intrument',
|
||||||
|
product: {
|
||||||
|
__typename: 'Future',
|
||||||
|
quoteName: '',
|
||||||
|
settlementAsset: {
|
||||||
|
__typename: 'Asset',
|
||||||
|
id: 'asset-id',
|
||||||
|
decimals: 1,
|
||||||
|
symbol: 'XYZ',
|
||||||
|
name: 'XYZ',
|
||||||
|
quantum: '1',
|
||||||
|
},
|
||||||
|
dataSourceSpecForTradingTermination: {
|
||||||
|
__typename: 'DataSourceSpec',
|
||||||
|
id: 'oracleId',
|
||||||
|
data: {
|
||||||
|
__typename: 'DataSourceDefinition',
|
||||||
|
sourceType: {
|
||||||
|
__typename: 'DataSourceDefinitionExternal',
|
||||||
|
sourceType: {
|
||||||
|
__typename: 'DataSourceSpecConfiguration',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dataSourceSpecForSettlementData: {
|
||||||
|
__typename: 'DataSourceSpec',
|
||||||
|
id: 'oracleId',
|
||||||
|
data: {
|
||||||
|
__typename: 'DataSourceDefinition',
|
||||||
|
sourceType: {
|
||||||
|
__typename: 'DataSourceDefinitionExternal',
|
||||||
|
sourceType: {
|
||||||
|
__typename: 'DataSourceSpecConfiguration',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dataSourceSpecBinding: {
|
||||||
|
__typename: 'DataSourceSpecToFutureBinding',
|
||||||
|
tradingTerminationProperty: 'trading-termination-property',
|
||||||
|
settlementDataProperty: 'settlement-data-property',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tradingMode: Schema.MarketTradingMode.TRADING_MODE_CONTINUOUS,
|
||||||
|
},
|
||||||
|
submission: {
|
||||||
|
marketId: 'market-id',
|
||||||
|
size: '10',
|
||||||
|
type: Schema.OrderType.TYPE_MARKET,
|
||||||
|
side: Schema.Side.SIDE_BUY,
|
||||||
|
price: '',
|
||||||
|
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||||
|
expiresAt: null,
|
||||||
|
},
|
||||||
|
status: Schema.StopOrderStatus.STATUS_PENDING,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: null,
|
||||||
|
expiresAt: null,
|
||||||
|
};
|
||||||
|
return merge(stopOrder, partialStopOrder);
|
||||||
|
};
|
@ -99,3 +99,54 @@ subscription OrdersUpdate($partyId: ID!, $marketIds: [ID!]) {
|
|||||||
...OrderUpdateFields
|
...OrderUpdateFields
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fragment OrderSubmissionFields on OrderSubmission {
|
||||||
|
marketId
|
||||||
|
price
|
||||||
|
size
|
||||||
|
side
|
||||||
|
timeInForce
|
||||||
|
expiresAt
|
||||||
|
type
|
||||||
|
reference
|
||||||
|
peggedOrder {
|
||||||
|
reference
|
||||||
|
offset
|
||||||
|
}
|
||||||
|
postOnly
|
||||||
|
reduceOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment StopOrderFields on StopOrder {
|
||||||
|
id
|
||||||
|
ocoLinkId
|
||||||
|
expiresAt
|
||||||
|
expiryStrategy
|
||||||
|
triggerDirection
|
||||||
|
status
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
partyId
|
||||||
|
marketId
|
||||||
|
trigger {
|
||||||
|
... on StopOrderPrice {
|
||||||
|
price
|
||||||
|
}
|
||||||
|
... on StopOrderTrailingPercentOffset {
|
||||||
|
trailingPercentOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
submission {
|
||||||
|
...OrderSubmissionFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query StopOrders($partyId: ID!) {
|
||||||
|
stopOrders(filter: { parties: [$partyId] }) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...StopOrderFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -32,6 +32,17 @@ export type OrdersUpdateSubscriptionVariables = Types.Exact<{
|
|||||||
|
|
||||||
export type OrdersUpdateSubscription = { __typename?: 'Subscription', orders?: Array<{ __typename?: 'OrderUpdate', id: string, marketId: 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, liquidityProvisionId?: string | null, peggedOrder?: { __typename: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null, icebergOrder?: { __typename: 'IcebergOrder', peakSize: string, minimumVisibleSize: string, reservedRemaining: string } | null }> | null };
|
export type OrdersUpdateSubscription = { __typename?: 'Subscription', orders?: Array<{ __typename?: 'OrderUpdate', id: string, marketId: 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, liquidityProvisionId?: string | null, peggedOrder?: { __typename: 'PeggedOrder', reference: Types.PeggedReference, offset: string } | null, icebergOrder?: { __typename: 'IcebergOrder', peakSize: string, minimumVisibleSize: string, reservedRemaining: string } | null }> | null };
|
||||||
|
|
||||||
|
export type OrderSubmissionFieldsFragment = { __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 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, trigger?: { __typename?: 'StopOrderPrice', price: string } | { __typename?: 'StopOrderTrailingPercentOffset', trailingPercentOffset: string } | null, 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'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type StopOrdersQuery = { __typename?: 'Query', stopOrders?: { __typename?: 'StopOrderConnection', edges?: Array<{ __typename?: 'StopOrderEdge', node?: { __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 } | null, 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 }> | null } | null };
|
||||||
|
|
||||||
export const OrderFieldsFragmentDoc = gql`
|
export const OrderFieldsFragmentDoc = gql`
|
||||||
fragment OrderFields on Order {
|
fragment OrderFields on Order {
|
||||||
id
|
id
|
||||||
@ -96,6 +107,49 @@ export const OrderUpdateFieldsFragmentDoc = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
export const OrderSubmissionFieldsFragmentDoc = gql`
|
||||||
|
fragment OrderSubmissionFields on OrderSubmission {
|
||||||
|
marketId
|
||||||
|
price
|
||||||
|
size
|
||||||
|
side
|
||||||
|
timeInForce
|
||||||
|
expiresAt
|
||||||
|
type
|
||||||
|
reference
|
||||||
|
peggedOrder {
|
||||||
|
reference
|
||||||
|
offset
|
||||||
|
}
|
||||||
|
postOnly
|
||||||
|
reduceOnly
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export const StopOrderFieldsFragmentDoc = gql`
|
||||||
|
fragment StopOrderFields on StopOrder {
|
||||||
|
id
|
||||||
|
ocoLinkId
|
||||||
|
expiresAt
|
||||||
|
expiryStrategy
|
||||||
|
triggerDirection
|
||||||
|
status
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
partyId
|
||||||
|
marketId
|
||||||
|
trigger {
|
||||||
|
... on StopOrderPrice {
|
||||||
|
price
|
||||||
|
}
|
||||||
|
... on StopOrderTrailingPercentOffset {
|
||||||
|
trailingPercentOffset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
submission {
|
||||||
|
...OrderSubmissionFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${OrderSubmissionFieldsFragmentDoc}`;
|
||||||
export const OrderByIdDocument = gql`
|
export const OrderByIdDocument = gql`
|
||||||
query OrderById($orderId: ID!) {
|
query OrderById($orderId: ID!) {
|
||||||
orderByID(id: $orderId) {
|
orderByID(id: $orderId) {
|
||||||
@ -216,4 +270,43 @@ export function useOrdersUpdateSubscription(baseOptions: Apollo.SubscriptionHook
|
|||||||
return Apollo.useSubscription<OrdersUpdateSubscription, OrdersUpdateSubscriptionVariables>(OrdersUpdateDocument, options);
|
return Apollo.useSubscription<OrdersUpdateSubscription, OrdersUpdateSubscriptionVariables>(OrdersUpdateDocument, options);
|
||||||
}
|
}
|
||||||
export type OrdersUpdateSubscriptionHookResult = ReturnType<typeof useOrdersUpdateSubscription>;
|
export type OrdersUpdateSubscriptionHookResult = ReturnType<typeof useOrdersUpdateSubscription>;
|
||||||
export type OrdersUpdateSubscriptionResult = Apollo.SubscriptionResult<OrdersUpdateSubscription>;
|
export type OrdersUpdateSubscriptionResult = Apollo.SubscriptionResult<OrdersUpdateSubscription>;
|
||||||
|
export const StopOrdersDocument = gql`
|
||||||
|
query StopOrders($partyId: ID!) {
|
||||||
|
stopOrders(filter: {parties: [$partyId]}) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...StopOrderFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${StopOrderFieldsFragmentDoc}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useStopOrdersQuery__
|
||||||
|
*
|
||||||
|
* To run a query within a React component, call `useStopOrdersQuery` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useStopOrdersQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||||
|
* you can use to render your UI.
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { data, loading, error } = useStopOrdersQuery({
|
||||||
|
* variables: {
|
||||||
|
* partyId: // value for 'partyId'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useStopOrdersQuery(baseOptions: Apollo.QueryHookOptions<StopOrdersQuery, StopOrdersQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useQuery<StopOrdersQuery, StopOrdersQueryVariables>(StopOrdersDocument, options);
|
||||||
|
}
|
||||||
|
export function useStopOrdersLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<StopOrdersQuery, StopOrdersQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useLazyQuery<StopOrdersQuery, StopOrdersQueryVariables>(StopOrdersDocument, options);
|
||||||
|
}
|
||||||
|
export type StopOrdersQueryHookResult = ReturnType<typeof useStopOrdersQuery>;
|
||||||
|
export type StopOrdersLazyQueryHookResult = ReturnType<typeof useStopOrdersLazyQuery>;
|
||||||
|
export type StopOrdersQueryResult = Apollo.QueryResult<StopOrdersQuery, StopOrdersQueryVariables>;
|
@ -0,0 +1,57 @@
|
|||||||
|
import {
|
||||||
|
makeDataProvider,
|
||||||
|
makeDerivedDataProvider,
|
||||||
|
} from '@vegaprotocol/data-provider';
|
||||||
|
import type { Market } from '@vegaprotocol/markets';
|
||||||
|
import { marketsMapProvider } from '@vegaprotocol/markets';
|
||||||
|
import type {
|
||||||
|
StopOrderFieldsFragment,
|
||||||
|
StopOrdersQuery,
|
||||||
|
StopOrdersQueryVariables,
|
||||||
|
} from './__generated__/Orders';
|
||||||
|
import { StopOrdersDocument } from './__generated__/Orders';
|
||||||
|
|
||||||
|
export type StopOrder = StopOrderFieldsFragment & {
|
||||||
|
market: Market;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getData = (
|
||||||
|
responseData: StopOrdersQuery | null
|
||||||
|
): StopOrderFieldsFragment[] =>
|
||||||
|
responseData?.stopOrders?.edges
|
||||||
|
?.map((edge) => edge.node)
|
||||||
|
.filter((node): node is StopOrderFieldsFragment => !!node) || [];
|
||||||
|
|
||||||
|
export const stopOrdersProvider = makeDataProvider<
|
||||||
|
StopOrdersQuery,
|
||||||
|
ReturnType<typeof getData>,
|
||||||
|
never,
|
||||||
|
never,
|
||||||
|
StopOrdersQueryVariables
|
||||||
|
>({
|
||||||
|
query: StopOrdersDocument,
|
||||||
|
getData,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const stopOrdersWithMarketProvider = makeDerivedDataProvider<
|
||||||
|
StopOrder[],
|
||||||
|
never,
|
||||||
|
StopOrdersQueryVariables
|
||||||
|
>(
|
||||||
|
[
|
||||||
|
stopOrdersProvider,
|
||||||
|
(callback, client) => marketsMapProvider(callback, client, undefined),
|
||||||
|
],
|
||||||
|
(partsData): StopOrder[] => {
|
||||||
|
return ((partsData[0] as ReturnType<typeof getData>) || []).map(
|
||||||
|
(stopOrder) => {
|
||||||
|
return {
|
||||||
|
...stopOrder,
|
||||||
|
market: (partsData[1] as Record<string, Market>)[
|
||||||
|
stopOrder.submission.marketId
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
@ -0,0 +1 @@
|
|||||||
|
export * from './stop-orders-manager';
|
@ -0,0 +1,43 @@
|
|||||||
|
import { render, screen, act } from '@testing-library/react';
|
||||||
|
import { StopOrdersManager } from './stop-orders-manager';
|
||||||
|
import * as useDataProviderHook from '@vegaprotocol/data-provider';
|
||||||
|
import type { StopOrder } from '../order-data-provider/stop-orders-data-provider';
|
||||||
|
import * as stopOrdersTableMock from '../stop-orders-table/stop-orders-table';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||||
|
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
|
||||||
|
// @ts-ignore StopOrdersTable is read only but we need to override with the forwardRef to
|
||||||
|
// avoid warnings about padding refs
|
||||||
|
stopOrdersTableMock.StopOrdersTable = forwardRef(() => (
|
||||||
|
<div>StopOrdersTable</div>
|
||||||
|
));
|
||||||
|
|
||||||
|
const generateJsx = () => {
|
||||||
|
const pubKey = '0x123';
|
||||||
|
return (
|
||||||
|
<MockedProvider>
|
||||||
|
<VegaWalletContext.Provider value={{ pubKey } as VegaWalletContextShape}>
|
||||||
|
<StopOrdersManager partyId={pubKey} isReadOnly={false} />
|
||||||
|
</VegaWalletContext.Provider>
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('StopOrdersManager', () => {
|
||||||
|
it('should render the stop orders table if data provided', async () => {
|
||||||
|
jest.spyOn(useDataProviderHook, 'useDataProvider').mockReturnValue({
|
||||||
|
data: [{ id: '1' } as StopOrder],
|
||||||
|
loading: false,
|
||||||
|
error: undefined,
|
||||||
|
flush: jest.fn(),
|
||||||
|
reload: jest.fn(),
|
||||||
|
load: jest.fn(),
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
render(generateJsx());
|
||||||
|
});
|
||||||
|
expect(await screen.findByText('StopOrdersTable')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,66 @@
|
|||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
import { StopOrdersTable } from '../stop-orders-table/stop-orders-table';
|
||||||
|
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
|
||||||
|
import { useVegaTransactionStore } from '@vegaprotocol/wallet';
|
||||||
|
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';
|
||||||
|
|
||||||
|
export interface StopOrdersManagerProps {
|
||||||
|
partyId: string;
|
||||||
|
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||||
|
isReadOnly: boolean;
|
||||||
|
gridProps?: ReturnType<typeof useDataGridEvents>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const POLLING_TIME = 2000;
|
||||||
|
|
||||||
|
export const StopOrdersManager = ({
|
||||||
|
partyId,
|
||||||
|
onMarketClick,
|
||||||
|
isReadOnly,
|
||||||
|
gridProps,
|
||||||
|
}: StopOrdersManagerProps) => {
|
||||||
|
const create = useVegaTransactionStore((state) => state.create);
|
||||||
|
const variables = { partyId };
|
||||||
|
|
||||||
|
const { data, error, reload } = useDataProvider({
|
||||||
|
dataProvider: stopOrdersWithMarketProvider,
|
||||||
|
variables,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
reload();
|
||||||
|
}, POLLING_TIME);
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, [reload]);
|
||||||
|
|
||||||
|
const cancel = useCallback(
|
||||||
|
(order: StopOrder) => {
|
||||||
|
if (!order.submission.marketId) return;
|
||||||
|
create({
|
||||||
|
stopOrdersCancellation: {
|
||||||
|
stopOrderId: order.id,
|
||||||
|
marketId: order.submission.marketId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[create]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StopOrdersTable
|
||||||
|
rowData={data}
|
||||||
|
onCancel={cancel}
|
||||||
|
onMarketClick={onMarketClick}
|
||||||
|
isReadOnly={isReadOnly}
|
||||||
|
suppressAutoSize
|
||||||
|
overlayNoRowsTemplate={error ? error.message : t('No stop orders')}
|
||||||
|
{...gridProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,237 @@
|
|||||||
|
import { act, render, screen } from '@testing-library/react';
|
||||||
|
import * as Schema from '@vegaprotocol/types';
|
||||||
|
import type { PartialDeep } from 'type-fest';
|
||||||
|
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||||
|
import { VegaWalletContext } from '@vegaprotocol/wallet';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import {
|
||||||
|
StopOrdersTable,
|
||||||
|
type StopOrdersTableProps,
|
||||||
|
} from './stop-orders-table';
|
||||||
|
import { generateStopOrder } from '../mocks/generate-stop-orders';
|
||||||
|
|
||||||
|
// Mock theme switcher to get around inconsistent mocking of zustand
|
||||||
|
// stores
|
||||||
|
jest.mock('@vegaprotocol/react-helpers', () => ({
|
||||||
|
...jest.requireActual('@vegaprotocol/react-helpers'),
|
||||||
|
useThemeSwitcher: () => ({
|
||||||
|
theme: 'light',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@vegaprotocol/utils', () => ({
|
||||||
|
...jest.requireActual('@vegaprotocol/utils'),
|
||||||
|
getDateTimeFormat: jest.fn(() => ({
|
||||||
|
format: (date: Date) => date.toISOString(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const defaultProps: StopOrdersTableProps = {
|
||||||
|
rowData: [],
|
||||||
|
onCancel: jest.fn(),
|
||||||
|
isReadOnly: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateJsx = (
|
||||||
|
props: Partial<StopOrdersTableProps> = defaultProps,
|
||||||
|
context: PartialDeep<VegaWalletContextShape> = { pubKey: '0x123' }
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<MockedProvider>
|
||||||
|
<VegaWalletContext.Provider value={context as VegaWalletContextShape}>
|
||||||
|
<StopOrdersTable {...defaultProps} {...props} />
|
||||||
|
</VegaWalletContext.Provider>
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rowData = [
|
||||||
|
generateStopOrder({
|
||||||
|
id: 'stop-order-1',
|
||||||
|
trigger: { price: '80', __typename: 'StopOrderPrice' },
|
||||||
|
triggerDirection:
|
||||||
|
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW,
|
||||||
|
expiresAt: '2023-08-26T08:31:22Z',
|
||||||
|
expiryStrategy: Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT,
|
||||||
|
submission: {
|
||||||
|
size: '100',
|
||||||
|
side: Schema.Side.SIDE_BUY,
|
||||||
|
type: Schema.OrderType.TYPE_LIMIT,
|
||||||
|
price: '120',
|
||||||
|
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||||
|
},
|
||||||
|
status: Schema.StopOrderStatus.STATUS_CANCELLED,
|
||||||
|
}),
|
||||||
|
generateStopOrder({
|
||||||
|
id: 'stop-order-2',
|
||||||
|
trigger: { price: '90', __typename: 'StopOrderPrice' },
|
||||||
|
triggerDirection:
|
||||||
|
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE,
|
||||||
|
expiresAt: '2023-08-26T08:31:22Z',
|
||||||
|
expiryStrategy: Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_CANCELS,
|
||||||
|
submission: {
|
||||||
|
size: '110',
|
||||||
|
side: Schema.Side.SIDE_SELL,
|
||||||
|
type: Schema.OrderType.TYPE_MARKET,
|
||||||
|
timeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_IOC,
|
||||||
|
},
|
||||||
|
status: Schema.StopOrderStatus.STATUS_EXPIRED,
|
||||||
|
}),
|
||||||
|
generateStopOrder({
|
||||||
|
id: 'stop-order-3',
|
||||||
|
trigger: {
|
||||||
|
trailingPercentOffset: '0.1',
|
||||||
|
__typename: 'StopOrderTrailingPercentOffset',
|
||||||
|
},
|
||||||
|
triggerDirection:
|
||||||
|
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW,
|
||||||
|
status: Schema.StopOrderStatus.STATUS_PENDING,
|
||||||
|
}),
|
||||||
|
generateStopOrder({
|
||||||
|
id: 'stop-order-4',
|
||||||
|
trigger: {
|
||||||
|
trailingPercentOffset: '0.2',
|
||||||
|
__typename: 'StopOrderTrailingPercentOffset',
|
||||||
|
},
|
||||||
|
triggerDirection:
|
||||||
|
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_RISES_ABOVE,
|
||||||
|
status: Schema.StopOrderStatus.STATUS_REJECTED,
|
||||||
|
}),
|
||||||
|
generateStopOrder({
|
||||||
|
id: 'stop-order-5',
|
||||||
|
status: Schema.StopOrderStatus.STATUS_STOPPED,
|
||||||
|
}),
|
||||||
|
generateStopOrder({
|
||||||
|
id: 'stop-order-6',
|
||||||
|
status: Schema.StopOrderStatus.STATUS_TRIGGERED,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('StopOrdersTable', () => {
|
||||||
|
it('should render correct columns', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(generateJsx({ rowData }));
|
||||||
|
});
|
||||||
|
const expectedHeaders = [
|
||||||
|
'Market',
|
||||||
|
'Trigger',
|
||||||
|
'Expires At',
|
||||||
|
'Size',
|
||||||
|
'Submission Type',
|
||||||
|
'Status',
|
||||||
|
'Submission Price',
|
||||||
|
'Submission Time In Force',
|
||||||
|
'Updated At',
|
||||||
|
'', // no cell header for edit/cancel
|
||||||
|
];
|
||||||
|
const headers = screen.getAllByRole('columnheader');
|
||||||
|
expect(headers).toHaveLength(expectedHeaders.length);
|
||||||
|
expect(headers.map((h) => h.textContent?.trim())).toEqual(expectedHeaders);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats trigger column', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(generateJsx({ rowData }));
|
||||||
|
});
|
||||||
|
const grid = screen.getByRole('treegrid');
|
||||||
|
const cells = grid.querySelectorAll('.ag-body [col-id="trigger"]');
|
||||||
|
|
||||||
|
const expectedValues: string[] = [
|
||||||
|
'Mark < 8.0',
|
||||||
|
'Mark > 9.0',
|
||||||
|
'Mark +10.0%',
|
||||||
|
'Mark -20.0%',
|
||||||
|
];
|
||||||
|
expectedValues.forEach((expectedValue, i) =>
|
||||||
|
expect(cells[i]).toHaveTextContent(expectedValue)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats expires at column', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(generateJsx({ rowData }));
|
||||||
|
});
|
||||||
|
const grid = screen.getByRole('treegrid');
|
||||||
|
const cells = grid.querySelectorAll('.ag-body [col-id="expiresAt"]');
|
||||||
|
|
||||||
|
const expectedValues: string[] = [
|
||||||
|
'Submit 2023-08-26T08:31:22.000Z',
|
||||||
|
'Cancels 2023-08-26T08:31:22.000Z',
|
||||||
|
];
|
||||||
|
expectedValues.forEach((expectedValue, i) =>
|
||||||
|
expect(cells[i]).toHaveTextContent(expectedValue)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats size column', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(generateJsx({ rowData }));
|
||||||
|
});
|
||||||
|
const grid = screen.getByRole('treegrid');
|
||||||
|
const cells = grid.querySelectorAll('.ag-body [col-id="submission.size"]');
|
||||||
|
|
||||||
|
const expectedValues: string[] = ['+1.00', '-1.10'];
|
||||||
|
expectedValues.forEach((expectedValue, i) =>
|
||||||
|
expect(cells[i]).toHaveTextContent(expectedValue)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats type column', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(generateJsx({ rowData }));
|
||||||
|
});
|
||||||
|
const grid = screen.getByRole('treegrid');
|
||||||
|
const cells = grid.querySelectorAll('.ag-body [col-id="submission.type"]');
|
||||||
|
|
||||||
|
const expectedValues: string[] = ['Limit', 'Market'];
|
||||||
|
expectedValues.forEach((expectedValue, i) =>
|
||||||
|
expect(cells[i]).toHaveTextContent(expectedValue)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('formats status column', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(generateJsx({ rowData }));
|
||||||
|
});
|
||||||
|
const grid = screen.getByRole('treegrid');
|
||||||
|
const cells = grid.querySelectorAll('.ag-body [col-id="status"]');
|
||||||
|
|
||||||
|
const expectedValues: string[] = [
|
||||||
|
'Cancelled',
|
||||||
|
'Expired',
|
||||||
|
'Pending',
|
||||||
|
'Rejected',
|
||||||
|
'Stopped',
|
||||||
|
'Triggered',
|
||||||
|
];
|
||||||
|
expectedValues.forEach((expectedValue, i) =>
|
||||||
|
expect(cells[i]).toHaveTextContent(expectedValue)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats price column', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(generateJsx({ rowData }));
|
||||||
|
});
|
||||||
|
const grid = screen.getByRole('treegrid');
|
||||||
|
const cells = grid.querySelectorAll('.ag-body [col-id="submission.price"]');
|
||||||
|
|
||||||
|
const expectedValues: string[] = ['12.0', '-'];
|
||||||
|
expectedValues.forEach((expectedValue, i) =>
|
||||||
|
expect(cells[i]).toHaveTextContent(expectedValue)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows cancel button only for pending stop orders', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
render(generateJsx({ rowData }));
|
||||||
|
});
|
||||||
|
const cancelButtons = screen.getAllByTestId('cancel');
|
||||||
|
expect(cancelButtons).toHaveLength(1);
|
||||||
|
cancelButtons.forEach((cancelButton) => {
|
||||||
|
const id = cancelButton.closest('[role="row"]')?.getAttribute('row-id');
|
||||||
|
expect(rowData.find((row) => row.id === id)?.status).toEqual(
|
||||||
|
Schema.StopOrderStatus.STATUS_PENDING
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,300 @@
|
|||||||
|
import {
|
||||||
|
addDecimalsFormatNumber,
|
||||||
|
getDateTimeFormat,
|
||||||
|
isNumeric,
|
||||||
|
toBigNum,
|
||||||
|
} from '@vegaprotocol/utils';
|
||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import * as Schema from '@vegaprotocol/types';
|
||||||
|
import { ButtonLink } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import type { ForwardedRef } from 'react';
|
||||||
|
import { memo, forwardRef, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
AgGridLazy as AgGrid,
|
||||||
|
SetFilter,
|
||||||
|
DateRangeFilter,
|
||||||
|
negativeClassNames,
|
||||||
|
positiveClassNames,
|
||||||
|
MarketNameCell,
|
||||||
|
COL_DEFS,
|
||||||
|
} from '@vegaprotocol/datagrid';
|
||||||
|
import type {
|
||||||
|
TypedDataAgGrid,
|
||||||
|
VegaICellRendererParams,
|
||||||
|
VegaValueFormatterParams,
|
||||||
|
VegaValueGetterParams,
|
||||||
|
} from '@vegaprotocol/datagrid';
|
||||||
|
import type { AgGridReact } from 'ag-grid-react';
|
||||||
|
import type { StopOrder } from '../order-data-provider/stop-orders-data-provider';
|
||||||
|
import type { ColDef } from 'ag-grid-community';
|
||||||
|
|
||||||
|
export type StopOrdersTableProps = TypedDataAgGrid<StopOrder> & {
|
||||||
|
onCancel: (order: StopOrder) => void;
|
||||||
|
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||||
|
isReadOnly: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StopOrdersTable = memo<
|
||||||
|
StopOrdersTableProps & { ref?: ForwardedRef<AgGridReact> }
|
||||||
|
>(
|
||||||
|
forwardRef<AgGridReact, StopOrdersTableProps>(
|
||||||
|
({ onCancel, onMarketClick, ...props }, ref) => {
|
||||||
|
const showAllActions = !props.isReadOnly;
|
||||||
|
const columnDefs: ColDef[] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
headerName: t('Market'),
|
||||||
|
field: 'market.tradableInstrument.instrument.code',
|
||||||
|
cellRenderer: 'MarketNameCell',
|
||||||
|
cellRendererParams: { idPath: 'market.id', onMarketClick },
|
||||||
|
minWidth: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headerName: t('Trigger'),
|
||||||
|
field: 'trigger',
|
||||||
|
cellClass: 'font-mono text-right',
|
||||||
|
type: 'rightAligned',
|
||||||
|
sortable: false,
|
||||||
|
valueFormatter: ({
|
||||||
|
data,
|
||||||
|
value,
|
||||||
|
}: VegaValueFormatterParams<StopOrder, 'trigger'>): string => {
|
||||||
|
if (data && value?.__typename === 'StopOrderPrice') {
|
||||||
|
return `${t('Mark')} ${
|
||||||
|
data?.triggerDirection ===
|
||||||
|
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
|
||||||
|
? '<'
|
||||||
|
: '>'
|
||||||
|
} ${addDecimalsFormatNumber(
|
||||||
|
value.price,
|
||||||
|
data.market.decimalPlaces
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
data &&
|
||||||
|
value?.__typename === 'StopOrderTrailingPercentOffset'
|
||||||
|
) {
|
||||||
|
return `${t('Mark')} ${
|
||||||
|
data?.triggerDirection ===
|
||||||
|
Schema.StopOrderTriggerDirection.TRIGGER_DIRECTION_FALLS_BELOW
|
||||||
|
? '+'
|
||||||
|
: '-'
|
||||||
|
}${(Number(value.trailingPercentOffset) * 100).toFixed(1)}%`;
|
||||||
|
}
|
||||||
|
return '-';
|
||||||
|
},
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'expiresAt',
|
||||||
|
valueFormatter: ({
|
||||||
|
value,
|
||||||
|
data,
|
||||||
|
}: VegaValueFormatterParams<StopOrder, 'expiresAt'>) => {
|
||||||
|
if (
|
||||||
|
data &&
|
||||||
|
value &&
|
||||||
|
data?.expiryStrategy !==
|
||||||
|
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_UNSPECIFIED
|
||||||
|
) {
|
||||||
|
const expiresAt = getDateTimeFormat().format(new Date(value));
|
||||||
|
const expiryStrategy =
|
||||||
|
data.expiryStrategy ===
|
||||||
|
Schema.StopOrderExpiryStrategy.EXPIRY_STRATEGY_SUBMIT
|
||||||
|
? t('Submit')
|
||||||
|
: t('Cancels');
|
||||||
|
return `${expiryStrategy} ${expiresAt}`;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
minWidth: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headerName: t('Size'),
|
||||||
|
field: 'submission.size',
|
||||||
|
cellClass: 'font-mono text-right',
|
||||||
|
type: 'rightAligned',
|
||||||
|
cellClassRules: {
|
||||||
|
[positiveClassNames]: ({ data }: { data: StopOrder }) =>
|
||||||
|
data?.submission.size === Schema.Side.SIDE_BUY,
|
||||||
|
[negativeClassNames]: ({ data }: { data: StopOrder }) =>
|
||||||
|
data?.submission.size === Schema.Side.SIDE_SELL,
|
||||||
|
},
|
||||||
|
valueGetter: ({ data }: VegaValueGetterParams<StopOrder>) => {
|
||||||
|
return data?.submission.size && data.market
|
||||||
|
? toBigNum(
|
||||||
|
data.submission.size,
|
||||||
|
data.market.positionDecimalPlaces ?? 0
|
||||||
|
)
|
||||||
|
.multipliedBy(
|
||||||
|
data.submission.side === Schema.Side.SIDE_SELL ? -1 : 1
|
||||||
|
)
|
||||||
|
.toNumber()
|
||||||
|
: undefined;
|
||||||
|
},
|
||||||
|
valueFormatter: ({
|
||||||
|
data,
|
||||||
|
}: VegaValueFormatterParams<StopOrder, 'size'>) => {
|
||||||
|
if (!data) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (!data?.market || !isNumeric(data.submission.size)) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
const prefix = data
|
||||||
|
? data.submission.side === Schema.Side.SIDE_BUY
|
||||||
|
? '+'
|
||||||
|
: '-'
|
||||||
|
: '';
|
||||||
|
return (
|
||||||
|
prefix +
|
||||||
|
addDecimalsFormatNumber(
|
||||||
|
data.submission.size,
|
||||||
|
data.market.positionDecimalPlaces
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
minWidth: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'submission.type',
|
||||||
|
filter: SetFilter,
|
||||||
|
filterParams: {
|
||||||
|
set: Schema.OrderTypeMapping,
|
||||||
|
},
|
||||||
|
cellRenderer: ({
|
||||||
|
value,
|
||||||
|
}: VegaICellRendererParams<StopOrder, 'submission.type'>) =>
|
||||||
|
value ? Schema.OrderTypeMapping[value] : '',
|
||||||
|
minWidth: 80,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
filter: SetFilter,
|
||||||
|
filterParams: {
|
||||||
|
set: Schema.StopOrderStatusMapping,
|
||||||
|
},
|
||||||
|
valueFormatter: ({
|
||||||
|
value,
|
||||||
|
}: VegaValueFormatterParams<StopOrder, 'status'>) => {
|
||||||
|
return value ? Schema.StopOrderStatusMapping[value] : '';
|
||||||
|
},
|
||||||
|
cellRenderer: ({
|
||||||
|
valueFormatted,
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
valueFormatted: string;
|
||||||
|
data: StopOrder;
|
||||||
|
}) => (
|
||||||
|
<span data-testid={`order-status-${data?.id}`}>
|
||||||
|
{valueFormatted}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'submission.price',
|
||||||
|
type: 'rightAligned',
|
||||||
|
cellClass: 'font-mono text-right',
|
||||||
|
valueFormatter: ({
|
||||||
|
value,
|
||||||
|
data,
|
||||||
|
}: VegaValueFormatterParams<StopOrder, 'submission.price'>) => {
|
||||||
|
if (!data) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!data?.market ||
|
||||||
|
data.submission.type === Schema.OrderType.TYPE_MARKET ||
|
||||||
|
!isNumeric(value)
|
||||||
|
) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
return addDecimalsFormatNumber(value, data.market.decimalPlaces);
|
||||||
|
},
|
||||||
|
minWidth: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'submission.timeInForce',
|
||||||
|
filter: SetFilter,
|
||||||
|
filterParams: {
|
||||||
|
set: Schema.OrderTimeInForceMapping,
|
||||||
|
},
|
||||||
|
valueFormatter: ({
|
||||||
|
value,
|
||||||
|
}: VegaValueFormatterParams<
|
||||||
|
StopOrder,
|
||||||
|
'submission.timeInForce'
|
||||||
|
>) => {
|
||||||
|
return value ? Schema.OrderTimeInForceCode[value] : '';
|
||||||
|
},
|
||||||
|
minWidth: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'updatedAt',
|
||||||
|
filter: DateRangeFilter,
|
||||||
|
valueGetter: ({ data }: VegaValueGetterParams<StopOrder>) =>
|
||||||
|
data?.updatedAt || data?.createdAt,
|
||||||
|
cellRenderer: ({
|
||||||
|
data,
|
||||||
|
}: VegaICellRendererParams<StopOrder, 'createdAt'>) => {
|
||||||
|
if (!data) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const value = data.updatedAt || data.createdAt;
|
||||||
|
return (
|
||||||
|
<span data-value={value}>
|
||||||
|
{value ? getDateTimeFormat().format(new Date(value)) : '-'}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
minWidth: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colId: 'actions',
|
||||||
|
...COL_DEFS.actions,
|
||||||
|
minWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth,
|
||||||
|
maxWidth: showAllActions ? 120 : COL_DEFS.actions.minWidth,
|
||||||
|
cellRenderer: ({ data }: { data?: StopOrder }) => {
|
||||||
|
if (!data) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2 items-center justify-end">
|
||||||
|
{data.status === Schema.StopOrderStatus.STATUS_PENDING &&
|
||||||
|
!props.isReadOnly && (
|
||||||
|
<ButtonLink
|
||||||
|
data-testid="cancel"
|
||||||
|
onClick={() => onCancel(data)}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</ButtonLink>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[onCancel, onMarketClick, props.isReadOnly, showAllActions]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AgGrid
|
||||||
|
ref={ref}
|
||||||
|
defaultColDef={{
|
||||||
|
resizable: true,
|
||||||
|
sortable: true,
|
||||||
|
filterParams: { buttons: ['reset'] },
|
||||||
|
}}
|
||||||
|
columnDefs={columnDefs}
|
||||||
|
style={{
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
}}
|
||||||
|
getRowId={({ data }) => data.id}
|
||||||
|
components={{ MarketNameCell }}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
@ -111,13 +111,13 @@ export const useOrder = (marketId: string) => {
|
|||||||
[marketId, _update]
|
[marketId, _update]
|
||||||
);
|
);
|
||||||
|
|
||||||
// add new order to store if it doesnt exist, but don't
|
// add new order to store if it doesn't exist, but don't
|
||||||
// persist until user has edited
|
// persist until user has edited
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!order) {
|
if (!order) {
|
||||||
update(
|
update(
|
||||||
getDefaultOrder(marketId),
|
getDefaultOrder(marketId),
|
||||||
false // dont persist the order
|
false // don't persist the order
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [order, marketId, update]);
|
}, [order, marketId, update]);
|
||||||
|
@ -23,6 +23,7 @@ import type {
|
|||||||
VoteValue,
|
VoteValue,
|
||||||
WithdrawalStatus,
|
WithdrawalStatus,
|
||||||
DispatchMetric,
|
DispatchMetric,
|
||||||
|
StopOrderStatus,
|
||||||
} from './__generated__/types';
|
} from './__generated__/types';
|
||||||
|
|
||||||
export const AccountTypeMapping: {
|
export const AccountTypeMapping: {
|
||||||
@ -234,6 +235,21 @@ export const OrderStatusMapping: {
|
|||||||
STATUS_STOPPED: 'Stopped',
|
STATUS_STOPPED: 'Stopped',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop order statuses, these determine several states for an stop order that cannot be expressed with other fields in StopOrder.
|
||||||
|
*/
|
||||||
|
export const StopOrderStatusMapping: {
|
||||||
|
[T in StopOrderStatus]: string;
|
||||||
|
} = {
|
||||||
|
STATUS_CANCELLED: 'Cancelled',
|
||||||
|
STATUS_EXPIRED: 'Expired',
|
||||||
|
STATUS_PENDING: 'Pending',
|
||||||
|
STATUS_REJECTED: 'Rejected',
|
||||||
|
STATUS_STOPPED: 'Stopped',
|
||||||
|
STATUS_TRIGGERED: 'Triggered',
|
||||||
|
STATUS_UNSPECIFIED: 'Unspecified',
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { forwardRef } from 'react';
|
||||||
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
|
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
@ -9,31 +10,43 @@ export interface RadioGroupProps {
|
|||||||
value?: string;
|
value?: string;
|
||||||
orientation?: 'horizontal' | 'vertical';
|
orientation?: 'horizontal' | 'vertical';
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RadioGroup = ({
|
export const RadioGroup = forwardRef<HTMLDivElement, RadioGroupProps>(
|
||||||
children,
|
(
|
||||||
name,
|
{
|
||||||
value,
|
children,
|
||||||
orientation = 'vertical',
|
name,
|
||||||
onChange,
|
value,
|
||||||
}: RadioGroupProps) => {
|
orientation = 'vertical',
|
||||||
const groupClasses = classNames('flex text-sm', {
|
onChange,
|
||||||
'flex-col gap-2': orientation === 'vertical',
|
className,
|
||||||
'flex-row gap-4': orientation === 'horizontal',
|
}: RadioGroupProps,
|
||||||
});
|
ref
|
||||||
return (
|
) => {
|
||||||
<RadioGroupPrimitive.Root
|
const groupClasses = classNames(
|
||||||
name={name}
|
'flex text-sm',
|
||||||
value={value}
|
{
|
||||||
onValueChange={onChange}
|
'flex-col gap-2': orientation === 'vertical',
|
||||||
orientation={orientation}
|
'flex-row gap-4': orientation === 'horizontal',
|
||||||
className={groupClasses}
|
},
|
||||||
>
|
className
|
||||||
{children}
|
);
|
||||||
</RadioGroupPrimitive.Root>
|
return (
|
||||||
);
|
<RadioGroupPrimitive.Root
|
||||||
};
|
ref={ref}
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
onValueChange={onChange}
|
||||||
|
orientation={orientation}
|
||||||
|
className={groupClasses}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</RadioGroupPrimitive.Root>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
interface RadioProps {
|
interface RadioProps {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -7,7 +7,7 @@ import classNames from 'classnames';
|
|||||||
import type { ReactElement, ReactNode } from 'react';
|
import type { ReactElement, ReactNode } from 'react';
|
||||||
import { Children, isValidElement, useState } from 'react';
|
import { Children, isValidElement, useState } from 'react';
|
||||||
export interface TabsProps extends TabsPrimitive.TabsProps {
|
export interface TabsProps extends TabsPrimitive.TabsProps {
|
||||||
children: ReactElement<TabProps>[];
|
children: (ReactElement<TabProps> | null)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Tabs = ({
|
export const Tabs = ({
|
||||||
@ -17,11 +17,11 @@ export const Tabs = ({
|
|||||||
onValueChange,
|
onValueChange,
|
||||||
...props
|
...props
|
||||||
}: TabsProps) => {
|
}: TabsProps) => {
|
||||||
const [activeTab, setActiveTab] = useState<string>(() => {
|
const [activeTab, setActiveTab] = useState<string | undefined>(() => {
|
||||||
if (defaultValue) {
|
if (defaultValue) {
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
return children[0].props.id;
|
return children.find((v) => v)?.props.id;
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -112,7 +112,10 @@ export const LocalStoragePersistTabs = ({
|
|||||||
children={children}
|
children={children}
|
||||||
value={getValidItem(
|
value={getValidItem(
|
||||||
value,
|
value,
|
||||||
Children.map(children, (child) => child.props.id),
|
Children.map(
|
||||||
|
children.filter((c): c is ReactElement<TabProps> => c !== null),
|
||||||
|
(child) => child.props.id
|
||||||
|
),
|
||||||
undefined
|
undefined
|
||||||
)}
|
)}
|
||||||
onValueChange={onValueChange}
|
onValueChange={onValueChange}
|
||||||
|
@ -75,7 +75,7 @@ export const TradingDropdownContent = forwardRef<
|
|||||||
<DropdownMenuPrimitive.Content
|
<DropdownMenuPrimitive.Content
|
||||||
ref={forwardedRef}
|
ref={forwardedRef}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'min-w-[290px] bg-vega-clight-700 dark:bg-vega-cdark-700',
|
'bg-vega-clight-700 dark:bg-vega-cdark-700',
|
||||||
'border border-vega-clight-500 dark:border-vega-cdark-500',
|
'border border-vega-clight-500 dark:border-vega-cdark-500',
|
||||||
'p-2 rounded z-20 text-default'
|
'p-2 rounded z-20 text-default'
|
||||||
)}
|
)}
|
||||||
|
@ -3,8 +3,8 @@ import { t } from '@vegaprotocol/i18n';
|
|||||||
export const validateAmount = (step: number | string, field: string) => {
|
export const validateAmount = (step: number | string, field: string) => {
|
||||||
const [, stepDecimals = ''] = String(step).split('.');
|
const [, stepDecimals = ''] = String(step).split('.');
|
||||||
|
|
||||||
return (value: string) => {
|
return (value?: string) => {
|
||||||
const [, valueDecimals = ''] = value.split('.');
|
const [, valueDecimals = ''] = (value || '').split('.');
|
||||||
if (stepDecimals.length < valueDecimals.length) {
|
if (stepDecimals.length < valueDecimals.length) {
|
||||||
if (stepDecimals === '') {
|
if (stepDecimals === '') {
|
||||||
return t(`${field} must be whole numbers for this market`);
|
return t(`${field} must be whole numbers for this market`);
|
||||||
|
@ -79,6 +79,32 @@ export interface OrderAmendmentBody {
|
|||||||
orderAmendment: OrderAmendment;
|
orderAmendment: OrderAmendment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StopOrderSetup {
|
||||||
|
orderSubmission: OrderSubmission;
|
||||||
|
expiresAt?: string;
|
||||||
|
expiryStrategy?: Schema.StopOrderExpiryStrategy;
|
||||||
|
price?: string;
|
||||||
|
trailingPercentOffset?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StopOrdersSubmission {
|
||||||
|
risesAbove?: StopOrderSetup;
|
||||||
|
fallsBelow?: StopOrderSetup;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StopOrdersCancellation {
|
||||||
|
stopOrderId?: string;
|
||||||
|
marketId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StopOrdersSubmissionBody {
|
||||||
|
stopOrdersSubmission: StopOrdersSubmission;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StopOrdersCancellationBody {
|
||||||
|
stopOrdersCancellation: StopOrdersCancellation;
|
||||||
|
}
|
||||||
|
|
||||||
export interface VoteSubmissionBody {
|
export interface VoteSubmissionBody {
|
||||||
voteSubmission: {
|
voteSubmission: {
|
||||||
value: Schema.VoteValue;
|
value: Schema.VoteValue;
|
||||||
@ -357,6 +383,8 @@ export interface TransferBody {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type Transaction =
|
export type Transaction =
|
||||||
|
| StopOrdersSubmissionBody
|
||||||
|
| StopOrdersCancellationBody
|
||||||
| OrderSubmissionBody
|
| OrderSubmissionBody
|
||||||
| OrderCancellationBody
|
| OrderCancellationBody
|
||||||
| WithdrawSubmissionBody
|
| WithdrawSubmissionBody
|
||||||
@ -381,6 +409,16 @@ export const isOrderCancellationTransaction = (
|
|||||||
transaction: Transaction
|
transaction: Transaction
|
||||||
): transaction is OrderCancellationBody => 'orderCancellation' in transaction;
|
): transaction is OrderCancellationBody => 'orderCancellation' in transaction;
|
||||||
|
|
||||||
|
export const isStopOrdersSubmissionTransaction = (
|
||||||
|
transaction: Transaction
|
||||||
|
): transaction is StopOrdersSubmissionBody =>
|
||||||
|
'stopOrdersSubmission' in transaction;
|
||||||
|
|
||||||
|
export const isStopOrdersCancellationTransaction = (
|
||||||
|
transaction: Transaction
|
||||||
|
): transaction is StopOrdersCancellationBody =>
|
||||||
|
'stopOrdersCancellation' in transaction;
|
||||||
|
|
||||||
export const isOrderAmendmentTransaction = (
|
export const isOrderAmendmentTransaction = (
|
||||||
transaction: Transaction
|
transaction: Transaction
|
||||||
): transaction is OrderAmendmentBody => 'orderAmendment' in transaction;
|
): transaction is OrderAmendmentBody => 'orderAmendment' in transaction;
|
||||||
|
@ -7,6 +7,8 @@ import {
|
|||||||
isOrderAmendmentTransaction,
|
isOrderAmendmentTransaction,
|
||||||
isBatchMarketInstructionsTransaction,
|
isBatchMarketInstructionsTransaction,
|
||||||
isTransferTransaction,
|
isTransferTransaction,
|
||||||
|
isStopOrdersSubmissionTransaction,
|
||||||
|
isStopOrdersCancellationTransaction,
|
||||||
} from './connectors';
|
} from './connectors';
|
||||||
import { determineId } from './utils';
|
import { determineId } from './utils';
|
||||||
|
|
||||||
@ -196,9 +198,16 @@ export const useVegaTransactionStore = create<VegaTransactionStore>()(
|
|||||||
isOrderCancellationTransaction(transaction.body) &&
|
isOrderCancellationTransaction(transaction.body) &&
|
||||||
!transaction.body.orderCancellation.orderId;
|
!transaction.body.orderCancellation.orderId;
|
||||||
const isConfirmedTransfer = isTransferTransaction(transaction.body);
|
const isConfirmedTransfer = isTransferTransaction(transaction.body);
|
||||||
|
const isConfirmedStopOrderCancellation =
|
||||||
|
isStopOrdersCancellationTransaction(transaction.body);
|
||||||
|
const isConfirmedStopOrderSubmission =
|
||||||
|
isStopOrdersSubmissionTransaction(transaction.body);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(isConfirmedOrderCancellation || isConfirmedTransfer) &&
|
(isConfirmedOrderCancellation ||
|
||||||
|
isConfirmedTransfer ||
|
||||||
|
isConfirmedStopOrderCancellation ||
|
||||||
|
isConfirmedStopOrderSubmission) &&
|
||||||
!transactionResult.error &&
|
!transactionResult.error &&
|
||||||
transactionResult.status
|
transactionResult.status
|
||||||
) {
|
) {
|
||||||
|
@ -22,6 +22,8 @@ import {
|
|||||||
isWithdrawTransaction,
|
isWithdrawTransaction,
|
||||||
useVegaTransactionStore,
|
useVegaTransactionStore,
|
||||||
VegaTxStatus,
|
VegaTxStatus,
|
||||||
|
isStopOrdersSubmissionTransaction,
|
||||||
|
isStopOrdersCancellationTransaction,
|
||||||
} from '@vegaprotocol/wallet';
|
} from '@vegaprotocol/wallet';
|
||||||
import type { Toast, ToastContent } from '@vegaprotocol/ui-toolkit';
|
import type { Toast, ToastContent } from '@vegaprotocol/ui-toolkit';
|
||||||
import { ToastHeading } from '@vegaprotocol/ui-toolkit';
|
import { ToastHeading } from '@vegaprotocol/ui-toolkit';
|
||||||
@ -85,6 +87,8 @@ const isTransactionTypeSupported = (tx: VegaStoredTxState) => {
|
|||||||
const withdraw = isWithdrawTransaction(tx.body);
|
const withdraw = isWithdrawTransaction(tx.body);
|
||||||
const submitOrder = isOrderSubmissionTransaction(tx.body);
|
const submitOrder = isOrderSubmissionTransaction(tx.body);
|
||||||
const cancelOrder = isOrderCancellationTransaction(tx.body);
|
const cancelOrder = isOrderCancellationTransaction(tx.body);
|
||||||
|
const submitStopOrder = isStopOrdersSubmissionTransaction(tx.body);
|
||||||
|
const cancelStopOrder = isStopOrdersCancellationTransaction(tx.body);
|
||||||
const editOrder = isOrderAmendmentTransaction(tx.body);
|
const editOrder = isOrderAmendmentTransaction(tx.body);
|
||||||
const batchMarketInstructions = isBatchMarketInstructionsTransaction(tx.body);
|
const batchMarketInstructions = isBatchMarketInstructionsTransaction(tx.body);
|
||||||
const transfer = isTransferTransaction(tx.body);
|
const transfer = isTransferTransaction(tx.body);
|
||||||
@ -92,6 +96,8 @@ const isTransactionTypeSupported = (tx: VegaStoredTxState) => {
|
|||||||
withdraw ||
|
withdraw ||
|
||||||
submitOrder ||
|
submitOrder ||
|
||||||
cancelOrder ||
|
cancelOrder ||
|
||||||
|
submitStopOrder ||
|
||||||
|
cancelStopOrder ||
|
||||||
editOrder ||
|
editOrder ||
|
||||||
batchMarketInstructions ||
|
batchMarketInstructions ||
|
||||||
transfer
|
transfer
|
||||||
|
Loading…
Reference in New Issue
Block a user