Compare commits

..
23 changed files with 565 additions and 637 deletions
@@ -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';
+25 -2
View File
@@ -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
View File
@@ -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;
+98 -45
View File
@@ -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()
);
});
});
+109 -34
View File
@@ -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,
};
};
-183
View File
@@ -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());
});
});
-164
View File
@@ -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 = () => {