feat(trading): fees page (#5055)
This commit is contained in:
parent
58ce016d4d
commit
52c96794f7
11
apps/trading/client-pages/fees/fees.tsx
Normal file
11
apps/trading/client-pages/fees/fees.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import { FeesContainer } from '../../components/fees-container';
|
||||||
|
|
||||||
|
export const Fees = () => {
|
||||||
|
return (
|
||||||
|
<div className="container p-4 mx-auto">
|
||||||
|
<h1 className="px-4 pb-4 text-2xl">{t('Fees')}</h1>
|
||||||
|
<FeesContainer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
1
apps/trading/client-pages/fees/index.ts
Normal file
1
apps/trading/client-pages/fees/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { Fees } from './fees';
|
@ -32,7 +32,7 @@ const WithdrawalsIndicator = () => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<span className="bg-vega-clight-500 dark:bg-vega-cdark-500 text-default rounded p-1 leading-none">
|
<span className="p-1 leading-none rounded bg-vega-clight-500 dark:bg-vega-cdark-500 text-default">
|
||||||
{ready.length}
|
{ready.length}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@ -128,7 +128,7 @@ interface PortfolioGridChildProps {
|
|||||||
const PortfolioGridChild = ({ children }: PortfolioGridChildProps) => {
|
const PortfolioGridChild = ({ children }: PortfolioGridChildProps) => {
|
||||||
return (
|
return (
|
||||||
<section className="h-full p-1">
|
<section className="h-full p-1">
|
||||||
<div className="border border-default h-full rounded-sm">{children}</div>
|
<div className="h-full border rounded-sm border-default">{children}</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
58
apps/trading/components/fees-container/Fees.graphql
Normal file
58
apps/trading/components/fees-container/Fees.graphql
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
query DiscountPrograms {
|
||||||
|
currentReferralProgram {
|
||||||
|
benefitTiers {
|
||||||
|
minimumEpochs
|
||||||
|
minimumRunningNotionalTakerVolume
|
||||||
|
referralDiscountFactor
|
||||||
|
}
|
||||||
|
windowLength
|
||||||
|
}
|
||||||
|
currentVolumeDiscountProgram {
|
||||||
|
benefitTiers {
|
||||||
|
minimumRunningNotionalTakerVolume
|
||||||
|
volumeDiscountFactor
|
||||||
|
}
|
||||||
|
windowLength
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query Fees(
|
||||||
|
$partyId: ID!
|
||||||
|
$volumeDiscountEpochs: Int!
|
||||||
|
$referralDiscountEpochs: Int!
|
||||||
|
) {
|
||||||
|
epoch {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
volumeDiscountStats(
|
||||||
|
partyId: $partyId
|
||||||
|
pagination: { last: $volumeDiscountEpochs }
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
atEpoch
|
||||||
|
discountFactor
|
||||||
|
runningVolume
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
referralSetReferees(referee: $partyId) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
atEpoch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
referralSetStats(
|
||||||
|
partyId: $partyId
|
||||||
|
pagination: { last: $referralDiscountEpochs }
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
atEpoch
|
||||||
|
discountFactor
|
||||||
|
referralSetRunningNotionalTakerVolume
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
131
apps/trading/components/fees-container/__generated__/Fees.ts
generated
Normal file
131
apps/trading/components/fees-container/__generated__/Fees.ts
generated
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import * as Types from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import * as Apollo from '@apollo/client';
|
||||||
|
const defaultOptions = {} as const;
|
||||||
|
export type DiscountProgramsQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
|
export type DiscountProgramsQuery = { __typename?: 'Query', currentReferralProgram?: { __typename?: 'CurrentReferralProgram', windowLength: number, benefitTiers: Array<{ __typename?: 'BenefitTier', minimumEpochs: number, minimumRunningNotionalTakerVolume: string, referralDiscountFactor: string }> } | null, currentVolumeDiscountProgram?: { __typename?: 'VolumeDiscountProgram', windowLength: number, benefitTiers: Array<{ __typename?: 'VolumeBenefitTier', minimumRunningNotionalTakerVolume: string, volumeDiscountFactor: string }> } | null };
|
||||||
|
|
||||||
|
export type FeesQueryVariables = Types.Exact<{
|
||||||
|
partyId: Types.Scalars['ID'];
|
||||||
|
volumeDiscountEpochs: Types.Scalars['Int'];
|
||||||
|
referralDiscountEpochs: Types.Scalars['Int'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type FeesQuery = { __typename?: 'Query', epoch: { __typename?: 'Epoch', id: string }, volumeDiscountStats: { __typename?: 'VolumeDiscountStatsConnection', edges: Array<{ __typename?: 'VolumeDiscountStatsEdge', node: { __typename?: 'VolumeDiscountStats', atEpoch: number, discountFactor: string, runningVolume: string } } | null> }, referralSetReferees: { __typename?: 'ReferralSetRefereeConnection', edges: Array<{ __typename?: 'ReferralSetRefereeEdge', node: { __typename?: 'ReferralSetReferee', atEpoch: number } } | null> }, referralSetStats: { __typename?: 'ReferralSetStatsConnection', edges: Array<{ __typename?: 'ReferralSetStatsEdge', node: { __typename?: 'ReferralSetStats', atEpoch: number, discountFactor: string, referralSetRunningNotionalTakerVolume: string } } | null> } };
|
||||||
|
|
||||||
|
|
||||||
|
export const DiscountProgramsDocument = gql`
|
||||||
|
query DiscountPrograms {
|
||||||
|
currentReferralProgram {
|
||||||
|
benefitTiers {
|
||||||
|
minimumEpochs
|
||||||
|
minimumRunningNotionalTakerVolume
|
||||||
|
referralDiscountFactor
|
||||||
|
}
|
||||||
|
windowLength
|
||||||
|
}
|
||||||
|
currentVolumeDiscountProgram {
|
||||||
|
benefitTiers {
|
||||||
|
minimumRunningNotionalTakerVolume
|
||||||
|
volumeDiscountFactor
|
||||||
|
}
|
||||||
|
windowLength
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useDiscountProgramsQuery__
|
||||||
|
*
|
||||||
|
* To run a query within a React component, call `useDiscountProgramsQuery` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useDiscountProgramsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||||
|
* you can use to render your UI.
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { data, loading, error } = useDiscountProgramsQuery({
|
||||||
|
* variables: {
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useDiscountProgramsQuery(baseOptions?: Apollo.QueryHookOptions<DiscountProgramsQuery, DiscountProgramsQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useQuery<DiscountProgramsQuery, DiscountProgramsQueryVariables>(DiscountProgramsDocument, options);
|
||||||
|
}
|
||||||
|
export function useDiscountProgramsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<DiscountProgramsQuery, DiscountProgramsQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useLazyQuery<DiscountProgramsQuery, DiscountProgramsQueryVariables>(DiscountProgramsDocument, options);
|
||||||
|
}
|
||||||
|
export type DiscountProgramsQueryHookResult = ReturnType<typeof useDiscountProgramsQuery>;
|
||||||
|
export type DiscountProgramsLazyQueryHookResult = ReturnType<typeof useDiscountProgramsLazyQuery>;
|
||||||
|
export type DiscountProgramsQueryResult = Apollo.QueryResult<DiscountProgramsQuery, DiscountProgramsQueryVariables>;
|
||||||
|
export const FeesDocument = gql`
|
||||||
|
query Fees($partyId: ID!, $volumeDiscountEpochs: Int!, $referralDiscountEpochs: Int!) {
|
||||||
|
epoch {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
volumeDiscountStats(
|
||||||
|
partyId: $partyId
|
||||||
|
pagination: {last: $volumeDiscountEpochs}
|
||||||
|
) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
atEpoch
|
||||||
|
discountFactor
|
||||||
|
runningVolume
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
referralSetReferees(referee: $partyId) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
atEpoch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
referralSetStats(partyId: $partyId, pagination: {last: $referralDiscountEpochs}) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
atEpoch
|
||||||
|
discountFactor
|
||||||
|
referralSetRunningNotionalTakerVolume
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useFeesQuery__
|
||||||
|
*
|
||||||
|
* To run a query within a React component, call `useFeesQuery` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useFeesQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||||
|
* you can use to render your UI.
|
||||||
|
*
|
||||||
|
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { data, loading, error } = useFeesQuery({
|
||||||
|
* variables: {
|
||||||
|
* partyId: // value for 'partyId'
|
||||||
|
* volumeDiscountEpochs: // value for 'volumeDiscountEpochs'
|
||||||
|
* referralDiscountEpochs: // value for 'referralDiscountEpochs'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useFeesQuery(baseOptions: Apollo.QueryHookOptions<FeesQuery, FeesQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useQuery<FeesQuery, FeesQueryVariables>(FeesDocument, options);
|
||||||
|
}
|
||||||
|
export function useFeesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<FeesQuery, FeesQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useLazyQuery<FeesQuery, FeesQueryVariables>(FeesDocument, options);
|
||||||
|
}
|
||||||
|
export type FeesQueryHookResult = ReturnType<typeof useFeesQuery>;
|
||||||
|
export type FeesLazyQueryHookResult = ReturnType<typeof useFeesLazyQuery>;
|
||||||
|
export type FeesQueryResult = Apollo.QueryResult<FeesQuery, FeesQueryVariables>;
|
36
apps/trading/components/fees-container/fees-card.tsx
Normal file
36
apps/trading/components/fees-container/fees-card.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
export const FeeCard = ({
|
||||||
|
children,
|
||||||
|
title,
|
||||||
|
className,
|
||||||
|
loading = false,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
title: string;
|
||||||
|
className?: string;
|
||||||
|
loading?: boolean;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'p-4 bg-vega-clight-800 dark:bg-vega-cdark-800 col-span-full lg:col-auto',
|
||||||
|
'rounded-lg',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<h2 className="mb-3">{title}</h2>
|
||||||
|
{loading ? <FeeCardLoader /> : children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FeeCardLoader = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="w-full h-5 bg-vega-clight-600 dark:bg-vega-cdark-600" />
|
||||||
|
<div className="w-3/4 h-6 bg-vega-clight-600 dark:bg-vega-cdark-600" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
103
apps/trading/components/fees-container/fees-container.spec.tsx
Normal file
103
apps/trading/components/fees-container/fees-container.spec.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { formatNumber } from '@vegaprotocol/utils';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
import { CurrentVolume, TradingFees } from './fees-container';
|
||||||
|
import { formatPercentage, getAdjustedFee } from './utils';
|
||||||
|
|
||||||
|
describe('TradingFees', () => {
|
||||||
|
it('renders correct fee data', () => {
|
||||||
|
const makerFee = 0.01;
|
||||||
|
const infraFee = 0.01;
|
||||||
|
const minLiqFee = 0.1;
|
||||||
|
const maxLiqFee = 0.3;
|
||||||
|
|
||||||
|
const referralDiscount = 0.01;
|
||||||
|
const volumeDiscount = 0.01;
|
||||||
|
|
||||||
|
const makerBigNum = new BigNumber(makerFee);
|
||||||
|
const infraBigNum = new BigNumber(infraFee);
|
||||||
|
const minLiqBigNum = new BigNumber(minLiqFee);
|
||||||
|
const maxLiqBigNum = new BigNumber(maxLiqFee);
|
||||||
|
const referralBigNum = new BigNumber(referralDiscount);
|
||||||
|
const volumeBigNum = new BigNumber(volumeDiscount);
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
params: {
|
||||||
|
market_fee_factors_makerFee: makerFee.toString(),
|
||||||
|
market_fee_factors_infrastructureFee: infraFee.toString(),
|
||||||
|
},
|
||||||
|
markets: [
|
||||||
|
{ fees: { factors: { liquidityFee: minLiqFee.toString() } } },
|
||||||
|
{ fees: { factors: { liquidityFee: '0.2' } } },
|
||||||
|
{ fees: { factors: { liquidityFee: maxLiqFee.toString() } } },
|
||||||
|
],
|
||||||
|
referralDiscount,
|
||||||
|
volumeDiscount,
|
||||||
|
};
|
||||||
|
render(<TradingFees {...props} />);
|
||||||
|
|
||||||
|
const minFee = formatPercentage(
|
||||||
|
makerBigNum.plus(infraFee).plus(minLiqFee).toNumber()
|
||||||
|
);
|
||||||
|
const maxFee = formatPercentage(
|
||||||
|
makerBigNum.plus(infraFee).plus(maxLiqFee).toNumber()
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText('Total fee before discount').nextElementSibling
|
||||||
|
).toHaveTextContent(`${minFee}%-${maxFee}%`);
|
||||||
|
expect(
|
||||||
|
screen.getByText('Infrastructure').nextElementSibling
|
||||||
|
).toHaveTextContent(formatPercentage(infraFee) + '%');
|
||||||
|
expect(screen.getByText('Maker').nextElementSibling).toHaveTextContent(
|
||||||
|
formatPercentage(makerFee) + '%'
|
||||||
|
);
|
||||||
|
|
||||||
|
const minAdjustedFees = formatPercentage(
|
||||||
|
getAdjustedFee(
|
||||||
|
[makerBigNum, infraBigNum, minLiqBigNum],
|
||||||
|
[referralBigNum, volumeBigNum]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const maxAdjustedFees = formatPercentage(
|
||||||
|
getAdjustedFee(
|
||||||
|
[makerBigNum, infraBigNum, maxLiqBigNum],
|
||||||
|
[referralBigNum, volumeBigNum]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('adjusted-fees')).toHaveTextContent(
|
||||||
|
`${minAdjustedFees}%-${maxAdjustedFees}%`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('CurerntVolume', () => {
|
||||||
|
it('renders the required amount for the next tier', () => {
|
||||||
|
const windowLengthVolume = 1500;
|
||||||
|
const nextTierVolume = 2000;
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
tiers: [
|
||||||
|
{ minimumRunningNotionalTakerVolume: '1000' },
|
||||||
|
{ minimumRunningNotionalTakerVolume: nextTierVolume.toString() },
|
||||||
|
{ minimumRunningNotionalTakerVolume: '3000' },
|
||||||
|
],
|
||||||
|
tierIndex: 0,
|
||||||
|
windowLengthVolume,
|
||||||
|
epochs: 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
render(<CurrentVolume {...props} />);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText(formatNumber(windowLengthVolume)).nextElementSibling
|
||||||
|
).toHaveTextContent(`Past ${props.epochs} epochs`);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByText(formatNumber(nextTierVolume - windowLengthVolume))
|
||||||
|
.nextElementSibling
|
||||||
|
).toHaveTextContent('Required for next tier');
|
||||||
|
});
|
||||||
|
});
|
490
apps/trading/components/fees-container/fees-container.tsx
Normal file
490
apps/trading/components/fees-container/fees-container.tsx
Normal file
@ -0,0 +1,490 @@
|
|||||||
|
import maxBy from 'lodash/maxBy';
|
||||||
|
import minBy from 'lodash/minBy';
|
||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
|
import {
|
||||||
|
useNetworkParams,
|
||||||
|
NetworkParams,
|
||||||
|
} from '@vegaprotocol/network-parameters';
|
||||||
|
import { useMarketList } from '@vegaprotocol/markets';
|
||||||
|
import { formatNumber } from '@vegaprotocol/utils';
|
||||||
|
import { useDiscountProgramsQuery, useFeesQuery } from './__generated__/Fees';
|
||||||
|
import { FeeCard } from './fees-card';
|
||||||
|
import { MarketFees } from './market-fees';
|
||||||
|
import { Stat } from './stat';
|
||||||
|
import { useVolumeStats } from './use-volume-stats';
|
||||||
|
import { useReferralStats } from './use-referral-stats';
|
||||||
|
import { formatPercentage, getAdjustedFee } from './utils';
|
||||||
|
import { Table, Td, Th, THead, Tr } from './table';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
|
||||||
|
export const FeesContainer = () => {
|
||||||
|
const { pubKey } = useVegaWallet();
|
||||||
|
const { params, loading: paramsLoading } = useNetworkParams([
|
||||||
|
NetworkParams.market_fee_factors_makerFee,
|
||||||
|
NetworkParams.market_fee_factors_infrastructureFee,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { data: markets, loading: marketsLoading } = useMarketList();
|
||||||
|
|
||||||
|
const { data: programData, loading: programLoading } =
|
||||||
|
useDiscountProgramsQuery();
|
||||||
|
|
||||||
|
const volumeDiscountEpochs =
|
||||||
|
programData?.currentVolumeDiscountProgram?.windowLength || 1;
|
||||||
|
const referralDiscountEpochs =
|
||||||
|
programData?.currentReferralProgram?.windowLength || 1;
|
||||||
|
|
||||||
|
const { data: feesData, loading: feesLoading } = useFeesQuery({
|
||||||
|
variables: {
|
||||||
|
partyId: pubKey || '',
|
||||||
|
volumeDiscountEpochs,
|
||||||
|
referralDiscountEpochs,
|
||||||
|
},
|
||||||
|
skip: !pubKey || !programData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { volumeDiscount, volumeTierIndex, volumeInWindow, volumeTiers } =
|
||||||
|
useVolumeStats(
|
||||||
|
feesData?.volumeDiscountStats,
|
||||||
|
programData?.currentVolumeDiscountProgram
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
referralDiscount,
|
||||||
|
referralVolumeInWindow,
|
||||||
|
referralTierIndex,
|
||||||
|
referralTiers,
|
||||||
|
epochsInSet,
|
||||||
|
} = useReferralStats(
|
||||||
|
feesData?.referralSetStats,
|
||||||
|
feesData?.referralSetReferees,
|
||||||
|
programData?.currentReferralProgram,
|
||||||
|
feesData?.epoch
|
||||||
|
);
|
||||||
|
|
||||||
|
const loading = paramsLoading || feesLoading || programLoading;
|
||||||
|
const isConnected = Boolean(pubKey);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid auto-rows-min grid-cols-4 gap-3">
|
||||||
|
{isConnected && (
|
||||||
|
<>
|
||||||
|
<FeeCard
|
||||||
|
title={t('My trading fees')}
|
||||||
|
className="sm:col-span-2"
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
<TradingFees
|
||||||
|
params={params}
|
||||||
|
markets={markets}
|
||||||
|
referralDiscount={referralDiscount}
|
||||||
|
volumeDiscount={volumeDiscount}
|
||||||
|
/>
|
||||||
|
</FeeCard>
|
||||||
|
<FeeCard
|
||||||
|
title={t('Total discount')}
|
||||||
|
className="sm:col-span-2"
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
<TotalDiscount
|
||||||
|
referralDiscount={referralDiscount}
|
||||||
|
volumeDiscount={volumeDiscount}
|
||||||
|
/>
|
||||||
|
</FeeCard>
|
||||||
|
<FeeCard
|
||||||
|
title={t('My current volume')}
|
||||||
|
className="sm:col-span-2"
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
<CurrentVolume
|
||||||
|
tiers={volumeTiers}
|
||||||
|
tierIndex={volumeTierIndex}
|
||||||
|
windowLengthVolume={volumeInWindow}
|
||||||
|
epochs={volumeDiscountEpochs}
|
||||||
|
/>
|
||||||
|
</FeeCard>
|
||||||
|
<FeeCard
|
||||||
|
title={t('Referral benefits')}
|
||||||
|
className="sm:col-span-2"
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
<ReferralBenefits
|
||||||
|
setRunningNotionalTakerVolume={referralVolumeInWindow}
|
||||||
|
epochsInSet={epochsInSet}
|
||||||
|
epochs={referralDiscountEpochs}
|
||||||
|
/>
|
||||||
|
</FeeCard>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<FeeCard
|
||||||
|
title={t('Volume discount')}
|
||||||
|
className="lg:col-span-full xl:col-span-2"
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
<VolumeTiers
|
||||||
|
tiers={volumeTiers}
|
||||||
|
tierIndex={volumeTierIndex}
|
||||||
|
lastEpochVolume={volumeInWindow}
|
||||||
|
/>
|
||||||
|
</FeeCard>
|
||||||
|
<FeeCard
|
||||||
|
title={t('Referral discount')}
|
||||||
|
className="lg:col-span-full xl:col-span-2"
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
<ReferralTiers
|
||||||
|
tiers={referralTiers}
|
||||||
|
tierIndex={referralTierIndex}
|
||||||
|
epochsInSet={epochsInSet}
|
||||||
|
referralVolumeInWindow={referralVolumeInWindow}
|
||||||
|
/>
|
||||||
|
</FeeCard>
|
||||||
|
<FeeCard
|
||||||
|
title={t('Liquidity fees')}
|
||||||
|
className="lg:col-span-full"
|
||||||
|
loading={marketsLoading}
|
||||||
|
>
|
||||||
|
<MarketFees
|
||||||
|
markets={markets}
|
||||||
|
referralDiscount={referralDiscount}
|
||||||
|
volumeDiscount={volumeDiscount}
|
||||||
|
/>
|
||||||
|
</FeeCard>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TradingFees = ({
|
||||||
|
params,
|
||||||
|
markets,
|
||||||
|
referralDiscount,
|
||||||
|
volumeDiscount,
|
||||||
|
}: {
|
||||||
|
params: {
|
||||||
|
market_fee_factors_infrastructureFee: string;
|
||||||
|
market_fee_factors_makerFee: string;
|
||||||
|
};
|
||||||
|
markets: Array<{ fees: { factors: { liquidityFee: string } } }> | null;
|
||||||
|
referralDiscount: number;
|
||||||
|
volumeDiscount: number;
|
||||||
|
}) => {
|
||||||
|
const referralDiscountBigNum = new BigNumber(referralDiscount);
|
||||||
|
const volumeDiscountBigNum = new BigNumber(volumeDiscount);
|
||||||
|
|
||||||
|
// Show min and max liquidity fees from all markets
|
||||||
|
const minLiq = minBy(markets, (m) => Number(m.fees.factors.liquidityFee));
|
||||||
|
const maxLiq = maxBy(markets, (m) => Number(m.fees.factors.liquidityFee));
|
||||||
|
|
||||||
|
const total = new BigNumber(params.market_fee_factors_makerFee).plus(
|
||||||
|
new BigNumber(params.market_fee_factors_infrastructureFee)
|
||||||
|
);
|
||||||
|
|
||||||
|
const adjustedTotal = getAdjustedFee(
|
||||||
|
[total],
|
||||||
|
[referralDiscountBigNum, volumeDiscountBigNum]
|
||||||
|
);
|
||||||
|
|
||||||
|
let minTotal;
|
||||||
|
let maxTotal;
|
||||||
|
|
||||||
|
let minAdjustedTotal;
|
||||||
|
let maxAdjustedTotal;
|
||||||
|
|
||||||
|
if (minLiq && maxLiq) {
|
||||||
|
const minLiqFee = new BigNumber(minLiq.fees.factors.liquidityFee);
|
||||||
|
const maxLiqFee = new BigNumber(maxLiq.fees.factors.liquidityFee);
|
||||||
|
|
||||||
|
minTotal = total.plus(minLiqFee);
|
||||||
|
maxTotal = total.plus(maxLiqFee);
|
||||||
|
|
||||||
|
minAdjustedTotal = getAdjustedFee(
|
||||||
|
[total, minLiqFee],
|
||||||
|
[referralDiscountBigNum, volumeDiscountBigNum]
|
||||||
|
);
|
||||||
|
|
||||||
|
maxAdjustedTotal = getAdjustedFee(
|
||||||
|
[total, maxLiqFee],
|
||||||
|
[referralDiscountBigNum, volumeDiscountBigNum]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="pt-6 leading-none">
|
||||||
|
<p className="block text-3xl leading-none" data-testid="adjusted-fees">
|
||||||
|
{minAdjustedTotal !== undefined && maxAdjustedTotal !== undefined
|
||||||
|
? `${formatPercentage(minAdjustedTotal)}%-${formatPercentage(
|
||||||
|
maxAdjustedTotal
|
||||||
|
)}%`
|
||||||
|
: `${formatPercentage(adjustedTotal)}%`}
|
||||||
|
</p>
|
||||||
|
<table className="w-full mt-0.5 text-xs text-muted">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th className="font-normal text-left text-default">
|
||||||
|
{t('Total fee before discount')}
|
||||||
|
</th>
|
||||||
|
<td className="text-right text-default">
|
||||||
|
{minTotal !== undefined && maxTotal !== undefined
|
||||||
|
? `${formatPercentage(
|
||||||
|
minTotal.toNumber()
|
||||||
|
)}%-${formatPercentage(maxTotal.toNumber())}%`
|
||||||
|
: `${formatPercentage(total.toNumber())}%`}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th className="font-normal text-left">{t('Infrastructure')}</th>
|
||||||
|
<td className="text-right">
|
||||||
|
{formatPercentage(
|
||||||
|
Number(params.market_fee_factors_infrastructureFee)
|
||||||
|
)}
|
||||||
|
%
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th className="font-normal text-left ">{t('Maker')}</th>
|
||||||
|
<td className="text-right">
|
||||||
|
{formatPercentage(Number(params.market_fee_factors_makerFee))}%
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{minLiq && maxLiq && (
|
||||||
|
<tr>
|
||||||
|
<th className="font-normal text-left ">{t('Liquidity')}</th>
|
||||||
|
<td className="text-right">
|
||||||
|
{formatPercentage(Number(minLiq.fees.factors.liquidityFee))}%
|
||||||
|
{'-'}
|
||||||
|
{formatPercentage(Number(maxLiq.fees.factors.liquidityFee))}%
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CurrentVolume = ({
|
||||||
|
tiers,
|
||||||
|
tierIndex,
|
||||||
|
windowLengthVolume,
|
||||||
|
epochs,
|
||||||
|
}: {
|
||||||
|
tiers: Array<{ minimumRunningNotionalTakerVolume: string }>;
|
||||||
|
tierIndex: number;
|
||||||
|
windowLengthVolume: number;
|
||||||
|
epochs: number;
|
||||||
|
}) => {
|
||||||
|
const nextTier = tiers[tierIndex + 1];
|
||||||
|
const requiredForNextTier = nextTier
|
||||||
|
? Number(nextTier.minimumRunningNotionalTakerVolume) - windowLengthVolume
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Stat
|
||||||
|
value={formatNumber(windowLengthVolume)}
|
||||||
|
text={t('Past %s epochs', epochs.toString())}
|
||||||
|
/>
|
||||||
|
{requiredForNextTier > 0 && (
|
||||||
|
<Stat
|
||||||
|
value={formatNumber(requiredForNextTier)}
|
||||||
|
text={t('Required for next tier')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ReferralBenefits = ({
|
||||||
|
epochsInSet,
|
||||||
|
setRunningNotionalTakerVolume,
|
||||||
|
epochs,
|
||||||
|
}: {
|
||||||
|
epochsInSet: number;
|
||||||
|
setRunningNotionalTakerVolume: number;
|
||||||
|
epochs: number;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Stat
|
||||||
|
// all sets volume (not just current party)
|
||||||
|
value={formatNumber(setRunningNotionalTakerVolume)}
|
||||||
|
text={t(
|
||||||
|
'Combined running notional over the %s epochs',
|
||||||
|
epochs.toString()
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Stat value={epochsInSet} text={t('epochs in referral set')} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TotalDiscount = ({
|
||||||
|
referralDiscount,
|
||||||
|
volumeDiscount,
|
||||||
|
}: {
|
||||||
|
referralDiscount: number;
|
||||||
|
volumeDiscount: number;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Stat
|
||||||
|
value={formatPercentage(referralDiscount + volumeDiscount) + '%'}
|
||||||
|
highlight={true}
|
||||||
|
/>
|
||||||
|
<table className="w-full mt-0.5 text-xs text-muted">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th className="font-normal text-left">{t('Volume discount')}</th>
|
||||||
|
<td className="text-right">{formatPercentage(volumeDiscount)}%</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th className="font-normal text-left ">{t('Referral discount')}</th>
|
||||||
|
<td className="text-right">
|
||||||
|
{formatPercentage(referralDiscount)}%
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const VolumeTiers = ({
|
||||||
|
tiers,
|
||||||
|
tierIndex,
|
||||||
|
lastEpochVolume,
|
||||||
|
}: {
|
||||||
|
tiers: Array<{
|
||||||
|
volumeDiscountFactor: string;
|
||||||
|
minimumRunningNotionalTakerVolume: string;
|
||||||
|
}>;
|
||||||
|
tierIndex: number;
|
||||||
|
lastEpochVolume: number;
|
||||||
|
}) => {
|
||||||
|
if (!tiers.length) {
|
||||||
|
return (
|
||||||
|
<p className="text-sm text-muted">
|
||||||
|
{t('No volume discount program active')}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<tr>
|
||||||
|
<Th>{t('Tier')}</Th>
|
||||||
|
<Th>{t('Discount')}</Th>
|
||||||
|
<Th>{t('Min. trading volume')}</Th>
|
||||||
|
<Th>{t('My volume (last epoch)')}</Th>
|
||||||
|
<Th />
|
||||||
|
</tr>
|
||||||
|
</THead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from(tiers)
|
||||||
|
.reverse()
|
||||||
|
.map((tier, i) => {
|
||||||
|
const isUserTier = tiers.length - 1 - tierIndex === i;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr key={i}>
|
||||||
|
<Td>{i + 1}</Td>
|
||||||
|
<Td>
|
||||||
|
{formatPercentage(Number(tier.volumeDiscountFactor))}%
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
{formatNumber(tier.minimumRunningNotionalTakerVolume)}
|
||||||
|
</Td>
|
||||||
|
<Td>{isUserTier ? formatNumber(lastEpochVolume) : ''}</Td>
|
||||||
|
<Td>{isUserTier ? <YourTier /> : null}</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ReferralTiers = ({
|
||||||
|
tiers,
|
||||||
|
tierIndex,
|
||||||
|
epochsInSet,
|
||||||
|
referralVolumeInWindow,
|
||||||
|
}: {
|
||||||
|
tiers: Array<{
|
||||||
|
referralDiscountFactor: string;
|
||||||
|
minimumRunningNotionalTakerVolume: string;
|
||||||
|
minimumEpochs: number;
|
||||||
|
}>;
|
||||||
|
tierIndex: number;
|
||||||
|
epochsInSet: number;
|
||||||
|
referralVolumeInWindow: number;
|
||||||
|
}) => {
|
||||||
|
if (!tiers.length) {
|
||||||
|
return (
|
||||||
|
<p className="text-sm text-muted">{t('No referral program active')}</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<tr>
|
||||||
|
<Th>{t('Tier')}</Th>
|
||||||
|
<Th>{t('Discount')}</Th>
|
||||||
|
<Th>{t('Min. trading volume')}</Th>
|
||||||
|
<Th>{t('Required epochs')}</Th>
|
||||||
|
<Th />
|
||||||
|
</tr>
|
||||||
|
</THead>
|
||||||
|
<tbody>
|
||||||
|
{Array.from(tiers)
|
||||||
|
.reverse()
|
||||||
|
.map((t, i) => {
|
||||||
|
const isUserTier = tiers.length - 1 - tierIndex === i;
|
||||||
|
|
||||||
|
const requiredVolume = Number(
|
||||||
|
t.minimumRunningNotionalTakerVolume
|
||||||
|
);
|
||||||
|
let unlocksIn = null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
referralVolumeInWindow >= requiredVolume &&
|
||||||
|
epochsInSet < t.minimumEpochs
|
||||||
|
) {
|
||||||
|
unlocksIn = (
|
||||||
|
<span className="text-muted">
|
||||||
|
Unlocks in {t.minimumEpochs - epochsInSet} epochs
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr key={i}>
|
||||||
|
<Td>{i + 1}</Td>
|
||||||
|
<Td>{formatPercentage(Number(t.referralDiscountFactor))}%</Td>
|
||||||
|
<Td>{formatNumber(t.minimumRunningNotionalTakerVolume)}</Td>
|
||||||
|
<Td>{t.minimumEpochs}</Td>
|
||||||
|
<Td>{isUserTier ? <YourTier /> : unlocksIn}</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const YourTier = () => {
|
||||||
|
return (
|
||||||
|
<span className="px-4 py-1.5 rounded-xl bg-rainbow whitespace-nowrap text-white">
|
||||||
|
{t('Your tier')}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
1
apps/trading/components/fees-container/index.ts
Normal file
1
apps/trading/components/fees-container/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { FeesContainer } from './fees-container';
|
90
apps/trading/components/fees-container/market-fees.tsx
Normal file
90
apps/trading/components/fees-container/market-fees.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import compact from 'lodash/compact';
|
||||||
|
import type { MarketMaybeWithDataAndCandles } from '@vegaprotocol/markets';
|
||||||
|
import { AgGrid } from '@vegaprotocol/datagrid';
|
||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import { formatPercentage, getAdjustedFee } from './utils';
|
||||||
|
import { MarketCodeCell } from '../../client-pages/markets/market-code-cell';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
|
||||||
|
const feesTableColumnDefs = [
|
||||||
|
{ field: 'code', cellRenderer: 'MarketCodeCell' },
|
||||||
|
{
|
||||||
|
field: 'feeAfterDiscount',
|
||||||
|
headerName: t('Total fee after discount'),
|
||||||
|
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'infraFee',
|
||||||
|
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'makerFee',
|
||||||
|
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'liquidityFee',
|
||||||
|
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'totalFee',
|
||||||
|
headerName: t('Total fee before discount'),
|
||||||
|
valueFormatter: ({ value }: { value: number }) => value + '%',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const feesTableDefaultColDef = {
|
||||||
|
flex: 1,
|
||||||
|
resizable: true,
|
||||||
|
sortable: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const components = {
|
||||||
|
MarketCodeCell,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MarketFees = ({
|
||||||
|
markets,
|
||||||
|
referralDiscount,
|
||||||
|
volumeDiscount,
|
||||||
|
}: {
|
||||||
|
markets: MarketMaybeWithDataAndCandles[] | null;
|
||||||
|
referralDiscount: number;
|
||||||
|
volumeDiscount: number;
|
||||||
|
}) => {
|
||||||
|
const rows = compact(markets || []).map((m) => {
|
||||||
|
const infraFee = new BigNumber(m.fees.factors.infrastructureFee);
|
||||||
|
const makerFee = new BigNumber(m.fees.factors.makerFee);
|
||||||
|
const liquidityFee = new BigNumber(m.fees.factors.liquidityFee);
|
||||||
|
const totalFee = infraFee.plus(makerFee).plus(liquidityFee);
|
||||||
|
|
||||||
|
const feeAfterDiscount = getAdjustedFee(
|
||||||
|
[infraFee, makerFee, liquidityFee],
|
||||||
|
[new BigNumber(referralDiscount), new BigNumber(volumeDiscount)]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: m.tradableInstrument.instrument.code,
|
||||||
|
productType: m.tradableInstrument.instrument.product.__typename,
|
||||||
|
infraFee: formatPercentage(infraFee.toNumber()),
|
||||||
|
makerFee: formatPercentage(makerFee.toNumber()),
|
||||||
|
liquidityFee: formatPercentage(liquidityFee.toNumber()),
|
||||||
|
totalFee: formatPercentage(totalFee.toNumber()),
|
||||||
|
feeAfterDiscount: formatPercentage(feeAfterDiscount),
|
||||||
|
parentMarketID: m.parentMarketID,
|
||||||
|
successorMarketID: m.successorMarketID,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="border rounded-sm border-default">
|
||||||
|
<AgGrid
|
||||||
|
columnDefs={feesTableColumnDefs}
|
||||||
|
rowData={rows}
|
||||||
|
defaultColDef={feesTableDefaultColDef}
|
||||||
|
domLayout="autoHeight"
|
||||||
|
components={components}
|
||||||
|
rowHeight={45}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
26
apps/trading/components/fees-container/stat.tsx
Normal file
26
apps/trading/components/fees-container/stat.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
export const Stat = ({
|
||||||
|
value,
|
||||||
|
text,
|
||||||
|
highlight,
|
||||||
|
}: {
|
||||||
|
value: string | number;
|
||||||
|
text?: string;
|
||||||
|
highlight?: boolean;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<p className="pt-3 leading-none first:pt-6">
|
||||||
|
<span
|
||||||
|
className={classNames('inline-block text-3xl leading-none', {
|
||||||
|
'text-transparent bg-rainbow bg-clip-text': highlight,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
{text && (
|
||||||
|
<small className="block mt-0.5 text-xs text-muted">{text}</small>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
};
|
40
apps/trading/components/fees-container/table.tsx
Normal file
40
apps/trading/components/fees-container/table.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
const cellClass = 'px-4 py-2 text-xs font-normal text-left last:text-right';
|
||||||
|
|
||||||
|
export const Th = ({ children }: { children?: ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<th className={classNames(cellClass, 'text-secondary leading-none py-3')}>
|
||||||
|
{children}
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Td = ({ children }: { children?: ReactNode }) => {
|
||||||
|
return <th className={cellClass}>{children}</th>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Tr = ({ children }: { children?: ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<tr className="hover:bg-vega-clight-600 dark:hover:bg-vega-cdark-700">
|
||||||
|
{children}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Table = ({ children }: { children: ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<table className="w-full border border-separate rounded-sm border-spacing-0 border-vega-clight-600 dark:border-vega-cdark-600">
|
||||||
|
{children}
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const THead = ({ children }: { children: ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<thead className="border-b bg-vega-clight-700 dark:bg-vega-cdark-700 border-vega-clight-600 dark:border-vega-cdark-600">
|
||||||
|
{children}
|
||||||
|
</thead>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,175 @@
|
|||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { useReferralStats } from './use-referral-stats';
|
||||||
|
|
||||||
|
describe('useReferralStats', () => {
|
||||||
|
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 sets = {
|
||||||
|
edges: [
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
atEpoch: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
node: {
|
||||||
|
atEpoch: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const epoch = {
|
||||||
|
id: '10',
|
||||||
|
};
|
||||||
|
|
||||||
|
const program = {
|
||||||
|
windowLength: 5,
|
||||||
|
benefitTiers: [
|
||||||
|
{
|
||||||
|
minimumEpochs: 4,
|
||||||
|
minimumRunningNotionalTakerVolume: '100',
|
||||||
|
referralDiscountFactor: '0.01',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
minimumEpochs: 6,
|
||||||
|
minimumRunningNotionalTakerVolume: '200',
|
||||||
|
referralDiscountFactor: '0.05',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
minimumEpochs: 8,
|
||||||
|
minimumRunningNotionalTakerVolume: '300',
|
||||||
|
referralDiscountFactor: '0.1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
it('returns correct default values', () => {
|
||||||
|
const { result } = renderHook(() => useReferralStats());
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
referralDiscount: 0,
|
||||||
|
referralVolumeInWindow: 0,
|
||||||
|
referralTierIndex: -1,
|
||||||
|
referralTiers: [],
|
||||||
|
epochsInSet: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns formatted data and tiers', () => {
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
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(stats.discountFactor),
|
||||||
|
referralVolumeInWindow: Number(
|
||||||
|
stats.referralSetRunningNotionalTakerVolume
|
||||||
|
),
|
||||||
|
referralTierIndex: 1,
|
||||||
|
referralTiers: program.benefitTiers,
|
||||||
|
epochsInSet: Number(epoch.id) - set.atEpoch,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
52
apps/trading/components/fees-container/use-referral-stats.ts
Normal file
52
apps/trading/components/fees-container/use-referral-stats.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import compact from 'lodash/compact';
|
||||||
|
import maxBy from 'lodash/maxBy';
|
||||||
|
import { getReferralBenefitTier } from './utils';
|
||||||
|
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
|
||||||
|
|
||||||
|
export const useReferralStats = (
|
||||||
|
setStats?: FeesQuery['referralSetStats'],
|
||||||
|
setReferees?: FeesQuery['referralSetReferees'],
|
||||||
|
program?: DiscountProgramsQuery['currentReferralProgram'],
|
||||||
|
epoch?: FeesQuery['epoch']
|
||||||
|
) => {
|
||||||
|
const referralTiers = program?.benefitTiers || [];
|
||||||
|
|
||||||
|
if (!setStats || !setReferees || !program || !epoch) {
|
||||||
|
return {
|
||||||
|
referralDiscount: 0,
|
||||||
|
referralVolumeInWindow: 0,
|
||||||
|
referralTierIndex: -1,
|
||||||
|
referralTiers,
|
||||||
|
epochsInSet: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = referralStats
|
||||||
|
? getReferralBenefitTier(
|
||||||
|
epochsInSet,
|
||||||
|
Number(referralStats.referralSetRunningNotionalTakerVolume),
|
||||||
|
referralTiers
|
||||||
|
)
|
||||||
|
: -1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
referralDiscount,
|
||||||
|
referralVolumeInWindow,
|
||||||
|
referralTierIndex,
|
||||||
|
referralTiers,
|
||||||
|
epochsInSet,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,95 @@
|
|||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { useVolumeStats } from './use-volume-stats';
|
||||||
|
|
||||||
|
describe('useReferralStats', () => {
|
||||||
|
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 = {
|
||||||
|
windowLength: 5,
|
||||||
|
benefitTiers: [
|
||||||
|
{
|
||||||
|
minimumRunningNotionalTakerVolume: '100',
|
||||||
|
volumeDiscountFactor: '0.01',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
minimumRunningNotionalTakerVolume: '200',
|
||||||
|
volumeDiscountFactor: '0.05',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
minimumRunningNotionalTakerVolume: '300',
|
||||||
|
volumeDiscountFactor: '0.1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
it('returns correct default values', () => {
|
||||||
|
const { result } = renderHook(() => useVolumeStats());
|
||||||
|
expect(result.current).toEqual({
|
||||||
|
volumeDiscount: 0,
|
||||||
|
volumeInWindow: 0,
|
||||||
|
volumeTierIndex: -1,
|
||||||
|
volumeTiers: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns formatted data and tiers', () => {
|
||||||
|
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),
|
||||||
|
volumeInWindow: Number(stats.runningVolume),
|
||||||
|
volumeTierIndex: 1,
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
33
apps/trading/components/fees-container/use-volume-stats.ts
Normal file
33
apps/trading/components/fees-container/use-volume-stats.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import compact from 'lodash/compact';
|
||||||
|
import maxBy from 'lodash/maxBy';
|
||||||
|
import { getVolumeTier } from './utils';
|
||||||
|
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
|
||||||
|
|
||||||
|
export const useVolumeStats = (
|
||||||
|
stats?: FeesQuery['volumeDiscountStats'],
|
||||||
|
program?: DiscountProgramsQuery['currentVolumeDiscountProgram']
|
||||||
|
) => {
|
||||||
|
const volumeTiers = program?.benefitTiers || [];
|
||||||
|
|
||||||
|
if (!stats || !program) {
|
||||||
|
return {
|
||||||
|
volumeDiscount: 0,
|
||||||
|
volumeTierIndex: -1,
|
||||||
|
volumeInWindow: 0,
|
||||||
|
volumeTiers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = getVolumeTier(volumeInWindow, volumeTiers);
|
||||||
|
|
||||||
|
return {
|
||||||
|
volumeDiscount,
|
||||||
|
volumeTierIndex,
|
||||||
|
volumeInWindow,
|
||||||
|
volumeTiers,
|
||||||
|
};
|
||||||
|
};
|
101
apps/trading/components/fees-container/utils.ts
Normal file
101
apps/trading/components/fees-container/utils.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { getUserLocale } from '@vegaprotocol/utils';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a number between 0-1 into a percentage value between 0-100
|
||||||
|
*
|
||||||
|
* Not using formatNumberPercentage from vegaprotocol/utils as this
|
||||||
|
* returns a string and includes extra 0s on the end. We need these
|
||||||
|
* values in aggrid as numbers for sorting
|
||||||
|
*/
|
||||||
|
export const formatPercentage = (num: number) => {
|
||||||
|
const pct = new BigNumber(num).times(100);
|
||||||
|
const dps = pct.decimalPlaces();
|
||||||
|
const formatter = new Intl.NumberFormat(getUserLocale(), {
|
||||||
|
minimumFractionDigits: dps || 0,
|
||||||
|
maximumFractionDigits: dps || 0,
|
||||||
|
});
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
export const getAdjustedFee = (fees: BigNumber[], discounts: BigNumber[]) => {
|
||||||
|
const totalFee = fees.reduce((sum, f) => sum.plus(f), new BigNumber(0));
|
||||||
|
const totalDiscount = discounts.reduce(
|
||||||
|
(sum, d) => sum.plus(d),
|
||||||
|
new BigNumber(0)
|
||||||
|
);
|
||||||
|
return totalFee
|
||||||
|
.times(BigNumber.max(0, new BigNumber(1).minus(totalDiscount)))
|
||||||
|
.toNumber();
|
||||||
|
};
|
@ -27,7 +27,7 @@ export const LayoutWithSidebar = ({
|
|||||||
<div className={gridClasses}>
|
<div className={gridClasses}>
|
||||||
<div className="col-span-full">{header}</div>
|
<div className="col-span-full">{header}</div>
|
||||||
<main
|
<main
|
||||||
className={classNames('col-start-1 col-end-1 overflow-hidden', {
|
className={classNames('col-start-1 col-end-1 overflow-y-auto', {
|
||||||
'lg:col-end-3': !sidebarOpen,
|
'lg:col-end-3': !sidebarOpen,
|
||||||
'hidden lg:block lg:col-end-2': sidebarOpen,
|
'hidden lg:block lg:col-end-2': sidebarOpen,
|
||||||
})}
|
})}
|
||||||
|
@ -67,6 +67,7 @@ describe('Navbar', () => {
|
|||||||
[`/markets/${marketId}`, 'Trading'],
|
[`/markets/${marketId}`, 'Trading'],
|
||||||
['/portfolio', 'Portfolio'],
|
['/portfolio', 'Portfolio'],
|
||||||
['/referrals', 'Referrals'],
|
['/referrals', 'Referrals'],
|
||||||
|
['/fees', 'Fees'],
|
||||||
[expect.stringContaining('governance'), 'Governance'],
|
[expect.stringContaining('governance'), 'Governance'],
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -100,6 +101,7 @@ describe('Navbar', () => {
|
|||||||
[`/markets/${marketId}`, 'Trading'],
|
[`/markets/${marketId}`, 'Trading'],
|
||||||
['/portfolio', 'Portfolio'],
|
['/portfolio', 'Portfolio'],
|
||||||
['/referrals', 'Referrals'],
|
['/referrals', 'Referrals'],
|
||||||
|
['/fees', 'Fees'],
|
||||||
[expect.stringContaining('governance'), 'Governance'],
|
[expect.stringContaining('governance'), 'Governance'],
|
||||||
];
|
];
|
||||||
const links = menu.getAllByRole('link');
|
const links = menu.getAllByRole('link');
|
||||||
|
@ -187,6 +187,11 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
|
|||||||
</NavbarLink>
|
</NavbarLink>
|
||||||
</NavbarItem>
|
</NavbarItem>
|
||||||
)}
|
)}
|
||||||
|
<NavbarItem>
|
||||||
|
<NavbarLink to={Links.FEES()} onClick={onClick}>
|
||||||
|
{t('Fees')}
|
||||||
|
</NavbarLink>
|
||||||
|
</NavbarItem>
|
||||||
<NavbarItem>
|
<NavbarItem>
|
||||||
<NavbarLinkExternal to={useLinks(DApp.Governance)()}>
|
<NavbarLinkExternal to={useLinks(DApp.Governance)()}>
|
||||||
{t('Governance')}
|
{t('Governance')}
|
||||||
|
@ -17,6 +17,7 @@ export const Routes = {
|
|||||||
REFERRALS_APPLY_CODE: '/referrals/apply-code',
|
REFERRALS_APPLY_CODE: '/referrals/apply-code',
|
||||||
REFERRALS_CREATE_CODE: '/referrals/create-code',
|
REFERRALS_CREATE_CODE: '/referrals/create-code',
|
||||||
TEAMS: '/teams',
|
TEAMS: '/teams',
|
||||||
|
FEES: '/fees',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type ConsoleLinks = {
|
type ConsoleLinks = {
|
||||||
@ -40,4 +41,5 @@ export const Links: ConsoleLinks = {
|
|||||||
REFERRALS_APPLY_CODE: () => Routes.REFERRALS_APPLY_CODE,
|
REFERRALS_APPLY_CODE: () => Routes.REFERRALS_APPLY_CODE,
|
||||||
REFERRALS_CREATE_CODE: () => Routes.REFERRALS_CREATE_CODE,
|
REFERRALS_CREATE_CODE: () => Routes.REFERRALS_CREATE_CODE,
|
||||||
TEAMS: () => Routes.TEAMS,
|
TEAMS: () => Routes.TEAMS,
|
||||||
|
FEES: () => Routes.FEES,
|
||||||
};
|
};
|
||||||
|
@ -13,6 +13,7 @@ import { Assets } from '../client-pages/assets';
|
|||||||
import { Deposit } from '../client-pages/deposit';
|
import { Deposit } from '../client-pages/deposit';
|
||||||
import { Withdraw } from '../client-pages/withdraw';
|
import { Withdraw } from '../client-pages/withdraw';
|
||||||
import { Transfer } from '../client-pages/transfer';
|
import { Transfer } from '../client-pages/transfer';
|
||||||
|
import { Fees } from '../client-pages/fees';
|
||||||
import { Routes as AppRoutes } from '../lib/links';
|
import { Routes as AppRoutes } from '../lib/links';
|
||||||
import { LayoutWithSky } from '../client-pages/referrals/layout';
|
import { LayoutWithSky } from '../client-pages/referrals/layout';
|
||||||
import { Referrals } from '../client-pages/referrals/referrals';
|
import { Referrals } from '../client-pages/referrals/referrals';
|
||||||
@ -85,6 +86,16 @@ export const routerConfig: RouteObject[] = compact([
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
|
{
|
||||||
|
path: 'fees/*',
|
||||||
|
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
index: true,
|
||||||
|
element: <Fees />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'markets/*',
|
path: 'markets/*',
|
||||||
element: (
|
element: (
|
||||||
|
@ -184,10 +184,10 @@ html [data-theme='dark'] {
|
|||||||
|
|
||||||
/* Light variables */
|
/* Light variables */
|
||||||
.ag-theme-balham {
|
.ag-theme-balham {
|
||||||
--ag-background-color: theme(colors.white);
|
--ag-background-color: transparent;
|
||||||
--ag-border-color: theme(colors.vega.clight.600);
|
--ag-border-color: theme(colors.vega.clight.600);
|
||||||
--ag-header-background-color: theme(colors.vega.clight.700);
|
--ag-header-background-color: theme(colors.vega.clight.700);
|
||||||
--ag-odd-row-background-color: theme(colors.white);
|
--ag-odd-row-background-color: transparent;
|
||||||
--ag-header-column-separator-color: theme(colors.vega.clight.500);
|
--ag-header-column-separator-color: theme(colors.vega.clight.500);
|
||||||
--ag-row-border-color: theme(colors.vega.clight.600);
|
--ag-row-border-color: theme(colors.vega.clight.600);
|
||||||
--ag-row-hover-color: theme(colors.vega.clight.800);
|
--ag-row-hover-color: theme(colors.vega.clight.800);
|
||||||
@ -196,10 +196,10 @@ html [data-theme='dark'] {
|
|||||||
|
|
||||||
/* Dark variables */
|
/* Dark variables */
|
||||||
.ag-theme-balham-dark {
|
.ag-theme-balham-dark {
|
||||||
--ag-background-color: theme(colors.vega.cdark.900);
|
--ag-background-color: transparent;
|
||||||
--ag-border-color: theme(colors.vega.cdark.600);
|
--ag-border-color: theme(colors.vega.cdark.600);
|
||||||
--ag-header-background-color: theme(colors.vega.cdark.700);
|
--ag-header-background-color: theme(colors.vega.cdark.700);
|
||||||
--ag-odd-row-background-color: theme(colors.vega.cdark.900);
|
--ag-odd-row-background-color: transparent;
|
||||||
--ag-header-column-separator-color: theme(colors.vega.cdark.500);
|
--ag-header-column-separator-color: theme(colors.vega.cdark.500);
|
||||||
--ag-row-border-color: theme(colors.vega.cdark.600);
|
--ag-row-border-color: theme(colors.vega.cdark.600);
|
||||||
--ag-row-hover-color: theme(colors.vega.cdark.800);
|
--ag-row-hover-color: theme(colors.vega.cdark.800);
|
||||||
|
@ -153,6 +153,8 @@ export const NetworkParams = {
|
|||||||
'spam_protection_minimumWithdrawalQuantumMultiple',
|
'spam_protection_minimumWithdrawalQuantumMultiple',
|
||||||
spam_protection_voting_min_tokens: 'spam_protection_voting_min_tokens',
|
spam_protection_voting_min_tokens: 'spam_protection_voting_min_tokens',
|
||||||
spam_protection_proposal_min_tokens: 'spam_protection_proposal_min_tokens',
|
spam_protection_proposal_min_tokens: 'spam_protection_proposal_min_tokens',
|
||||||
|
market_fee_factors_infrastructureFee: 'market_fee_factors_infrastructureFee',
|
||||||
|
market_fee_factors_makerFee: 'market_fee_factors_makerFee',
|
||||||
market_liquidity_targetstake_triggering_ratio:
|
market_liquidity_targetstake_triggering_ratio:
|
||||||
'market_liquidity_targetstake_triggering_ratio',
|
'market_liquidity_targetstake_triggering_ratio',
|
||||||
market_liquidity_bondPenaltyParameter:
|
market_liquidity_bondPenaltyParameter:
|
||||||
|
Loading…
Reference in New Issue
Block a user