feat: 1486 add details of expected fees margin close out to deal ticket (#1771)

* fix: #1486 move deal ticket hooks from console-lite to be re-used in trading app for console v2

* fix: #1486 typo

* feat: #1486 deal ticket query update, console-lite fix

* feat: #1486 console-lite fix

* feat: #1486 initial hook to get fee details

* feat: #1486 add tooltips

* feat: #1486 add fees cell from market-info in tooltip

* fix: #1486 edit deal-ticket.spec.ts titles and index.ts of deal ticket hooks

* feat: #1486 move all hooks for slippage into deal ticket

* fix: #1486 fix linting deal-ticket issue

* fix: set price, fix NaN percentage, watch full order object

* fix: update only when market price is updated

* feat: #1486 add fees from est. order query, fees breakdown, fix BigNumber NaN issue

* feat: #1486 add fee factors in generate deal ticket query

* fix: #1486 show margin on short

* fix: #1486 format price and fix dal ticket use order margin import

* fix: #1486 fix price memo

* feat: #1486 update estimate ordr query with order price or mark price

* fix: #1486 revert apps/console-lite/.env

* fix: #1486 fix NaN value on close out

* fix: #1486 revert close out calculation

* fix: #1486 prevent NaN close out

* fix: #1486 revert close out

* feat: #1486 add fee factor percentages in tooltip and fix NaN

* fix: #1486 fix deal-ticket-steps est close out null handling

* fix: #1486 fix deal-ticket-steps est close out null handling

* fix: #1486 add tooltip for fees

* fix: #1486 fix console-lite formatting on notional size and close out

* fix: #1486 total fees formatting inside the hook

* feat: #1486 add qutote to fees tooltip

* fix: #1486 update hook, price, console-lite and styling

* chore: fix mock types

* fix: #1486 fix tests in console-lite

* fix: #1486 add declaration.d.ts to console-lite-e2e

* fix: #1486 fix deal ticket test

* fix: #1486 fix deal ticket test

Co-authored-by: Rado <szpiechrados@gmail.com>
This commit is contained in:
m.ray 2022-10-23 14:08:02 +01:00 committed by GitHub
parent 513c7f2b1a
commit bf34f1c060
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 912 additions and 991 deletions

View File

@ -0,0 +1 @@
declare module '*.scss';

View File

@ -27,7 +27,7 @@ describe('market selector', { tags: '@smoke' }, () => {
aliasQuery(req, 'MarketTags', generateMarketTags());
aliasQuery(req, 'MarketPositions', generateMarketPositions());
aliasQuery(req, 'EstimateOrder', generateEstimateOrder());
aliasQuery(req, 'PartyBalanceQuery', generatePartyBalance());
aliasQuery(req, 'PartyBalance', generatePartyBalance());
aliasQuery(req, 'PartyMarketData', generatePartyMarketData());
aliasQuery(req, 'MarketMarkPrice', generateMarketMarkPrice());
aliasQuery(req, 'MarketNames', generateMarketNames());

View File

@ -28,7 +28,7 @@ describe('Market trade', { tags: '@smoke' }, () => {
aliasQuery(req, 'MarketTags', generateMarketTags());
aliasQuery(req, 'MarketPositions', generateMarketPositions());
aliasQuery(req, 'EstimateOrder', generateEstimateOrder());
aliasQuery(req, 'PartyBalanceQuery', generatePartyBalance());
aliasQuery(req, 'PartyBalance', generatePartyBalance());
aliasQuery(req, 'PartyMarketData', generatePartyMarketData());
aliasQuery(req, 'MarketMarkPrice', generateMarketMarkPrice());
aliasQuery(req, 'MarketDepth', generateMarketDepth());

View File

@ -1,13 +1,21 @@
export const generateDealTicket = () => {
return {
import type { DealTicketQuery } from '@vegaprotocol/deal-ticket';
import { MarketTradingMode, MarketState } from '@vegaprotocol/types';
import merge from 'lodash/merge';
import type { PartialDeep } from 'type-fest';
export const generateDealTicket = (
override?: PartialDeep<DealTicketQuery>
): DealTicketQuery => {
const defaultResult: DealTicketQuery = {
market: {
id: 'ca7768f6de84bf86a21bbb6b0109d9659c81917b0e0339b2c262566c9b581a15',
decimalPlaces: 5,
positionDecimalPlaces: 0,
state: 'STATE_ACTIVE',
tradingMode: 'Continuous',
state: MarketState.STATE_ACTIVE,
tradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
tradableInstrument: {
instrument: {
id: 'c9f5acd348796011c075077e4d58d9b7f1689b7c1c8e030a5e886b83aa96923d',
name: 'AAVEDAI Monthly (30 Jun 2022)',
product: {
quoteName: 'DAI',
@ -15,6 +23,7 @@ export const generateDealTicket = () => {
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
symbol: 'tDAI',
name: 'tDAI TEST',
decimals: 5,
__typename: 'Asset',
},
__typename: 'Future',
@ -27,7 +36,17 @@ export const generateDealTicket = () => {
lastTrade: { price: '9893006', __typename: 'Trade' },
__typename: 'MarketDepth',
},
fees: {
factors: {
makerFee: '0.0002',
infrastructureFee: '0.0005',
liquidityFee: '0.001',
__typename: 'FeeFactors',
},
__typename: 'Fees',
},
__typename: 'Market',
},
};
return merge(defaultResult, override);
};

View File

@ -1,10 +1,17 @@
export const generatePartyBalance = () => {
return {
import merge from 'lodash/merge';
import type { PartyBalanceQuery } from '@vegaprotocol/deal-ticket';
import type { PartialDeep } from 'type-fest';
import { AccountType } from '@vegaprotocol/types';
export const generatePartyBalance = (
override?: PartialDeep<PartyBalanceQuery>
): PartyBalanceQuery => {
const defaultResult: PartyBalanceQuery = {
party: {
accounts: [
{
balance: '88474051',
type: 'ACCOUNT_TYPE_GENERAL',
type: AccountType.ACCOUNT_TYPE_GENERAL,
asset: {
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
symbol: 'tDAI',
@ -16,7 +23,7 @@ export const generatePartyBalance = () => {
},
{
balance: '100000000',
type: 'ACCOUNT_TYPE_GENERAL',
type: AccountType.ACCOUNT_TYPE_GENERAL,
asset: {
id: '8b52d4a3a4b0ffe733cddbc2b67be273816cfeb6ca4c8b339bac03ffba08e4e4',
symbol: 'tEURO',
@ -28,7 +35,7 @@ export const generatePartyBalance = () => {
},
{
balance: '3412867',
type: 'ACCOUNT_TYPE_GENERAL',
type: AccountType.ACCOUNT_TYPE_GENERAL,
asset: {
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
symbol: 'tDAI',
@ -40,7 +47,7 @@ export const generatePartyBalance = () => {
},
{
balance: '70007',
type: 'ACCOUNT_TYPE_GENERAL',
type: AccountType.ACCOUNT_TYPE_GENERAL,
asset: {
id: '6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61',
symbol: 'tDAI',
@ -54,4 +61,6 @@ export const generatePartyBalance = () => {
__typename: 'Party',
},
};
return merge(defaultResult, override);
};

View File

@ -9,5 +9,5 @@
"allowJs": true,
"types": ["cypress", "node", "cypress-real-events", "cypress-grep"]
},
"include": ["src/**/*.ts", "src/**/*.js"]
"include": ["src/**/*.ts", "src/**/*.js", "./declaration.d.ts"]
}

View File

@ -1,65 +0,0 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { AccountType } from "@vegaprotocol/types";
// ====================================================
// GraphQL query operation: PartyBalanceQuery
// ====================================================
export interface PartyBalanceQuery_party_accounts_asset {
__typename: "Asset";
/**
* The ID of the asset
*/
id: string;
/**
* The symbol of the asset (e.g: GBP)
*/
symbol: string;
/**
* The full name of the asset (e.g: Great British Pound)
*/
name: string;
/**
* The precision of the asset. Should match the decimal precision of the asset on its native chain, e.g: for ERC20 assets, it is often 18
*/
decimals: number;
}
export interface PartyBalanceQuery_party_accounts {
__typename: "Account";
/**
* Account type (General, Margin, etc)
*/
type: AccountType;
/**
* Balance as string - current account balance (approx. as balances can be updated several times per second)
*/
balance: string;
/**
* Asset, the 'currency'
*/
asset: PartyBalanceQuery_party_accounts_asset;
}
export interface PartyBalanceQuery_party {
__typename: "Party";
/**
* Collateral accounts relating to a party
*/
accounts: PartyBalanceQuery_party_accounts[] | null;
}
export interface PartyBalanceQuery {
/**
* An entity that is trading on the Vega network
*/
party: PartyBalanceQuery_party | null;
}
export interface PartyBalanceQueryVariables {
partyId: string;
}

View File

@ -1,14 +1,15 @@
import * as React from 'react';
import classNames from 'classnames';
import type { DealTicketMarketFragment } from '@vegaprotocol/deal-ticket';
import type { PartyBalanceQuery_party_accounts } from './__generated__/PartyBalanceQuery';
import { useSettlementAccount } from '../../hooks/use-settlement-account';
import { addDecimalsFormatNumber, t } from '@vegaprotocol/react-helpers';
import { AccountType } from '@vegaprotocol/types';
import type {
AccountFragment,
DealTicketMarketFragment,
} from '@vegaprotocol/deal-ticket';
import { useSettlementAccount } from '@vegaprotocol/deal-ticket';
interface DealTicketBalanceProps {
settlementAsset: DealTicketMarketFragment['tradableInstrument']['instrument']['product']['settlementAsset'];
accounts: PartyBalanceQuery_party_accounts[];
accounts: AccountFragment[];
isWalletConnected: boolean;
className?: string;
}
@ -26,7 +27,7 @@ export const DealTicketBalance = ({
accounts,
AccountType.ACCOUNT_TYPE_GENERAL
);
const formatedNumber =
const formattedNumber =
settlementAccount?.balance &&
settlementAccount.asset.decimals &&
addDecimalsFormatNumber(
@ -37,7 +38,7 @@ export const DealTicketBalance = ({
const balance = (
<p className="text-blue text-lg font-semibold">
{settlementAccount
? t(`${formatedNumber}`)
? t(`${formattedNumber}`)
: `No ${settlementAssetSymbol} left to trade`}
</p>
);

View File

@ -1,9 +1,8 @@
import * as React from 'react';
import { useParams } from 'react-router-dom';
import { gql, useQuery } from '@apollo/client';
import {
DealTicketManager,
DealTicketContainer as Container,
usePartyBalanceQuery,
} from '@vegaprotocol/deal-ticket';
import { Loader } from '@vegaprotocol/ui-toolkit';
import { t } from '@vegaprotocol/react-helpers';
@ -11,41 +10,20 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
import { DealTicketSteps } from './deal-ticket-steps';
import { DealTicketBalance } from './deal-ticket-balance';
import Baubles from './baubles-decor';
import type { PartyBalanceQuery } from './__generated__/PartyBalanceQuery';
import ConnectWallet from '../wallet-connector';
const tempEmptyText = (
<p>{t('Please select a market from the markets page')}</p>
);
const PARTY_BALANCE_QUERY = gql`
query PartyBalanceQuery($partyId: ID!) {
party(id: $partyId) {
accounts {
type
balance
asset {
id
symbol
name
decimals
}
}
}
}
`;
export const DealTicketContainer = () => {
const { marketId } = useParams<{ marketId: string }>();
const { pubKey } = useVegaWallet();
const { data: partyData, loading } = useQuery<PartyBalanceQuery>(
PARTY_BALANCE_QUERY,
{
variables: { partyId: pubKey },
skip: !pubKey,
}
);
const { data: partyData, loading } = usePartyBalanceQuery({
variables: { partyId: pubKey || '' },
skip: !pubKey,
});
const loader = <Loader />;
@ -70,7 +48,7 @@ export const DealTicketContainer = () => {
return (
<DealTicketManager market={data.market}>
{loading ? loader : balance}
<DealTicketSteps market={data.market} partyData={partyData} />
<DealTicketSteps market={data.market} />
</DealTicketManager>
);
}}

View File

@ -1,5 +1,4 @@
import React from 'react';
import { DealTicketEstimates } from './deal-ticket-estimates';
import { DealTicketEstimates } from '@vegaprotocol/deal-ticket';
import { DealTicketSizeInput } from './deal-ticket-size-input';
interface DealTicketSizeProps {

View File

@ -1,11 +1,19 @@
import React, { useCallback, useState } from 'react';
import { t } from '@vegaprotocol/react-helpers';
import * as constants from './constants';
import { TrafficLight } from '../traffic-light';
import { Dialog, Icon, Intent, Tooltip } from '@vegaprotocol/ui-toolkit';
import {
Dialog,
Icon,
Intent,
Tooltip,
TrafficLight,
} from '@vegaprotocol/ui-toolkit';
import { InputSetter } from '../../components/input-setter';
import { IconNames } from '@blueprintjs/icons';
import { DataTitle, ValueTooltipRow } from './deal-ticket-estimates';
import {
DataTitle,
EST_SLIPPAGE,
ValueTooltipRow,
} from '@vegaprotocol/deal-ticket';
interface DealTicketSlippageProps {
step?: number;
@ -40,12 +48,12 @@ export const DealTicketSlippage = ({
const formLabel = (
<label className="flex items-center mb-1">
<span className="mr-1">{t('Adjust slippage tolerance')}</span>
<Tooltip align="center" description={constants.EST_SLIPPAGE}>
<Tooltip align="center" description={EST_SLIPPAGE}>
<div className="cursor-help" tabIndex={-1}>
<Icon
name={IconNames.ISSUE}
className="block rotate-180"
ariaLabel={constants.EST_SLIPPAGE}
ariaLabel={EST_SLIPPAGE}
/>
</div>
</Tooltip>
@ -83,7 +91,7 @@ export const DealTicketSlippage = ({
<DataTitle>{t('Est. Price Impact / Slippage')}</DataTitle>
<div className="flex">
<div className="mr-1">
<ValueTooltipRow description={constants.EST_SLIPPAGE}>
<ValueTooltipRow description={EST_SLIPPAGE}>
<TrafficLight value={value} q1={1} q2={5}>
{value}%
</TrafficLight>

View File

@ -3,6 +3,13 @@ import { useNavigate } from 'react-router-dom';
import { useForm, Controller } from 'react-hook-form';
import { Stepper } from '../stepper';
import type { DealTicketMarketFragment } from '@vegaprotocol/deal-ticket';
import {
useOrderCloseOut,
useOrderMargin,
usePartyBalanceQuery,
useMaximumPositionSize,
useCalculateSlippage,
} from '@vegaprotocol/deal-ticket';
import {
getDefaultOrder,
useOrderValidation,
@ -12,12 +19,13 @@ import { InputError } from '@vegaprotocol/ui-toolkit';
import { BigNumber } from 'bignumber.js';
import { MarketSelector } from '@vegaprotocol/deal-ticket';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { useVegaWallet, VegaTxStatus } from '@vegaprotocol/wallet';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { VegaTxStatus } from '@vegaprotocol/wallet';
import {
t,
addDecimalsFormatNumber,
toDecimal,
removeDecimal,
addDecimalsFormatNumber,
} from '@vegaprotocol/react-helpers';
import {
useOrderSubmit,
@ -30,23 +38,14 @@ import { DealTicketSize } from './deal-ticket-size';
import MarketNameRenderer from '../simple-market-list/simple-market-renderer';
import SideSelector, { SIDE_NAMES } from './side-selector';
import ReviewTrade from './review-trade';
import type { PartyBalanceQuery } from './__generated__/PartyBalanceQuery';
import useOrderCloseOut from '../../hooks/use-order-closeout';
import useOrderMargin from '../../hooks/use-order-margin';
import useMaximumPositionSize from '../../hooks/use-maximum-position-size';
import useCalculateSlippage from '../../hooks/use-calculate-slippage';
import { Side, OrderType } from '@vegaprotocol/types';
import { DealTicketSlippage } from './deal-ticket-slippage';
interface DealTicketMarketProps {
market: DealTicketMarketFragment;
partyData?: PartyBalanceQuery;
}
export const DealTicketSteps = ({
market,
partyData,
}: DealTicketMarketProps) => {
export const DealTicketSteps = ({ market }: DealTicketMarketProps) => {
const navigate = useNavigate();
const setMarket = useCallback(
(marketId: string) => {
@ -68,15 +67,11 @@ export const DealTicketSteps = ({
const emptyString = ' - ';
const step = toDecimal(market.positionDecimalPlaces);
const orderType = watch('type');
const orderTimeInForce = watch('timeInForce');
const orderSide = watch('side');
const orderSize = watch('size');
const order = watch();
const { message: invalidText, isDisabled } = useOrderValidation({
market,
orderType,
orderTimeInForce,
orderType: order.type,
orderTimeInForce: order.timeInForce,
fieldErrors: errors,
});
const { submit, transaction, finalizedOrder, Dialog } = useOrderSubmit();
@ -87,9 +82,14 @@ export const DealTicketSteps = ({
partyId: pubKey || '',
});
const { data: partyBalance } = usePartyBalanceQuery({
variables: { partyId: pubKey || '' },
skip: !pubKey,
});
const maxTrade = useMaximumPositionSize({
partyId: pubKey || '',
accounts: partyData?.party?.accounts || [],
accounts: partyBalance?.party?.accounts || [],
marketId: market.id,
settlementAssetId:
market.tradableInstrument.instrument.product.settlementAsset.id,
@ -97,7 +97,11 @@ export const DealTicketSteps = ({
order,
});
const estCloseOut = useOrderCloseOut({ order, market, partyData });
const estCloseOut = useOrderCloseOut({
order,
market,
partyData: partyBalance,
});
const slippage = useCalculateSlippage({ marketId: market.id, order });
const [slippageValue, setSlippageValue] = useState(
slippage ? parseFloat(slippage) : 0
@ -130,26 +134,26 @@ export const DealTicketSteps = ({
const notionalSize = useMemo(() => {
if (price) {
const size = new BigNumber(price).multipliedBy(orderSize).toNumber();
const size = new BigNumber(price).multipliedBy(order.size).toNumber();
return addDecimalsFormatNumber(size, market.decimalPlaces);
}
return null;
}, [market.decimalPlaces, orderSize, price]);
}, [market.decimalPlaces, order.size, price]);
const fees = useMemo(() => {
if (estMargin?.fees && notionalSize) {
const percentage = new BigNumber(estMargin?.fees)
if (estMargin?.totalFees && notionalSize) {
const percentage = new BigNumber(estMargin?.totalFees)
.dividedBy(notionalSize)
.multipliedBy(100)
.decimalPlaces(2)
.toString();
return `${estMargin.fees} (${percentage}%)`;
return `${estMargin.totalFees} (${percentage}%)`;
}
return null;
}, [estMargin?.fees, notionalSize]);
}, [estMargin?.totalFees, notionalSize]);
const max = useMemo(() => {
return new BigNumber(maxTrade)
@ -157,6 +161,10 @@ export const DealTicketSteps = ({
.toNumber();
}, [market.positionDecimalPlaces, maxTrade]);
useEffect(() => {
setSlippageValue(slippage ? parseFloat(slippage) : 0);
}, [slippage]);
const onSizeChange = useCallback(
(value: number) => {
const newVal = new BigNumber(value)
@ -185,7 +193,7 @@ export const DealTicketSteps = ({
setValue('price', bestAskPrice);
if (orderType === OrderType.TYPE_MARKET) {
if (order.type === OrderType.TYPE_MARKET) {
setValue('type', OrderType.TYPE_LIMIT);
}
} else {
@ -199,7 +207,8 @@ export const DealTicketSteps = ({
market.decimalPlaces,
market?.depth?.lastTrade?.price,
order.side,
orderType,
order.type,
setSlippageValue,
setValue,
]
);
@ -246,7 +255,7 @@ export const DealTicketSteps = ({
)}
/>
),
value: SIDE_NAMES[orderSide] || '',
value: SIDE_NAMES[order.side] || '',
},
{
label: t('Choose Position Size'),
@ -258,7 +267,7 @@ export const DealTicketSteps = ({
min={step}
max={max}
onSizeChange={onSizeChange}
size={new BigNumber(orderSize).toNumber()}
size={new BigNumber(order.size).toNumber()}
name="size"
price={formattedPrice || emptyString}
positionDecimalPlaces={market.positionDecimalPlaces}
@ -267,7 +276,7 @@ export const DealTicketSteps = ({
.symbol
}
notionalSize={notionalSize || emptyString}
estCloseOut={estCloseOut}
estCloseOut={estCloseOut || emptyString}
fees={fees || emptyString}
estMargin={estMargin?.margin || emptyString}
/>
@ -277,9 +286,9 @@ export const DealTicketSteps = ({
/>
</>
) : (
'loading...'
t('Loading...')
),
value: orderSize,
value: order.size,
},
{
label: t('Review Trade'),
@ -297,7 +306,7 @@ export const DealTicketSteps = ({
isDisabled={isDisabled}
transactionStatus={transactionStatus}
order={order}
estCloseOut={estCloseOut}
estCloseOut={estCloseOut || emptyString}
estMargin={estMargin?.margin || emptyString}
price={formattedPrice || emptyString}
quoteName={

View File

@ -4,9 +4,9 @@ import {
KeyValueTable,
KeyValueTableRow,
} from '@vegaprotocol/ui-toolkit';
import * as React from 'react';
import classNames from 'classnames';
import type { DealTicketMarketFragment } from '@vegaprotocol/deal-ticket';
import { DealTicketEstimates } from '@vegaprotocol/deal-ticket';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { SIDE_NAMES } from './side-selector';
import { gql, useQuery } from '@apollo/client';
@ -14,7 +14,6 @@ import type {
MarketTags,
MarketTagsVariables,
} from './__generated__/MarketTags';
import { DealTicketEstimates } from './deal-ticket-estimates';
import { Side } from '@vegaprotocol/types';
import { MarketExpires } from '@vegaprotocol/market-info';

View File

@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import classNames from 'classnames';
import { useNavigate, useParams, Link } from 'react-router-dom';
import {
@ -14,11 +14,11 @@ import {
Icon,
} from '@vegaprotocol/ui-toolkit';
import { MarketState } from '@vegaprotocol/types';
import useMarketFiltersData from '../../hooks/use-markets-filter';
import type { Market } from '@vegaprotocol/market-list';
import { HorizontalMenu } from '../horizontal-menu';
import type { HorizontalMenuItem } from '../horizontal-menu';
import * as constants from './constants';
import { useMarketFilters } from '../../hooks/use-markets-filter';
interface Props {
data: Market[];
@ -27,7 +27,7 @@ interface Props {
const SimpleMarketToolbar = ({ data }: Props) => {
const navigate = useNavigate();
const params = useParams();
const { products, assetsPerProduct } = useMarketFiltersData(data);
const { products, assetsPerProduct } = useMarketFilters(data);
const [isOpen, setOpen] = useState(false);
const onStateChange = useCallback(

View File

@ -1,52 +0,0 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
// ====================================================
// GraphQL query operation: MarketMarkPrice
// ====================================================
export interface MarketMarkPrice_market_data {
__typename: "MarketData";
/**
* The mark price (an unsigned integer)
*/
markPrice: string;
}
export interface MarketMarkPrice_market {
__typename: "Market";
/**
* The number of decimal places that an integer must be shifted by in order to get a correct
* number denominated in the currency of the market. (uint64)
*
* Examples:
* Currency Balance decimalPlaces Real Balance
* GBP 100 0 GBP 100
* GBP 100 2 GBP 1.00
* GBP 100 4 GBP 0.01
* GBP 1 4 GBP 0.0001 ( 0.01p )
*
* GBX (pence) 100 0 GBP 1.00 (100p )
* GBX (pence) 100 2 GBP 0.01 ( 1p )
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
*/
decimalPlaces: number;
/**
* marketData for the given market
*/
data: MarketMarkPrice_market_data | null;
}
export interface MarketMarkPrice {
/**
* An instrument that is trading on the Vega network
*/
market: MarketMarkPrice_market | null;
}
export interface MarketMarkPriceVariables {
marketId: string;
}

View File

@ -1,118 +0,0 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { AccountType } from "@vegaprotocol/types";
// ====================================================
// GraphQL query operation: PartyMarketData
// ====================================================
export interface PartyMarketData_party_accounts_asset {
__typename: "Asset";
/**
* The ID of the asset
*/
id: string;
/**
* The precision of the asset. Should match the decimal precision of the asset on its native chain, e.g: for ERC20 assets, it is often 18
*/
decimals: number;
}
export interface PartyMarketData_party_accounts_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
}
export interface PartyMarketData_party_accounts {
__typename: "Account";
/**
* Account type (General, Margin, etc)
*/
type: AccountType;
/**
* Balance as string - current account balance (approx. as balances can be updated several times per second)
*/
balance: string;
/**
* Asset, the 'currency'
*/
asset: PartyMarketData_party_accounts_asset;
/**
* Market (only relevant to margin accounts)
*/
market: PartyMarketData_party_accounts_market | null;
}
export interface PartyMarketData_party_marginsConnection_edges_node_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
}
export interface PartyMarketData_party_marginsConnection_edges_node {
__typename: "MarginLevels";
/**
* Market in which the margin is required for this party
*/
market: PartyMarketData_party_marginsConnection_edges_node_market;
/**
* This is the minimum margin required for a party to place a new order on the network (unsigned integer)
*/
initialLevel: string;
/**
* Minimal margin for the position to be maintained in the network (unsigned integer)
*/
maintenanceLevel: string;
/**
* If the margin is between maintenance and search, the network will initiate a collateral search (unsigned integer)
*/
searchLevel: string;
}
export interface PartyMarketData_party_marginsConnection_edges {
__typename: "MarginEdge";
node: PartyMarketData_party_marginsConnection_edges_node;
}
export interface PartyMarketData_party_marginsConnection {
__typename: "MarginConnection";
/**
* The margin levels in this connection
*/
edges: PartyMarketData_party_marginsConnection_edges[] | null;
}
export interface PartyMarketData_party {
__typename: "Party";
/**
* Party identifier
*/
id: string;
/**
* Collateral accounts relating to a party
*/
accounts: PartyMarketData_party_accounts[] | null;
/**
* Margin levels for a market
*/
marginsConnection: PartyMarketData_party_marginsConnection | null;
}
export interface PartyMarketData {
/**
* An entity that is trading on the Vega network
*/
party: PartyMarketData_party | null;
}
export interface PartyMarketDataVariables {
partyId: string;
}

View File

@ -1,64 +0,0 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { Side, OrderTimeInForce, OrderType } from "@vegaprotocol/types";
// ====================================================
// GraphQL query operation: EstimateOrder
// ====================================================
export interface EstimateOrder_estimateOrder_fee {
__typename: "TradeFee";
/**
* The maker fee, paid by the aggressive party to the other party (the one who had an order in the book)
*/
makerFee: string;
/**
* The infrastructure fee, a fee paid to the validators to maintain the Vega network
*/
infrastructureFee: string;
/**
* The fee paid to the liquidity providers that committed liquidity to the market
*/
liquidityFee: string;
}
export interface EstimateOrder_estimateOrder_marginLevels {
__typename: "MarginLevels";
/**
* This is the minimum margin required for a party to place a new order on the network (unsigned integer)
*/
initialLevel: string;
}
export interface EstimateOrder_estimateOrder {
__typename: "OrderEstimate";
/**
* The estimated fee if the order was to trade
*/
fee: EstimateOrder_estimateOrder_fee;
/**
* The margin requirement for this order
*/
marginLevels: EstimateOrder_estimateOrder_marginLevels;
}
export interface EstimateOrder {
/**
* Return an estimation of the potential cost for a new order
*/
estimateOrder: EstimateOrder_estimateOrder;
}
export interface EstimateOrderVariables {
marketId: string;
partyId: string;
price?: string | null;
size: string;
side: Side;
timeInForce: OrderTimeInForce;
expiration?: string | null;
type: OrderType;
}

View File

@ -1,109 +0,0 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
import { AccountType } from "@vegaprotocol/types";
// ====================================================
// GraphQL query operation: MarketPositions
// ====================================================
export interface MarketPositions_party_accounts_asset {
__typename: "Asset";
/**
* The precision of the asset. Should match the decimal precision of the asset on its native chain, e.g: for ERC20 assets, it is often 18
*/
decimals: number;
}
export interface MarketPositions_party_accounts_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
}
export interface MarketPositions_party_accounts {
__typename: "Account";
/**
* Account type (General, Margin, etc)
*/
type: AccountType;
/**
* Balance as string - current account balance (approx. as balances can be updated several times per second)
*/
balance: string;
/**
* Asset, the 'currency'
*/
asset: MarketPositions_party_accounts_asset;
/**
* Market (only relevant to margin accounts)
*/
market: MarketPositions_party_accounts_market | null;
}
export interface MarketPositions_party_positionsConnection_edges_node_market {
__typename: "Market";
/**
* Market ID
*/
id: string;
}
export interface MarketPositions_party_positionsConnection_edges_node {
__typename: "Position";
/**
* Open volume (int64)
*/
openVolume: string;
/**
* Market relating to this position
*/
market: MarketPositions_party_positionsConnection_edges_node_market;
}
export interface MarketPositions_party_positionsConnection_edges {
__typename: "PositionEdge";
/**
* The position
*/
node: MarketPositions_party_positionsConnection_edges_node;
}
export interface MarketPositions_party_positionsConnection {
__typename: "PositionConnection";
/**
* The positions in this connection
*/
edges: MarketPositions_party_positionsConnection_edges[] | null;
}
export interface MarketPositions_party {
__typename: "Party";
/**
* Party identifier
*/
id: string;
/**
* Collateral accounts relating to a party
*/
accounts: MarketPositions_party_accounts[] | null;
/**
* Trading positions relating to a party
*/
positionsConnection: MarketPositions_party_positionsConnection | null;
}
export interface MarketPositions {
/**
* An entity that is trading on the Vega network
*/
party: MarketPositions_party | null;
}
export interface MarketPositionsVariables {
partyId: string;
}

View File

@ -1,38 +0,0 @@
import { gql, useQuery } from '@apollo/client';
import { useMemo, useRef } from 'react';
import type {
MarketMarkPrice,
MarketMarkPriceVariables,
} from './__generated__/MarketMarkPrice';
const MARKET_MARK_PRICE = gql`
query MarketMarkPrice($marketId: ID!) {
market(id: $marketId) {
decimalPlaces
data {
markPrice
}
}
}
`;
export default (marketId: string) => {
const memoRef = useRef<MarketMarkPrice | null>(null);
const { data } = useQuery<MarketMarkPrice, MarketMarkPriceVariables>(
MARKET_MARK_PRICE,
{
pollInterval: 5000,
variables: { marketId },
skip: !marketId,
}
);
return useMemo(() => {
if (
data &&
data.market?.data?.markPrice !== memoRef.current?.market?.data?.markPrice
) {
memoRef.current = data;
}
return memoRef.current;
}, [data, memoRef]);
};

View File

@ -1,78 +0,0 @@
import { gql, useQuery } from '@apollo/client';
import { BigNumber } from 'bignumber.js';
import type {
MarketPositions,
MarketPositionsVariables,
} from './__generated__/marketPositions';
const MARKET_POSITIONS_QUERY = gql`
query MarketPositions($partyId: ID!) {
party(id: $partyId) {
id
accounts {
type
balance
asset {
decimals
}
market {
id
}
}
positionsConnection {
edges {
node {
openVolume
market {
id
}
}
}
}
}
}
`;
interface Props {
marketId: string;
partyId: string;
}
export type PositionMargin = {
openVolume: BigNumber;
balance: BigNumber;
balanceDecimals?: number;
} | null;
export default ({ marketId, partyId }: Props): PositionMargin => {
const { data } = useQuery<MarketPositions, MarketPositionsVariables>(
MARKET_POSITIONS_QUERY,
{
pollInterval: 5000,
variables: { partyId },
skip: !partyId,
fetchPolicy: 'no-cache',
}
);
const account = data?.party?.accounts?.find(
(nodes) => nodes.market?.id === marketId
);
if (account) {
const balance = new BigNumber(account.balance || 0);
const openVolume = new BigNumber(
data?.party?.positionsConnection?.edges?.find(
(nodes) => nodes.node.market.id === marketId
)?.node.openVolume || 0
);
if (!balance.isZero() && !openVolume.isZero()) {
return {
balance,
balanceDecimals: account?.asset.decimals,
openVolume,
};
}
}
return null;
};

View File

@ -1,7 +1,7 @@
import type { Market } from '@vegaprotocol/market-list';
import { useEffect, useState } from 'react';
const useMarketFilters = (data: Market[]) => {
export const useMarketFilters = (data: Market[]) => {
const [products, setProducts] = useState<string[]>([]);
const [assetsPerProduct, setAssetsPerProduct] = useState<
Record<string, string[]>
@ -36,5 +36,3 @@ const useMarketFilters = (data: Market[]) => {
}, [data]);
return { products, assetsPerProduct };
};
export default useMarketFilters;

View File

@ -1,119 +0,0 @@
import { BigNumber } from 'bignumber.js';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { gql, useQuery } from '@apollo/client';
import type {
EstimateOrder,
EstimateOrderVariables,
EstimateOrder_estimateOrder_fee,
} from './__generated__/estimateOrder';
import type { DealTicketMarketFragment } from '@vegaprotocol/deal-ticket';
import { Side } from '@vegaprotocol/types';
import { addDecimal, removeDecimal } from '@vegaprotocol/react-helpers';
import useMarketPositions from './use-market-positions';
import useMarketData from './use-market-data';
export const ESTIMATE_ORDER_QUERY = gql`
query EstimateOrder(
$marketId: ID!
$partyId: ID!
$price: String
$size: String!
$side: Side!
$timeInForce: OrderTimeInForce!
$expiration: String
$type: OrderType!
) {
estimateOrder(
marketId: $marketId
partyId: $partyId
price: $price
size: $size
side: $side
timeInForce: $timeInForce
expiration: $expiration
type: $type
) {
fee {
makerFee
infrastructureFee
liquidityFee
}
marginLevels {
initialLevel
}
}
}
`;
interface Props {
order: OrderSubmissionBody['orderSubmission'];
market: DealTicketMarketFragment;
partyId: string;
}
const addFees = (feeObj: EstimateOrder_estimateOrder_fee) => {
return new BigNumber(feeObj.makerFee)
.plus(feeObj.liquidityFee)
.plus(feeObj.infrastructureFee);
};
export interface OrderMargin {
margin: string;
fees: string | null;
}
const useOrderMargin = ({
order,
market,
partyId,
}: Props): OrderMargin | null => {
const marketPositions = useMarketPositions({ marketId: market.id, partyId });
const markPriceData = useMarketData(market.id);
const { data } = useQuery<EstimateOrder, EstimateOrderVariables>(
ESTIMATE_ORDER_QUERY,
{
variables: {
marketId: market.id,
partyId,
price: markPriceData?.market?.data?.markPrice || '',
size: removeDecimal(
BigNumber.maximum(
0,
new BigNumber(marketPositions?.openVolume || 0)[
order.side === Side.SIDE_BUY ? 'plus' : 'minus'
](order.size)
).toString(),
market.positionDecimalPlaces
),
side: order.side === Side.SIDE_BUY ? Side.SIDE_BUY : Side.SIDE_SELL,
timeInForce: order.timeInForce,
type: order.type,
},
skip:
!partyId ||
!market.id ||
!order.size ||
!markPriceData?.market?.data?.markPrice,
}
);
if (data?.estimateOrder.marginLevels.initialLevel) {
const fees =
data?.estimateOrder?.fee && addFees(data.estimateOrder.fee).toString();
return {
margin: addDecimal(
BigNumber.maximum(
0,
new BigNumber(data.estimateOrder.marginLevels.initialLevel).minus(
marketPositions?.balance || 0
)
).toString(),
market.decimalPlaces
),
fees: addDecimal(fees, market.decimalPlaces),
};
}
return null;
};
export default useOrderMargin;

View File

@ -230,7 +230,7 @@ describe('deal ticket validation', { tags: '@smoke' }, () => {
it('order connect vega wallet button should connect', () => {
cy.getByTestId(toggleLimit).click();
cy.getByTestId(orderPriceField).type('101');
cy.getByTestId(orderPriceField).clear().type('101');
cy.getByTestId('order-connect-wallet').click();
cy.getByTestId('dialog-content').should('be.visible');
cy.getByTestId('connectors-list')

View File

@ -14,6 +14,13 @@ export const generateDealTicketQuery = (
positionDecimalPlaces: 0,
state: MarketState.STATE_ACTIVE,
tradingMode: MarketTradingMode.TRADING_MODE_CONTINUOUS,
fees: {
factors: {
makerFee: '0.0002',
infrastructureFee: '0.0005',
liquidityFee: '0.0005',
},
},
tradableInstrument: {
__typename: 'TradableInstrument',
instrument: {

View File

@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
addDecimalsFormatNumber,
formatNumberPercentage,
PriceCell,
signedNumberCssClass,
t,
@ -13,19 +12,15 @@ import {
MarketTradingModeMapping,
} from '@vegaprotocol/types';
import { PriceCellChange, Sparkline, Tooltip } from '@vegaprotocol/ui-toolkit';
import BigNumber from 'bignumber.js';
import Link from 'next/link';
import {
calcCandleHigh,
calcCandleLow,
totalFees,
} from '@vegaprotocol/market-list';
import { calcCandleHigh, calcCandleLow } from '@vegaprotocol/market-list';
import type { CandleClose } from '@vegaprotocol/types';
import type {
MarketWithData,
MarketWithCandles,
} from '@vegaprotocol/market-list';
import isNil from 'lodash/isNil';
import { FeesCell } from '@vegaprotocol/market-info';
type Market = MarketWithData & MarketWithCandles;
@ -502,43 +497,3 @@ export const columnsPositionMarkets = (
];
return selectMarketColumns;
};
const FeesCell = ({
feeFactors,
}: {
feeFactors: Market['fees']['factors'];
}) => (
<Tooltip description={<FeesBreakdown feeFactors={feeFactors} />}>
<span>{totalFees(feeFactors) ?? '-'}</span>
</Tooltip>
);
export const FeesBreakdown = ({
feeFactors,
}: {
feeFactors?: Market['fees']['factors'];
}) => {
if (!feeFactors) return null;
return (
<dl className="grid grid-cols-2 gap-x-2">
<dt>{t('Infrastructure fee')}</dt>
<dd className="text-right">
{formatNumberPercentage(
new BigNumber(feeFactors.infrastructureFee).times(100)
)}
</dd>
<dt>{t('Liquidity fee')}</dt>
<dd className="text-right">
{formatNumberPercentage(
new BigNumber(feeFactors.liquidityFee).times(100)
)}
</dd>
<dt>{t('Maker fee')}</dt>
<dd className="text-right">
{formatNumberPercentage(new BigNumber(feeFactors.makerFee).times(100))}
</dd>
<dt>{t('Total fees')}</dt>
<dd className="text-right">{totalFees(feeFactors)}</dd>
</dl>
);
};

View File

@ -1,10 +1,9 @@
import React from 'react';
import type { ReactNode } from 'react';
import { t } from '@vegaprotocol/react-helpers';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import { Icon, Tooltip, TrafficLight } from '@vegaprotocol/ui-toolkit';
import { IconNames } from '@blueprintjs/icons';
import * as constants from './constants';
import { TrafficLight } from '../traffic-light';
interface DealTicketEstimatesProps {
quoteName?: string;
@ -17,45 +16,6 @@ interface DealTicketEstimatesProps {
slippage?: string;
}
interface DataTitleProps {
children: ReactNode;
quoteName?: string;
}
export const DataTitle = ({ children, quoteName = '' }: DataTitleProps) => (
<dt>
{children}
{quoteName && <small> ({quoteName})</small>}
</dt>
);
interface ValueTooltipProps {
value?: string;
children?: ReactNode;
description: string;
id?: string;
}
export const ValueTooltipRow = ({
value,
children,
description,
id,
}: ValueTooltipProps) => (
<dd className="flex gap-x-2 items-center">
{value || children}
<Tooltip align="center" description={description}>
<div className="cursor-help" id={id || ''} tabIndex={-1}>
<Icon
name={IconNames.ISSUE}
className="block rotate-180"
ariaLabel={description}
/>
</div>
</Tooltip>
</dd>
);
export const DealTicketEstimates = ({
price,
quoteName,
@ -131,3 +91,42 @@ export const DealTicketEstimates = ({
)}
</dl>
);
interface DataTitleProps {
children: ReactNode;
quoteName?: string;
}
export const DataTitle = ({ children, quoteName = '' }: DataTitleProps) => (
<dt>
{children}
{quoteName && <small> ({quoteName})</small>}
</dt>
);
interface ValueTooltipProps {
value?: string;
children?: ReactNode;
description: string;
id?: string;
}
export const ValueTooltipRow = ({
value,
children,
description,
id,
}: ValueTooltipProps) => (
<dd className="flex gap-x-2 items-center">
{value || children}
<Tooltip align="center" description={description}>
<div className="cursor-help" id={id || ''} tabIndex={-1}>
<Icon
name={IconNames.ISSUE}
className="block rotate-180"
ariaLabel={description}
/>
</div>
</Tooltip>
</dd>
);

View File

@ -12,7 +12,7 @@ import type { ValidationProps } from './use-order-validation';
import { marketTranslations } from './use-order-validation';
import { useOrderValidation } from './use-order-validation';
import { ERROR_SIZE_DECIMAL } from './validate-size';
import type { DealTicketMarketFragment } from '../deal-ticket/__generated__/DealTicket';
import type { DealTicketMarketFragment } from '../deal-ticket/__generated___/DealTicket';
jest.mock('@vegaprotocol/wallet');

View File

@ -16,6 +16,13 @@ fragment DealTicketMarket on Market {
auctionEnd
trigger
}
fees {
factors {
makerFee
infrastructureFee
liquidityFee
}
}
tradableInstrument {
instrument {
id

View File

@ -3,14 +3,14 @@ import { Schema as Types } from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type DealTicketMarketFragment = { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, data?: { __typename?: 'MarketData', indicativePrice: string, indicativeVolume: string, targetStake?: string | null, suppliedStake?: string | null, auctionStart?: string | null, auctionEnd?: string | null, trigger: Types.AuctionTrigger, market: { __typename?: 'Market', id: string } } | null, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, name: string } } } }, depth: { __typename?: 'MarketDepth', lastTrade?: { __typename?: 'Trade', price: string } | null } };
export type DealTicketMarketFragment = { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, data?: { __typename?: 'MarketData', indicativePrice: string, indicativeVolume: string, targetStake?: string | null, suppliedStake?: string | null, auctionStart?: string | null, auctionEnd?: string | null, trigger: Types.AuctionTrigger, market: { __typename?: 'Market', id: string } } | null, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, name: string } } } }, depth: { __typename?: 'MarketDepth', lastTrade?: { __typename?: 'Trade', price: string } | null } };
export type DealTicketQueryVariables = Types.Exact<{
marketId: Types.Scalars['ID'];
}>;
export type DealTicketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, data?: { __typename?: 'MarketData', indicativePrice: string, indicativeVolume: string, targetStake?: string | null, suppliedStake?: string | null, auctionStart?: string | null, auctionEnd?: string | null, trigger: Types.AuctionTrigger, market: { __typename?: 'Market', id: string } } | null, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, name: string } } } }, depth: { __typename?: 'MarketDepth', lastTrade?: { __typename?: 'Trade', price: string } | null } } | null };
export type DealTicketQuery = { __typename?: 'Query', market?: { __typename?: 'Market', id: string, decimalPlaces: number, positionDecimalPlaces: number, state: Types.MarketState, tradingMode: Types.MarketTradingMode, data?: { __typename?: 'MarketData', indicativePrice: string, indicativeVolume: string, targetStake?: string | null, suppliedStake?: string | null, auctionStart?: string | null, auctionEnd?: string | null, trigger: Types.AuctionTrigger, market: { __typename?: 'Market', id: string } } | null, fees: { __typename?: 'Fees', factors: { __typename?: 'FeeFactors', makerFee: string, infrastructureFee: string, liquidityFee: string } }, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', id: string, name: string, product: { __typename?: 'Future', quoteName: string, settlementAsset: { __typename?: 'Asset', id: string, symbol: string, decimals: number, name: string } } } }, depth: { __typename?: 'MarketDepth', lastTrade?: { __typename?: 'Trade', price: string } | null } } | null };
export const DealTicketMarketFragmentDoc = gql`
fragment DealTicketMarket on Market {
@ -31,6 +31,13 @@ export const DealTicketMarketFragmentDoc = gql`
auctionEnd
trigger
}
fees {
factors {
makerFee
infrastructureFee
liquidityFee
}
}
tradableInstrument {
instrument {
id

View File

@ -0,0 +1,37 @@
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import type { ReactNode } from 'react';
interface DealTicketFeeDetailsProps {
details: DealTicketFeeDetails[];
}
export interface DealTicketFeeDetails {
label: string;
value?: string | number | null;
labelDescription?: string | ReactNode;
quoteName?: string;
}
export const DealTicketFeeDetails = ({
details,
}: DealTicketFeeDetailsProps) => {
return (
<div>
{details.map(({ label, value, labelDescription, quoteName }) => (
<div
key={label}
className="text-sm mt-2 flex justify-between items-center gap-4 flex-wrap"
>
<div>
<Tooltip description={labelDescription}>
<div>{label}</div>
</Tooltip>
</div>
<div className="text-neutral-500 dark:text-neutral-300">{`${
value ?? '-'
} ${quoteName || ''}`}</div>
</div>
))}
</div>
);
};

View File

@ -1,5 +1,5 @@
import { FormGroup, Input, Tooltip } from '@vegaprotocol/ui-toolkit';
import { t, toDecimal } from '@vegaprotocol/react-helpers';
import { formatNumber, t, toDecimal } from '@vegaprotocol/react-helpers';
import type { DealTicketAmountProps } from './deal-ticket-amount';
import { validateSize } from '../deal-ticket-validation/validate-size';
import { isMarketInAuction } from '../deal-ticket-validation/use-order-validation';
@ -52,7 +52,7 @@ export const DealTicketMarketAmount = ({
<div className="text-sm text-right">
{price && quoteName ? (
<>
~{price} {quoteName}
~{formatNumber(price, market.decimalPlaces)} {quoteName}
</>
) : (
'-'

View File

@ -1,10 +1,14 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { VegaWalletContext } from '@vegaprotocol/wallet';
import { addDecimal } from '@vegaprotocol/react-helpers';
import { fireEvent, render, screen, act } from '@testing-library/react';
import { DealTicket } from './deal-ticket';
import type { DealTicketMarketFragment } from './__generated___/DealTicket';
import { Schema } from '@vegaprotocol/types';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import type { ChainIdQuery } from '@vegaprotocol/react-helpers';
import { ChainIdDocument, addDecimal } from '@vegaprotocol/react-helpers';
const market: DealTicketMarketFragment = {
__typename: 'Market',
@ -32,6 +36,13 @@ const market: DealTicketMarketFragment = {
},
},
},
fees: {
factors: {
makerFee: '0.001',
infrastructureFee: '0.002',
liquidityFee: '0.003',
},
},
depth: {
__typename: 'MarketDepth',
lastTrade: {
@ -43,22 +54,37 @@ const market: DealTicketMarketFragment = {
const submit = jest.fn();
const transactionStatus = 'default';
const mockChainId = 'chain-id';
function generateJsx(order?: OrderSubmissionBody['orderSubmission']) {
const chainIdMock: MockedResponse<ChainIdQuery> = {
request: {
query: ChainIdDocument,
},
result: {
data: {
statistics: {
chainId: mockChainId,
},
},
},
};
return (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
<VegaWalletContext.Provider value={{} as any}>
<DealTicket
defaultOrder={order}
market={market}
submit={submit}
transactionStatus={transactionStatus}
/>
</VegaWalletContext.Provider>
<MockedProvider mocks={[chainIdMock]}>
<VegaWalletContext.Provider value={{} as any}>
<DealTicket
defaultOrder={order}
market={market}
submit={submit}
transactionStatus={transactionStatus}
/>
</VegaWalletContext.Provider>
</MockedProvider>
);
}
describe('DealTicket', () => {
it('Displays ticket defaults', () => {
it('should display ticket defaults', () => {
render(generateJsx());
// Assert defaults are used
@ -87,7 +113,7 @@ describe('DealTicket', () => {
);
});
it('Can edit deal ticket', async () => {
it('can edit deal ticket', async () => {
render(generateJsx());
// BUY is selected by default
@ -119,7 +145,7 @@ describe('DealTicket', () => {
);
});
it('Handles TIF select box dependent on order type', () => {
it('handles TIF select box dependent on order type', () => {
render(generateJsx());
// Check only IOC and

View File

@ -1,10 +1,6 @@
import { useCallback } from 'react';
import { useCallback, useEffect } from 'react';
import { useForm, Controller } from 'react-hook-form';
import {
t,
addDecimalsFormatNumber,
removeDecimal,
} from '@vegaprotocol/react-helpers';
import { t, removeDecimal, addDecimal } from '@vegaprotocol/react-helpers';
import { Button, InputError } from '@vegaprotocol/ui-toolkit';
import { TypeSelector } from './type-selector';
import { SideSelector } from './side-selector';
@ -20,6 +16,11 @@ import {
isMarketInAuction,
useOrderValidation,
} from '../deal-ticket-validation/use-order-validation';
import { DealTicketFeeDetails } from './deal-ticket-fee-details';
import {
useFeeDealTicketDetails,
getFeeDetailsValues,
} from '../../hooks/use-fee-deal-ticket-details';
export type TransactionStatus = 'default' | 'pending';
@ -44,18 +45,17 @@ export const DealTicket = ({
control,
handleSubmit,
watch,
setValue,
formState: { errors },
} = useForm<OrderSubmissionBody['orderSubmission']>({
mode: 'onChange',
defaultValues: getDefaultOrder(market),
});
const orderType = watch('type');
const orderTimeInForce = watch('timeInForce');
const order = watch();
const { message, isDisabled: disabled } = useOrderValidation({
market,
orderType,
orderTimeInForce,
orderType: order.type,
orderTimeInForce: order.timeInForce,
fieldErrors: errors,
});
const isDisabled = transactionStatus === 'pending' || disabled;
@ -78,20 +78,31 @@ export const DealTicket = ({
[isDisabled, submit, market.decimalPlaces, market.positionDecimalPlaces]
);
const getPrice = () => {
const getEstimatedMarketPrice = () => {
if (isMarketInAuction(market)) {
// 0 can never be a valid uncrossing price as it would require there being orders on the book at that price.
// 0 can never be a valid uncrossing price
// as it would require there being orders on the book at that price.
if (
market.data?.indicativePrice &&
BigInt(market.data?.indicativePrice) !== BigInt(0)
) {
return market.data.indicativePrice;
}
return '-';
return undefined;
}
return market.depth.lastTrade?.price;
};
const price = getPrice();
const marketPrice = getEstimatedMarketPrice();
const marketPriceFormatted =
marketPrice && addDecimal(marketPrice, market.decimalPlaces);
useEffect(() => {
if (marketPriceFormatted && order.type === OrderType.TYPE_MARKET) {
setValue('price', marketPriceFormatted);
}
}, [marketPriceFormatted, order.type, setValue]);
const feeDetails = useFeeDealTicketDetails(order, market);
const details = getFeeDetailsValues(feeDetails);
return (
<form onSubmit={handleSubmit(onSubmit)} className="p-4" noValidate>
@ -110,14 +121,10 @@ export const DealTicket = ({
)}
/>
<DealTicketAmount
orderType={orderType}
orderType={order.type}
market={market}
register={register}
price={
price
? addDecimalsFormatNumber(price, market.decimalPlaces)
: undefined
}
price={order.price}
quoteName={market.tradableInstrument.instrument.product.quoteName}
/>
<Controller
@ -126,13 +133,13 @@ export const DealTicket = ({
render={({ field }) => (
<TimeInForceSelector
value={field.value}
orderType={orderType}
orderType={order.type}
onSelect={field.onChange}
/>
)}
/>
{orderType === OrderType.TYPE_LIMIT &&
orderTimeInForce === OrderTimeInForce.TIME_IN_FORCE_GTT && (
{order.type === OrderType.TYPE_LIMIT &&
order.timeInForce === OrderTimeInForce.TIME_IN_FORCE_GTT && (
<Controller
name="expiresAt"
control={control}
@ -174,6 +181,7 @@ export const DealTicket = ({
{t('Connect wallet')}
</Button>
)}
<DealTicketFeeDetails details={details} />
</form>
);
};

View File

@ -1,3 +1,5 @@
export * from './deal-ticket';
export * from './deal-ticket-validation';
export * from './trading-mode-tooltip';
export * from './deal-ticket-estimates';
export * from './constants';

View File

@ -26,5 +26,6 @@ query EstimateOrder(
marginLevels {
initialLevel
}
totalFeeAmount
}
}

View File

@ -3,6 +3,9 @@ query MarketMarkPrice($marketId: ID!) {
decimalPlaces
data {
markPrice
market {
id
}
}
}
}

View File

@ -0,0 +1,18 @@
query PartyBalance($partyId: ID!) {
party(id: $partyId) {
accounts {
...Account
}
}
}
fragment Account on Account {
type
balance
asset {
id
symbol
name
decimals
}
}

View File

@ -15,7 +15,7 @@ export type EstimateOrderQueryVariables = Types.Exact<{
}>;
export type EstimateOrderQuery = { __typename?: 'Query', estimateOrder: { __typename?: 'OrderEstimate', fee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string }, marginLevels: { __typename?: 'MarginLevels', initialLevel: string } } };
export type EstimateOrderQuery = { __typename?: 'Query', estimateOrder: { __typename?: 'OrderEstimate', totalFeeAmount: string, fee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string }, marginLevels: { __typename?: 'MarginLevels', initialLevel: string } } };
export const EstimateOrderDocument = gql`
@ -38,6 +38,7 @@ export const EstimateOrderDocument = gql`
marginLevels {
initialLevel
}
totalFeeAmount
}
}
`;

View File

@ -8,7 +8,7 @@ export type MarketMarkPriceQueryVariables = Types.Exact<{
}>;
export type MarketMarkPriceQuery = { __typename?: 'Query', market?: { __typename?: 'Market', decimalPlaces: number, data?: { __typename?: 'MarketData', markPrice: string } | null } | null };
export type MarketMarkPriceQuery = { __typename?: 'Query', market?: { __typename?: 'Market', decimalPlaces: number, data?: { __typename?: 'MarketData', markPrice: string, market: { __typename?: 'Market', id: string } } | null } | null };
export const MarketMarkPriceDocument = gql`
@ -17,6 +17,9 @@ export const MarketMarkPriceDocument = gql`
decimalPlaces
data {
markPrice
market {
id
}
}
}
}

View File

@ -0,0 +1,63 @@
import { Schema as Types } from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type PartyBalanceQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID'];
}>;
export type PartyBalanceQuery = { __typename?: 'Query', party?: { __typename?: 'Party', accounts?: Array<{ __typename?: 'Account', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number } }> | null } | null };
export type AccountFragment = { __typename?: 'Account', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', id: string, symbol: string, name: string, decimals: number } };
export const AccountFragmentDoc = gql`
fragment Account on Account {
type
balance
asset {
id
symbol
name
decimals
}
}
`;
export const PartyBalanceDocument = gql`
query PartyBalance($partyId: ID!) {
party(id: $partyId) {
accounts {
...Account
}
}
}
${AccountFragmentDoc}`;
/**
* __usePartyBalanceQuery__
*
* To run a query within a React component, call `usePartyBalanceQuery` and pass it any options that fit your needs.
* When your component renders, `usePartyBalanceQuery` 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 } = usePartyBalanceQuery({
* variables: {
* partyId: // value for 'partyId'
* },
* });
*/
export function usePartyBalanceQuery(baseOptions: Apollo.QueryHookOptions<PartyBalanceQuery, PartyBalanceQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<PartyBalanceQuery, PartyBalanceQueryVariables>(PartyBalanceDocument, options);
}
export function usePartyBalanceLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<PartyBalanceQuery, PartyBalanceQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<PartyBalanceQuery, PartyBalanceQueryVariables>(PartyBalanceDocument, options);
}
export type PartyBalanceQueryHookResult = ReturnType<typeof usePartyBalanceQuery>;
export type PartyBalanceLazyQueryHookResult = ReturnType<typeof usePartyBalanceLazyQuery>;
export type PartyBalanceQueryResult = Apollo.QueryResult<PartyBalanceQuery, PartyBalanceQueryVariables>;

View File

@ -0,0 +1,13 @@
export * from './__generated__/EstimateOrder';
export * from './__generated__/MarketMarkPrice';
export * from './__generated__/MarketPositions';
export * from './__generated__/PartyBalance';
export * from './__generated__/PartyMarketData';
export * from './use-calculate-slippage';
export * from './use-fee-deal-ticket-details';
export * from './use-market-data-mark-price';
export * from './use-market-positions';
export * from './use-maximum-position-size';
export * from './use-order-closeout';
export * from './use-order-margin';
export * from './use-settlement-account';

View File

@ -2,7 +2,7 @@ import { MockedProvider } from '@apollo/client/testing';
import { renderHook } from '@testing-library/react';
import { Side } from '@vegaprotocol/types';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import useCalculateSlippage from './use-calculate-slippage';
import { useCalculateSlippage } from './use-calculate-slippage';
const mockData = {
decimalPlaces: 0,

View File

@ -16,7 +16,7 @@ interface Props {
order: OrderSubmissionBody['orderSubmission'];
}
const useCalculateSlippage = ({ marketId, order }: Props) => {
export const useCalculateSlippage = ({ marketId, order }: Props) => {
const variables = useMemo(() => ({ marketId }), [marketId]);
const { data } = useOrderBookData({
variables,
@ -69,5 +69,3 @@ const useCalculateSlippage = ({ marketId, order }: Props) => {
}
return null;
};
export default useCalculateSlippage;

View File

@ -0,0 +1,142 @@
import { FeesBreakdown } from '@vegaprotocol/market-info';
import { formatNumber, t } from '@vegaprotocol/react-helpers';
import { Side } from '@vegaprotocol/types';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { useVegaWallet } from '@vegaprotocol/wallet';
import BigNumber from 'bignumber.js';
import { useMemo } from 'react';
import type { DealTicketMarketFragment } from '../components';
import {
NOTIONAL_SIZE_TOOLTIP_TEXT,
EST_MARGIN_TOOLTIP_TEXT,
EST_CLOSEOUT_TOOLTIP_TEXT,
} from '../components/constants';
import { useCalculateSlippage } from './use-calculate-slippage';
import { useOrderCloseOut } from './use-order-closeout';
import type { OrderMargin } from './use-order-margin';
import { useOrderMargin } from './use-order-margin';
import { usePartyBalanceQuery } from './__generated__/PartyBalance';
export const useFeeDealTicketDetails = (
order: OrderSubmissionBody['orderSubmission'],
market: DealTicketMarketFragment
) => {
const { pubKey } = useVegaWallet();
const slippage = useCalculateSlippage({ marketId: market.id, order });
const price = useMemo(() => {
const estPrice = order.price || market.depth.lastTrade?.price;
if (estPrice) {
if (slippage && parseFloat(slippage) !== 0) {
const isLong = order.side === Side.SIDE_BUY;
const multiplier = new BigNumber(1)[isLong ? 'plus' : 'minus'](
parseFloat(slippage) / 100
);
return new BigNumber(estPrice).multipliedBy(multiplier).toNumber();
}
return order.price;
}
return null;
}, [market.depth.lastTrade?.price, order.price, order.side, slippage]);
const estMargin: OrderMargin | null = useOrderMargin({
order,
market,
partyId: pubKey || '',
});
const { data: partyBalance } = usePartyBalanceQuery({
variables: { partyId: pubKey || '' },
skip: !pubKey,
});
const estCloseOut = useOrderCloseOut({
order,
market,
partyData: partyBalance,
});
const notionalSize = useMemo(() => {
if (order.price && order.size) {
return new BigNumber(order.size).multipliedBy(order.price).toString();
}
return null;
}, [order.price, order.size]);
const quoteName = market.tradableInstrument.instrument.product.quoteName;
return {
market,
quoteName,
notionalSize,
estMargin,
estCloseOut,
slippage,
price,
partyData: partyBalance,
};
};
export interface FeeDetails {
market: DealTicketMarketFragment;
quoteName: string;
notionalSize: string | null;
estMargin: OrderMargin | null;
estCloseOut: string | null;
slippage: string | null;
price?: string | number | null;
}
export const getFeeDetailsValues = ({
quoteName,
notionalSize,
estMargin,
estCloseOut,
market,
}: FeeDetails) => {
const formatValue = (value: string | number | null | undefined): string => {
return value && !isNaN(Number(value))
? formatNumber(value, market.decimalPlaces)
: '-';
};
return [
{
label: t('Notional value'),
value: formatValue(notionalSize),
quoteName,
labelDescription: NOTIONAL_SIZE_TOOLTIP_TEXT,
},
{
label: t('Fees'),
value: estMargin?.totalFees && `~${formatValue(estMargin?.totalFees)}`,
labelDescription: (
<>
<span>
{t(
'The most you would be expected to pay in fees, the actual amount may vary.'
)}
</span>
<FeesBreakdown
fees={estMargin?.fees}
feeFactors={market.fees.factors}
quoteName={quoteName}
/>
</>
),
quoteName,
},
{
label: t('Margin required'),
value: estMargin?.margin && `~${formatValue(estMargin?.margin)}`,
quoteName,
labelDescription: EST_MARGIN_TOOLTIP_TEXT,
},
{
label: t('Liquidation price (variable)'),
value: formatValue(estCloseOut),
quoteName,
labelDescription: EST_CLOSEOUT_TOOLTIP_TEXT,
},
];
};

View File

@ -0,0 +1,21 @@
import { useMemo, useRef } from 'react';
import type { MarketMarkPriceQuery } from './__generated__/MarketMarkPrice';
import { useMarketMarkPriceQuery } from './__generated__/MarketMarkPrice';
export const useMarketDataMarkPrice = (marketId: string) => {
const memoRef = useRef<MarketMarkPriceQuery | null>(null);
const { data } = useMarketMarkPriceQuery({
pollInterval: 5000,
variables: { marketId },
skip: !marketId,
});
return useMemo(() => {
if (
data &&
data.market?.data?.markPrice !== memoRef.current?.market?.data?.markPrice
) {
memoRef.current = data;
}
return memoRef.current;
}, [data, memoRef]);
};

View File

@ -1,5 +1,5 @@
import { renderHook } from '@testing-library/react';
import useMarketPositions from './use-market-positions';
import { useMarketPositions } from './use-market-positions';
let mockNotEmptyData = {
party: {

View File

@ -0,0 +1,46 @@
import { BigNumber } from 'bignumber.js';
import { useMarketPositionsQuery } from './__generated__/MarketPositions';
interface Props {
marketId: string;
partyId: string;
}
export type PositionMargin = {
openVolume: BigNumber;
balance: BigNumber;
balanceDecimals?: number;
} | null;
export const useMarketPositions = ({
marketId,
partyId,
}: Props): PositionMargin => {
const { data } = useMarketPositionsQuery({
pollInterval: 5000,
variables: { partyId },
fetchPolicy: 'no-cache',
});
const account = data?.party?.accounts?.find(
(nodes) => nodes.market?.id === marketId
);
if (account) {
const positionConnectionNode =
data?.party?.positionsConnection?.edges?.find(
(nodes) => nodes.node.market.id === marketId
);
const balance = new BigNumber(account.balance || 0);
const openVolume = new BigNumber(
positionConnectionNode?.node.openVolume || 0
);
if (!balance.isZero() && !openVolume.isZero()) {
return {
balance,
balanceDecimals: account?.asset.decimals,
openVolume,
};
}
}
return null;
};

View File

@ -1,6 +1,4 @@
import { renderHook } from '@testing-library/react';
import useMaximumPositionSize from './use-maximum-position-size';
import type { PartyBalanceQuery_party_accounts } from '../components/deal-ticket/__generated__/PartyBalanceQuery';
import {
AccountType,
OrderTimeInForce,
@ -9,6 +7,9 @@ import {
} from '@vegaprotocol/types';
import type { PositionMargin } from './use-market-positions';
import { BigNumber } from 'bignumber.js';
import { useMaximumPositionSize } from './use-maximum-position-size';
import type { AccountFragment as Account } from './__generated__/PartyBalance';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
const defaultMockMarketPositions = {
openVolume: new BigNumber(1),
@ -17,7 +18,7 @@ const defaultMockMarketPositions = {
let mockMarketPositions: PositionMargin | null = defaultMockMarketPositions;
const mockAccount: PartyBalanceQuery_party_accounts = {
const mockAccount: Account = {
__typename: 'Account',
type: AccountType.ACCOUNT_TYPE_GENERAL,
balance: '200000',
@ -30,11 +31,12 @@ const mockAccount: PartyBalanceQuery_party_accounts = {
},
};
const mockOrder = {
const mockOrder: OrderSubmissionBody['orderSubmission'] = {
type: OrderType.TYPE_MARKET,
size: '1',
side: Side.SIDE_BUY,
timeInForce: OrderTimeInForce.TIME_IN_FORCE_IOC,
marketId: 'market-id',
};
jest.mock('./use-settlement-account', () => {
@ -42,9 +44,18 @@ jest.mock('./use-settlement-account', () => {
useSettlementAccount: jest.fn(() => mockAccount),
};
});
jest.mock('./use-market-positions', () => jest.fn(() => mockMarketPositions));
describe('useMaximumPositionSize Hook', () => {
jest.mock('./use-market-positions', () => ({
useMarketPositions: ({
marketId,
partyId,
}: {
marketId: string;
partyId: string;
}) => mockMarketPositions,
}));
describe('useMaximumPositionSize', () => {
it('should return correct size when no open positions', () => {
mockMarketPositions = null;
const price = '50';

View File

@ -1,13 +1,13 @@
import useMarketPositions from './use-market-positions';
import { useMarketPositions } from './use-market-positions';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import type { PartyBalanceQuery_party_accounts } from '../components/deal-ticket/__generated__/PartyBalanceQuery';
import { useSettlementAccount } from './use-settlement-account';
import { AccountType, Side } from '@vegaprotocol/types';
import { BigNumber } from 'bignumber.js';
import type { AccountFragment as Account } from './__generated__/PartyBalance';
interface Props {
partyId: string;
accounts: PartyBalanceQuery_party_accounts[];
accounts: Account[];
marketId: string;
price?: string;
settlementAssetId: string;
@ -17,7 +17,7 @@ interface Props {
const getSize = (balance: string, price: string) =>
new BigNumber(balance).dividedBy(new BigNumber(price));
export default ({
export const useMaximumPositionSize = ({
marketId,
accounts,
partyId,

View File

@ -1,17 +1,17 @@
import * as React from 'react';
import { renderHook } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import useOrderCloseOut from './use-order-closeout';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import type { DealTicketMarketFragment } from '@vegaprotocol/deal-ticket';
import type { PartyBalanceQuery } from '../components/deal-ticket/__generated__/PartyBalanceQuery';
import type { PartyBalanceQuery } from './__generated__/PartyBalance';
import { useOrderCloseOut } from './use-order-closeout';
import type { DealTicketMarketFragment } from '../components/deal-ticket/__generated___/DealTicket';
jest.mock('@vegaprotocol/wallet', () => ({
...jest.requireActual('@vegaprotocol/wallet'),
useVegaWallet: jest.fn().mockReturnValue('wallet-pub-key'),
}));
describe('useOrderCloseOut Hook', () => {
describe('useOrderCloseOut', () => {
const order = { size: '2', side: 'SIDE_BUY' };
const market = {
decimalPlaces: 5,
@ -44,7 +44,7 @@ describe('useOrderCloseOut Hook', () => {
},
};
it('return proper buy value', () => {
it('should return proper null value', () => {
const { result } = renderHook(
() =>
useOrderCloseOut({
@ -58,10 +58,10 @@ describe('useOrderCloseOut Hook', () => {
),
}
);
expect(result.current).toEqual(' - ');
expect(result.current).toEqual(null);
});
it('return proper sell value', () => {
it('should return proper sell value', () => {
const { result } = renderHook(
() =>
useOrderCloseOut({
@ -81,7 +81,7 @@ describe('useOrderCloseOut Hook', () => {
expect(result.current).toEqual('1.00000');
});
it('return proper empty value', () => {
it('should return proper empty value', () => {
const { result } = renderHook(
() =>
useOrderCloseOut({

View File

@ -1,49 +1,14 @@
import { BigNumber } from 'bignumber.js';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import type { DealTicketMarketFragment } from '@vegaprotocol/deal-ticket';
import type { PartyBalanceQuery } from '../components/deal-ticket/__generated__/PartyBalanceQuery';
import { useSettlementAccount } from './use-settlement-account';
import { useVegaWallet } from '@vegaprotocol/wallet';
import { addDecimal, formatNumber } from '@vegaprotocol/react-helpers';
import { gql, useQuery } from '@apollo/client';
import useMarketPositions from './use-market-positions';
import useMarketData from './use-market-data';
import type {
PartyMarketData,
PartyMarketDataVariables,
} from './__generated__/PartyMarketData';
import { useMarketPositions } from './use-market-positions';
import { useMarketDataMarkPrice } from './use-market-data-mark-price';
import { usePartyMarketDataQuery } from './__generated__/PartyMarketData';
import { Side } from '@vegaprotocol/types';
const CLOSEOUT_PRICE_QUERY = gql`
query PartyMarketData($partyId: ID!) {
party(id: $partyId) {
id
accounts {
type
balance
asset {
id
decimals
}
market {
id
}
}
marginsConnection {
edges {
node {
market {
id
}
initialLevel
maintenanceLevel
searchLevel
}
}
}
}
}
`;
import type { DealTicketMarketFragment } from '../components/deal-ticket/__generated___/DealTicket';
import type { PartyBalanceQuery } from './__generated__/PartyBalance';
import { useSettlementAccount } from './use-settlement-account';
interface Props {
order: OrderSubmissionBody['orderSubmission'];
@ -51,22 +16,23 @@ interface Props {
partyData?: PartyBalanceQuery;
}
const useOrderCloseOut = ({ order, market, partyData }: Props): string => {
export const useOrderCloseOut = ({
order,
market,
partyData,
}: Props): string | null => {
const { pubKey } = useVegaWallet();
const account = useSettlementAccount(
market.tradableInstrument.instrument.product.settlementAsset.id,
partyData?.party?.accounts || []
);
const { data } = useQuery<PartyMarketData, PartyMarketDataVariables>(
CLOSEOUT_PRICE_QUERY,
{
pollInterval: 5000,
variables: { partyId: pubKey || '' },
skip: !pubKey,
}
);
const { data } = usePartyMarketDataQuery({
pollInterval: 5000,
variables: { partyId: pubKey || '' },
skip: !pubKey,
});
const markPriceData = useMarketData(market.id);
const markPriceData = useMarketDataMarkPrice(market.id);
const marketPositions = useMarketPositions({
marketId: market.id,
partyId: pubKey || '',
@ -94,7 +60,7 @@ const useOrderCloseOut = ({ order, market, partyData }: Props): string => {
);
const volume = new BigNumber(
addDecimal(
marketPositions?.openVolume.toNumber() || 0,
marketPositions?.openVolume.toString() || '0',
market.positionDecimalPlaces
)
)[order.side === Side.SIDE_BUY ? 'plus' : 'minus'](order.size);
@ -112,7 +78,5 @@ const useOrderCloseOut = ({ order, market, partyData }: Props): string => {
if (closeOut.isPositive()) {
return formatNumber(closeOut, market.decimalPlaces);
}
return ' - ';
return null;
};
export default useOrderCloseOut;

View File

@ -2,9 +2,9 @@ import { renderHook } from '@testing-library/react';
import { useQuery } from '@apollo/client';
import { BigNumber } from 'bignumber.js';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import type { DealTicketMarketFragment } from '@vegaprotocol/deal-ticket';
import type { PositionMargin } from './use-market-positions';
import useOrderMargin from './use-order-margin';
import { useOrderMargin } from './use-order-margin';
import type { DealTicketMarketFragment } from '../components/deal-ticket/__generated___/DealTicket';
let mockEstimateData = {
estimateOrder: {
@ -27,9 +27,18 @@ let mockMarketPositions: PositionMargin = {
openVolume: new BigNumber(1),
balance: new BigNumber(100000),
};
jest.mock('./use-market-positions', () => jest.fn(() => mockMarketPositions));
describe('useOrderMargin Hook', () => {
jest.mock('./use-market-positions', () => ({
useMarketPositions: ({
marketId,
partyId,
}: {
marketId: string;
partyId: string;
}) => mockMarketPositions,
}));
describe('useOrderMargin', () => {
const order = {
size: '2',
side: 'SIDE_BUY',
@ -50,7 +59,7 @@ describe('useOrderMargin Hook', () => {
jest.clearAllMocks();
});
it('margin should be properly calculated', () => {
it('should calculate margin correctly', () => {
const { result } = renderHook(() =>
useOrderMargin({
order: order as OrderSubmissionBody['orderSubmission'],
@ -68,7 +77,7 @@ describe('useOrderMargin Hook', () => {
);
});
it('fees should be properly calculated', () => {
it('should calculate fees correctly', () => {
const { result } = renderHook(() =>
useOrderMargin({
order: order as OrderSubmissionBody['orderSubmission'],
@ -76,10 +85,10 @@ describe('useOrderMargin Hook', () => {
partyId,
})
);
expect(result.current?.fees).toEqual('300000');
expect(result.current?.totalFees).toEqual('300000');
});
it('if there is no positions initialMargin should not be subtracted', () => {
it('should not subtract initialMargin if there is no position', () => {
mockMarketPositions = null;
const { result } = renderHook(() =>
useOrderMargin({
@ -95,7 +104,7 @@ describe('useOrderMargin Hook', () => {
);
});
it('if api fails, should return empty value', () => {
it('should return empty value if API fails', () => {
mockEstimateData = {
estimateOrder: {
fee: {

View File

@ -0,0 +1,89 @@
import { BigNumber } from 'bignumber.js';
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
import { Side } from '@vegaprotocol/types';
import { addDecimal, removeDecimal } from '@vegaprotocol/react-helpers';
import { useMarketPositions } from './use-market-positions';
import { useMarketDataMarkPrice } from './use-market-data-mark-price';
import type { EstimateOrderQuery } from './__generated__/EstimateOrder';
import { useEstimateOrderQuery } from './__generated__/EstimateOrder';
import type { DealTicketMarketFragment } from '../components/deal-ticket/__generated___/DealTicket';
interface Props {
order: OrderSubmissionBody['orderSubmission'];
market: DealTicketMarketFragment;
partyId: string;
}
const addFees = (feeObj: EstimateOrderQuery['estimateOrder']['fee']) => {
return new BigNumber(feeObj.makerFee)
.plus(feeObj.liquidityFee)
.plus(feeObj.infrastructureFee);
};
export interface OrderMargin {
margin: string;
totalFees: string | null;
fees: {
makerFee: string;
liquidityFee: string;
infrastructureFee: string;
};
}
export const useOrderMargin = ({
order,
market,
partyId,
}: Props): OrderMargin | null => {
const marketPositions = useMarketPositions({ marketId: market.id, partyId });
const markPriceData = useMarketDataMarkPrice(market.id);
const { data } = useEstimateOrderQuery({
variables: {
marketId: market.id,
partyId,
price: order.price
? removeDecimal(order.price, market.decimalPlaces)
: markPriceData?.market?.data?.markPrice || '',
size: removeDecimal(
BigNumber.maximum(
0,
new BigNumber(marketPositions?.openVolume || 0)
[order.side === Side.SIDE_BUY ? 'plus' : 'minus'](order.size)
.absoluteValue()
).toString(),
market.positionDecimalPlaces
),
side: order.side === Side.SIDE_BUY ? Side.SIDE_BUY : Side.SIDE_SELL,
timeInForce: order.timeInForce,
type: order.type,
},
skip:
!partyId ||
!market.id ||
!order.size ||
!markPriceData?.market?.data?.markPrice,
});
if (data?.estimateOrder.marginLevels.initialLevel) {
const fees =
data?.estimateOrder?.fee && addFees(data.estimateOrder.fee).toString();
const margin = BigNumber.maximum(
0,
new BigNumber(data.estimateOrder.marginLevels.initialLevel).minus(
marketPositions?.balance || 0
)
).toString();
const { makerFee, liquidityFee, infrastructureFee } =
data.estimateOrder.fee;
return {
margin: addDecimal(margin, market.decimalPlaces),
totalFees: addDecimal(fees, market.decimalPlaces),
fees: {
makerFee: addDecimal(makerFee, market.decimalPlaces),
liquidityFee: addDecimal(liquidityFee, market.decimalPlaces),
infrastructureFee: addDecimal(infrastructureFee, market.decimalPlaces),
},
};
}
return null;
};

View File

@ -1,11 +1,11 @@
import { renderHook } from '@testing-library/react';
import { useSettlementAccount } from './use-settlement-account';
import type { PartyBalanceQuery_party_accounts } from '../components/deal-ticket/__generated__/PartyBalanceQuery';
import { AccountType } from '@vegaprotocol/types';
import type { AccountFragment as Account } from './__generated__/PartyBalance';
describe('useSettlementAccount Hook', () => {
it('should filter accounts by settlementAssetId', () => {
const accounts: PartyBalanceQuery_party_accounts[] = [
const accounts: Account[] = [
{
__typename: 'Account',
type: AccountType.ACCOUNT_TYPE_GENERAL,
@ -75,12 +75,12 @@ describe('useSettlementAccount Hook', () => {
});
it('should return null if no accounts', () => {
const accounts: PartyBalanceQuery_party_accounts[] = [];
const accounts: Account[] = [];
const settlementAssetId =
'6d9d35f657589e40ddfb448b7ad4a7463b66efb307527fedd2aa7df1bbd5ea61';
const { result } = renderHook(() =>
useSettlementAccount(settlementAssetId, accounts)
);
expect(result.current).toBe(undefined);
expect(result.current).toBe(null);
});
});

View File

@ -1,12 +1,12 @@
import type { PartyBalanceQuery_party_accounts } from '../components/deal-ticket/__generated__/PartyBalanceQuery';
import type { AccountType } from '@vegaprotocol/types';
import { useMemo } from 'react';
import type { AccountFragment as Account } from './__generated__/PartyBalance';
export const useSettlementAccount = (
settlementAssetId: string,
accounts: PartyBalanceQuery_party_accounts[],
accounts: Account[],
type?: AccountType
): PartyBalanceQuery_party_accounts | null => {
): Account | null => {
const callback = () =>
accounts.find((account) => {
if (type) {
@ -16,5 +16,5 @@ export const useSettlementAccount = (
return account.asset.id === settlementAssetId;
});
const account = useMemo(callback, [accounts, settlementAssetId, type]);
return account as PartyBalanceQuery_party_accounts;
return account || null;
};

View File

@ -1 +1,2 @@
export * from './components';
export * from './hooks';

View File

@ -0,0 +1,109 @@
import type { Market } from '@vegaprotocol/market-list';
import { totalFeesPercentage } from '@vegaprotocol/market-list';
import { t, formatNumberPercentage } from '@vegaprotocol/react-helpers';
import { Tooltip } from '@vegaprotocol/ui-toolkit';
import BigNumber from 'bignumber.js';
export const FeesCell = ({
feeFactors,
}: {
feeFactors: Market['fees']['factors'];
}) => (
<Tooltip description={<FeesBreakdownPercentage feeFactors={feeFactors} />}>
<span>{totalFeesPercentage(feeFactors) ?? '-'}</span>
</Tooltip>
);
export const FeesBreakdownPercentage = ({
feeFactors,
}: {
feeFactors?: Market['fees']['factors'];
}) => {
if (!feeFactors) return null;
return (
<dl className="grid grid-cols-2 gap-x-2">
<dt>{t('Infrastructure fee')}</dt>
<dd className="text-right">
{formatNumberPercentage(
new BigNumber(feeFactors.infrastructureFee).times(100)
)}
</dd>
<dt>{t('Liquidity fee')}</dt>
<dd className="text-right">
{formatNumberPercentage(
new BigNumber(feeFactors.liquidityFee).times(100)
)}
</dd>
<dt>{t('Maker fee')}</dt>
<dd className="text-right">
{formatNumberPercentage(new BigNumber(feeFactors.makerFee).times(100))}
</dd>
<dt>{t('Total fees')}</dt>
<dd className="text-right">{totalFeesPercentage(feeFactors)}</dd>
</dl>
);
};
export const FeesBreakdown = ({
fees,
feeFactors,
quoteName,
}: {
fees?: {
infrastructureFee: string;
liquidityFee: string;
makerFee: string;
};
feeFactors?: Market['fees']['factors'];
quoteName?: string;
}) => {
if (!fees) return null;
const totalFees = new BigNumber(fees.makerFee)
.plus(fees.infrastructureFee)
.plus(fees.liquidityFee)
.toString();
return (
<dl className="grid grid-cols-3 gap-x-3">
<dt>{t('Infrastructure fee')}</dt>
{feeFactors && (
<dd className="text-right">
{formatNumberPercentage(
new BigNumber(feeFactors.infrastructureFee).times(100)
)}
</dd>
)}
<dd className="text-right">
{fees.infrastructureFee} {quoteName || ''}
</dd>
<dt>{t('Liquidity fee')}</dt>
{feeFactors && (
<dd className="text-right">
{formatNumberPercentage(
new BigNumber(feeFactors.liquidityFee).times(100)
)}
</dd>
)}
<dd className="text-right">
{fees.liquidityFee} {quoteName || ''}
</dd>
<dt>{t('Maker fee')}</dt>
{feeFactors && (
<dd className="text-right">
{formatNumberPercentage(
new BigNumber(feeFactors.makerFee).times(100)
)}
</dd>
)}
<dd className="text-right">
{fees.makerFee} {quoteName || ''}
</dd>
<dt>{t('Total fees')}</dt>
{feeFactors && (
<dd className="text-right">{totalFeesPercentage(feeFactors)}</dd>
)}
<dd className="text-right">
{totalFees} {quoteName || ''}
</dd>
</dl>
);
};

View File

@ -0,0 +1 @@
export * from './fees-breakdown';

View File

@ -1,2 +1,3 @@
export * from './market-expires';
export * from './market-info';
export * from './fees-breakdown';

View File

@ -8,7 +8,7 @@ import {
import { AsyncRenderer, Splash, Accordion } from '@vegaprotocol/ui-toolkit';
import pick from 'lodash/pick';
import BigNumber from 'bignumber.js';
import { totalFees } from '@vegaprotocol/market-list';
import { totalFeesPercentage } from '@vegaprotocol/market-list';
import {
AccountType,
Interval,
@ -101,7 +101,7 @@ export const Info = ({ market, onSelect }: InfoProps) => {
<MarketInfoTable
data={{
...market.fees.factors,
totalFees: totalFees(market.fees.factors),
totalFees: totalFeesPercentage(market.fees.factors),
}}
asPercentage={true}
/>

View File

@ -1,6 +1,6 @@
import { MarketState, MarketTradingMode } from '@vegaprotocol/types';
import type { Market } from '../markets-provider';
import { filterAndSortMarkets, totalFees } from './market-utils';
import { filterAndSortMarkets, totalFeesPercentage } from './market-utils';
const MARKET_A: Partial<Market> = {
id: '1',
@ -73,6 +73,6 @@ describe('totalFees', () => {
{ i: createFee(0.01, 0.056782, 0.003), o: '6.9782%' },
{ i: createFee(0.01, 0.056782, 0), o: '6.6782%' },
])('adds fees correctly', ({ i, o }) => {
expect(totalFees(i)).toEqual(o);
expect(totalFeesPercentage(i)).toEqual(o);
});
});

View File

@ -5,15 +5,17 @@ import orderBy from 'lodash/orderBy';
import type { Market, Candle } from '../';
export const totalFees = (fees: Market['fees']['factors']) => {
if (!fees) {
return undefined;
}
return formatNumberPercentage(
new BigNumber(fees.makerFee)
.plus(fees.liquidityFee)
.plus(fees.infrastructureFee)
.times(100)
);
return fees
? new BigNumber(fees.makerFee)
.plus(fees.liquidityFee)
.plus(fees.infrastructureFee)
.times(100)
: undefined;
};
export const totalFeesPercentage = (fees: Market['fees']['factors']) => {
const total = fees && totalFees(fees);
return total ? formatNumberPercentage(total) : undefined;
};
export const filterAndSortMarkets = (markets: Market[]) => {

View File

@ -35,3 +35,4 @@ export * from './toggle';
export * from './tooltip';
export * from './vega-icons';
export * from './vega-logo';
export * from './traffic-light';

View File

@ -1,4 +1,3 @@
import React from 'react';
import type { ReactNode } from 'react';
import classNames from 'classnames';

View File

@ -1,7 +1,10 @@
import { t } from '@vegaprotocol/react-helpers';
import { Link } from '@vegaprotocol/ui-toolkit';
import type { ReactNode } from 'react';
import * as constants from '../constants';
import {
VEGA_WALLET_CONCEPTS_URL,
VEGA_WALLET_RELEASE_URL,
} from '../constants';
export const ConnectDialogTitle = ({ children }: { children: ReactNode }) => {
return (
@ -25,13 +28,9 @@ export const ConnectDialogFooter = ({ children }: { children?: ReactNode }) => {
children
) : (
<>
<Link href={constants.VEGA_WALLET_RELEASE_URL}>
{t('Get a Vega Wallet')}
</Link>
<Link href={VEGA_WALLET_RELEASE_URL}>{t('Get a Vega Wallet')}</Link>
{' | '}
<Link href={constants.VEGA_WALLET_CONCEPTS_URL}>
{t('Having trouble?')}
</Link>
<Link href={VEGA_WALLET_CONCEPTS_URL}>{t('Having trouble?')}</Link>
</>
)}
</footer>