Feat/1660 validate available margin (#1860)
* feat: deal ticket margin validation * feat: deal ticket margin validation - working solution * feat: deal ticket margin validation - add int test * feat: deal ticket margin validation - improve int test * feat: deal ticket margin validation - improve int test * feat: deal ticket margin validation - fix unit test * feat: deal ticket margin validation - improve case when no account * feat: deal ticket margin validation - fix unit test * feat: deal ticket margin validation - fix int test * feat: deal ticket margin validation - fix int test * feat: deal ticket margin validation - fix int test Co-authored-by: maciek <maciek@vegaprotocol.io>
This commit is contained in:
parent
75db1d3ec6
commit
5ecf7d6a9f
@ -1,15 +1,29 @@
|
|||||||
|
import { Schema as Types } from '@vegaprotocol/types';
|
||||||
|
|
||||||
export const generatePartyMarketData = () => {
|
export const generatePartyMarketData = () => {
|
||||||
return {
|
return {
|
||||||
party: {
|
party: {
|
||||||
id: '2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
|
id: '2e1ef32e5804e14232406aebaad719087d326afa5c648b7824d0823d8a46c8d1',
|
||||||
accounts: [
|
accounts: [
|
||||||
{
|
{
|
||||||
type: 'General',
|
type: Types.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||||
balance: '1200000',
|
balance: '1200000',
|
||||||
asset: { id: 'fBTC', decimals: 5, __typename: 'Asset' },
|
asset: { id: 'fBTC', decimals: 5, __typename: 'Asset' },
|
||||||
market: null,
|
market: null,
|
||||||
__typename: 'AccountBalance',
|
__typename: 'AccountBalance',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
__typename: 'AccountBalance',
|
||||||
|
type: Types.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||||
|
balance: '0.000000001',
|
||||||
|
asset: {
|
||||||
|
__typename: 'Asset',
|
||||||
|
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||||
|
symbol: 'tUSD',
|
||||||
|
name: 'usd',
|
||||||
|
decimals: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
marginsConnection: { edges: null, __typename: 'MarginConnection' },
|
marginsConnection: { edges: null, __typename: 'MarginConnection' },
|
||||||
positionsConnection: { edges: null, __typename: 'PositionConnection' },
|
positionsConnection: { edges: null, __typename: 'PositionConnection' },
|
||||||
|
@ -68,19 +68,20 @@ export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
|
|||||||
const emptyString = ' - ';
|
const emptyString = ' - ';
|
||||||
const step = toDecimal(market.positionDecimalPlaces);
|
const step = toDecimal(market.positionDecimalPlaces);
|
||||||
const order = watch();
|
const order = watch();
|
||||||
const { message: invalidText, isDisabled } = useOrderValidation({
|
|
||||||
market,
|
|
||||||
orderType: order.type,
|
|
||||||
orderTimeInForce: order.timeInForce,
|
|
||||||
fieldErrors: errors,
|
|
||||||
});
|
|
||||||
const { submit, transaction, finalizedOrder, Dialog } = useOrderSubmit();
|
|
||||||
const { pubKey } = useVegaWallet();
|
const { pubKey } = useVegaWallet();
|
||||||
const estMargin = useOrderMargin({
|
const estMargin = useOrderMargin({
|
||||||
order,
|
order,
|
||||||
market,
|
market,
|
||||||
partyId: pubKey || '',
|
partyId: pubKey || '',
|
||||||
});
|
});
|
||||||
|
const { message: invalidText, isDisabled } = useOrderValidation({
|
||||||
|
market,
|
||||||
|
orderType: order.type,
|
||||||
|
orderTimeInForce: order.timeInForce,
|
||||||
|
fieldErrors: errors,
|
||||||
|
estMargin,
|
||||||
|
});
|
||||||
|
const { submit, transaction, finalizedOrder, Dialog } = useOrderSubmit();
|
||||||
|
|
||||||
const { data: partyBalance } = usePartyBalanceQuery({
|
const { data: partyBalance } = usePartyBalanceQuery({
|
||||||
variables: { partyId: pubKey || '' },
|
variables: { partyId: pubKey || '' },
|
||||||
|
@ -3,7 +3,9 @@ import {
|
|||||||
MarketTradingMode,
|
MarketTradingMode,
|
||||||
AuctionTrigger,
|
AuctionTrigger,
|
||||||
} from '@vegaprotocol/types';
|
} from '@vegaprotocol/types';
|
||||||
|
import { generateEstimateOrder } from '../support/mocks/generate-fees';
|
||||||
import { connectVegaWallet } from '../support/vega-wallet';
|
import { connectVegaWallet } from '../support/vega-wallet';
|
||||||
|
import { aliasQuery } from '@vegaprotocol/cypress';
|
||||||
|
|
||||||
const orderSizeField = 'order-size';
|
const orderSizeField = 'order-size';
|
||||||
const orderPriceField = 'order-price';
|
const orderPriceField = 'order-price';
|
||||||
@ -201,7 +203,7 @@ describe('must submit order', { tags: '@smoke' }, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('deal ticket validation', { tags: '@smoke' }, () => {
|
describe('deal ticket validation', { tags: '@smoke' }, () => {
|
||||||
before(() => {
|
beforeEach(() => {
|
||||||
cy.mockTradingPage();
|
cy.mockTradingPage();
|
||||||
cy.visit('/markets/market-0');
|
cy.visit('/markets/market-0');
|
||||||
cy.wait('@Market');
|
cy.wait('@Market');
|
||||||
@ -441,3 +443,40 @@ describe('suspended market validation', { tags: '@regression' }, () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('margin required validation', { tags: '@regression' }, () => {
|
||||||
|
before(() => {
|
||||||
|
cy.mockTradingPage();
|
||||||
|
cy.mockGQL((req) => {
|
||||||
|
aliasQuery(
|
||||||
|
req,
|
||||||
|
'EstimateOrder',
|
||||||
|
generateEstimateOrder({
|
||||||
|
estimateOrder: {
|
||||||
|
marginLevels: { __typename: 'MarginLevels', initialLevel: '1000' },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
cy.visit('/markets/market-0');
|
||||||
|
connectVegaWallet();
|
||||||
|
cy.wait('@Market');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display info and button for deposit', () => {
|
||||||
|
cy.getByTestId('place-order').should('be.disabled');
|
||||||
|
cy.getByTestId('dealticket-error-message').should(
|
||||||
|
'contain.text',
|
||||||
|
"You don't have enough margin available to open this position"
|
||||||
|
);
|
||||||
|
cy.getByTestId('dealticket-error-message').should(
|
||||||
|
'contain.text',
|
||||||
|
'0.01000 tBTC currently required, 0.00100 tBTC available'
|
||||||
|
);
|
||||||
|
cy.getByTestId('deal-ticket-deposit-dialog-button').click();
|
||||||
|
cy.getByTestId('dialog-content')
|
||||||
|
.find('h1')
|
||||||
|
.eq(0)
|
||||||
|
.should('have.text', 'Deposit');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
98
apps/trading-e2e/src/support/mocks/generate-fees.ts
Normal file
98
apps/trading-e2e/src/support/mocks/generate-fees.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import type { PartialDeep } from 'type-fest';
|
||||||
|
import merge from 'lodash/merge';
|
||||||
|
import { Schema as Types } from '@vegaprotocol/types';
|
||||||
|
import type {
|
||||||
|
EstimateOrderQuery,
|
||||||
|
MarketMarkPriceQuery,
|
||||||
|
PartyBalanceQuery,
|
||||||
|
PartyMarketDataQuery,
|
||||||
|
} from '@vegaprotocol/deal-ticket';
|
||||||
|
|
||||||
|
const estimateOrderMock: EstimateOrderQuery = {
|
||||||
|
estimateOrder: {
|
||||||
|
__typename: 'OrderEstimate',
|
||||||
|
totalFeeAmount: '0.0006',
|
||||||
|
fee: {
|
||||||
|
__typename: 'TradeFee',
|
||||||
|
makerFee: '0.0001',
|
||||||
|
infrastructureFee: '0.0002',
|
||||||
|
liquidityFee: '0.0003',
|
||||||
|
},
|
||||||
|
marginLevels: { __typename: 'MarginLevels', initialLevel: '1' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateEstimateOrder = (
|
||||||
|
override?: PartialDeep<EstimateOrderQuery>
|
||||||
|
) => {
|
||||||
|
return merge(estimateOrderMock, override);
|
||||||
|
};
|
||||||
|
|
||||||
|
const marketMarkPriceMock: MarketMarkPriceQuery = {
|
||||||
|
market: {
|
||||||
|
__typename: 'Market',
|
||||||
|
decimalPlaces: 5,
|
||||||
|
data: {
|
||||||
|
__typename: 'MarketData',
|
||||||
|
markPrice: '100',
|
||||||
|
market: { __typename: 'Market', id: 'market-0' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateMarkPrice = () => {
|
||||||
|
return marketMarkPriceMock;
|
||||||
|
};
|
||||||
|
|
||||||
|
const partyBalanceMock: PartyBalanceQuery = {
|
||||||
|
party: {
|
||||||
|
__typename: 'Party',
|
||||||
|
accounts: [
|
||||||
|
{
|
||||||
|
__typename: 'AccountBalance',
|
||||||
|
type: Types.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||||
|
balance: '100',
|
||||||
|
asset: {
|
||||||
|
__typename: 'Asset',
|
||||||
|
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||||
|
symbol: 'tBTC',
|
||||||
|
name: 'BTC',
|
||||||
|
decimals: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generatePartyBalance = () => {
|
||||||
|
return partyBalanceMock;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generatePartyMarketData = (): PartyMarketDataQuery => {
|
||||||
|
return {
|
||||||
|
party: {
|
||||||
|
id: Cypress.env('VEGA_PUBLIC_KEY'),
|
||||||
|
accounts: [
|
||||||
|
{
|
||||||
|
type: Types.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||||
|
balance: '1200000',
|
||||||
|
asset: { id: 'fBTC', decimals: 5, __typename: 'Asset' },
|
||||||
|
market: null,
|
||||||
|
__typename: 'AccountBalance',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
__typename: 'AccountBalance',
|
||||||
|
type: Types.AccountType.ACCOUNT_TYPE_GENERAL,
|
||||||
|
balance: '100',
|
||||||
|
asset: {
|
||||||
|
__typename: 'Asset',
|
||||||
|
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||||
|
decimals: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
marginsConnection: { edges: null, __typename: 'MarginConnection' },
|
||||||
|
__typename: 'Party',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
@ -22,6 +22,12 @@ import { generateMargins, generatePositions } from './mocks/generate-positions';
|
|||||||
import { generateStatistics } from './mocks/generate-statistics';
|
import { generateStatistics } from './mocks/generate-statistics';
|
||||||
import { generateTrades } from './mocks/generate-trades';
|
import { generateTrades } from './mocks/generate-trades';
|
||||||
import { generateWithdrawals } from './mocks/generate-withdrawals';
|
import { generateWithdrawals } from './mocks/generate-withdrawals';
|
||||||
|
import {
|
||||||
|
generateEstimateOrder,
|
||||||
|
generateMarkPrice,
|
||||||
|
generatePartyBalance,
|
||||||
|
generatePartyMarketData,
|
||||||
|
} from './mocks/generate-fees';
|
||||||
|
|
||||||
const mockTradingPage = (
|
const mockTradingPage = (
|
||||||
req: CyHttpMessages.IncomingHttpRequest,
|
req: CyHttpMessages.IncomingHttpRequest,
|
||||||
@ -96,6 +102,11 @@ const mockTradingPage = (
|
|||||||
aliasQuery(req, 'Candles', generateCandles());
|
aliasQuery(req, 'Candles', generateCandles());
|
||||||
aliasQuery(req, 'Withdrawals', generateWithdrawals());
|
aliasQuery(req, 'Withdrawals', generateWithdrawals());
|
||||||
aliasQuery(req, 'NetworkParamsQuery', generateNetworkParameters());
|
aliasQuery(req, 'NetworkParamsQuery', generateNetworkParameters());
|
||||||
|
aliasQuery(req, 'EstimateOrder', generateEstimateOrder());
|
||||||
|
aliasQuery(req, 'MarketMarkPrice', generateMarkPrice());
|
||||||
|
aliasQuery(req, 'PartyBalance', generatePartyBalance());
|
||||||
|
aliasQuery(req, 'MarketPositions', generatePositions());
|
||||||
|
aliasQuery(req, 'PartyMarketData', generatePartyMarketData());
|
||||||
};
|
};
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -60,6 +60,12 @@ export function createClient(base?: string) {
|
|||||||
AccountUpdate: {
|
AccountUpdate: {
|
||||||
keyFields: false,
|
keyFields: false,
|
||||||
},
|
},
|
||||||
|
Party: {
|
||||||
|
keyFields: false,
|
||||||
|
},
|
||||||
|
Fees: {
|
||||||
|
keyFields: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -41,6 +41,9 @@ function AppBody({ Component, pageProps }: AppProps) {
|
|||||||
const [theme, toggleTheme] = useThemeSwitcher();
|
const [theme, toggleTheme] = useThemeSwitcher();
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={theme}>
|
<ThemeContext.Provider value={theme}>
|
||||||
|
<Head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
</Head>
|
||||||
<Title />
|
<Title />
|
||||||
<div className="h-full relative dark:bg-black dark:text-white z-0 grid grid-rows-[min-content,1fr,min-content]">
|
<div className="h-full relative dark:bg-black dark:text-white z-0 grid grid-rows-[min-content,1fr,min-content]">
|
||||||
<AppLoader>
|
<AppLoader>
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
|
import { AccountType } from '@vegaprotocol/types';
|
||||||
|
import { toBigNum } from '@vegaprotocol/react-helpers';
|
||||||
|
import type { DealTicketMarketFragment } from '../deal-ticket/__generated___/DealTicket';
|
||||||
|
import type { OrderMargin } from '../../hooks/use-order-margin';
|
||||||
|
import { usePartyBalanceQuery, useSettlementAccount } from '../../hooks';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
market: DealTicketMarketFragment;
|
||||||
|
estMargin: OrderMargin | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useOrderMarginValidation = ({ market, estMargin }: Props) => {
|
||||||
|
const { pubKey } = useVegaWallet();
|
||||||
|
|
||||||
|
const { data: partyBalance } = usePartyBalanceQuery({
|
||||||
|
variables: { partyId: pubKey || '' },
|
||||||
|
skip: !pubKey,
|
||||||
|
fetchPolicy: 'no-cache',
|
||||||
|
});
|
||||||
|
|
||||||
|
const settlementAccount = useSettlementAccount(
|
||||||
|
market.tradableInstrument.instrument.product.settlementAsset.id,
|
||||||
|
partyBalance?.party?.accounts || [],
|
||||||
|
AccountType.ACCOUNT_TYPE_GENERAL
|
||||||
|
);
|
||||||
|
const balance = settlementAccount
|
||||||
|
? toBigNum(
|
||||||
|
settlementAccount.balance || 0,
|
||||||
|
settlementAccount.asset.decimals || 0
|
||||||
|
)
|
||||||
|
: toBigNum('0', 0);
|
||||||
|
const margin = toBigNum(estMargin?.margin || 0, 0);
|
||||||
|
const { id, symbol, decimals } =
|
||||||
|
market.tradableInstrument.instrument.product.settlementAsset;
|
||||||
|
if (balance.isZero() || balance.isLessThan(margin)) {
|
||||||
|
return {
|
||||||
|
balance: balance.toString(),
|
||||||
|
margin: margin.toString(),
|
||||||
|
id,
|
||||||
|
symbol,
|
||||||
|
decimals,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
@ -1,5 +1,7 @@
|
|||||||
|
import * as React from 'react';
|
||||||
import { renderHook } from '@testing-library/react';
|
import { renderHook } from '@testing-library/react';
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
|
||||||
import {
|
import {
|
||||||
MarketState,
|
MarketState,
|
||||||
@ -8,10 +10,11 @@ import {
|
|||||||
Schema,
|
Schema,
|
||||||
} from '@vegaprotocol/types';
|
} from '@vegaprotocol/types';
|
||||||
import type { ValidationProps } from './use-order-validation';
|
import type { ValidationProps } from './use-order-validation';
|
||||||
import { marketTranslations } from './use-order-validation';
|
import { marketTranslations, useOrderValidation } from './use-order-validation';
|
||||||
import { useOrderValidation } from './use-order-validation';
|
|
||||||
import { ERROR_SIZE_DECIMAL } from './validate-size';
|
import { ERROR_SIZE_DECIMAL } from './validate-size';
|
||||||
import type { DealTicketMarketFragment } from '../deal-ticket/__generated___/DealTicket';
|
import type { DealTicketMarketFragment } from '../deal-ticket/__generated___/DealTicket';
|
||||||
|
import * as OrderMarginValidation from './use-order-margin-validation';
|
||||||
|
import { ValidateMargin } from './validate-margin';
|
||||||
|
|
||||||
jest.mock('@vegaprotocol/wallet');
|
jest.mock('@vegaprotocol/wallet');
|
||||||
|
|
||||||
@ -73,6 +76,15 @@ const defaultOrder = {
|
|||||||
step: 0.1,
|
step: 0.1,
|
||||||
orderType: Schema.OrderType.TYPE_MARKET,
|
orderType: Schema.OrderType.TYPE_MARKET,
|
||||||
orderTimeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
orderTimeInForce: Schema.OrderTimeInForce.TIME_IN_FORCE_FOK,
|
||||||
|
estMargin: {
|
||||||
|
margin: '0,000001',
|
||||||
|
totalFees: '0,000006',
|
||||||
|
fees: {
|
||||||
|
makerFee: '0,000003',
|
||||||
|
liquidityFee: '0,000002',
|
||||||
|
infrastructureFee: '0,000001',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const ERROR = {
|
const ERROR = {
|
||||||
@ -99,16 +111,29 @@ function setup(
|
|||||||
) {
|
) {
|
||||||
const mockUseVegaWallet = useVegaWallet as jest.Mock;
|
const mockUseVegaWallet = useVegaWallet as jest.Mock;
|
||||||
mockUseVegaWallet.mockReturnValue({ ...defaultWalletContext, context });
|
mockUseVegaWallet.mockReturnValue({ ...defaultWalletContext, context });
|
||||||
return renderHook(() => useOrderValidation({ ...defaultOrder, ...props }));
|
return renderHook(() => useOrderValidation({ ...defaultOrder, ...props }), {
|
||||||
|
wrapper: MockedProvider,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('useOrderValidation', () => {
|
describe('useOrderValidation', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('Returns empty string when given valid data', () => {
|
it('Returns empty string when given valid data', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(OrderMarginValidation, 'useOrderMarginValidation')
|
||||||
|
.mockReturnValue(false);
|
||||||
|
|
||||||
const { result } = setup();
|
const { result } = setup();
|
||||||
expect(result.current).toStrictEqual({ isDisabled: false, message: `` });
|
expect(result.current).toStrictEqual({ isDisabled: false, message: `` });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Returns an error message when no keypair found', () => {
|
it('Returns an error message when no keypair found', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(OrderMarginValidation, 'useOrderMarginValidation')
|
||||||
|
.mockReturnValue(false);
|
||||||
const { result } = setup(defaultOrder, { pubKey: null });
|
const { result } = setup(defaultOrder, { pubKey: null });
|
||||||
expect(result.current).toStrictEqual({ isDisabled: false, message: `` });
|
expect(result.current).toStrictEqual({ isDisabled: false, message: `` });
|
||||||
});
|
});
|
||||||
@ -239,4 +264,28 @@ describe('useOrderValidation', () => {
|
|||||||
message: ERROR.FIELD_PRICE_STEP_DECIMAL,
|
message: ERROR.FIELD_PRICE_STEP_DECIMAL,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Returns an error message when the estimated margin is higher than collateral', async () => {
|
||||||
|
const invalidatedMockValue = {
|
||||||
|
balance: '0000000,1',
|
||||||
|
margin: '000,1',
|
||||||
|
id: 'instrument-id',
|
||||||
|
symbol: 'asset-symbol',
|
||||||
|
decimals: 5,
|
||||||
|
};
|
||||||
|
jest
|
||||||
|
.spyOn(OrderMarginValidation, 'useOrderMarginValidation')
|
||||||
|
.mockReturnValue(invalidatedMockValue);
|
||||||
|
const { result } = setup({});
|
||||||
|
|
||||||
|
expect(result.current.isDisabled).toBe(true);
|
||||||
|
|
||||||
|
const testElement = <ValidateMargin {...invalidatedMockValue} />;
|
||||||
|
expect((result.current.message as React.ReactElement)?.props).toEqual(
|
||||||
|
testElement.props
|
||||||
|
);
|
||||||
|
expect((result.current.message as React.ReactElement)?.type).toEqual(
|
||||||
|
testElement.type
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -15,6 +15,9 @@ import { ERROR_SIZE_DECIMAL } from './validate-size';
|
|||||||
import { MarketDataGrid } from '../trading-mode-tooltip';
|
import { MarketDataGrid } from '../trading-mode-tooltip';
|
||||||
import { compileGridData } from '../trading-mode-tooltip/compile-grid-data';
|
import { compileGridData } from '../trading-mode-tooltip/compile-grid-data';
|
||||||
import type { DealTicketMarketFragment } from '../deal-ticket/__generated___/DealTicket';
|
import type { DealTicketMarketFragment } from '../deal-ticket/__generated___/DealTicket';
|
||||||
|
import { ValidateMargin } from './validate-margin';
|
||||||
|
import type { OrderMargin } from '../../hooks/use-order-margin';
|
||||||
|
import { useOrderMarginValidation } from './use-order-margin-validation';
|
||||||
|
|
||||||
export const isMarketInAuction = (market: DealTicketMarketFragment) => {
|
export const isMarketInAuction = (market: DealTicketMarketFragment) => {
|
||||||
return [
|
return [
|
||||||
@ -30,6 +33,7 @@ export type ValidationProps = {
|
|||||||
orderType: Schema.OrderType;
|
orderType: Schema.OrderType;
|
||||||
orderTimeInForce: Schema.OrderTimeInForce;
|
orderTimeInForce: Schema.OrderTimeInForce;
|
||||||
fieldErrors?: FieldErrors<OrderSubmissionBody['orderSubmission']>;
|
fieldErrors?: FieldErrors<OrderSubmissionBody['orderSubmission']>;
|
||||||
|
estMargin: OrderMargin | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const marketTranslations = (marketState: MarketState) => {
|
export const marketTranslations = (marketState: MarketState) => {
|
||||||
@ -46,6 +50,7 @@ export const useOrderValidation = ({
|
|||||||
fieldErrors = {},
|
fieldErrors = {},
|
||||||
orderType,
|
orderType,
|
||||||
orderTimeInForce,
|
orderTimeInForce,
|
||||||
|
estMargin,
|
||||||
}: ValidationProps): {
|
}: ValidationProps): {
|
||||||
message: React.ReactNode | string;
|
message: React.ReactNode | string;
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
@ -53,6 +58,8 @@ export const useOrderValidation = ({
|
|||||||
const { pubKey } = useVegaWallet();
|
const { pubKey } = useVegaWallet();
|
||||||
const minSize = toDecimal(market.positionDecimalPlaces);
|
const minSize = toDecimal(market.positionDecimalPlaces);
|
||||||
|
|
||||||
|
const isInvalidOrderMargin = useOrderMarginValidation({ market, estMargin });
|
||||||
|
|
||||||
const { message, isDisabled } = useMemo(() => {
|
const { message, isDisabled } = useMemo(() => {
|
||||||
if (!pubKey) {
|
if (!pubKey) {
|
||||||
return { message: t('No public key selected'), isDisabled: true };
|
return { message: t('No public key selected'), isDisabled: true };
|
||||||
@ -266,6 +273,13 @@ export const useOrderValidation = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isInvalidOrderMargin) {
|
||||||
|
return {
|
||||||
|
isDisabled: true,
|
||||||
|
message: <ValidateMargin {...isInvalidOrderMargin} />,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
[
|
[
|
||||||
MarketTradingMode.TRADING_MODE_BATCH_AUCTION,
|
MarketTradingMode.TRADING_MODE_BATCH_AUCTION,
|
||||||
@ -291,6 +305,7 @@ export const useOrderValidation = ({
|
|||||||
fieldErrors?.price?.type,
|
fieldErrors?.price?.type,
|
||||||
orderType,
|
orderType,
|
||||||
orderTimeInForce,
|
orderTimeInForce,
|
||||||
|
isInvalidOrderMargin,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { message, isDisabled };
|
return { message, isDisabled };
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { formatNumber, t } from '@vegaprotocol/react-helpers';
|
||||||
|
import { Button, Dialog } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { DepositContainer } from '@vegaprotocol/deposits';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
margin: string;
|
||||||
|
symbol: string;
|
||||||
|
id: string;
|
||||||
|
balance: string;
|
||||||
|
decimals: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ValidateMargin = ({
|
||||||
|
margin,
|
||||||
|
symbol,
|
||||||
|
id,
|
||||||
|
balance,
|
||||||
|
decimals,
|
||||||
|
}: Props) => {
|
||||||
|
const [depositDialog, setDepositDialog] = useState(false);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="flex flex-col center pb-3"
|
||||||
|
data-testid="deal-ticket-margin-invalidated"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{t("You don't have enough margin available to open this position.")}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{`${formatNumber(margin, decimals)} ${symbol} ${t(
|
||||||
|
'currently required'
|
||||||
|
)}, ${formatNumber(balance, decimals)} ${symbol} ${t('available')}`}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
className="center mt-2"
|
||||||
|
variant="default"
|
||||||
|
size="xs"
|
||||||
|
data-testid="deal-ticket-deposit-dialog-button"
|
||||||
|
onClick={() => setDepositDialog(true)}
|
||||||
|
>
|
||||||
|
{t('Deposit')} {symbol}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Dialog open={depositDialog} onChange={setDepositDialog}>
|
||||||
|
<h1 className="text-2xl mb-4">{t('Deposit')}</h1>
|
||||||
|
<DepositContainer assetId={id} />
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -52,11 +52,16 @@ export const DealTicket = ({
|
|||||||
defaultValues: getDefaultOrder(market),
|
defaultValues: getDefaultOrder(market),
|
||||||
});
|
});
|
||||||
const order = watch();
|
const order = watch();
|
||||||
|
|
||||||
|
const feeDetails = useFeeDealTicketDetails(order, market);
|
||||||
|
const details = getFeeDetailsValues(feeDetails);
|
||||||
|
|
||||||
const { message, isDisabled: disabled } = useOrderValidation({
|
const { message, isDisabled: disabled } = useOrderValidation({
|
||||||
market,
|
market,
|
||||||
orderType: order.type,
|
orderType: order.type,
|
||||||
orderTimeInForce: order.timeInForce,
|
orderTimeInForce: order.timeInForce,
|
||||||
fieldErrors: errors,
|
fieldErrors: errors,
|
||||||
|
estMargin: feeDetails.estMargin,
|
||||||
});
|
});
|
||||||
const isDisabled = transactionStatus === 'pending' || disabled;
|
const isDisabled = transactionStatus === 'pending' || disabled;
|
||||||
|
|
||||||
@ -101,9 +106,6 @@ export const DealTicket = ({
|
|||||||
}
|
}
|
||||||
}, [marketPriceFormatted, order.type, setValue]);
|
}, [marketPriceFormatted, order.type, setValue]);
|
||||||
|
|
||||||
const feeDetails = useFeeDealTicketDetails(order, market);
|
|
||||||
const details = getFeeDetailsValues(feeDetails);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="p-4" noValidate>
|
<form onSubmit={handleSubmit(onSubmit)} className="p-4" noValidate>
|
||||||
<Controller
|
<Controller
|
||||||
|
@ -1 +1,2 @@
|
|||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
|
import 'jest-canvas-mock';
|
||||||
|
@ -29,7 +29,7 @@ export function Dialog({
|
|||||||
size = 'small',
|
size = 'small',
|
||||||
}: DialogProps) {
|
}: DialogProps) {
|
||||||
const contentClasses = classNames(
|
const contentClasses = classNames(
|
||||||
'fixed top-0 left-0 z-20 flex justify-center items-start overflow-scroll',
|
'fixed top-0 left-0 z-20 flex justify-center items-start overflow-auto',
|
||||||
'w-full h-full'
|
'w-full h-full'
|
||||||
);
|
);
|
||||||
const wrapperClasses = classNames(
|
const wrapperClasses = classNames(
|
||||||
|
Loading…
Reference in New Issue
Block a user