vega-frontend-monorepo/libs/fills/src/lib/fills-table.tsx

354 lines
10 KiB
TypeScript

import type {
AgGridReact,
AgGridReactProps,
AgReactUiProps,
} from 'ag-grid-react';
import type { ITooltipParams } from 'ag-grid-community';
import {
addDecimal,
addDecimalsFormatNumber,
formatNumber,
getDateTimeFormat,
isNumeric,
} from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n';
import * as Schema from '@vegaprotocol/types';
import { AgGridColumn } from 'ag-grid-react';
import {
AgGridDynamic as AgGrid,
positiveClassNames,
negativeClassNames,
MarketNameCell,
} from '@vegaprotocol/datagrid';
import type { VegaValueFormatterParams } from '@vegaprotocol/datagrid';
import { forwardRef } from 'react';
import BigNumber from 'bignumber.js';
import type { Trade } from './fills-data-provider';
import type { FillFieldsFragment } from './__generated__/Fills';
const TAKER = 'Taker';
const MAKER = 'Maker';
export type Role = typeof TAKER | typeof MAKER | '-';
export type Props = (AgGridReactProps | AgReactUiProps) & {
partyId: string;
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
};
export const FillsTable = forwardRef<AgGridReact, Props>(
({ partyId, onMarketClick, ...props }, ref) => {
return (
<AgGrid
ref={ref}
overlayNoRowsTemplate={t('No fills')}
defaultColDef={{ flex: 1, resizable: true }}
style={{ width: '100%', height: '100%' }}
getRowId={({ data }) => data?.id}
tooltipShowDelay={0}
tooltipHideDelay={2000}
components={{ MarketNameCell }}
{...props}
>
<AgGridColumn
headerName={t('Market')}
field="market.tradableInstrument.instrument.name"
cellRenderer="MarketNameCell"
cellRendererParams={{ idPath: 'market.id', onMarketClick }}
/>
<AgGridColumn
headerName={t('Size')}
type="rightAligned"
field="size"
cellClassRules={{
[positiveClassNames]: ({ data }: { data: Trade }) => {
const partySide = getPartySide(data, partyId);
return partySide === 'buyer';
},
[negativeClassNames]: ({ data }: { data: Trade }) => {
const partySide = getPartySide(data, partyId);
return partySide === 'seller';
},
}}
valueFormatter={formatSize(partyId)}
/>
<AgGridColumn
headerName={t('Price')}
field="price"
valueFormatter={formatPrice}
type="rightAligned"
/>
<AgGridColumn
headerName={t('Notional')}
field="price"
valueFormatter={formatTotal}
type="rightAligned"
/>
<AgGridColumn
headerName={t('Role')}
field="aggressor"
valueFormatter={formatRole(partyId)}
/>
<AgGridColumn
headerName={t('Fee')}
field="market.tradableInstrument.instrument.product"
valueFormatter={formatFee(partyId)}
type="rightAligned"
tooltipField="market.tradableInstrument.instrument.product"
tooltipComponent={FeesBreakdownTooltip}
tooltipComponentParams={{ partyId }}
/>
<AgGridColumn
headerName={t('Date')}
field="createdAt"
valueFormatter={({
value,
}: VegaValueFormatterParams<Trade, 'createdAt'>) => {
return value ? getDateTimeFormat().format(new Date(value)) : '';
}}
/>
</AgGrid>
);
}
);
const formatPrice = ({
value,
data,
}: VegaValueFormatterParams<Trade, 'price'>) => {
if (!data?.market || !isNumeric(value)) {
return '-';
}
const asset =
data?.market.tradableInstrument.instrument.product.settlementAsset.symbol;
const valueFormatted = addDecimalsFormatNumber(
value,
data?.market.decimalPlaces
);
return `${valueFormatted} ${asset}`;
};
const formatSize = (partyId: string) => {
return ({ value, data }: VegaValueFormatterParams<Trade, 'size'>) => {
if (!data?.market || !isNumeric(value)) {
return '-';
}
let prefix = '';
const partySide = getPartySide(data, partyId);
if (partySide === 'buyer') {
prefix = '+';
} else if (partySide === 'seller') {
prefix = '-';
}
const size = addDecimalsFormatNumber(
value,
data?.market.positionDecimalPlaces
);
return `${prefix}${size}`;
};
};
const getPartySide = (
data: Trade,
partyId: string
): 'buyer' | 'seller' | undefined => {
let result = undefined;
if (data?.buyer.id === partyId) {
result = 'buyer' as const;
} else if (data?.seller.id === partyId) {
result = 'seller' as const;
}
return result;
};
const formatTotal = ({
value,
data,
}: VegaValueFormatterParams<Trade, 'price'>) => {
if (!data?.market || !isNumeric(value)) {
return '-';
}
const { symbol: assetSymbol, decimals: assetDecimals } =
data?.market.tradableInstrument.instrument.product.settlementAsset ?? {};
const size = new BigNumber(
addDecimal(data?.size, data?.market.positionDecimalPlaces)
);
const price = new BigNumber(addDecimal(value, data?.market.decimalPlaces));
const total = size.times(price).toString();
const valueFormatted = formatNumber(total, assetDecimals);
return `${valueFormatted} ${assetSymbol}`;
};
const formatRole = (partyId: string) => {
return ({ data }: VegaValueFormatterParams<Trade, 'aggressor'>) => {
if (!data) return '-';
const { role } = getRoleAndFees({ data, partyId });
return role;
};
};
const formatFee = (partyId: string) => {
return ({
value,
data,
}: VegaValueFormatterParams<
Trade,
'market.tradableInstrument.instrument.product'
>) => {
if (!value?.settlementAsset || !data) {
return '-';
}
const asset = value.settlementAsset;
const { fees: feesObj, role } = getRoleAndFees({ data, partyId });
if (!feesObj) return '-';
const { totalFee } = getFeesBreakdown(role, feesObj);
const totalFees = addDecimalsFormatNumber(totalFee, asset.decimals);
return `${totalFees} ${asset.symbol}`;
};
};
export const isEmptyFeeObj = (feeObj: Schema.TradeFee) => {
if (!feeObj) return true;
return (
feeObj.liquidityFee === '0' &&
feeObj.makerFee === '0' &&
feeObj.infrastructureFee === '0'
);
};
export const getRoleAndFees = ({
data,
partyId,
}: {
data: Pick<
FillFieldsFragment,
'buyerFee' | 'sellerFee' | 'buyer' | 'seller' | 'aggressor'
>;
partyId?: string;
}) => {
let role: Role;
let feesObj;
if (data?.buyer.id === partyId) {
if (data.aggressor === Schema.Side.SIDE_BUY) {
role = TAKER;
feesObj = data?.buyerFee;
} else if (data.aggressor === Schema.Side.SIDE_SELL) {
role = MAKER;
feesObj = data?.sellerFee;
} else {
role = '-';
feesObj = !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;
} else if (data.aggressor === Schema.Side.SIDE_BUY) {
role = MAKER;
feesObj = data?.buyerFee;
} else {
role = '-';
feesObj = !isEmptyFeeObj(data.sellerFee) ? data.sellerFee : data.buyerFee;
}
} else {
return { role: '-', feesObj: '-' };
}
return { role, fees: feesObj };
};
const FeesBreakdownTooltip = ({
data,
value,
partyId,
}: ITooltipParams & { partyId?: string }) => {
if (!value?.settlementAsset || !data) {
return null;
}
const asset = value.settlementAsset;
const { role, fees: feesObj } = getRoleAndFees({ data, partyId }) ?? {};
if (!feesObj) return null;
const { infrastructureFee, liquidityFee, makerFee, totalFee } =
getFeesBreakdown(role, feesObj);
return (
<div
data-testid="fee-breakdown-tooltip"
className="max-w-sm border border-neutral-600 bg-neutral-100 dark:bg-neutral-800 px-4 py-2 z-20 rounded text-sm break-word text-black dark:text-white"
>
{role === MAKER && (
<>
<p className="mb-1">{t('The maker will receive the maker fee.')}</p>
<p className="mb-1">
{t(
'If the market is active the maker will pay zero infrastructure and liquidity fees.'
)}
</p>
</>
)}
{role === TAKER && (
<p className="mb-1">{t('Fees to be paid by the taker.')}</p>
)}
{role === '-' && (
<p className="mb-1">
{t(
'If the market is in monitoring auction, half of the infrastructure and liquidity fees will be paid.'
)}
</p>
)}
<dl className="grid grid-cols-2 gap-x-1">
<dt className="col-span-1">{t('Infrastructure fee')}</dt>
<dd className="text-right col-span-1">
{addDecimalsFormatNumber(infrastructureFee, asset.decimals)}{' '}
{asset.symbol}
</dd>
<dt className="col-span-1">{t('Liquidity fee')}</dt>
<dd className="text-right col-span-1">
{addDecimalsFormatNumber(liquidityFee, asset.decimals)} {asset.symbol}
</dd>
<dt className="col-span-1">{t('Maker fee')}</dt>
<dd className="text-right col-span-1">
{addDecimalsFormatNumber(makerFee, asset.decimals)} {asset.symbol}
</dd>
<dt className="col-span-1">{t('Total fees')}</dt>
<dd className="text-right col-span-1">
{addDecimalsFormatNumber(totalFee, asset.decimals)} {asset.symbol}
</dd>
</dl>
</div>
);
};
export const getFeesBreakdown = (
role: Role,
feesObj: {
__typename?: 'TradeFee' | undefined;
makerFee: string;
infrastructureFee: string;
liquidityFee: string;
}
) => {
const makerFee =
role === MAKER
? new BigNumber(feesObj.makerFee).times(-1).toString()
: feesObj.makerFee;
const infrastructureFee = feesObj.infrastructureFee;
const liquidityFee = feesObj.liquidityFee;
const totalFee = new BigNumber(infrastructureFee)
.plus(makerFee)
.plus(liquidityFee)
.toString();
return {
infrastructureFee,
liquidityFee,
makerFee,
totalFee,
};
};