feat(trading): display fees discounts (#4800)
This commit is contained in:
parent
efe9b7a0e7
commit
852c9bb8f4
@ -1,6 +1,6 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { FeesBreakdown, getAsset, getQuoteName } from '@vegaprotocol/markets';
|
||||
import { getAsset, getQuoteName } from '@vegaprotocol/markets';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
|
||||
@ -8,7 +8,11 @@ import type { Market } from '@vegaprotocol/markets';
|
||||
import type { EstimatePositionQuery } from '@vegaprotocol/positions';
|
||||
import { AccountBreakdownDialog } from '@vegaprotocol/accounts';
|
||||
|
||||
import { formatRange, formatValue } from '@vegaprotocol/utils';
|
||||
import {
|
||||
formatNumberPercentage,
|
||||
formatRange,
|
||||
formatValue,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { marketMarginDataProvider } from '@vegaprotocol/accounts';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import * as AccordionPrimitive from '@radix-ui/react-accordion';
|
||||
@ -22,15 +26,23 @@ import {
|
||||
EST_TOTAL_MARGIN_TOOLTIP_TEXT,
|
||||
MARGIN_ACCOUNT_TOOLTIP_TEXT,
|
||||
} from '../../constants';
|
||||
import { useEstimateFees } from '../../hooks';
|
||||
import {
|
||||
sumFees,
|
||||
sumFeesDiscounts,
|
||||
useEstimateFees,
|
||||
} from '../../hooks/use-estimate-fees';
|
||||
import { KeyValue } from './key-value';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionChevron,
|
||||
AccordionPanel,
|
||||
Intent,
|
||||
Pill,
|
||||
Tooltip,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { FeesBreakdown } from '../fees-breakdown';
|
||||
|
||||
const emptyValue = '-';
|
||||
|
||||
@ -50,7 +62,18 @@ export const DealTicketFeeDetails = ({
|
||||
const feeEstimate = useEstimateFees(order, isMarketInAuction);
|
||||
const asset = getAsset(market);
|
||||
const { decimals: assetDecimals, quantum } = asset;
|
||||
const totalFees = feeEstimate?.fees && sumFees(feeEstimate?.fees);
|
||||
const feesDiscounts =
|
||||
feeEstimate?.fees && sumFeesDiscounts(feeEstimate?.fees);
|
||||
|
||||
const totalPercentageDiscount =
|
||||
feesDiscounts &&
|
||||
totalFees &&
|
||||
feesDiscounts.total !== '0' &&
|
||||
totalFees !== '0' &&
|
||||
new BigNumber(feesDiscounts.total)
|
||||
.dividedBy(BigNumber.sum(totalFees, feesDiscounts.total))
|
||||
.times(100);
|
||||
return (
|
||||
<KeyValue
|
||||
label={t('Fees')}
|
||||
@ -59,8 +82,19 @@ export const DealTicketFeeDetails = ({
|
||||
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals)}`
|
||||
}
|
||||
formattedValue={
|
||||
feeEstimate?.totalFeeAmount &&
|
||||
`~${formatValue(feeEstimate?.totalFeeAmount, assetDecimals, quantum)}`
|
||||
<>
|
||||
{totalPercentageDiscount && (
|
||||
<Pill size="xxs" intent={Intent.Warning} className="mr-1">
|
||||
-{formatNumberPercentage(totalPercentageDiscount, 2)}
|
||||
</Pill>
|
||||
)}
|
||||
{feeEstimate?.totalFeeAmount &&
|
||||
`~${formatValue(
|
||||
feeEstimate?.totalFeeAmount,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}`}
|
||||
</>
|
||||
}
|
||||
labelDescription={
|
||||
<>
|
||||
|
@ -9,7 +9,7 @@ export interface KeyValuePros {
|
||||
symbol: string;
|
||||
indent?: boolean | undefined;
|
||||
labelDescription?: ReactNode;
|
||||
formattedValue?: string;
|
||||
formattedValue?: ReactNode;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
@ -23,7 +23,11 @@ export const KeyValue = ({
|
||||
onClick,
|
||||
formattedValue,
|
||||
}: KeyValuePros) => {
|
||||
const displayValue = `${formattedValue ?? '-'} ${symbol || ''}`;
|
||||
const displayValue = (
|
||||
<>
|
||||
{formattedValue || '-'} {symbol || ''}
|
||||
</>
|
||||
);
|
||||
const valueElement = onClick ? (
|
||||
<button onClick={onClick} className="font-mono ml-auto">
|
||||
{displayValue}
|
||||
|
@ -0,0 +1,130 @@
|
||||
import { sumFeesFactors } from '@vegaprotocol/markets';
|
||||
import type { TradeFee, FeeFactors } from '@vegaprotocol/types';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumberPercentage,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { sumFees, sumFeesDiscounts } from '../../hooks';
|
||||
|
||||
const formatValue = (
|
||||
value: string | number | null | undefined,
|
||||
decimals: number
|
||||
): string =>
|
||||
value && !isNaN(Number(value))
|
||||
? addDecimalsFormatNumber(value, decimals)
|
||||
: '-';
|
||||
|
||||
const FeesBreakdownItem = ({
|
||||
label,
|
||||
factor,
|
||||
value,
|
||||
symbol,
|
||||
decimals,
|
||||
}: {
|
||||
label: string;
|
||||
factor?: BigNumber;
|
||||
value: string;
|
||||
symbol?: string;
|
||||
decimals: number;
|
||||
}) => (
|
||||
<>
|
||||
<dt className="col-span-2">{label}</dt>
|
||||
{factor && (
|
||||
<dd className="text-right col-span-1">
|
||||
{formatNumberPercentage(new BigNumber(factor).times(100), 2)}
|
||||
</dd>
|
||||
)}
|
||||
<dd className="text-right col-span-3">
|
||||
{formatValue(value, decimals)} {symbol || ''}
|
||||
</dd>
|
||||
</>
|
||||
);
|
||||
|
||||
export const FeesBreakdown = ({
|
||||
fees,
|
||||
feeFactors,
|
||||
symbol,
|
||||
decimals,
|
||||
}: {
|
||||
fees?: TradeFee;
|
||||
feeFactors?: FeeFactors;
|
||||
symbol?: string;
|
||||
decimals: number;
|
||||
}) => {
|
||||
if (!fees) return null;
|
||||
const totalFees = sumFees(fees);
|
||||
const {
|
||||
total: totalDiscount,
|
||||
referral: referralDiscount,
|
||||
volume: volumeDiscount,
|
||||
} = sumFeesDiscounts(fees);
|
||||
if (totalFees === '0') return null;
|
||||
return (
|
||||
<dl className="grid grid-cols-6">
|
||||
<FeesBreakdownItem
|
||||
label={t('Infrastructure fee')}
|
||||
factor={
|
||||
feeFactors?.infrastructureFee
|
||||
? new BigNumber(feeFactors?.infrastructureFee)
|
||||
: undefined
|
||||
}
|
||||
value={fees.infrastructureFee}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
|
||||
<FeesBreakdownItem
|
||||
label={t('Liquidity fee')}
|
||||
factor={
|
||||
feeFactors?.liquidityFee
|
||||
? new BigNumber(feeFactors?.liquidityFee)
|
||||
: undefined
|
||||
}
|
||||
value={fees.liquidityFee}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
|
||||
<FeesBreakdownItem
|
||||
label={t('Maker fee')}
|
||||
factor={
|
||||
feeFactors?.makerFee ? new BigNumber(feeFactors?.makerFee) : undefined
|
||||
}
|
||||
value={fees.makerFee}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
{volumeDiscount && volumeDiscount !== '0' && (
|
||||
<FeesBreakdownItem
|
||||
label={t('Volume discount')}
|
||||
factor={new BigNumber(volumeDiscount).dividedBy(
|
||||
BigNumber.sum(totalFees, totalDiscount)
|
||||
)}
|
||||
value={volumeDiscount}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
)}
|
||||
{referralDiscount && referralDiscount !== '0' && (
|
||||
<FeesBreakdownItem
|
||||
label={t('Referral discount')}
|
||||
factor={new BigNumber(referralDiscount).dividedBy(
|
||||
BigNumber.sum(totalFees, totalDiscount)
|
||||
)}
|
||||
value={referralDiscount}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
)}
|
||||
<FeesBreakdownItem
|
||||
label={t('Total fees')}
|
||||
factor={feeFactors ? sumFeesFactors(feeFactors) : undefined}
|
||||
value={totalFees}
|
||||
symbol={symbol}
|
||||
decimals={decimals}
|
||||
/>
|
||||
</dl>
|
||||
);
|
||||
};
|
@ -1,3 +1,4 @@
|
||||
export * from './deal-ticket';
|
||||
export * from './deal-ticket-validation';
|
||||
export * from './fees-breakdown';
|
||||
export * from './trading-mode-tooltip';
|
||||
|
@ -22,6 +22,12 @@ query EstimateFees(
|
||||
makerFee
|
||||
infrastructureFee
|
||||
liquidityFee
|
||||
makerFeeReferralDiscount
|
||||
makerFeeVolumeDiscount
|
||||
infrastructureFeeReferralDiscount
|
||||
infrastructureFeeVolumeDiscount
|
||||
liquidityFeeReferralDiscount
|
||||
liquidityFeeVolumeDiscount
|
||||
}
|
||||
totalFeeAmount
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ export type EstimateFeesQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type EstimateFeesQuery = { __typename?: 'Query', estimateFees: { __typename?: 'FeeEstimate', totalFeeAmount: string, fees: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string } } };
|
||||
export type EstimateFeesQuery = { __typename?: 'Query', estimateFees: { __typename?: 'FeeEstimate', totalFeeAmount: string, fees: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null } } };
|
||||
|
||||
|
||||
export const EstimateFeesDocument = gql`
|
||||
@ -34,6 +34,12 @@ export const EstimateFeesDocument = gql`
|
||||
makerFee
|
||||
infrastructureFee
|
||||
liquidityFee
|
||||
makerFeeReferralDiscount
|
||||
makerFeeVolumeDiscount
|
||||
infrastructureFeeReferralDiscount
|
||||
infrastructureFeeVolumeDiscount
|
||||
liquidityFeeReferralDiscount
|
||||
liquidityFeeVolumeDiscount
|
||||
}
|
||||
totalFeeAmount
|
||||
}
|
||||
|
@ -6,11 +6,17 @@ import type { EstimateFeesQuery } from './__generated__/EstimateOrder';
|
||||
|
||||
const data: EstimateFeesQuery = {
|
||||
estimateFees: {
|
||||
totalFeeAmount: '12',
|
||||
totalFeeAmount: '120',
|
||||
fees: {
|
||||
infrastructureFee: '2',
|
||||
liquidityFee: '4',
|
||||
makerFee: '6',
|
||||
infrastructureFee: '20',
|
||||
infrastructureFeeReferralDiscount: '2',
|
||||
infrastructureFeeVolumeDiscount: '4',
|
||||
liquidityFee: '40',
|
||||
liquidityFeeReferralDiscount: '6',
|
||||
liquidityFeeVolumeDiscount: '8',
|
||||
makerFee: '60',
|
||||
makerFeeReferralDiscount: '10',
|
||||
makerFeeVolumeDiscount: '12',
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -67,11 +73,17 @@ describe('useEstimateFees', () => {
|
||||
)
|
||||
);
|
||||
expect(result.current).toEqual({
|
||||
totalFeeAmount: '6',
|
||||
totalFeeAmount: '60',
|
||||
fees: {
|
||||
infrastructureFee: '1',
|
||||
liquidityFee: '2',
|
||||
makerFee: '3',
|
||||
infrastructureFee: '10',
|
||||
infrastructureFeeReferralDiscount: '1',
|
||||
infrastructureFeeVolumeDiscount: '2',
|
||||
liquidityFee: '20',
|
||||
liquidityFeeReferralDiscount: '3',
|
||||
liquidityFeeVolumeDiscount: '4',
|
||||
makerFee: '30',
|
||||
makerFeeReferralDiscount: '5',
|
||||
makerFeeVolumeDiscount: '6',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -5,6 +5,32 @@ import type { EstimateFeesQuery } from './__generated__/EstimateOrder';
|
||||
import { useEstimateFeesQuery } from './__generated__/EstimateOrder';
|
||||
|
||||
const divideByTwo = (n: string) => (BigInt(n) / BigInt(2)).toString();
|
||||
export const sumFeesDiscounts = (
|
||||
fees: EstimateFeesQuery['estimateFees']['fees']
|
||||
) => {
|
||||
const volume = (
|
||||
BigInt(fees.makerFeeVolumeDiscount || '0') +
|
||||
BigInt(fees.infrastructureFeeVolumeDiscount || '0') +
|
||||
BigInt(fees.liquidityFeeVolumeDiscount || '0')
|
||||
).toString();
|
||||
const referral = (
|
||||
BigInt(fees.makerFeeReferralDiscount || '0') +
|
||||
BigInt(fees.infrastructureFeeReferralDiscount || '0') +
|
||||
BigInt(fees.liquidityFeeReferralDiscount || '0')
|
||||
).toString();
|
||||
return {
|
||||
volume,
|
||||
referral,
|
||||
total: (BigInt(volume) + BigInt(referral)).toString(),
|
||||
};
|
||||
};
|
||||
|
||||
export const sumFees = (fees: EstimateFeesQuery['estimateFees']['fees']) =>
|
||||
(
|
||||
BigInt(fees.makerFee || '0') +
|
||||
BigInt(fees.infrastructureFee || '0') +
|
||||
BigInt(fees.liquidityFee || '0')
|
||||
).toString();
|
||||
|
||||
export const useEstimateFees = (
|
||||
order?: OrderSubmissionBody['orderSubmission'],
|
||||
@ -43,6 +69,26 @@ export const useEstimateFees = (
|
||||
),
|
||||
liquidityFee: divideByTwo(data.estimateFees.fees.liquidityFee),
|
||||
makerFee: divideByTwo(data.estimateFees.fees.makerFee),
|
||||
infrastructureFeeReferralDiscount:
|
||||
data.estimateFees.fees.infrastructureFeeReferralDiscount &&
|
||||
divideByTwo(
|
||||
data.estimateFees.fees.infrastructureFeeReferralDiscount
|
||||
),
|
||||
infrastructureFeeVolumeDiscount:
|
||||
data.estimateFees.fees.infrastructureFeeVolumeDiscount &&
|
||||
divideByTwo(data.estimateFees.fees.infrastructureFeeVolumeDiscount),
|
||||
liquidityFeeReferralDiscount:
|
||||
data.estimateFees.fees.liquidityFeeReferralDiscount &&
|
||||
divideByTwo(data.estimateFees.fees.liquidityFeeReferralDiscount),
|
||||
liquidityFeeVolumeDiscount:
|
||||
data.estimateFees.fees.liquidityFeeVolumeDiscount &&
|
||||
divideByTwo(data.estimateFees.fees.liquidityFeeVolumeDiscount),
|
||||
makerFeeReferralDiscount:
|
||||
data.estimateFees.fees.makerFeeReferralDiscount &&
|
||||
divideByTwo(data.estimateFees.fees.makerFeeReferralDiscount),
|
||||
makerFeeVolumeDiscount:
|
||||
data.estimateFees.fees.makerFeeVolumeDiscount &&
|
||||
divideByTwo(data.estimateFees.fees.makerFeeVolumeDiscount),
|
||||
},
|
||||
}
|
||||
: data?.estimateFees;
|
||||
|
@ -1,3 +1,15 @@
|
||||
fragment TradeFeeFields on TradeFee {
|
||||
makerFee
|
||||
infrastructureFee
|
||||
liquidityFee
|
||||
makerFeeReferralDiscount
|
||||
makerFeeVolumeDiscount
|
||||
infrastructureFeeReferralDiscount
|
||||
infrastructureFeeVolumeDiscount
|
||||
liquidityFeeReferralDiscount
|
||||
liquidityFeeVolumeDiscount
|
||||
}
|
||||
|
||||
fragment FillFields on Trade {
|
||||
id
|
||||
market {
|
||||
@ -16,14 +28,10 @@ fragment FillFields on Trade {
|
||||
id
|
||||
}
|
||||
buyerFee {
|
||||
makerFee
|
||||
infrastructureFee
|
||||
liquidityFee
|
||||
...TradeFeeFields
|
||||
}
|
||||
sellerFee {
|
||||
makerFee
|
||||
infrastructureFee
|
||||
liquidityFee
|
||||
...TradeFeeFields
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,14 +69,10 @@ fragment FillUpdateFields on TradeUpdate {
|
||||
createdAt
|
||||
type
|
||||
buyerFee {
|
||||
makerFee
|
||||
infrastructureFee
|
||||
liquidityFee
|
||||
...TradeFeeFields
|
||||
}
|
||||
sellerFee {
|
||||
makerFee
|
||||
infrastructureFee
|
||||
liquidityFee
|
||||
...TradeFeeFields
|
||||
}
|
||||
}
|
||||
|
||||
|
45
libs/fills/src/lib/__generated__/Fills.ts
generated
45
libs/fills/src/lib/__generated__/Fills.ts
generated
@ -3,9 +3,11 @@ import * as Types from '@vegaprotocol/types';
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type FillFieldsFragment = { __typename?: 'Trade', id: string, createdAt: any, price: string, size: string, buyOrder: string, sellOrder: string, aggressor: Types.Side, market: { __typename?: 'Market', id: string }, buyer: { __typename?: 'Party', id: string }, seller: { __typename?: 'Party', id: string }, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string } };
|
||||
export type TradeFeeFieldsFragment = { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null };
|
||||
|
||||
export type FillEdgeFragment = { __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, createdAt: any, price: string, size: string, buyOrder: string, sellOrder: string, aggressor: Types.Side, market: { __typename?: 'Market', id: string }, buyer: { __typename?: 'Party', id: string }, seller: { __typename?: 'Party', id: string }, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string } } };
|
||||
export type FillFieldsFragment = { __typename?: 'Trade', id: string, createdAt: any, price: string, size: string, buyOrder: string, sellOrder: string, aggressor: Types.Side, market: { __typename?: 'Market', id: string }, buyer: { __typename?: 'Party', id: string }, seller: { __typename?: 'Party', id: string }, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null } };
|
||||
|
||||
export type FillEdgeFragment = { __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, createdAt: any, price: string, size: string, buyOrder: string, sellOrder: string, aggressor: Types.Side, market: { __typename?: 'Market', id: string }, buyer: { __typename?: 'Party', id: string }, seller: { __typename?: 'Party', id: string }, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null } } };
|
||||
|
||||
export type FillsQueryVariables = Types.Exact<{
|
||||
filter?: Types.InputMaybe<Types.TradesFilter>;
|
||||
@ -13,17 +15,30 @@ export type FillsQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type FillsQuery = { __typename?: 'Query', trades?: { __typename?: 'TradeConnection', edges: Array<{ __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, createdAt: any, price: string, size: string, buyOrder: string, sellOrder: string, aggressor: Types.Side, market: { __typename?: 'Market', id: string }, buyer: { __typename?: 'Party', id: string }, seller: { __typename?: 'Party', id: string }, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string } } }>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } | null };
|
||||
export type FillsQuery = { __typename?: 'Query', trades?: { __typename?: 'TradeConnection', edges: Array<{ __typename?: 'TradeEdge', cursor: string, node: { __typename?: 'Trade', id: string, createdAt: any, price: string, size: string, buyOrder: string, sellOrder: string, aggressor: Types.Side, market: { __typename?: 'Market', id: string }, buyer: { __typename?: 'Party', id: string }, seller: { __typename?: 'Party', id: string }, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null } } }>, pageInfo: { __typename?: 'PageInfo', startCursor: string, endCursor: string, hasNextPage: boolean, hasPreviousPage: boolean } } | null };
|
||||
|
||||
export type FillUpdateFieldsFragment = { __typename?: 'TradeUpdate', id: string, marketId: string, buyOrder: string, sellOrder: string, buyerId: string, sellerId: string, aggressor: Types.Side, price: string, size: string, createdAt: any, type: Types.TradeType, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string } };
|
||||
export type FillUpdateFieldsFragment = { __typename?: 'TradeUpdate', id: string, marketId: string, buyOrder: string, sellOrder: string, buyerId: string, sellerId: string, aggressor: Types.Side, price: string, size: string, createdAt: any, type: Types.TradeType, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null } };
|
||||
|
||||
export type FillsEventSubscriptionVariables = Types.Exact<{
|
||||
filter: Types.TradesSubscriptionFilter;
|
||||
}>;
|
||||
|
||||
|
||||
export type FillsEventSubscription = { __typename?: 'Subscription', tradesStream?: Array<{ __typename?: 'TradeUpdate', id: string, marketId: string, buyOrder: string, sellOrder: string, buyerId: string, sellerId: string, aggressor: Types.Side, price: string, size: string, createdAt: any, type: Types.TradeType, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string } }> | null };
|
||||
export type FillsEventSubscription = { __typename?: 'Subscription', tradesStream?: Array<{ __typename?: 'TradeUpdate', id: string, marketId: string, buyOrder: string, sellOrder: string, buyerId: string, sellerId: string, aggressor: Types.Side, price: string, size: string, createdAt: any, type: Types.TradeType, buyerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null }, sellerFee: { __typename?: 'TradeFee', makerFee: string, infrastructureFee: string, liquidityFee: string, makerFeeReferralDiscount?: string | null, makerFeeVolumeDiscount?: string | null, infrastructureFeeReferralDiscount?: string | null, infrastructureFeeVolumeDiscount?: string | null, liquidityFeeReferralDiscount?: string | null, liquidityFeeVolumeDiscount?: string | null } }> | null };
|
||||
|
||||
export const TradeFeeFieldsFragmentDoc = gql`
|
||||
fragment TradeFeeFields on TradeFee {
|
||||
makerFee
|
||||
infrastructureFee
|
||||
liquidityFee
|
||||
makerFeeReferralDiscount
|
||||
makerFeeVolumeDiscount
|
||||
infrastructureFeeReferralDiscount
|
||||
infrastructureFeeVolumeDiscount
|
||||
liquidityFeeReferralDiscount
|
||||
liquidityFeeVolumeDiscount
|
||||
}
|
||||
`;
|
||||
export const FillFieldsFragmentDoc = gql`
|
||||
fragment FillFields on Trade {
|
||||
id
|
||||
@ -43,17 +58,13 @@ export const FillFieldsFragmentDoc = gql`
|
||||
id
|
||||
}
|
||||
buyerFee {
|
||||
makerFee
|
||||
infrastructureFee
|
||||
liquidityFee
|
||||
...TradeFeeFields
|
||||
}
|
||||
sellerFee {
|
||||
makerFee
|
||||
infrastructureFee
|
||||
liquidityFee
|
||||
...TradeFeeFields
|
||||
}
|
||||
}
|
||||
`;
|
||||
${TradeFeeFieldsFragmentDoc}`;
|
||||
export const FillEdgeFragmentDoc = gql`
|
||||
fragment FillEdge on TradeEdge {
|
||||
node {
|
||||
@ -76,17 +87,13 @@ export const FillUpdateFieldsFragmentDoc = gql`
|
||||
createdAt
|
||||
type
|
||||
buyerFee {
|
||||
makerFee
|
||||
infrastructureFee
|
||||
liquidityFee
|
||||
...TradeFeeFields
|
||||
}
|
||||
sellerFee {
|
||||
makerFee
|
||||
infrastructureFee
|
||||
liquidityFee
|
||||
...TradeFeeFields
|
||||
}
|
||||
}
|
||||
`;
|
||||
${TradeFeeFieldsFragmentDoc}`;
|
||||
export const FillsDocument = gql`
|
||||
query Fills($filter: TradesFilter, $pagination: Pagination) {
|
||||
trades(filter: $filter, pagination: $pagination) {
|
||||
|
@ -4,36 +4,38 @@ import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import type { Trade } from './fills-data-provider';
|
||||
import { FillsTable, getFeesBreakdown } from './fills-table';
|
||||
import {
|
||||
FeesDiscountBreakdownTooltip,
|
||||
FillsTable,
|
||||
getFeesBreakdown,
|
||||
getTotalFeesDiscounts,
|
||||
} from './fills-table';
|
||||
import { generateFill } from './test-helpers';
|
||||
import type { TradeFeeFieldsFragment } from './__generated__/Fills';
|
||||
|
||||
describe('FillsTable', () => {
|
||||
let defaultFill: PartialDeep<Trade>;
|
||||
|
||||
beforeEach(() => {
|
||||
defaultFill = {
|
||||
price: '100',
|
||||
size: '300000',
|
||||
market: {
|
||||
decimalPlaces: 2,
|
||||
positionDecimalPlaces: 5,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'test market',
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
settlementAsset: {
|
||||
decimals: 2,
|
||||
symbol: 'BTC',
|
||||
},
|
||||
},
|
||||
const partyId = 'party-id';
|
||||
const defaultFill: PartialDeep<Trade> = {
|
||||
price: '100',
|
||||
size: '300000',
|
||||
market: {
|
||||
decimalPlaces: 2,
|
||||
positionDecimalPlaces: 5,
|
||||
tradableInstrument: {
|
||||
instrument: {
|
||||
code: 'test market',
|
||||
product: {
|
||||
__typename: 'Future',
|
||||
settlementAsset: {
|
||||
decimals: 2,
|
||||
symbol: 'BTC',
|
||||
},
|
||||
},
|
||||
},
|
||||
createdAt: new Date('2022-02-02T14:00:00').toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
},
|
||||
},
|
||||
createdAt: new Date('2022-02-02T14:00:00').toISOString(),
|
||||
};
|
||||
describe('FillsTable', () => {
|
||||
it('correct columns are rendered', async () => {
|
||||
await act(async () => {
|
||||
render(<FillsTable partyId="party-id" rowData={[generateFill()]} />);
|
||||
@ -47,6 +49,7 @@ describe('FillsTable', () => {
|
||||
'Notional',
|
||||
'Role',
|
||||
'Fee',
|
||||
'Fee Discount',
|
||||
'Date',
|
||||
'', // action column
|
||||
];
|
||||
@ -55,7 +58,6 @@ describe('FillsTable', () => {
|
||||
});
|
||||
|
||||
it('formats cells correctly for buyer fill', async () => {
|
||||
const partyId = 'party-id';
|
||||
const buyerFill = generateFill({
|
||||
...defaultFill,
|
||||
buyer: {
|
||||
@ -68,7 +70,9 @@ describe('FillsTable', () => {
|
||||
liquidityFee: '2',
|
||||
},
|
||||
});
|
||||
render(<FillsTable partyId={partyId} rowData={[{ ...buyerFill }]} />);
|
||||
await act(async () => {
|
||||
render(<FillsTable partyId={partyId} rowData={[{ ...buyerFill }]} />);
|
||||
});
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const expectedValues = [
|
||||
buyerFill.market?.tradableInstrument.instrument.code || '',
|
||||
@ -77,6 +81,7 @@ describe('FillsTable', () => {
|
||||
'3.00 BTC',
|
||||
'Maker',
|
||||
'2.00 BTC',
|
||||
'0.27 BTC',
|
||||
getDateTimeFormat().format(new Date(buyerFill.createdAt)),
|
||||
'', // action column
|
||||
];
|
||||
@ -89,7 +94,6 @@ describe('FillsTable', () => {
|
||||
});
|
||||
|
||||
it('should format cells correctly for seller fill', async () => {
|
||||
const partyId = 'party-id';
|
||||
const buyerFill = generateFill({
|
||||
...defaultFill,
|
||||
seller: {
|
||||
@ -102,7 +106,9 @@ describe('FillsTable', () => {
|
||||
liquidityFee: '1',
|
||||
},
|
||||
});
|
||||
render(<FillsTable partyId={partyId} rowData={[buyerFill]} />);
|
||||
await act(async () => {
|
||||
render(<FillsTable partyId={partyId} rowData={[buyerFill]} />);
|
||||
});
|
||||
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const expectedValues = [
|
||||
@ -112,6 +118,7 @@ describe('FillsTable', () => {
|
||||
'3.00 BTC',
|
||||
'Taker',
|
||||
'0.03 BTC',
|
||||
'0.27 BTC',
|
||||
getDateTimeFormat().format(new Date(buyerFill.createdAt)),
|
||||
'', // action column
|
||||
];
|
||||
@ -124,7 +131,6 @@ describe('FillsTable', () => {
|
||||
});
|
||||
|
||||
it('should format cells correctly for side unspecified', async () => {
|
||||
const partyId = 'party-id';
|
||||
const buyerFill = generateFill({
|
||||
...defaultFill,
|
||||
seller: {
|
||||
@ -137,7 +143,9 @@ describe('FillsTable', () => {
|
||||
liquidityFee: '1',
|
||||
},
|
||||
});
|
||||
render(<FillsTable partyId={partyId} rowData={[buyerFill]} />);
|
||||
await act(async () => {
|
||||
render(<FillsTable partyId={partyId} rowData={[buyerFill]} />);
|
||||
});
|
||||
|
||||
const cells = screen.getAllByRole('gridcell');
|
||||
const expectedValues = [
|
||||
@ -147,6 +155,7 @@ describe('FillsTable', () => {
|
||||
'3.00 BTC',
|
||||
'-',
|
||||
'0.03 BTC',
|
||||
'0.27 BTC',
|
||||
getDateTimeFormat().format(new Date(buyerFill.createdAt)),
|
||||
'', // action column
|
||||
];
|
||||
@ -158,30 +167,31 @@ describe('FillsTable', () => {
|
||||
expect(amountCell).toHaveClass('text-market-red');
|
||||
});
|
||||
|
||||
it('should render correct maker or taker role', async () => {
|
||||
const partyId = 'party-id';
|
||||
it('should render correct taker role', async () => {
|
||||
const takerFill = generateFill({
|
||||
seller: {
|
||||
id: partyId,
|
||||
},
|
||||
aggressor: Schema.Side.SIDE_SELL,
|
||||
});
|
||||
const { rerender } = render(
|
||||
<FillsTable partyId={partyId} rowData={[takerFill]} />
|
||||
);
|
||||
await act(async () => {
|
||||
render(<FillsTable partyId={partyId} rowData={[takerFill]} />);
|
||||
});
|
||||
expect(
|
||||
screen
|
||||
.getAllByRole('gridcell')
|
||||
.find((c) => c.getAttribute('col-id') === 'aggressor')
|
||||
).toHaveTextContent('Taker');
|
||||
});
|
||||
|
||||
it('should render correct maker role', async () => {
|
||||
const makerFill = generateFill({
|
||||
seller: {
|
||||
id: partyId,
|
||||
},
|
||||
aggressor: Schema.Side.SIDE_BUY,
|
||||
});
|
||||
rerender(<FillsTable partyId={partyId} rowData={[makerFill]} />);
|
||||
render(<FillsTable partyId={partyId} rowData={[makerFill]} />);
|
||||
|
||||
expect(
|
||||
screen
|
||||
@ -191,22 +201,19 @@ describe('FillsTable', () => {
|
||||
});
|
||||
|
||||
it('should render tooltip over fees', async () => {
|
||||
const partyId = 'party-id';
|
||||
const takerFill = generateFill({
|
||||
seller: {
|
||||
id: partyId,
|
||||
},
|
||||
aggressor: Schema.Side.SIDE_SELL,
|
||||
});
|
||||
render(<FillsTable partyId={partyId} rowData={[takerFill]} />);
|
||||
await act(async () => {
|
||||
render(<FillsTable partyId={partyId} rowData={[takerFill]} />);
|
||||
});
|
||||
|
||||
const feeCell = screen
|
||||
.getAllByRole('gridcell')
|
||||
.find(
|
||||
(c) =>
|
||||
c.getAttribute('col-id') ===
|
||||
'market.tradableInstrument.instrument.product'
|
||||
);
|
||||
.find((c) => c.getAttribute('col-id') === 'fee');
|
||||
|
||||
expect(feeCell).toBeInTheDocument();
|
||||
await userEvent.hover(feeCell as HTMLElement);
|
||||
@ -215,8 +222,29 @@ describe('FillsTable', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render tooltip over fees discounts', async () => {
|
||||
const takerFill = generateFill({
|
||||
seller: {
|
||||
id: partyId,
|
||||
},
|
||||
aggressor: Schema.Side.SIDE_SELL,
|
||||
});
|
||||
await act(async () => {
|
||||
render(<FillsTable partyId={partyId} rowData={[takerFill]} />);
|
||||
});
|
||||
|
||||
const cell = screen
|
||||
.getAllByRole('gridcell')
|
||||
.find((c) => c.getAttribute('col-id') === 'fee-discount');
|
||||
|
||||
expect(cell).toBeInTheDocument();
|
||||
await userEvent.hover(cell as HTMLElement);
|
||||
expect(
|
||||
await screen.findByTestId('fee-discount-breakdown-tooltip')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('negative positionDecimalPoints should be properly rendered in size column', async () => {
|
||||
const partyId = 'party-id';
|
||||
const negativeDecimalPositionFill = generateFill({
|
||||
...defaultFill,
|
||||
market: {
|
||||
@ -235,36 +263,83 @@ describe('FillsTable', () => {
|
||||
.find((c) => c.getAttribute('col-id') === 'size');
|
||||
expect(sizeCell).toHaveTextContent('3,000,000,000');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFeesBreakdown', () => {
|
||||
it('should return correct fees breakdown for a taker', () => {
|
||||
const fees = {
|
||||
makerFee: '1000',
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
};
|
||||
const expectedBreakdown = {
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
makerFee: '1000',
|
||||
totalFee: '6000',
|
||||
};
|
||||
expect(getFeesBreakdown('Taker', fees)).toEqual(expectedBreakdown);
|
||||
describe('FeesDiscountBreakdownTooltip', () => {
|
||||
it('shows all discounts', () => {
|
||||
const data = generateFill({
|
||||
...defaultFill,
|
||||
buyer: {
|
||||
id: partyId,
|
||||
},
|
||||
});
|
||||
|
||||
it('should return correct fees breakdown for a maker', () => {
|
||||
const fees = {
|
||||
makerFee: '1000',
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
};
|
||||
const expectedBreakdown = {
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
makerFee: '-1000',
|
||||
totalFee: '4000',
|
||||
};
|
||||
expect(getFeesBreakdown('Maker', fees)).toEqual(expectedBreakdown);
|
||||
const props = {
|
||||
data,
|
||||
partyId,
|
||||
value: data.market,
|
||||
} as Parameters<typeof FeesDiscountBreakdownTooltip>['0'];
|
||||
const { container } = render(<FeesDiscountBreakdownTooltip {...props} />);
|
||||
const dt = container.querySelectorAll('dt');
|
||||
const dd = container.querySelectorAll('dd');
|
||||
const expected = [
|
||||
{ label: 'Infrastructure Fee Referral Discount', value: '0.05 BTC' },
|
||||
{ label: 'Infrastructure Fee Volume Discount', value: '0.06 BTC' },
|
||||
{ label: 'Liquidity Fee Referral Discount', value: '0.01 BTC' },
|
||||
{ label: 'Liquidity Fee Volume Discount', value: '0.02 BTC' },
|
||||
{ label: 'Maker Fee Referral Discount', value: '0.03 BTC' },
|
||||
{ label: 'Maker Fee Volume Discount', value: '0.04 BTC' },
|
||||
];
|
||||
expected.forEach(({ label, value }, i) => {
|
||||
expect(dt[i]).toHaveTextContent(label);
|
||||
expect(dd[i]).toHaveTextContent(value);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFeesBreakdown', () => {
|
||||
it('should return correct fees breakdown for a taker', () => {
|
||||
const fees = {
|
||||
makerFee: '1000',
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
};
|
||||
const expectedBreakdown = {
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
makerFee: '1000',
|
||||
totalFee: '6000',
|
||||
};
|
||||
expect(getFeesBreakdown('Taker', fees)).toEqual(expectedBreakdown);
|
||||
});
|
||||
|
||||
it('should return correct fees breakdown for a maker', () => {
|
||||
const fees = {
|
||||
makerFee: '1000',
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
};
|
||||
const expectedBreakdown = {
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
makerFee: '-1000',
|
||||
totalFee: '4000',
|
||||
};
|
||||
expect(getFeesBreakdown('Maker', fees)).toEqual(expectedBreakdown);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTotalFeesDiscounts', () => {
|
||||
it('should return correct total value', () => {
|
||||
const fees = {
|
||||
infrastructureFeeReferralDiscount: '1',
|
||||
infrastructureFeeVolumeDiscount: '2',
|
||||
liquidityFeeReferralDiscount: '3',
|
||||
liquidityFeeVolumeDiscount: '4',
|
||||
makerFeeReferralDiscount: '5',
|
||||
makerFeeVolumeDiscount: '6',
|
||||
};
|
||||
expect(getTotalFeesDiscounts(fees as TradeFeeFieldsFragment)).toEqual(
|
||||
(1 + 2 + 3 + 4 + 5 + 6).toString()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -28,7 +28,10 @@ import type {
|
||||
import { forwardRef } from 'react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { Trade } from './fills-data-provider';
|
||||
import type { FillFieldsFragment } from './__generated__/Fills';
|
||||
import type {
|
||||
FillFieldsFragment,
|
||||
TradeFeeFieldsFragment,
|
||||
} from './__generated__/Fills';
|
||||
import { FillActionsDropdown } from './fill-actions-dropdown';
|
||||
import { getAsset } from '@vegaprotocol/markets';
|
||||
|
||||
@ -87,13 +90,34 @@ export const FillsTable = forwardRef<AgGridReact, Props>(
|
||||
},
|
||||
{
|
||||
headerName: t('Fee'),
|
||||
field: 'market.tradableInstrument.instrument.product',
|
||||
colId: 'fee',
|
||||
field: 'market',
|
||||
valueFormatter: formatFee(partyId),
|
||||
type: 'rightAligned',
|
||||
tooltipField: 'market.tradableInstrument.instrument.product',
|
||||
tooltipField: 'market',
|
||||
tooltipComponent: FeesBreakdownTooltip,
|
||||
tooltipComponentParams: { partyId },
|
||||
},
|
||||
{
|
||||
headerName: t('Fee Discount'),
|
||||
colId: 'fee-discount',
|
||||
field: 'market',
|
||||
valueFormatter: formatFeeDiscount(partyId),
|
||||
type: 'rightAligned',
|
||||
// return null to disable tooltip if fee discount is 0 or empty
|
||||
tooltipValueGetter: ({ valueFormatted, value }) => {
|
||||
return valueFormatted && /[1-9]/.test(valueFormatted)
|
||||
? valueFormatted
|
||||
: null;
|
||||
},
|
||||
cellRenderer: ({
|
||||
value,
|
||||
valueFormatted,
|
||||
}: VegaICellRendererParams<Trade, 'market'>) =>
|
||||
`${valueFormatted} ${(value && getAsset(value))?.symbol}`,
|
||||
tooltipComponent: FeesDiscountBreakdownTooltip,
|
||||
tooltipComponentParams: { partyId },
|
||||
},
|
||||
{
|
||||
headerName: t('Date'),
|
||||
field: 'createdAt',
|
||||
@ -214,14 +238,11 @@ const formatRole = (partyId: string) => {
|
||||
|
||||
const formatFee = (partyId: string) => {
|
||||
return ({
|
||||
value,
|
||||
value: market,
|
||||
data,
|
||||
}: VegaValueFormatterParams<
|
||||
Trade,
|
||||
'market.tradableInstrument.instrument.product'
|
||||
>) => {
|
||||
if (!value || !data || !data?.market) return '-';
|
||||
const asset = getAsset(data.market);
|
||||
}: VegaValueFormatterParams<Trade, 'market'>) => {
|
||||
if (!market || !data) return '-';
|
||||
const asset = getAsset(market);
|
||||
const { fees: feesObj, role } = getRoleAndFees({ data, partyId });
|
||||
if (!feesObj) return '-';
|
||||
|
||||
@ -231,6 +252,21 @@ const formatFee = (partyId: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
const formatFeeDiscount = (partyId: string) => {
|
||||
return ({
|
||||
value: market,
|
||||
data,
|
||||
}: VegaValueFormatterParams<Trade, 'market'>) => {
|
||||
if (!market || !data) return '-';
|
||||
const asset = getAsset(market);
|
||||
const { fees } = getRoleAndFees({ data, partyId });
|
||||
if (!fees) return '-';
|
||||
|
||||
const total = getTotalFeesDiscounts(fees);
|
||||
return addDecimalsFormatNumber(total, asset.decimals);
|
||||
};
|
||||
};
|
||||
|
||||
export const isEmptyFeeObj = (feeObj: Schema.TradeFee) => {
|
||||
if (!feeObj) return true;
|
||||
return (
|
||||
@ -251,50 +287,50 @@ export const getRoleAndFees = ({
|
||||
partyId?: string;
|
||||
}) => {
|
||||
let role: Role;
|
||||
let feesObj;
|
||||
let fees;
|
||||
if (data?.buyer.id === partyId) {
|
||||
if (data.aggressor === Schema.Side.SIDE_BUY) {
|
||||
role = TAKER;
|
||||
feesObj = data?.buyerFee;
|
||||
fees = data?.buyerFee;
|
||||
} else if (data.aggressor === Schema.Side.SIDE_SELL) {
|
||||
role = MAKER;
|
||||
feesObj = data?.sellerFee;
|
||||
fees = data?.sellerFee;
|
||||
} else {
|
||||
role = '-';
|
||||
feesObj = !isEmptyFeeObj(data?.buyerFee) ? data.buyerFee : data.sellerFee;
|
||||
fees = !isEmptyFeeObj(data?.buyerFee) ? data.buyerFee : data.sellerFee;
|
||||
}
|
||||
} else if (data?.seller.id === partyId) {
|
||||
if (data.aggressor === Schema.Side.SIDE_SELL) {
|
||||
role = TAKER;
|
||||
feesObj = data?.sellerFee;
|
||||
fees = data?.sellerFee;
|
||||
} else if (data.aggressor === Schema.Side.SIDE_BUY) {
|
||||
role = MAKER;
|
||||
feesObj = data?.buyerFee;
|
||||
fees = data?.buyerFee;
|
||||
} else {
|
||||
role = '-';
|
||||
feesObj = !isEmptyFeeObj(data.sellerFee) ? data.sellerFee : data.buyerFee;
|
||||
fees = !isEmptyFeeObj(data.sellerFee) ? data.sellerFee : data.buyerFee;
|
||||
}
|
||||
} else {
|
||||
return { role: '-', feesObj: '-' };
|
||||
return { role: '-', fees: undefined };
|
||||
}
|
||||
return { role, fees: feesObj };
|
||||
return { role, fees };
|
||||
};
|
||||
|
||||
const FeesBreakdownTooltip = ({
|
||||
data,
|
||||
value,
|
||||
value: market,
|
||||
partyId,
|
||||
}: ITooltipParams & { partyId?: string }) => {
|
||||
if (!value?.settlementAsset || !data) {
|
||||
}: ITooltipParams<Trade, Trade['market']> & { partyId?: string }) => {
|
||||
if (!market || !data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const asset = value.settlementAsset;
|
||||
const asset = getAsset(market);
|
||||
|
||||
const { role, fees: feesObj } = getRoleAndFees({ data, partyId }) ?? {};
|
||||
if (!feesObj) return null;
|
||||
const { role, fees } = getRoleAndFees({ data, partyId }) ?? {};
|
||||
if (!fees) return null;
|
||||
const { infrastructureFee, liquidityFee, makerFee, totalFee } =
|
||||
getFeesBreakdown(role, feesObj);
|
||||
getFeesBreakdown(role, fees);
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -344,14 +380,91 @@ const FeesBreakdownTooltip = ({
|
||||
);
|
||||
};
|
||||
|
||||
const FeesDiscountBreakdownTooltipItem = ({
|
||||
value,
|
||||
label,
|
||||
asset,
|
||||
}: {
|
||||
value?: string | null;
|
||||
label: string;
|
||||
asset: ReturnType<typeof getAsset>;
|
||||
}) =>
|
||||
value ? (
|
||||
<>
|
||||
<dt className="col-span-1">{label}</dt>
|
||||
<dd className="text-right col-span-1">
|
||||
{addDecimalsFormatNumber(value, asset.decimals)} {asset.symbol}
|
||||
</dd>
|
||||
</>
|
||||
) : null;
|
||||
|
||||
export const FeesDiscountBreakdownTooltip = ({
|
||||
data,
|
||||
partyId,
|
||||
}: ITooltipParams<Trade, Trade['market']> & { partyId?: string }) => {
|
||||
if (!data || !data.market) {
|
||||
return null;
|
||||
}
|
||||
const asset = getAsset(data.market);
|
||||
|
||||
const { fees } = getRoleAndFees({ data, partyId }) ?? {};
|
||||
if (!fees) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="fee-discount-breakdown-tooltip"
|
||||
className="max-w-sm bg-vega-light-100 dark:bg-vega-dark-100 border border-vega-light-200 dark:border-vega-dark-200 px-4 py-2 z-20 rounded text-sm break-word text-black dark:text-white"
|
||||
>
|
||||
<dl className="grid grid-cols-2 gap-x-1">
|
||||
<FeesDiscountBreakdownTooltipItem
|
||||
value={fees.infrastructureFeeReferralDiscount}
|
||||
label={t('Infrastructure Fee Referral Discount')}
|
||||
asset={asset}
|
||||
/>
|
||||
<FeesDiscountBreakdownTooltipItem
|
||||
value={fees.infrastructureFeeVolumeDiscount}
|
||||
label={t('Infrastructure Fee Volume Discount')}
|
||||
asset={asset}
|
||||
/>
|
||||
<FeesDiscountBreakdownTooltipItem
|
||||
value={fees.liquidityFeeReferralDiscount}
|
||||
label={t('Liquidity Fee Referral Discount')}
|
||||
asset={asset}
|
||||
/>
|
||||
<FeesDiscountBreakdownTooltipItem
|
||||
value={fees.liquidityFeeVolumeDiscount}
|
||||
label={t('Liquidity Fee Volume Discount')}
|
||||
asset={asset}
|
||||
/>
|
||||
<FeesDiscountBreakdownTooltipItem
|
||||
value={fees.makerFeeReferralDiscount}
|
||||
label={t('Maker Fee Referral Discount')}
|
||||
asset={asset}
|
||||
/>
|
||||
<FeesDiscountBreakdownTooltipItem
|
||||
value={fees.makerFeeVolumeDiscount}
|
||||
label={t('Maker Fee Volume Discount')}
|
||||
asset={asset}
|
||||
/>
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const getTotalFeesDiscounts = (fees: TradeFeeFieldsFragment) => {
|
||||
return (
|
||||
BigInt(fees.infrastructureFeeReferralDiscount || '0') +
|
||||
BigInt(fees.infrastructureFeeVolumeDiscount || '0') +
|
||||
BigInt(fees.liquidityFeeReferralDiscount || '0') +
|
||||
BigInt(fees.liquidityFeeVolumeDiscount || '0') +
|
||||
BigInt(fees.makerFeeReferralDiscount || '0') +
|
||||
BigInt(fees.makerFeeVolumeDiscount || '0')
|
||||
).toString();
|
||||
};
|
||||
|
||||
export const getFeesBreakdown = (
|
||||
role: Role,
|
||||
feesObj: {
|
||||
__typename?: 'TradeFee' | undefined;
|
||||
makerFee: string;
|
||||
infrastructureFee: string;
|
||||
liquidityFee: string;
|
||||
}
|
||||
feesObj: TradeFeeFieldsFragment
|
||||
) => {
|
||||
const makerFee =
|
||||
role === MAKER
|
||||
|
@ -28,12 +28,24 @@ export const generateFill = (override?: PartialDeep<Trade>) => {
|
||||
makerFee: '100',
|
||||
infrastructureFee: '100',
|
||||
liquidityFee: '100',
|
||||
liquidityFeeReferralDiscount: '1',
|
||||
liquidityFeeVolumeDiscount: '2',
|
||||
makerFeeReferralDiscount: '3',
|
||||
makerFeeVolumeDiscount: '4',
|
||||
infrastructureFeeReferralDiscount: '5',
|
||||
infrastructureFeeVolumeDiscount: '6',
|
||||
},
|
||||
sellerFee: {
|
||||
__typename: 'TradeFee',
|
||||
makerFee: '200',
|
||||
infrastructureFee: '200',
|
||||
liquidityFee: '200',
|
||||
liquidityFeeReferralDiscount: '2',
|
||||
liquidityFeeVolumeDiscount: '3',
|
||||
makerFeeReferralDiscount: '4',
|
||||
makerFeeVolumeDiscount: '5',
|
||||
infrastructureFeeReferralDiscount: '6',
|
||||
infrastructureFeeVolumeDiscount: '7',
|
||||
},
|
||||
market: {
|
||||
__typename: 'Market',
|
||||
|
@ -1,115 +0,0 @@
|
||||
import { totalFeesPercentage } from '../../market-utils';
|
||||
import type { TradeFee, FeeFactors } from '@vegaprotocol/types';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumberPercentage,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import BigNumber from 'bignumber.js';
|
||||
|
||||
export const FeesCell = ({ feeFactors }: { feeFactors: FeeFactors }) => (
|
||||
<Tooltip description={<FeesBreakdownPercentage feeFactors={feeFactors} />}>
|
||||
<span>{totalFeesPercentage(feeFactors) ?? '-'}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
export const FeesBreakdownPercentage = ({
|
||||
feeFactors,
|
||||
}: {
|
||||
feeFactors?: FeeFactors;
|
||||
}) => {
|
||||
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,
|
||||
symbol,
|
||||
decimals,
|
||||
}: {
|
||||
fees?: TradeFee;
|
||||
feeFactors?: FeeFactors;
|
||||
symbol?: string;
|
||||
decimals: number;
|
||||
}) => {
|
||||
if (!fees) return null;
|
||||
const totalFees = new BigNumber(fees.makerFee)
|
||||
.plus(fees.infrastructureFee)
|
||||
.plus(fees.liquidityFee)
|
||||
.toString();
|
||||
if (totalFees === '0') return null;
|
||||
const formatValue = (value: string | number | null | undefined): string => {
|
||||
return value && !isNaN(Number(value))
|
||||
? addDecimalsFormatNumber(value, decimals)
|
||||
: '-';
|
||||
};
|
||||
return (
|
||||
<dl className="grid grid-cols-6">
|
||||
<dt className="col-span-2">{t('Infrastructure fee')}</dt>
|
||||
{feeFactors && (
|
||||
<dd className="text-right col-span-1">
|
||||
{formatNumberPercentage(
|
||||
new BigNumber(feeFactors.infrastructureFee).times(100)
|
||||
)}
|
||||
</dd>
|
||||
)}
|
||||
<dd className="text-right col-span-3">
|
||||
{formatValue(fees.infrastructureFee)} {symbol || ''}
|
||||
</dd>
|
||||
<dt className="col-span-2">{t('Liquidity fee')}</dt>
|
||||
{feeFactors && (
|
||||
<dd className="text-right col-span-1">
|
||||
{formatNumberPercentage(
|
||||
new BigNumber(feeFactors.liquidityFee).times(100)
|
||||
)}
|
||||
</dd>
|
||||
)}
|
||||
<dd className="text-right col-span-3">
|
||||
{formatValue(fees.liquidityFee)} {symbol || ''}
|
||||
</dd>
|
||||
<dt className="col-span-2">{t('Maker fee')}</dt>
|
||||
{feeFactors && (
|
||||
<dd className="text-right col-span-1">
|
||||
{formatNumberPercentage(
|
||||
new BigNumber(feeFactors.makerFee).times(100)
|
||||
)}
|
||||
</dd>
|
||||
)}
|
||||
<dd className="text-right col-span-3">
|
||||
{formatValue(fees.makerFee)} {symbol || ''}
|
||||
</dd>
|
||||
<dt className="col-span-2">{t('Total fees')}</dt>
|
||||
{feeFactors && (
|
||||
<dd className="text-right col-span-1">
|
||||
{totalFeesPercentage(feeFactors)}
|
||||
</dd>
|
||||
)}
|
||||
<dd className="text-right col-span-3">
|
||||
{formatValue(totalFees)} {symbol || ''}
|
||||
</dd>
|
||||
</dl>
|
||||
);
|
||||
};
|
@ -1,4 +1,3 @@
|
||||
export * from './fees-breakdown';
|
||||
export * from './last-24h-price-change';
|
||||
export * from './last-24h-volume';
|
||||
export * from './market-info';
|
||||
|
@ -4,7 +4,7 @@ import { Fragment, useMemo, useState } from 'react';
|
||||
import { AssetDetailsTable, useAssetDataProvider } from '@vegaprotocol/assets';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { marketDataProvider } from '../../market-data-provider';
|
||||
import { totalFeesPercentage } from '../../market-utils';
|
||||
import { totalFeesFactorsPercentage } from '../../market-utils';
|
||||
import {
|
||||
Accordion,
|
||||
AccordionChevron,
|
||||
@ -91,7 +91,7 @@ export const CurrentFeesInfoPanel = ({ market }: MarketInfoProps) => (
|
||||
makerFee: market.fees.factors.makerFee,
|
||||
infrastructureFee: market.fees.factors.infrastructureFee,
|
||||
liquidityFee: market.fees.factors.liquidityFee,
|
||||
totalFees: totalFeesPercentage(market.fees.factors),
|
||||
totalFees: totalFeesFactorsPercentage(market.fees.factors),
|
||||
}}
|
||||
asPercentage={true}
|
||||
/>
|
||||
|
@ -3,7 +3,7 @@ import type { Market, MarketMaybeWithDataAndCandles } from './markets-provider';
|
||||
import {
|
||||
calcTradedFactor,
|
||||
filterAndSortMarkets,
|
||||
totalFeesPercentage,
|
||||
totalFeesFactorsPercentage,
|
||||
} from './market-utils';
|
||||
const { MarketState, MarketTradingMode } = Schema;
|
||||
|
||||
@ -63,7 +63,7 @@ describe('mapDataToMarketList', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('totalFees', () => {
|
||||
describe('totalFeesFactorsPercentage', () => {
|
||||
const createFee = (...f: number[]): Market['fees']['factors'] => ({
|
||||
__typename: 'FeeFactors',
|
||||
infrastructureFee: f[0].toString(),
|
||||
@ -78,7 +78,7 @@ 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(totalFeesPercentage(i)).toEqual(o);
|
||||
expect(totalFeesFactorsPercentage(i)).toEqual(o);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -49,18 +49,17 @@ export const getQuoteName = (market: Partial<Market>) => {
|
||||
throw new Error('Failed to retrieve quoteName. Invalid product type');
|
||||
};
|
||||
|
||||
export const totalFees = (fees: Market['fees']['factors']) => {
|
||||
export const sumFeesFactors = (fees: Market['fees']['factors']) => {
|
||||
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 totalFeesFactorsPercentage = (fees: Market['fees']['factors']) => {
|
||||
const total = fees && sumFeesFactors(fees);
|
||||
return total ? formatNumberPercentage(total.times(100)) : undefined;
|
||||
};
|
||||
|
||||
export const filterAndSortMarkets = (markets: MarketMaybeWithData[]) => {
|
||||
|
@ -123,11 +123,9 @@ export const OrderViewDialog = ({
|
||||
{t('rejection reason')}
|
||||
</div>
|
||||
<div data-testid={`order-rejection-reason-value`}>
|
||||
{
|
||||
Schema.OrderRejectionReasonMapping[
|
||||
order.rejectionReason as Schema.OrderRejectionReason
|
||||
]
|
||||
}
|
||||
{Schema.OrderRejectionReasonMapping[
|
||||
order.rejectionReason as Schema.OrderRejectionReason
|
||||
] || order.rejectionReason}
|
||||
</div>
|
||||
</KeyValueTableRow>
|
||||
)}
|
||||
|
Loading…
Reference in New Issue
Block a user