Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec87858b22 | ||
|
|
ee98dfd5fd | ||
|
|
7c30ad9998 | ||
|
|
2bbd64b757 | ||
|
|
aaad3cf402 | ||
|
|
bb69f5c05f | ||
|
|
e1aa355c97 |
@@ -12,7 +12,7 @@ import { type VegaICellRendererParams } from '@vegaprotocol/datagrid';
|
||||
import { useRef, useLayoutEffect } from 'react';
|
||||
import { BREAKPOINT_MD } from '../../config/breakpoints';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { type ColDef } from 'ag-grid-community';
|
||||
import { ColDef } from 'ag-grid-community';
|
||||
import type { RowClickedEvent } from 'ag-grid-community';
|
||||
|
||||
type AssetsTableProps = {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
type VegaValueFormatterParams,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { type ColDef } from 'ag-grid-community';
|
||||
import { ColDef } from 'ag-grid-community';
|
||||
import type { RowClickedEvent } from 'ag-grid-community';
|
||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
@@ -1,11 +1,34 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { useNavigateToLastMarket } from '../../lib/hooks/use-navigate-to-last-market';
|
||||
import { useGlobalStore } from '../../stores';
|
||||
import { useTopTradedMarkets } from '../../lib/hooks/use-top-traded-markets';
|
||||
import { Links } from '../../lib/links';
|
||||
|
||||
// The home pages only purpose is to redirect to the users last market,
|
||||
// the top traded if they are new, or fall back to the list of markets.
|
||||
// Thats why we just render a loader here
|
||||
export const Home = () => {
|
||||
useNavigateToLastMarket();
|
||||
const navigate = useNavigate();
|
||||
const { data } = useTopTradedMarkets();
|
||||
const marketId = useGlobalStore((store) => store.marketId);
|
||||
|
||||
useEffect(() => {
|
||||
if (marketId) {
|
||||
navigate(Links.MARKET(marketId), {
|
||||
replace: true,
|
||||
});
|
||||
} else if (data) {
|
||||
const marketDataId = data[0]?.id;
|
||||
if (marketDataId) {
|
||||
navigate(Links.MARKET(marketDataId), {
|
||||
replace: true,
|
||||
});
|
||||
} else {
|
||||
navigate(Links.MARKETS());
|
||||
}
|
||||
}
|
||||
}, [marketId, data, navigate]);
|
||||
|
||||
return (
|
||||
<Splash>
|
||||
|
||||
@@ -127,7 +127,7 @@ export const useStats = ({
|
||||
t.discountFactor === discountFactorValue
|
||||
);
|
||||
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
|
||||
const epochsValue =
|
||||
!isNaN(currentEpoch) && refereeInfo?.atEpoch
|
||||
|
||||
@@ -16,11 +16,18 @@ query DiscountPrograms {
|
||||
}
|
||||
}
|
||||
|
||||
query Fees($partyId: ID!) {
|
||||
query Fees(
|
||||
$partyId: ID!
|
||||
$volumeDiscountEpochs: Int!
|
||||
$referralDiscountEpochs: Int!
|
||||
) {
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
volumeDiscountStats(partyId: $partyId, pagination: { last: 1 }) {
|
||||
volumeDiscountStats(
|
||||
partyId: $partyId
|
||||
pagination: { last: $volumeDiscountEpochs }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
@@ -52,7 +59,10 @@ query Fees($partyId: ID!) {
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetStats(partyId: $partyId, pagination: { last: 1 }) {
|
||||
referralSetStats(
|
||||
partyId: $partyId
|
||||
pagination: { last: $referralDiscountEpochs }
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
|
||||
+10
-3
@@ -10,6 +10,8 @@ export type DiscountProgramsQuery = { __typename?: 'Query', currentReferralProgr
|
||||
|
||||
export type FeesQueryVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
volumeDiscountEpochs: Types.Scalars['Int'];
|
||||
referralDiscountEpochs: Types.Scalars['Int'];
|
||||
}>;
|
||||
|
||||
|
||||
@@ -63,11 +65,14 @@ export type DiscountProgramsQueryHookResult = ReturnType<typeof useDiscountProgr
|
||||
export type DiscountProgramsLazyQueryHookResult = ReturnType<typeof useDiscountProgramsLazyQuery>;
|
||||
export type DiscountProgramsQueryResult = Apollo.QueryResult<DiscountProgramsQuery, DiscountProgramsQueryVariables>;
|
||||
export const FeesDocument = gql`
|
||||
query Fees($partyId: ID!) {
|
||||
query Fees($partyId: ID!, $volumeDiscountEpochs: Int!, $referralDiscountEpochs: Int!) {
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
volumeDiscountStats(partyId: $partyId, pagination: {last: 1}) {
|
||||
volumeDiscountStats(
|
||||
partyId: $partyId
|
||||
pagination: {last: $volumeDiscountEpochs}
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
@@ -99,7 +104,7 @@ export const FeesDocument = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetStats(partyId: $partyId, pagination: {last: 1}) {
|
||||
referralSetStats(partyId: $partyId, pagination: {last: $referralDiscountEpochs}) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
@@ -124,6 +129,8 @@ export const FeesDocument = gql`
|
||||
* const { data, loading, error } = useFeesQuery({
|
||||
* variables: {
|
||||
* partyId: // value for 'partyId'
|
||||
* volumeDiscountEpochs: // value for 'volumeDiscountEpochs'
|
||||
* referralDiscountEpochs: // value for 'referralDiscountEpochs'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
@@ -42,19 +42,19 @@ export const FeesContainer = () => {
|
||||
programData?.currentVolumeDiscountProgram?.windowLength || 1;
|
||||
const referralDiscountWindowLength =
|
||||
programData?.currentReferralProgram?.windowLength || 1;
|
||||
|
||||
const { data: feesData, loading: feesLoading } = useFeesQuery({
|
||||
variables: {
|
||||
partyId: pubKey || '',
|
||||
volumeDiscountEpochs: volumeDiscountWindowLength,
|
||||
referralDiscountEpochs: referralDiscountWindowLength,
|
||||
},
|
||||
skip: !pubKey,
|
||||
skip: !pubKey || !programData,
|
||||
});
|
||||
|
||||
const previousEpoch = (Number(feesData?.epoch.id) || 0) - 1;
|
||||
|
||||
const { volumeDiscount, volumeTierIndex, volumeInWindow, volumeTiers } =
|
||||
useVolumeStats(
|
||||
previousEpoch,
|
||||
feesData?.volumeDiscountStats.edges?.[0]?.node,
|
||||
feesData?.volumeDiscountStats,
|
||||
programData?.currentVolumeDiscountProgram
|
||||
);
|
||||
|
||||
@@ -67,12 +67,12 @@ export const FeesContainer = () => {
|
||||
code,
|
||||
isReferrer,
|
||||
} = useReferralStats(
|
||||
previousEpoch,
|
||||
feesData?.referralSetStats.edges?.[0]?.node,
|
||||
feesData?.referralSetReferees.edges?.[0]?.node,
|
||||
feesData?.referralSetStats,
|
||||
feesData?.referralSetReferees,
|
||||
programData?.currentReferralProgram,
|
||||
feesData?.referrer.edges?.[0]?.node,
|
||||
feesData?.referee.edges?.[0]?.node
|
||||
feesData?.epoch,
|
||||
feesData?.referrer,
|
||||
feesData?.referee
|
||||
);
|
||||
|
||||
const loading = paramsLoading || feesLoading || programLoading;
|
||||
@@ -466,7 +466,7 @@ const VolumeTiers = ({
|
||||
</THead>
|
||||
<tbody>
|
||||
{Array.from(tiers).map((tier, i) => {
|
||||
const isUserTier = tierIndex === i;
|
||||
const isUserTier = tiers.length - 1 - tierIndex === i;
|
||||
|
||||
return (
|
||||
<Tr key={i}>
|
||||
@@ -521,7 +521,7 @@ const ReferralTiers = ({
|
||||
</THead>
|
||||
<tbody>
|
||||
{Array.from(tiers).map((t, i) => {
|
||||
const isUserTier = tierIndex === i;
|
||||
const isUserTier = tiers.length - 1 - tierIndex === i;
|
||||
|
||||
const requiredVolume = Number(t.minimumRunningNotionalTakerVolume);
|
||||
let unlocksIn = null;
|
||||
|
||||
@@ -2,15 +2,46 @@ import { renderHook } from '@testing-library/react';
|
||||
import { useReferralStats } from './use-referral-stats';
|
||||
|
||||
describe('useReferralStats', () => {
|
||||
const stat = {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 9,
|
||||
discountFactor: '0.01',
|
||||
referralSetRunningNotionalTakerVolume: '100',
|
||||
const setStats = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'ReferralSetStatsEdge' as const,
|
||||
node: {
|
||||
__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 set = {
|
||||
atEpoch: 4,
|
||||
const sets = {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
atEpoch: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
atEpoch: 4,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const epoch = {
|
||||
id: '10',
|
||||
};
|
||||
|
||||
const program = {
|
||||
@@ -47,36 +78,102 @@ describe('useReferralStats', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns default values if set is not from previous epoch', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useReferralStats(10, stat, set, program)
|
||||
);
|
||||
expect(result.current).toEqual({
|
||||
referralDiscount: 0,
|
||||
referralVolumeInWindow: 0,
|
||||
referralTierIndex: -1,
|
||||
referralTiers: program.benefitTiers,
|
||||
epochsInSet: 0,
|
||||
code: undefined,
|
||||
isReferrer: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns formatted data and tiers', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useReferralStats(9, stat, set, program)
|
||||
useReferralStats(setStats, sets, program, epoch)
|
||||
);
|
||||
|
||||
// should use stats from latest epoch
|
||||
const stats = setStats.edges[1].node;
|
||||
const set = sets.edges[1].node;
|
||||
|
||||
expect(result.current).toEqual({
|
||||
referralDiscount: Number(stat.discountFactor),
|
||||
referralDiscount: Number(stats.discountFactor),
|
||||
referralVolumeInWindow: Number(
|
||||
stat.referralSetRunningNotionalTakerVolume
|
||||
stats.referralSetRunningNotionalTakerVolume
|
||||
),
|
||||
referralTierIndex: 0,
|
||||
referralTierIndex: 1,
|
||||
referralTiers: program.benefitTiers,
|
||||
epochsInSet: stat.atEpoch - set.atEpoch,
|
||||
epochsInSet: Number(epoch.id) - set.atEpoch,
|
||||
code: undefined,
|
||||
isReferrer: false,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ 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(() =>
|
||||
useReferralStats(statsA, setsA, program, epoch)
|
||||
);
|
||||
|
||||
expect(result.current.referralTierIndex).toEqual(obj.index);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ volume: '50', index: -1 },
|
||||
{ volume: '100', index: 0 },
|
||||
{ volume: '150', index: 0 },
|
||||
{ volume: '200', index: 1 },
|
||||
{ volume: '250', index: 1 },
|
||||
{ volume: '300', index: 2 },
|
||||
{ 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,24 +1,20 @@
|
||||
import compact from 'lodash/compact';
|
||||
import maxBy from 'lodash/maxBy';
|
||||
import { getReferralBenefitTier } from './utils';
|
||||
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
|
||||
import { first } from 'lodash';
|
||||
|
||||
export const useReferralStats = (
|
||||
previousEpoch?: number,
|
||||
referralStats?: NonNullable<
|
||||
FeesQuery['referralSetStats']['edges']['0']
|
||||
>['node'],
|
||||
setReferees?: NonNullable<
|
||||
FeesQuery['referralSetReferees']['edges']['0']
|
||||
>['node'],
|
||||
setStats?: FeesQuery['referralSetStats'],
|
||||
setReferees?: FeesQuery['referralSetReferees'],
|
||||
program?: DiscountProgramsQuery['currentReferralProgram'],
|
||||
setIfReferrer?: NonNullable<FeesQuery['referrer']['edges']['0']>['node'],
|
||||
setIfReferee?: NonNullable<FeesQuery['referee']['edges']['0']>['node']
|
||||
epoch?: FeesQuery['epoch'],
|
||||
setIfReferrer?: FeesQuery['referrer'],
|
||||
setIfReferee?: FeesQuery['referee']
|
||||
) => {
|
||||
const referralTiers = program?.benefitTiers || [];
|
||||
|
||||
if (
|
||||
!previousEpoch ||
|
||||
referralStats?.atEpoch !== previousEpoch ||
|
||||
!program ||
|
||||
!setReferees
|
||||
) {
|
||||
if (!setStats || !setReferees || !program || !epoch) {
|
||||
return {
|
||||
referralDiscount: 0,
|
||||
referralVolumeInWindow: 0,
|
||||
@@ -30,22 +26,41 @@ 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 referralVolumeInWindow = Number(
|
||||
referralStats?.referralSetRunningNotionalTakerVolume || 0
|
||||
);
|
||||
|
||||
const referralTierIndex = referralTiers.findIndex(
|
||||
(tier) => tier.referralDiscountFactor === referralStats?.discountFactor
|
||||
);
|
||||
const referralTierIndex = referralStats
|
||||
? getReferralBenefitTier(
|
||||
epochsInSet,
|
||||
Number(referralStats.referralSetRunningNotionalTakerVolume),
|
||||
referralTiers
|
||||
)
|
||||
: -1;
|
||||
|
||||
return {
|
||||
referralDiscount,
|
||||
referralVolumeInWindow,
|
||||
referralTierIndex,
|
||||
referralTiers,
|
||||
epochsInSet: referralStats.atEpoch - setReferees.atEpoch,
|
||||
code: (setIfReferrer || setIfReferee)?.id,
|
||||
isReferrer: Boolean(setIfReferrer),
|
||||
epochsInSet,
|
||||
code: (setIfReferrerData || setIfRefereeData)?.id,
|
||||
isReferrer: Boolean(setIfReferrerData),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,11 +2,27 @@ import { renderHook } from '@testing-library/react';
|
||||
import { useVolumeStats } from './use-volume-stats';
|
||||
|
||||
describe('useReferralStats', () => {
|
||||
const stats = {
|
||||
__typename: 'VolumeDiscountStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.05',
|
||||
runningVolume: '200',
|
||||
const statsList = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'VolumeDiscountStatsEdge' as const,
|
||||
node: {
|
||||
__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 = {
|
||||
@@ -28,7 +44,7 @@ describe('useReferralStats', () => {
|
||||
};
|
||||
|
||||
it('returns correct default values', () => {
|
||||
const { result } = renderHook(() => useVolumeStats(10));
|
||||
const { result } = renderHook(() => useVolumeStats());
|
||||
expect(result.current).toEqual({
|
||||
volumeDiscount: 0,
|
||||
volumeInWindow: 0,
|
||||
@@ -37,18 +53,11 @@ describe('useReferralStats', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns default values if no stat is not from previous epoch', () => {
|
||||
const { result } = renderHook(() => useVolumeStats(11, stats, program));
|
||||
expect(result.current).toEqual({
|
||||
volumeDiscount: 0,
|
||||
volumeInWindow: 0,
|
||||
volumeTierIndex: -1,
|
||||
volumeTiers: program.benefitTiers,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns formatted data and tiers', () => {
|
||||
const { result } = renderHook(() => useVolumeStats(10, stats, program));
|
||||
const { result } = renderHook(() => useVolumeStats(statsList, program));
|
||||
|
||||
// should use stats from latest epoch
|
||||
const stats = statsList.edges[1].node;
|
||||
|
||||
expect(result.current).toEqual({
|
||||
volumeDiscount: Number(stats.discountFactor),
|
||||
@@ -57,4 +66,30 @@ describe('useReferralStats', () => {
|
||||
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';
|
||||
|
||||
export const useVolumeStats = (
|
||||
previousEpoch: number,
|
||||
lastEpochStats?: NonNullable<
|
||||
FeesQuery['volumeDiscountStats']['edges']['0']
|
||||
>['node'],
|
||||
stats?: FeesQuery['volumeDiscountStats'],
|
||||
program?: DiscountProgramsQuery['currentVolumeDiscountProgram']
|
||||
) => {
|
||||
const volumeTiers = program?.benefitTiers || [];
|
||||
|
||||
if (!lastEpochStats || lastEpochStats.atEpoch !== previousEpoch || !program) {
|
||||
if (!stats || !program) {
|
||||
return {
|
||||
volumeDiscount: 0,
|
||||
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 volumeInWindow = Number(lastEpochStats?.runningVolume || 0);
|
||||
const volumeTierIndex = volumeTiers.findIndex(
|
||||
(tier) => tier.volumeDiscountFactor === lastEpochStats?.discountFactor
|
||||
);
|
||||
const volumeTierIndex = getVolumeTier(volumeInWindow, volumeTiers);
|
||||
|
||||
return {
|
||||
volumeDiscount,
|
||||
|
||||
@@ -20,6 +20,73 @@ export const formatPercentage = (num: number) => {
|
||||
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
|
||||
* the adjusted fee factor
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
import * as router from 'react-router';
|
||||
import { useNavigateToLastMarket } from './use-navigate-to-last-market';
|
||||
import { useGlobalStore } from '../../stores';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useTopTradedMarkets } from './use-top-traded-markets';
|
||||
import { Links } from '../links';
|
||||
|
||||
const mockLastMarketId = 'LAST';
|
||||
|
||||
jest.mock('../../stores', () => {
|
||||
const original = jest.requireActual('../../stores');
|
||||
return {
|
||||
...original,
|
||||
useGlobalStore: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('./use-top-traded-markets', () => {
|
||||
return {
|
||||
useTopTradedMarkets: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('useNavigateToLastMarket', () => {
|
||||
const navigate = jest.fn();
|
||||
beforeAll(() => {
|
||||
jest.spyOn(router, 'useNavigate').mockImplementation(() => navigate);
|
||||
});
|
||||
|
||||
it('navigates to the last market when it is active', () => {
|
||||
(useGlobalStore as unknown as jest.Mock).mockReturnValue(mockLastMarketId);
|
||||
(useTopTradedMarkets as jest.Mock).mockReturnValue({
|
||||
data: [{ id: mockLastMarketId }],
|
||||
});
|
||||
renderHook(() => useNavigateToLastMarket());
|
||||
expect(navigate).toHaveBeenCalledWith(Links.MARKET(mockLastMarketId), {
|
||||
replace: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('navigates to the top traded market if the last one is not active', () => {
|
||||
(useGlobalStore as unknown as jest.Mock).mockReturnValue(mockLastMarketId);
|
||||
(useTopTradedMarkets as jest.Mock).mockReturnValue({
|
||||
data: [{ id: 'TOP' }],
|
||||
});
|
||||
renderHook(() => useNavigateToLastMarket());
|
||||
expect(navigate).toHaveBeenCalledWith(Links.MARKET('TOP'), {
|
||||
replace: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('navigates to the list of markets when all of the markets are not active', () => {
|
||||
(useGlobalStore as unknown as jest.Mock).mockReturnValue(mockLastMarketId);
|
||||
(useTopTradedMarkets as jest.Mock).mockReturnValue({
|
||||
data: [],
|
||||
});
|
||||
renderHook(() => useNavigateToLastMarket());
|
||||
expect(navigate).toHaveBeenCalledWith(Links.MARKETS());
|
||||
});
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
import { useGlobalStore } from '../../stores';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTopTradedMarkets } from './use-top-traded-markets';
|
||||
import { useEffect } from 'react';
|
||||
import { Links } from '../links';
|
||||
|
||||
export const useNavigateToLastMarket = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
// this returns a list of active markets ordered by traded factor
|
||||
// hence there's no need to pull markets again or find out in separate
|
||||
// query of the state of last market
|
||||
const { data } = useTopTradedMarkets();
|
||||
const lastMarketId = useGlobalStore((store) => store.marketId);
|
||||
const isLastMarketActive = data?.some((m) => m.id === lastMarketId);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
// if last market id is set and it is active, navigate to that market
|
||||
if (lastMarketId && isLastMarketActive) {
|
||||
navigate(Links.MARKET(lastMarketId), {
|
||||
replace: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise if there's a top traded market, navigate to that market
|
||||
const marketDataId = data[0]?.id;
|
||||
if (marketDataId) {
|
||||
navigate(Links.MARKET(marketDataId), {
|
||||
replace: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise navigate to the list of all markets
|
||||
navigate(Links.MARKETS());
|
||||
}, [lastMarketId, data, navigate, isLastMarketActive]);
|
||||
};
|
||||
@@ -13,10 +13,10 @@ import { type IterableElement } from 'type-fest';
|
||||
import {
|
||||
AccountEventsDocument,
|
||||
AccountsDocument,
|
||||
type AccountFieldsFragment,
|
||||
type AccountsQuery,
|
||||
type AccountEventsSubscription,
|
||||
type AccountsQueryVariables,
|
||||
AccountFieldsFragment,
|
||||
AccountsQuery,
|
||||
AccountEventsSubscription,
|
||||
AccountsQueryVariables,
|
||||
} from './__generated__/Accounts';
|
||||
import { type Asset } from '@vegaprotocol/assets';
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
import { ApolloError } from '@apollo/client';
|
||||
import type { GraphQLErrors } from '@apollo/client/errors';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import { type Subscription, type Observable } from 'zen-observable-ts';
|
||||
import { Subscription, Observable } from 'zen-observable-ts';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
|
||||
type Item = {
|
||||
|
||||
@@ -5,10 +5,7 @@ import {
|
||||
type NodeCheckTimeUpdateSubscription,
|
||||
} from '../../utils/__generated__/NodeCheck';
|
||||
import { Networks } from '../../types';
|
||||
import {
|
||||
createMockClient,
|
||||
type RequestHandlerResponse,
|
||||
} from 'mock-apollo-client';
|
||||
import { createMockClient, RequestHandlerResponse } from 'mock-apollo-client';
|
||||
|
||||
export type MockRequestConfig = {
|
||||
hasError?: boolean;
|
||||
|
||||
@@ -4,8 +4,14 @@ 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 { FeesDiscountBreakdownTooltip, FillsTable } from './fills-table';
|
||||
import {
|
||||
FeesDiscountBreakdownTooltip,
|
||||
FillsTable,
|
||||
getFeesBreakdown,
|
||||
getTotalFeesDiscounts,
|
||||
} from './fills-table';
|
||||
import { generateFill } from './test-helpers';
|
||||
import type { TradeFeeFieldsFragment } from './__generated__/Fills';
|
||||
|
||||
const partyId = 'party-id';
|
||||
const defaultFill: PartialDeep<Trade> = {
|
||||
@@ -29,7 +35,6 @@ const defaultFill: PartialDeep<Trade> = {
|
||||
},
|
||||
createdAt: new Date('2022-02-02T14:00:00').toISOString(),
|
||||
};
|
||||
|
||||
describe('FillsTable', () => {
|
||||
it('correct columns are rendered', async () => {
|
||||
// 7005-FILL-001
|
||||
@@ -60,7 +65,7 @@ describe('FillsTable', () => {
|
||||
expect(headers.map((h) => h.textContent?.trim())).toEqual(expectedHeaders);
|
||||
});
|
||||
|
||||
it('formats cells correctly for buyer fill for maker', async () => {
|
||||
it('formats cells correctly for buyer fill', async () => {
|
||||
const buyerFill = generateFill({
|
||||
...defaultFill,
|
||||
buyer: {
|
||||
@@ -84,7 +89,7 @@ describe('FillsTable', () => {
|
||||
'3.00 BTC',
|
||||
'Maker',
|
||||
'2.00 BTC',
|
||||
'0.09 BTC',
|
||||
'0.27 BTC',
|
||||
getDateTimeFormat().format(new Date(buyerFill.createdAt)),
|
||||
'', // action column
|
||||
];
|
||||
@@ -266,48 +271,96 @@ describe('FillsTable', () => {
|
||||
.find((c) => c.getAttribute('col-id') === 'size');
|
||||
expect(sizeCell).toHaveTextContent('3,000,000,000');
|
||||
});
|
||||
});
|
||||
|
||||
describe('FeesDiscountBreakdownTooltip', () => {
|
||||
it('shows all discounts', () => {
|
||||
const data = generateFill({
|
||||
...defaultFill,
|
||||
buyer: {
|
||||
id: partyId,
|
||||
},
|
||||
});
|
||||
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 expectedDt = [
|
||||
'Infrastructure Fee',
|
||||
'Referral Discount',
|
||||
'Volume Discount',
|
||||
'Liquidity Fee',
|
||||
'Referral Discount',
|
||||
'Volume Discount',
|
||||
'Maker Fee',
|
||||
'Referral Discount',
|
||||
'Volume Discount',
|
||||
];
|
||||
const expectedDD = [
|
||||
'0.05 BTC',
|
||||
'0.06 BTC',
|
||||
'0.01 BTC',
|
||||
'0.02 BTC',
|
||||
'0.03 BTC',
|
||||
'0.04 BTC',
|
||||
];
|
||||
expectedDt.forEach((label, i) => {
|
||||
expect(dt[i]).toHaveTextContent(label);
|
||||
});
|
||||
expectedDD.forEach((label, i) => {
|
||||
expect(dd[i]).toHaveTextContent(label);
|
||||
});
|
||||
describe('FeesDiscountBreakdownTooltip', () => {
|
||||
it('shows all discounts', () => {
|
||||
const data = generateFill({
|
||||
...defaultFill,
|
||||
buyer: {
|
||||
id: partyId,
|
||||
},
|
||||
});
|
||||
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 expectedDt = [
|
||||
'Infrastructure Fee',
|
||||
'Referral Discount',
|
||||
'Volume Discount',
|
||||
'Liquidity Fee',
|
||||
'Referral Discount',
|
||||
'Volume Discount',
|
||||
'Maker Fee',
|
||||
'Referral Discount',
|
||||
'Volume Discount',
|
||||
];
|
||||
const expectedDD = [
|
||||
'0.05 BTC',
|
||||
'0.06 BTC',
|
||||
'0.01 BTC',
|
||||
'0.02 BTC',
|
||||
'0.03 BTC',
|
||||
'0.04 BTC',
|
||||
];
|
||||
expectedDt.forEach((label, i) => {
|
||||
expect(dt[i]).toHaveTextContent(label);
|
||||
});
|
||||
expectedDD.forEach((label, i) => {
|
||||
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()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,12 +28,20 @@ import {
|
||||
import { forwardRef } from 'react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { type Trade } from './fills-data-provider';
|
||||
import {
|
||||
type FillFieldsFragment,
|
||||
type TradeFeeFieldsFragment,
|
||||
} from './__generated__/Fills';
|
||||
import { FillActionsDropdown } from './fill-actions-dropdown';
|
||||
import { getAsset } from '@vegaprotocol/markets';
|
||||
import { useT } from './use-t';
|
||||
import { MAKER, TAKER, getFeesBreakdown, getRoleAndFees } from './fills-utils';
|
||||
|
||||
type Props = (AgGridReactProps | AgReactUiProps) & {
|
||||
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;
|
||||
};
|
||||
@@ -254,13 +262,63 @@ const formatFeeDiscount = (partyId: string) => {
|
||||
}: VegaValueFormatterParams<Trade, 'market'>) => {
|
||||
if (!market || !data) return '-';
|
||||
const asset = getAsset(market);
|
||||
const { fees: roleFees, role } = getRoleAndFees({ data, partyId });
|
||||
if (!roleFees) return '-';
|
||||
const { totalFeeDiscount } = getFeesBreakdown(role, roleFees);
|
||||
return addDecimalsFormatNumber(totalFeeDiscount, asset.decimals);
|
||||
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 (
|
||||
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 = ({
|
||||
data,
|
||||
value: market,
|
||||
@@ -273,23 +331,16 @@ const FeesBreakdownTooltip = ({
|
||||
|
||||
const asset = getAsset(market);
|
||||
|
||||
const { role, fees, marketState } = getRoleAndFees({ data, partyId }) ?? {};
|
||||
const { role, fees } = getRoleAndFees({ data, partyId }) ?? {};
|
||||
if (!fees) return null;
|
||||
const { infrastructureFee, liquidityFee, makerFee, totalFee } =
|
||||
getFeesBreakdown(role, fees, marketState);
|
||||
getFeesBreakdown(role, fees);
|
||||
|
||||
return (
|
||||
<div
|
||||
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-xs 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"
|
||||
>
|
||||
{marketState && (
|
||||
<p className="mb-1 italic">
|
||||
{t('If the market was {{state}}', {
|
||||
state: Schema.MarketStateMapping[marketState].toLowerCase(),
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
{role === MAKER && (
|
||||
<>
|
||||
<p className="mb-1">{t('The maker will receive the maker fee.')}</p>
|
||||
@@ -303,7 +354,7 @@ const FeesBreakdownTooltip = ({
|
||||
{role === TAKER && (
|
||||
<p className="mb-1">{t('Fees to be paid by the taker.')}</p>
|
||||
)}
|
||||
{(role === '-' || marketState === Schema.MarketState.STATE_SUSPENDED) && (
|
||||
{role === '-' && (
|
||||
<p className="mb-1">
|
||||
{t(
|
||||
'If the market is in monitoring auction, half of the infrastructure and liquidity fees will be paid.'
|
||||
@@ -344,8 +395,8 @@ const FeesDiscountBreakdownTooltipItem = ({
|
||||
}) =>
|
||||
value && value !== '0' ? (
|
||||
<>
|
||||
<dt className="col-span-2">{label}</dt>
|
||||
<dd className="col-span-2 text-right">
|
||||
<dt className="col-span-1">{label}</dt>
|
||||
<dd className="col-span-1 text-right">
|
||||
{addDecimalsFormatNumber(value, asset.decimals)} {asset.symbol}
|
||||
</dd>
|
||||
</>
|
||||
@@ -361,19 +412,15 @@ export const FeesDiscountBreakdownTooltip = ({
|
||||
}
|
||||
const asset = getAsset(data.market);
|
||||
|
||||
const {
|
||||
fees: roleFees,
|
||||
marketState,
|
||||
role,
|
||||
} = getRoleAndFees({ data, partyId }) ?? {};
|
||||
if (!roleFees) return null;
|
||||
const fees = getFeesBreakdown(role, roleFees, marketState);
|
||||
const { fees } = getRoleAndFees({ data, partyId }) ?? {};
|
||||
if (!fees) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
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"
|
||||
>
|
||||
<dl className="grid grid-cols-6 gap-x-1 text-xs">
|
||||
<dl className="grid grid-cols-2 gap-x-1">
|
||||
{(fees.infrastructureFeeReferralDiscount || '0') !== '0' ||
|
||||
(fees.infrastructureFeeVolumeDiscount || '0') !== '0' ? (
|
||||
<dt className="col-span-2">{t('Infrastructure Fee')}</dt>
|
||||
@@ -417,14 +464,42 @@ export const FeesDiscountBreakdownTooltip = ({
|
||||
label={t('Volume Discount')}
|
||||
asset={asset}
|
||||
/>
|
||||
|
||||
<dt className="col-span-2">{t('Total Fee Discount')}</dt>
|
||||
<FeesDiscountBreakdownTooltipItem
|
||||
value={fees.totalFeeDiscount}
|
||||
label={''}
|
||||
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: 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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
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());
|
||||
});
|
||||
});
|
||||
@@ -1,164 +0,0 @@
|
||||
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,11 +7,7 @@ import {
|
||||
} from '@vegaprotocol/data-provider';
|
||||
import { type Market } from '@vegaprotocol/markets';
|
||||
import { marketsMapProvider } from '@vegaprotocol/markets';
|
||||
import {
|
||||
type Cursor,
|
||||
type PageInfo,
|
||||
type Edge,
|
||||
} from '@vegaprotocol/data-provider';
|
||||
import { Cursor, type PageInfo, type Edge } from '@vegaprotocol/data-provider';
|
||||
import { OrderStatus } from '@vegaprotocol/types';
|
||||
import {
|
||||
OrdersDocument,
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from './__generated__/Erc20Approval';
|
||||
import {
|
||||
PendingWithdrawalFragmentDoc,
|
||||
type PendingWithdrawalFragment,
|
||||
PendingWithdrawalFragment,
|
||||
} from './__generated__/Withdrawal';
|
||||
|
||||
export const useCompleteWithdraw = () => {
|
||||
|
||||
Reference in New Issue
Block a user