Compare commits

...
Author SHA1 Message Date
Madalina Raicu 361622dc5b Merge branch 'develop' of github.com:vegaprotocol/frontend-monorepo into fix/fills-fees-maker-discounts-develop 2023-12-01 16:47:43 +00:00
Madalina Raicu 21888cac64 Merge branch 'main' of github.com:vegaprotocol/frontend-monorepo into fix/fills-fees-maker-discounts-develop 2023-12-01 16:47:34 +00:00
m.ray a59f7dfd29 fix(trading): fills fees maker discounts (#5406) 2023-12-01 16:34:22 +00:00
Bartłomiej Głowniaandasiaznik 61471228aa fix(trading): use discount stats only from previous epoch (#5411)
Co-authored-by: asiaznik <artur@vegaprotocol.io>
2023-12-01 16:34:05 +00:00
Madalina Raicu cd8374ad56 Merge branch '5400-volume-discount-tier-incorrect' of github.com:vegaprotocol/frontend-monorepo into fix/fills-fees-maker-discounts-develop 2023-12-01 12:52:00 +00:00
Madalina Raicu 996a8b8722 fix: translation format 2023-12-01 12:49:37 +00:00
Madalina Raicu c1fb1c692a chore: update imports 2023-12-01 12:29:59 +00:00
Madalina Raicu 9113c0fabe Merge branch 'develop' of github.com:vegaprotocol/frontend-monorepo into fix/fills-fees-maker-discounts-develop 2023-12-01 12:23:03 +00:00
asiaznik 5923eac56b fix(trading): next tier in referral stats 2023-12-01 13:13:12 +01:00
Madalina Raicu e1c9323e3c fix: update unit test 2023-12-01 11:36:02 +00:00
Bartłomiej Głownia aa7c33b1a9 fix(trading): use discount stats only from previous epoch 2023-12-01 12:34:55 +01:00
Madalina Raicu b83c10a532 Merge branch 'main' of github.com:vegaprotocol/frontend-monorepo into fix/fills-fees-maker-discounts 2023-12-01 11:24:40 +00:00
m.ray 70d748fb15 fix(trading): fills fees fixes for maker (#5405) 2023-12-01 11:23:04 +00:00
Madalina Raicu 6551c2b02b fix: rename to fills-utils 2023-12-01 11:20:15 +00:00
Madalina Raicu dcf36acb0b Merge branch 'fix/fills-fees-maker' of github.com:vegaprotocol/frontend-monorepo into fix/fills-fees-maker-discounts 2023-12-01 11:18:50 +00:00
Madalina Raicu 44719c9ea5 fix: re-group types 2023-12-01 10:51:58 +00:00
Madalina Raicu 773b1cdc13 fix: re-group imports and add extra tests 2023-12-01 10:49:47 +00:00
Madalina Raicu 8d9f2cdda3 fix: rename new fees to role fees 2023-11-30 18:32:02 +00:00
Madalina Raicu 3b4fa06927 fix: layout of discounts 2023-11-30 18:15:40 +00:00
Madalina Raicu 6de1e0b83d fix(trading): override fees discounts if aggresor is maker and refactor 2023-11-30 18:07:44 +00:00
Madalina Raicu 85e9c1300c fix: if the market was active tootip copy change 2023-11-30 16:19:19 +00:00
Madalina Raicu 4e9c47c27e fix: market suspended tooltip 2023-11-30 16:12:41 +00:00
Madalina Raicu 9c3c0a30f6 fix: show correct total of fills fees 2023-11-30 16:08:19 +00:00
20 changed files with 534 additions and 539 deletions
@@ -12,7 +12,7 @@ import { type VegaICellRendererParams } from '@vegaprotocol/datagrid';
import { useRef, useLayoutEffect } from 'react'; import { useRef, useLayoutEffect } from 'react';
import { BREAKPOINT_MD } from '../../config/breakpoints'; import { BREAKPOINT_MD } from '../../config/breakpoints';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { ColDef } from 'ag-grid-community'; import { type ColDef } from 'ag-grid-community';
import type { RowClickedEvent } from 'ag-grid-community'; import type { RowClickedEvent } from 'ag-grid-community';
type AssetsTableProps = { type AssetsTableProps = {
@@ -8,7 +8,7 @@ import {
type VegaValueFormatterParams, type VegaValueFormatterParams,
} from '@vegaprotocol/datagrid'; } from '@vegaprotocol/datagrid';
import { useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useLayoutEffect, useMemo, useRef, useState } from 'react';
import { ColDef } from 'ag-grid-community'; import { type ColDef } from 'ag-grid-community';
import type { RowClickedEvent } from 'ag-grid-community'; import type { RowClickedEvent } from 'ag-grid-community';
import { getDateTimeFormat } from '@vegaprotocol/utils'; import { getDateTimeFormat } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
@@ -127,7 +127,7 @@ export const useStats = ({
t.discountFactor === discountFactorValue t.discountFactor === discountFactorValue
); );
const nextBenefitTierValue = currentBenefitTierValue const nextBenefitTierValue = currentBenefitTierValue
? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier - 1) ? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier + 1)
: minBy(benefitTiers, (bt) => bt.tier); // min tier number is lowest tier : minBy(benefitTiers, (bt) => bt.tier); // min tier number is lowest tier
const epochsValue = const epochsValue =
!isNaN(currentEpoch) && refereeInfo?.atEpoch !isNaN(currentEpoch) && refereeInfo?.atEpoch
@@ -16,18 +16,11 @@ query DiscountPrograms {
} }
} }
query Fees( query Fees($partyId: ID!) {
$partyId: ID!
$volumeDiscountEpochs: Int!
$referralDiscountEpochs: Int!
) {
epoch { epoch {
id id
} }
volumeDiscountStats( volumeDiscountStats(partyId: $partyId, pagination: { last: 1 }) {
partyId: $partyId
pagination: { last: $volumeDiscountEpochs }
) {
edges { edges {
node { node {
atEpoch atEpoch
@@ -59,10 +52,7 @@ query Fees(
} }
} }
} }
referralSetStats( referralSetStats(partyId: $partyId, pagination: { last: 1 }) {
partyId: $partyId
pagination: { last: $referralDiscountEpochs }
) {
edges { edges {
node { node {
atEpoch atEpoch
+3 -10
View File
@@ -10,8 +10,6 @@ export type DiscountProgramsQuery = { __typename?: 'Query', currentReferralProgr
export type FeesQueryVariables = Types.Exact<{ export type FeesQueryVariables = Types.Exact<{
partyId: Types.Scalars['ID']; partyId: Types.Scalars['ID'];
volumeDiscountEpochs: Types.Scalars['Int'];
referralDiscountEpochs: Types.Scalars['Int'];
}>; }>;
@@ -65,14 +63,11 @@ export type DiscountProgramsQueryHookResult = ReturnType<typeof useDiscountProgr
export type DiscountProgramsLazyQueryHookResult = ReturnType<typeof useDiscountProgramsLazyQuery>; export type DiscountProgramsLazyQueryHookResult = ReturnType<typeof useDiscountProgramsLazyQuery>;
export type DiscountProgramsQueryResult = Apollo.QueryResult<DiscountProgramsQuery, DiscountProgramsQueryVariables>; export type DiscountProgramsQueryResult = Apollo.QueryResult<DiscountProgramsQuery, DiscountProgramsQueryVariables>;
export const FeesDocument = gql` export const FeesDocument = gql`
query Fees($partyId: ID!, $volumeDiscountEpochs: Int!, $referralDiscountEpochs: Int!) { query Fees($partyId: ID!) {
epoch { epoch {
id id
} }
volumeDiscountStats( volumeDiscountStats(partyId: $partyId, pagination: {last: 1}) {
partyId: $partyId
pagination: {last: $volumeDiscountEpochs}
) {
edges { edges {
node { node {
atEpoch atEpoch
@@ -104,7 +99,7 @@ export const FeesDocument = gql`
} }
} }
} }
referralSetStats(partyId: $partyId, pagination: {last: $referralDiscountEpochs}) { referralSetStats(partyId: $partyId, pagination: {last: 1}) {
edges { edges {
node { node {
atEpoch atEpoch
@@ -129,8 +124,6 @@ export const FeesDocument = gql`
* const { data, loading, error } = useFeesQuery({ * const { data, loading, error } = useFeesQuery({
* variables: { * variables: {
* partyId: // value for 'partyId' * partyId: // value for 'partyId'
* volumeDiscountEpochs: // value for 'volumeDiscountEpochs'
* referralDiscountEpochs: // value for 'referralDiscountEpochs'
* }, * },
* }); * });
*/ */
@@ -42,19 +42,19 @@ export const FeesContainer = () => {
programData?.currentVolumeDiscountProgram?.windowLength || 1; programData?.currentVolumeDiscountProgram?.windowLength || 1;
const referralDiscountWindowLength = const referralDiscountWindowLength =
programData?.currentReferralProgram?.windowLength || 1; programData?.currentReferralProgram?.windowLength || 1;
const { data: feesData, loading: feesLoading } = useFeesQuery({ const { data: feesData, loading: feesLoading } = useFeesQuery({
variables: { variables: {
partyId: pubKey || '', partyId: pubKey || '',
volumeDiscountEpochs: volumeDiscountWindowLength,
referralDiscountEpochs: referralDiscountWindowLength,
}, },
skip: !pubKey || !programData, skip: !pubKey,
}); });
const previousEpoch = (Number(feesData?.epoch.id) || 0) - 1;
const { volumeDiscount, volumeTierIndex, volumeInWindow, volumeTiers } = const { volumeDiscount, volumeTierIndex, volumeInWindow, volumeTiers } =
useVolumeStats( useVolumeStats(
feesData?.volumeDiscountStats, previousEpoch,
feesData?.volumeDiscountStats.edges?.[0]?.node,
programData?.currentVolumeDiscountProgram programData?.currentVolumeDiscountProgram
); );
@@ -67,12 +67,12 @@ export const FeesContainer = () => {
code, code,
isReferrer, isReferrer,
} = useReferralStats( } = useReferralStats(
feesData?.referralSetStats, previousEpoch,
feesData?.referralSetReferees, feesData?.referralSetStats.edges?.[0]?.node,
feesData?.referralSetReferees.edges?.[0]?.node,
programData?.currentReferralProgram, programData?.currentReferralProgram,
feesData?.epoch, feesData?.referrer.edges?.[0]?.node,
feesData?.referrer, feesData?.referee.edges?.[0]?.node
feesData?.referee
); );
const loading = paramsLoading || feesLoading || programLoading; const loading = paramsLoading || feesLoading || programLoading;
@@ -466,7 +466,7 @@ const VolumeTiers = ({
</THead> </THead>
<tbody> <tbody>
{Array.from(tiers).map((tier, i) => { {Array.from(tiers).map((tier, i) => {
const isUserTier = tiers.length - 1 - tierIndex === i; const isUserTier = tierIndex === i;
return ( return (
<Tr key={i}> <Tr key={i}>
@@ -521,7 +521,7 @@ const ReferralTiers = ({
</THead> </THead>
<tbody> <tbody>
{Array.from(tiers).map((t, i) => { {Array.from(tiers).map((t, i) => {
const isUserTier = tiers.length - 1 - tierIndex === i; const isUserTier = tierIndex === i;
const requiredVolume = Number(t.minimumRunningNotionalTakerVolume); const requiredVolume = Number(t.minimumRunningNotionalTakerVolume);
let unlocksIn = null; let unlocksIn = null;
@@ -2,46 +2,15 @@ import { renderHook } from '@testing-library/react';
import { useReferralStats } from './use-referral-stats'; import { useReferralStats } from './use-referral-stats';
describe('useReferralStats', () => { describe('useReferralStats', () => {
const setStats = { const stat = {
edges: [ __typename: 'ReferralSetStats' as const,
{ atEpoch: 9,
__typename: 'ReferralSetStatsEdge' as const, discountFactor: '0.01',
node: { referralSetRunningNotionalTakerVolume: '100',
__typename: 'ReferralSetStats' as const,
atEpoch: 9,
discountFactor: '0.2',
referralSetRunningNotionalTakerVolume: '100',
},
},
{
__typename: 'ReferralSetStatsEdge' as const,
node: {
__typename: 'ReferralSetStats' as const,
atEpoch: 10,
discountFactor: '0.3',
referralSetRunningNotionalTakerVolume: '200',
},
},
],
}; };
const sets = { const set = {
edges: [ atEpoch: 4,
{
node: {
atEpoch: 3,
},
},
{
node: {
atEpoch: 4,
},
},
],
};
const epoch = {
id: '10',
}; };
const program = { const program = {
@@ -78,102 +47,36 @@ describe('useReferralStats', () => {
}); });
}); });
it('returns formatted data and tiers', () => { it('returns default values if set is not from previous epoch', () => {
const { result } = renderHook(() => const { result } = renderHook(() =>
useReferralStats(setStats, sets, program, epoch) useReferralStats(10, stat, set, program)
); );
// should use stats from latest epoch
const stats = setStats.edges[1].node;
const set = sets.edges[1].node;
expect(result.current).toEqual({ expect(result.current).toEqual({
referralDiscount: Number(stats.discountFactor), referralDiscount: 0,
referralVolumeInWindow: Number( referralVolumeInWindow: 0,
stats.referralSetRunningNotionalTakerVolume referralTierIndex: -1,
),
referralTierIndex: 1,
referralTiers: program.benefitTiers, referralTiers: program.benefitTiers,
epochsInSet: Number(epoch.id) - set.atEpoch, epochsInSet: 0,
code: undefined, code: undefined,
isReferrer: false, isReferrer: false,
}); });
}); });
it.each([ it('returns formatted data and tiers', () => {
{ joinedAt: 2, index: -1 },
{ joinedAt: 3, index: -1 },
{ joinedAt: 4, index: 0 },
{ joinedAt: 5, index: 0 },
{ joinedAt: 6, index: 1 },
{ joinedAt: 7, index: 1 },
{ joinedAt: 8, index: 2 },
{ joinedAt: 9, index: 2 },
])('joined at epoch: $joinedAt should be index: $index', (obj) => {
const statsA = {
edges: [
{
__typename: 'ReferralSetStatsEdge' as const,
node: {
__typename: 'ReferralSetStats' as const,
atEpoch: 10,
discountFactor: '0.3',
referralSetRunningNotionalTakerVolume: '100000',
},
},
],
};
const setsA = {
edges: [
{
node: {
atEpoch: Number(epoch.id) - obj.joinedAt,
},
},
],
};
const { result } = renderHook(() => const { result } = renderHook(() =>
useReferralStats(statsA, setsA, program, epoch) useReferralStats(9, stat, set, program)
); );
expect(result.current.referralTierIndex).toEqual(obj.index); expect(result.current).toEqual({
}); referralDiscount: Number(stat.discountFactor),
referralVolumeInWindow: Number(
it.each([ stat.referralSetRunningNotionalTakerVolume
{ volume: '50', index: -1 }, ),
{ volume: '100', index: 0 }, referralTierIndex: 0,
{ volume: '150', index: 0 }, referralTiers: program.benefitTiers,
{ volume: '200', index: 1 }, epochsInSet: stat.atEpoch - set.atEpoch,
{ volume: '250', index: 1 }, code: undefined,
{ volume: '300', index: 2 }, isReferrer: false,
{ volume: '999', index: 2 }, });
])('volume: $volume should be index: $index', (obj) => {
const statsA = {
edges: [
{
__typename: 'ReferralSetStatsEdge' as const,
node: {
__typename: 'ReferralSetStats' as const,
atEpoch: 10,
discountFactor: '0.3',
referralSetRunningNotionalTakerVolume: obj.volume,
},
},
],
};
const setsA = {
edges: [
{
node: {
atEpoch: 1,
},
},
],
};
const { result } = renderHook(() =>
useReferralStats(statsA, setsA, program, epoch)
);
expect(result.current.referralTierIndex).toEqual(obj.index);
}); });
}); });
@@ -1,20 +1,24 @@
import compact from 'lodash/compact';
import maxBy from 'lodash/maxBy';
import { getReferralBenefitTier } from './utils';
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees'; import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
import { first } from 'lodash';
export const useReferralStats = ( export const useReferralStats = (
setStats?: FeesQuery['referralSetStats'], previousEpoch?: number,
setReferees?: FeesQuery['referralSetReferees'], referralStats?: NonNullable<
FeesQuery['referralSetStats']['edges']['0']
>['node'],
setReferees?: NonNullable<
FeesQuery['referralSetReferees']['edges']['0']
>['node'],
program?: DiscountProgramsQuery['currentReferralProgram'], program?: DiscountProgramsQuery['currentReferralProgram'],
epoch?: FeesQuery['epoch'], setIfReferrer?: NonNullable<FeesQuery['referrer']['edges']['0']>['node'],
setIfReferrer?: FeesQuery['referrer'], setIfReferee?: NonNullable<FeesQuery['referee']['edges']['0']>['node']
setIfReferee?: FeesQuery['referee']
) => { ) => {
const referralTiers = program?.benefitTiers || []; const referralTiers = program?.benefitTiers || [];
if (!setStats || !setReferees || !program || !epoch) { if (
!previousEpoch ||
referralStats?.atEpoch !== previousEpoch ||
!program ||
!setReferees
) {
return { return {
referralDiscount: 0, referralDiscount: 0,
referralVolumeInWindow: 0, referralVolumeInWindow: 0,
@@ -26,41 +30,22 @@ export const useReferralStats = (
}; };
} }
const setIfReferrerData = first(
compact(setIfReferrer?.edges).map((e) => e.node)
);
const setIfRefereeData = first(
compact(setIfReferee?.edges).map((e) => e.node)
);
const referralSetsStats = compact(setStats.edges).map((e) => e.node);
const referralSets = compact(setReferees.edges).map((e) => e.node);
const referralSet = maxBy(referralSets, (s) => s.atEpoch);
const referralStats = maxBy(referralSetsStats, (s) => s.atEpoch);
const epochsInSet = referralSet ? Number(epoch.id) - referralSet.atEpoch : 0;
const referralDiscount = Number(referralStats?.discountFactor || 0); const referralDiscount = Number(referralStats?.discountFactor || 0);
const referralVolumeInWindow = Number( const referralVolumeInWindow = Number(
referralStats?.referralSetRunningNotionalTakerVolume || 0 referralStats?.referralSetRunningNotionalTakerVolume || 0
); );
const referralTierIndex = referralStats const referralTierIndex = referralTiers.findIndex(
? getReferralBenefitTier( (tier) => tier.referralDiscountFactor === referralStats?.discountFactor
epochsInSet, );
Number(referralStats.referralSetRunningNotionalTakerVolume),
referralTiers
)
: -1;
return { return {
referralDiscount, referralDiscount,
referralVolumeInWindow, referralVolumeInWindow,
referralTierIndex, referralTierIndex,
referralTiers, referralTiers,
epochsInSet, epochsInSet: referralStats.atEpoch - setReferees.atEpoch,
code: (setIfReferrerData || setIfRefereeData)?.id, code: (setIfReferrer || setIfReferee)?.id,
isReferrer: Boolean(setIfReferrerData), isReferrer: Boolean(setIfReferrer),
}; };
}; };
@@ -2,27 +2,11 @@ import { renderHook } from '@testing-library/react';
import { useVolumeStats } from './use-volume-stats'; import { useVolumeStats } from './use-volume-stats';
describe('useReferralStats', () => { describe('useReferralStats', () => {
const statsList = { const stats = {
edges: [ __typename: 'VolumeDiscountStats' as const,
{ atEpoch: 10,
__typename: 'VolumeDiscountStatsEdge' as const, discountFactor: '0.05',
node: { runningVolume: '200',
__typename: 'VolumeDiscountStats' as const,
atEpoch: 9,
discountFactor: '0.1',
runningVolume: '100',
},
},
{
__typename: 'VolumeDiscountStatsEdge' as const,
node: {
__typename: 'VolumeDiscountStats' as const,
atEpoch: 10,
discountFactor: '0.3',
runningVolume: '200',
},
},
],
}; };
const program = { const program = {
@@ -44,7 +28,7 @@ describe('useReferralStats', () => {
}; };
it('returns correct default values', () => { it('returns correct default values', () => {
const { result } = renderHook(() => useVolumeStats()); const { result } = renderHook(() => useVolumeStats(10));
expect(result.current).toEqual({ expect(result.current).toEqual({
volumeDiscount: 0, volumeDiscount: 0,
volumeInWindow: 0, volumeInWindow: 0,
@@ -53,11 +37,18 @@ describe('useReferralStats', () => {
}); });
}); });
it('returns formatted data and tiers', () => { it('returns default values if no stat is not from previous epoch', () => {
const { result } = renderHook(() => useVolumeStats(statsList, program)); const { result } = renderHook(() => useVolumeStats(11, stats, program));
expect(result.current).toEqual({
volumeDiscount: 0,
volumeInWindow: 0,
volumeTierIndex: -1,
volumeTiers: program.benefitTiers,
});
});
// should use stats from latest epoch it('returns formatted data and tiers', () => {
const stats = statsList.edges[1].node; const { result } = renderHook(() => useVolumeStats(10, stats, program));
expect(result.current).toEqual({ expect(result.current).toEqual({
volumeDiscount: Number(stats.discountFactor), volumeDiscount: Number(stats.discountFactor),
@@ -66,30 +57,4 @@ describe('useReferralStats', () => {
volumeTiers: program.benefitTiers, volumeTiers: program.benefitTiers,
}); });
}); });
it.each([
{ volume: '100', index: 0 },
{ volume: '150', index: 0 },
{ volume: '200', index: 1 },
{ volume: '250', index: 1 },
{ volume: '300', index: 2 },
{ volume: '350', index: 2 },
])('returns index: $index for the running volume: $volume', (obj) => {
const statsA = {
edges: [
{
__typename: 'VolumeDiscountStatsEdge' as const,
node: {
__typename: 'VolumeDiscountStats' as const,
atEpoch: 10,
discountFactor: '0.3',
runningVolume: obj.volume,
},
},
],
};
const { result } = renderHook(() => useVolumeStats(statsA, program));
expect(result.current.volumeTierIndex).toBe(obj.index);
});
}); });
@@ -1,15 +1,15 @@
import compact from 'lodash/compact';
import maxBy from 'lodash/maxBy';
import { getVolumeTier } from './utils';
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees'; import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
export const useVolumeStats = ( export const useVolumeStats = (
stats?: FeesQuery['volumeDiscountStats'], previousEpoch: number,
lastEpochStats?: NonNullable<
FeesQuery['volumeDiscountStats']['edges']['0']
>['node'],
program?: DiscountProgramsQuery['currentVolumeDiscountProgram'] program?: DiscountProgramsQuery['currentVolumeDiscountProgram']
) => { ) => {
const volumeTiers = program?.benefitTiers || []; const volumeTiers = program?.benefitTiers || [];
if (!stats || !program) { if (!lastEpochStats || lastEpochStats.atEpoch !== previousEpoch || !program) {
return { return {
volumeDiscount: 0, volumeDiscount: 0,
volumeTierIndex: -1, volumeTierIndex: -1,
@@ -18,11 +18,11 @@ export const useVolumeStats = (
}; };
} }
const volumeStats = compact(stats.edges).map((e) => e.node);
const lastEpochStats = maxBy(volumeStats, (s) => s.atEpoch);
const volumeDiscount = Number(lastEpochStats?.discountFactor || 0); const volumeDiscount = Number(lastEpochStats?.discountFactor || 0);
const volumeInWindow = Number(lastEpochStats?.runningVolume || 0); const volumeInWindow = Number(lastEpochStats?.runningVolume || 0);
const volumeTierIndex = getVolumeTier(volumeInWindow, volumeTiers); const volumeTierIndex = volumeTiers.findIndex(
(tier) => tier.volumeDiscountFactor === lastEpochStats?.discountFactor
);
return { return {
volumeDiscount, volumeDiscount,
@@ -20,73 +20,6 @@ export const formatPercentage = (num: number) => {
return formatter.format(parseFloat(pct.toFixed(5))); return formatter.format(parseFloat(pct.toFixed(5)));
}; };
/**
* Return the index of the benefit tier for volume discounts. A user
* only needs to fulfill a minimum volume requirement for the tier
*/
export const getVolumeTier = (
volume: number,
tiers: Array<{
minimumRunningNotionalTakerVolume: string;
}>
) => {
return tiers.findIndex((tier, i) => {
const nextTier = tiers[i + 1];
const validVolume =
volume >= Number(tier.minimumRunningNotionalTakerVolume);
if (nextTier) {
return (
validVolume &&
volume < Number(nextTier.minimumRunningNotionalTakerVolume)
);
}
return validVolume;
});
};
/**
* Return the index of the benefit tiers for referrals. A user must
* fulfill both the minimum epochs in the referral set, and the set
* must reach the combined total volume
*/
export const getReferralBenefitTier = (
epochsInSet: number,
volume: number,
tiers: Array<{
minimumRunningNotionalTakerVolume: string;
minimumEpochs: number;
}>
) => {
const indexByEpoch = tiers.findIndex((tier, i) => {
const nextTier = tiers[i + 1];
const validEpochs = epochsInSet >= tier.minimumEpochs;
if (nextTier) {
return validEpochs && epochsInSet < nextTier.minimumEpochs;
}
return validEpochs;
});
const indexByVolume = tiers.findIndex((tier, i) => {
const nextTier = tiers[i + 1];
const validVolume =
volume >= Number(tier.minimumRunningNotionalTakerVolume);
if (nextTier) {
return (
validVolume &&
volume < Number(nextTier.minimumRunningNotionalTakerVolume)
);
}
return validVolume;
});
return Math.min(indexByEpoch, indexByVolume);
};
/** /**
* Given a set of fees and a set of discounts return * Given a set of fees and a set of discounts return
* the adjusted fee factor * the adjusted fee factor
@@ -13,10 +13,10 @@ import { type IterableElement } from 'type-fest';
import { import {
AccountEventsDocument, AccountEventsDocument,
AccountsDocument, AccountsDocument,
AccountFieldsFragment, type AccountFieldsFragment,
AccountsQuery, type AccountsQuery,
AccountEventsSubscription, type AccountEventsSubscription,
AccountsQueryVariables, type AccountsQueryVariables,
} from './__generated__/Accounts'; } from './__generated__/Accounts';
import { type Asset } from '@vegaprotocol/assets'; import { type Asset } from '@vegaprotocol/assets';
@@ -25,7 +25,7 @@ import {
import { ApolloError } from '@apollo/client'; import { ApolloError } from '@apollo/client';
import type { GraphQLErrors } from '@apollo/client/errors'; import type { GraphQLErrors } from '@apollo/client/errors';
import { GraphQLError } from 'graphql'; import { GraphQLError } from 'graphql';
import { Subscription, Observable } from 'zen-observable-ts'; import { type Subscription, type Observable } from 'zen-observable-ts';
import { waitFor } from '@testing-library/react'; import { waitFor } from '@testing-library/react';
type Item = { type Item = {
@@ -5,7 +5,10 @@ import {
type NodeCheckTimeUpdateSubscription, type NodeCheckTimeUpdateSubscription,
} from '../../utils/__generated__/NodeCheck'; } from '../../utils/__generated__/NodeCheck';
import { Networks } from '../../types'; import { Networks } from '../../types';
import { createMockClient, RequestHandlerResponse } from 'mock-apollo-client'; import {
createMockClient,
type RequestHandlerResponse,
} from 'mock-apollo-client';
export type MockRequestConfig = { export type MockRequestConfig = {
hasError?: boolean; hasError?: boolean;
+45 -98
View File
@@ -4,14 +4,8 @@ import { getDateTimeFormat } from '@vegaprotocol/utils';
import * as Schema from '@vegaprotocol/types'; import * as Schema from '@vegaprotocol/types';
import type { PartialDeep } from 'type-fest'; import type { PartialDeep } from 'type-fest';
import type { Trade } from './fills-data-provider'; import type { Trade } from './fills-data-provider';
import { import { FeesDiscountBreakdownTooltip, FillsTable } from './fills-table';
FeesDiscountBreakdownTooltip,
FillsTable,
getFeesBreakdown,
getTotalFeesDiscounts,
} from './fills-table';
import { generateFill } from './test-helpers'; import { generateFill } from './test-helpers';
import type { TradeFeeFieldsFragment } from './__generated__/Fills';
const partyId = 'party-id'; const partyId = 'party-id';
const defaultFill: PartialDeep<Trade> = { const defaultFill: PartialDeep<Trade> = {
@@ -35,6 +29,7 @@ const defaultFill: PartialDeep<Trade> = {
}, },
createdAt: new Date('2022-02-02T14:00:00').toISOString(), createdAt: new Date('2022-02-02T14:00:00').toISOString(),
}; };
describe('FillsTable', () => { describe('FillsTable', () => {
it('correct columns are rendered', async () => { it('correct columns are rendered', async () => {
// 7005-FILL-001 // 7005-FILL-001
@@ -65,7 +60,7 @@ describe('FillsTable', () => {
expect(headers.map((h) => h.textContent?.trim())).toEqual(expectedHeaders); expect(headers.map((h) => h.textContent?.trim())).toEqual(expectedHeaders);
}); });
it('formats cells correctly for buyer fill', async () => { it('formats cells correctly for buyer fill for maker', async () => {
const buyerFill = generateFill({ const buyerFill = generateFill({
...defaultFill, ...defaultFill,
buyer: { buyer: {
@@ -89,7 +84,7 @@ describe('FillsTable', () => {
'3.00 BTC', '3.00 BTC',
'Maker', 'Maker',
'2.00 BTC', '2.00 BTC',
'0.27 BTC', '0.09 BTC',
getDateTimeFormat().format(new Date(buyerFill.createdAt)), getDateTimeFormat().format(new Date(buyerFill.createdAt)),
'', // action column '', // action column
]; ];
@@ -271,96 +266,48 @@ describe('FillsTable', () => {
.find((c) => c.getAttribute('col-id') === 'size'); .find((c) => c.getAttribute('col-id') === 'size');
expect(sizeCell).toHaveTextContent('3,000,000,000'); expect(sizeCell).toHaveTextContent('3,000,000,000');
}); });
});
describe('FeesDiscountBreakdownTooltip', () => { describe('FeesDiscountBreakdownTooltip', () => {
it('shows all discounts', () => { it('shows all discounts', () => {
const data = generateFill({ const data = generateFill({
...defaultFill, ...defaultFill,
buyer: { buyer: {
id: partyId, id: partyId,
}, },
}); });
const props = { const props = {
data, data,
partyId, partyId,
value: data.market, value: data.market,
} as Parameters<typeof FeesDiscountBreakdownTooltip>['0']; } as Parameters<typeof FeesDiscountBreakdownTooltip>['0'];
const { container } = render(<FeesDiscountBreakdownTooltip {...props} />); const { container } = render(<FeesDiscountBreakdownTooltip {...props} />);
const dt = container.querySelectorAll('dt'); const dt = container.querySelectorAll('dt');
const dd = container.querySelectorAll('dd'); const dd = container.querySelectorAll('dd');
const expectedDt = [ const expectedDt = [
'Infrastructure Fee', 'Infrastructure Fee',
'Referral Discount', 'Referral Discount',
'Volume Discount', 'Volume Discount',
'Liquidity Fee', 'Liquidity Fee',
'Referral Discount', 'Referral Discount',
'Volume Discount', 'Volume Discount',
'Maker Fee', 'Maker Fee',
'Referral Discount', 'Referral Discount',
'Volume Discount', 'Volume Discount',
]; ];
const expectedDD = [ const expectedDD = [
'0.05 BTC', '0.05 BTC',
'0.06 BTC', '0.06 BTC',
'0.01 BTC', '0.01 BTC',
'0.02 BTC', '0.02 BTC',
'0.03 BTC', '0.03 BTC',
'0.04 BTC', '0.04 BTC',
]; ];
expectedDt.forEach((label, i) => { expectedDt.forEach((label, i) => {
expect(dt[i]).toHaveTextContent(label); expect(dt[i]).toHaveTextContent(label);
}); });
expectedDD.forEach((label, i) => { expectedDD.forEach((label, i) => {
expect(dd[i]).toHaveTextContent(label); expect(dd[i]).toHaveTextContent(label);
});
}); });
}); });
}); });
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()
);
});
});
+34 -109
View File
@@ -28,20 +28,12 @@ import {
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { type Trade } from './fills-data-provider'; import { type Trade } from './fills-data-provider';
import {
type FillFieldsFragment,
type TradeFeeFieldsFragment,
} from './__generated__/Fills';
import { FillActionsDropdown } from './fill-actions-dropdown'; import { FillActionsDropdown } from './fill-actions-dropdown';
import { getAsset } from '@vegaprotocol/markets'; import { getAsset } from '@vegaprotocol/markets';
import { useT } from './use-t'; import { useT } from './use-t';
import { MAKER, TAKER, getFeesBreakdown, getRoleAndFees } from './fills-utils';
const TAKER = 'Taker'; type Props = (AgGridReactProps | AgReactUiProps) & {
const MAKER = 'Maker';
export type Role = typeof TAKER | typeof MAKER | '-';
export type Props = (AgGridReactProps | AgReactUiProps) & {
partyId: string; partyId: string;
onMarketClick?: (marketId: string, metaKey?: boolean) => void; onMarketClick?: (marketId: string, metaKey?: boolean) => void;
}; };
@@ -262,63 +254,13 @@ const formatFeeDiscount = (partyId: string) => {
}: VegaValueFormatterParams<Trade, 'market'>) => { }: VegaValueFormatterParams<Trade, 'market'>) => {
if (!market || !data) return '-'; if (!market || !data) return '-';
const asset = getAsset(market); const asset = getAsset(market);
const { fees } = getRoleAndFees({ data, partyId }); const { fees: roleFees, role } = getRoleAndFees({ data, partyId });
if (!fees) return '-'; if (!roleFees) return '-';
const { totalFeeDiscount } = getFeesBreakdown(role, roleFees);
const total = getTotalFeesDiscounts(fees); return addDecimalsFormatNumber(totalFeeDiscount, asset.decimals);
return addDecimalsFormatNumber(total, asset.decimals);
}; };
}; };
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 fees;
if (data?.buyer.id === partyId) {
if (data.aggressor === Schema.Side.SIDE_BUY) {
role = TAKER;
fees = data?.buyerFee;
} else if (data.aggressor === Schema.Side.SIDE_SELL) {
role = MAKER;
fees = data?.sellerFee;
} else {
role = '-';
fees = !isEmptyFeeObj(data?.buyerFee) ? data.buyerFee : data.sellerFee;
}
} else if (data?.seller.id === partyId) {
if (data.aggressor === Schema.Side.SIDE_SELL) {
role = TAKER;
fees = data?.sellerFee;
} else if (data.aggressor === Schema.Side.SIDE_BUY) {
role = MAKER;
fees = data?.buyerFee;
} else {
role = '-';
fees = !isEmptyFeeObj(data.sellerFee) ? data.sellerFee : data.buyerFee;
}
} else {
return { role: '-', fees: undefined };
}
return { role, fees };
};
const FeesBreakdownTooltip = ({ const FeesBreakdownTooltip = ({
data, data,
value: market, value: market,
@@ -331,16 +273,23 @@ const FeesBreakdownTooltip = ({
const asset = getAsset(market); const asset = getAsset(market);
const { role, fees } = getRoleAndFees({ data, partyId }) ?? {}; const { role, fees, marketState } = getRoleAndFees({ data, partyId }) ?? {};
if (!fees) return null; if (!fees) return null;
const { infrastructureFee, liquidityFee, makerFee, totalFee } = const { infrastructureFee, liquidityFee, makerFee, totalFee } =
getFeesBreakdown(role, fees); getFeesBreakdown(role, fees, marketState);
return ( return (
<div <div
data-testid="fee-breakdown-tooltip" data-testid="fee-breakdown-tooltip"
className="bg-vega-light-100 dark:bg-vega-dark-100 border-vega-light-200 dark:border-vega-dark-200 break-word z-20 max-w-sm rounded border px-4 py-2 text-sm text-black dark:text-white" className="bg-vega-light-100 dark:bg-vega-dark-100 border-vega-light-200 dark:border-vega-dark-200 break-word z-20 max-w-sm rounded border px-4 py-2 text-xs text-black dark:text-white"
> >
{marketState && (
<p className="mb-1 italic">
{t('If the market was {{state}}', {
state: Schema.MarketStateMapping[marketState].toLowerCase(),
})}
</p>
)}
{role === MAKER && ( {role === MAKER && (
<> <>
<p className="mb-1">{t('The maker will receive the maker fee.')}</p> <p className="mb-1">{t('The maker will receive the maker fee.')}</p>
@@ -354,7 +303,7 @@ const FeesBreakdownTooltip = ({
{role === TAKER && ( {role === TAKER && (
<p className="mb-1">{t('Fees to be paid by the taker.')}</p> <p className="mb-1">{t('Fees to be paid by the taker.')}</p>
)} )}
{role === '-' && ( {(role === '-' || marketState === Schema.MarketState.STATE_SUSPENDED) && (
<p className="mb-1"> <p className="mb-1">
{t( {t(
'If the market is in monitoring auction, half of the infrastructure and liquidity fees will be paid.' 'If the market is in monitoring auction, half of the infrastructure and liquidity fees will be paid.'
@@ -395,8 +344,8 @@ const FeesDiscountBreakdownTooltipItem = ({
}) => }) =>
value && value !== '0' ? ( value && value !== '0' ? (
<> <>
<dt className="col-span-1">{label}</dt> <dt className="col-span-2">{label}</dt>
<dd className="col-span-1 text-right"> <dd className="col-span-2 text-right">
{addDecimalsFormatNumber(value, asset.decimals)} {asset.symbol} {addDecimalsFormatNumber(value, asset.decimals)} {asset.symbol}
</dd> </dd>
</> </>
@@ -412,15 +361,19 @@ export const FeesDiscountBreakdownTooltip = ({
} }
const asset = getAsset(data.market); const asset = getAsset(data.market);
const { fees } = getRoleAndFees({ data, partyId }) ?? {}; const {
if (!fees) return null; fees: roleFees,
marketState,
role,
} = getRoleAndFees({ data, partyId }) ?? {};
if (!roleFees) return null;
const fees = getFeesBreakdown(role, roleFees, marketState);
return ( return (
<div <div
data-testid="fee-discount-breakdown-tooltip" data-testid="fee-discount-breakdown-tooltip"
className="bg-vega-light-100 dark:bg-vega-dark-100 border-vega-light-200 dark:border-vega-dark-200 break-word z-20 max-w-sm rounded border px-4 py-2 text-sm text-black dark:text-white" className="bg-vega-light-100 dark:bg-vega-dark-100 border-vega-light-200 dark:border-vega-dark-200 break-word z-20 max-w-sm rounded border px-4 py-2 text-sm text-black dark:text-white"
> >
<dl className="grid grid-cols-2 gap-x-1"> <dl className="grid grid-cols-6 gap-x-1 text-xs">
{(fees.infrastructureFeeReferralDiscount || '0') !== '0' || {(fees.infrastructureFeeReferralDiscount || '0') !== '0' ||
(fees.infrastructureFeeVolumeDiscount || '0') !== '0' ? ( (fees.infrastructureFeeVolumeDiscount || '0') !== '0' ? (
<dt className="col-span-2">{t('Infrastructure Fee')}</dt> <dt className="col-span-2">{t('Infrastructure Fee')}</dt>
@@ -464,42 +417,14 @@ export const FeesDiscountBreakdownTooltip = ({
label={t('Volume Discount')} label={t('Volume Discount')}
asset={asset} asset={asset}
/> />
<dt className="col-span-2">{t('Total Fee Discount')}</dt>
<FeesDiscountBreakdownTooltipItem
value={fees.totalFeeDiscount}
label={''}
asset={asset}
/>
</dl> </dl>
</div> </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: TradeFeeFieldsFragment
) => {
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,
};
};
+183
View File
@@ -0,0 +1,183 @@
import { getFeesBreakdown } from './fills-utils';
import * as Schema from '@vegaprotocol/types';
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',
totalFeeDiscount: '0',
};
expect(getFeesBreakdown('Taker', fees)).toEqual(expectedBreakdown);
});
it('should return correct fees breakdown for a maker if market is active', () => {
const fees = {
makerFee: '1000',
infrastructureFee: '2000',
liquidityFee: '3000',
};
const expectedBreakdown = {
infrastructureFee: '0',
liquidityFee: '0',
makerFee: '-1000',
totalFee: '-1000',
totalFeeDiscount: '0',
};
expect(
getFeesBreakdown('Maker', fees, Schema.MarketState.STATE_ACTIVE)
).toEqual(expectedBreakdown);
});
it('should return correct fees breakdown for a maker if the market is suspended', () => {
const fees = {
infrastructureFee: '2000',
liquidityFee: '3000',
makerFee: '0',
};
const expectedBreakdown = {
infrastructureFee: '1000',
liquidityFee: '1500',
makerFee: '0',
totalFee: '2500',
totalFeeDiscount: '0',
};
expect(
getFeesBreakdown('Maker', fees, Schema.MarketState.STATE_SUSPENDED)
).toEqual(expectedBreakdown);
});
it('should return correct fees breakdown for a taker if the market is suspended', () => {
const fees = {
infrastructureFee: '2000',
liquidityFee: '3000',
makerFee: '0',
};
const expectedBreakdown = {
infrastructureFee: '1000',
liquidityFee: '1500',
makerFee: '0',
totalFee: '2500',
totalFeeDiscount: '0',
};
expect(
getFeesBreakdown('Taker', fees, Schema.MarketState.STATE_SUSPENDED)
).toEqual(expectedBreakdown);
});
it('should return correct fees breakdown for a taker if market is active', () => {
const fees = {
makerFee: '1000',
infrastructureFee: '2000',
liquidityFee: '3000',
};
const expectedBreakdown = {
infrastructureFee: '2000',
liquidityFee: '3000',
makerFee: '1000',
totalFee: '6000',
totalFeeDiscount: '0',
};
expect(
getFeesBreakdown('Taker', fees, Schema.MarketState.STATE_ACTIVE)
).toEqual(expectedBreakdown);
});
it('should return correct fees breakdown for a maker', () => {
const fees = {
makerFee: '1000',
infrastructureFee: '2000',
liquidityFee: '3000',
};
const expectedBreakdown = {
infrastructureFee: '0',
liquidityFee: '0',
makerFee: '-1000',
totalFee: '-1000',
totalFeeDiscount: '0',
};
expect(getFeesBreakdown('Maker', fees)).toEqual(expectedBreakdown);
});
it('should return correct total fees discount value for a taker (if the market is active - default)', () => {
const fees = {
infrastructureFeeReferralDiscount: '1',
infrastructureFeeVolumeDiscount: '2',
liquidityFeeReferralDiscount: '3',
liquidityFeeVolumeDiscount: '4',
makerFeeReferralDiscount: '5',
makerFeeVolumeDiscount: '6',
infrastructureFee: '1000',
liquidityFee: '2000',
makerFee: '3000',
};
const { totalFeeDiscount } = getFeesBreakdown('Taker', fees);
expect(totalFeeDiscount).toEqual((1 + 2 + 3 + 4 + 5 + 6).toString());
});
it('should return correct total fees discount value for a maker (if the market is active - default)', () => {
const fees = {
infrastructureFeeReferralDiscount: '1',
infrastructureFeeVolumeDiscount: '2',
liquidityFeeReferralDiscount: '3',
liquidityFeeVolumeDiscount: '4',
makerFeeReferralDiscount: '5',
makerFeeVolumeDiscount: '6',
infrastructureFee: '1000',
liquidityFee: '2000',
makerFee: '3000',
};
const { totalFeeDiscount } = getFeesBreakdown('Maker', fees);
// makerFeeReferralDiscount and makerFeeVolumeDiscount are added, infra and liq. fees are zeroed
expect(totalFeeDiscount).toEqual((5 + 6).toString());
});
it('should return correct total fees discount value for a maker (if the market is suspended)', () => {
const fees = {
infrastructureFeeReferralDiscount: '1',
infrastructureFeeVolumeDiscount: '2',
liquidityFeeReferralDiscount: '3',
liquidityFeeVolumeDiscount: '4',
makerFeeReferralDiscount: '5',
makerFeeVolumeDiscount: '6',
infrastructureFee: '1000',
liquidityFee: '2000',
makerFee: '3000',
};
const { totalFeeDiscount } = getFeesBreakdown(
'Maker',
fees,
Schema.MarketState.STATE_SUSPENDED
);
// makerFeeReferralDiscount and makerFeeVolumeDiscount are zeroed, infra and liq. fees are halved
expect(totalFeeDiscount).toEqual(((1 + 2 + 3 + 4) / 2).toString());
});
it('should return correct total fees discount value for a taker (if the market is suspended)', () => {
const fees = {
infrastructureFeeReferralDiscount: '1',
infrastructureFeeVolumeDiscount: '2',
liquidityFeeReferralDiscount: '3',
liquidityFeeVolumeDiscount: '4',
makerFeeReferralDiscount: '5',
makerFeeVolumeDiscount: '6',
infrastructureFee: '1000',
liquidityFee: '2000',
makerFee: '3000',
};
const { totalFeeDiscount } = getFeesBreakdown(
'Taker',
fees,
Schema.MarketState.STATE_SUSPENDED
);
// makerFeeReferralDiscount and makerFeeVolumeDiscount are zeroed, infra and liq. fees are halved
expect(totalFeeDiscount).toEqual(((1 + 2 + 3 + 4) / 2).toString());
});
});
+164
View File
@@ -0,0 +1,164 @@
import BigNumber from 'bignumber.js';
import type {
FillFieldsFragment,
TradeFeeFieldsFragment,
} from './__generated__/Fills';
import * as Schema from '@vegaprotocol/types';
export const TAKER = 'Taker';
export const MAKER = 'Maker';
export type Role = typeof TAKER | typeof MAKER | '-';
export const getRoleAndFees = ({
data,
partyId,
}: {
data: Pick<
FillFieldsFragment,
'buyerFee' | 'sellerFee' | 'buyer' | 'seller' | 'aggressor'
>;
partyId?: string;
}): {
role: Role;
fees?: TradeFeeFieldsFragment;
marketState?: Schema.MarketState;
} => {
let role: Role;
let fees;
if (data?.buyer.id === partyId) {
if (data.aggressor === Schema.Side.SIDE_BUY) {
role = TAKER;
fees = data?.buyerFee;
} else if (data.aggressor === Schema.Side.SIDE_SELL) {
role = MAKER;
fees = data?.sellerFee;
} else {
role = '-';
fees = !isEmptyFeeObj(data?.buyerFee) ? data.buyerFee : data.sellerFee;
}
} else if (data?.seller.id === partyId) {
if (data.aggressor === Schema.Side.SIDE_SELL) {
role = TAKER;
fees = data?.sellerFee;
} else if (data.aggressor === Schema.Side.SIDE_BUY) {
role = MAKER;
fees = data?.buyerFee;
} else {
role = '-';
fees = !isEmptyFeeObj(data.sellerFee) ? data.sellerFee : data.buyerFee;
}
} else {
return { role: '-', fees: undefined };
}
// We make the assumption that the market state is active if the maker fee is zero on both sides
// This needs to be updated when we have a way to get the correct market state when that fill happened from the API
// because the maker fee factor can be set to 0 via governance
const marketState =
data?.buyerFee.makerFee === data.sellerFee.makerFee &&
new BigNumber(data?.buyerFee.makerFee).isZero()
? Schema.MarketState.STATE_SUSPENDED
: Schema.MarketState.STATE_ACTIVE;
return { role, fees, marketState };
};
export const getFeesBreakdown = (
role: Role,
fees: TradeFeeFieldsFragment,
marketState: Schema.MarketState = Schema.MarketState.STATE_ACTIVE
) => {
// If market is in auction we assume maker fee is zero
const isMarketActive = marketState === Schema.MarketState.STATE_ACTIVE;
// If role is taker, then these are the fees to be paid
let { makerFee, infrastructureFee, liquidityFee } = fees;
// If role is taker, then these are the fees discounts to be applied
let {
makerFeeVolumeDiscount,
makerFeeReferralDiscount,
infrastructureFeeVolumeDiscount,
infrastructureFeeReferralDiscount,
liquidityFeeVolumeDiscount,
liquidityFeeReferralDiscount,
} = fees;
if (isMarketActive) {
if (role === MAKER) {
makerFee = new BigNumber(fees.makerFee).times(-1).toString();
infrastructureFee = '0';
liquidityFee = '0';
// discounts are also zero or we can leave them undefined
infrastructureFeeReferralDiscount =
infrastructureFeeReferralDiscount && '0';
infrastructureFeeVolumeDiscount = infrastructureFeeVolumeDiscount && '0';
liquidityFeeReferralDiscount = liquidityFeeReferralDiscount && '0';
liquidityFeeVolumeDiscount = liquidityFeeVolumeDiscount && '0';
// we leave maker discount fees as they are defined
}
} else {
// If market is suspended (in monitoring auction), then half of the fees are paid
infrastructureFee = new BigNumber(infrastructureFee)
.dividedBy(2)
.toString();
liquidityFee = new BigNumber(liquidityFee).dividedBy(2).toString();
// maker fee is already zero
makerFee = '0';
// discounts are also halved
infrastructureFeeReferralDiscount =
infrastructureFeeReferralDiscount &&
new BigNumber(infrastructureFeeReferralDiscount).dividedBy(2).toString();
infrastructureFeeVolumeDiscount =
infrastructureFeeVolumeDiscount &&
new BigNumber(infrastructureFeeVolumeDiscount).dividedBy(2).toString();
liquidityFeeReferralDiscount =
liquidityFeeReferralDiscount &&
new BigNumber(liquidityFeeReferralDiscount).dividedBy(2).toString();
liquidityFeeVolumeDiscount =
liquidityFeeVolumeDiscount &&
new BigNumber(liquidityFeeVolumeDiscount).dividedBy(2).toString();
// maker discount fees should already be zero
makerFeeReferralDiscount = makerFeeReferralDiscount && '0';
makerFeeVolumeDiscount = makerFeeVolumeDiscount && '0';
}
const totalFee = new BigNumber(infrastructureFee)
.plus(makerFee)
.plus(liquidityFee)
.toString();
const totalFeeDiscount = new BigNumber(makerFeeVolumeDiscount || '0')
.plus(makerFeeReferralDiscount || '0')
.plus(infrastructureFeeReferralDiscount || '0')
.plus(infrastructureFeeVolumeDiscount || '0')
.plus(liquidityFeeReferralDiscount || '0')
.plus(liquidityFeeVolumeDiscount || '0')
.toString();
return {
infrastructureFee,
infrastructureFeeReferralDiscount,
infrastructureFeeVolumeDiscount,
liquidityFee,
liquidityFeeReferralDiscount,
liquidityFeeVolumeDiscount,
makerFee,
makerFeeReferralDiscount,
makerFeeVolumeDiscount,
totalFee,
totalFeeDiscount,
};
};
export const isEmptyFeeObj = (feeObj: Schema.TradeFee) => {
if (!feeObj) return true;
return (
feeObj.liquidityFee === '0' &&
feeObj.makerFee === '0' &&
feeObj.infrastructureFee === '0'
);
};
@@ -7,7 +7,11 @@ import {
} from '@vegaprotocol/data-provider'; } from '@vegaprotocol/data-provider';
import { type Market } from '@vegaprotocol/markets'; import { type Market } from '@vegaprotocol/markets';
import { marketsMapProvider } from '@vegaprotocol/markets'; import { marketsMapProvider } from '@vegaprotocol/markets';
import { Cursor, type PageInfo, type Edge } from '@vegaprotocol/data-provider'; import {
type Cursor,
type PageInfo,
type Edge,
} from '@vegaprotocol/data-provider';
import { OrderStatus } from '@vegaprotocol/types'; import { OrderStatus } from '@vegaprotocol/types';
import { import {
OrdersDocument, OrdersDocument,
@@ -14,7 +14,7 @@ import {
} from './__generated__/Erc20Approval'; } from './__generated__/Erc20Approval';
import { import {
PendingWithdrawalFragmentDoc, PendingWithdrawalFragmentDoc,
PendingWithdrawalFragment, type PendingWithdrawalFragment,
} from './__generated__/Withdrawal'; } from './__generated__/Withdrawal';
export const useCompleteWithdraw = () => { export const useCompleteWithdraw = () => {