Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2297cd2f5 | ||
|
|
cc3035d3ee | ||
|
|
e0b57922b0 | ||
|
|
54c1a6f1ed | ||
|
|
c440abc77d | ||
|
|
fbafc726a4 | ||
|
|
dd1890d8c6 | ||
|
|
c8e624eaba | ||
|
|
cc6629ad27 | ||
|
|
9838efa00e | ||
|
|
dac7142a98 | ||
|
|
1397aafd25 | ||
|
|
b192603e57 | ||
|
|
8c2c8d987c | ||
|
|
52c96794f7 |
@@ -39,7 +39,6 @@ context('Market page', { tags: '@regression' }, function () {
|
||||
cy.contains('Test market 1').click();
|
||||
cy.getByTestId(marketHeaders).should('have.text', 'Test market 1');
|
||||
cy.validate_element_from_table('Name', 'Test market 1');
|
||||
cy.validate_element_from_table('Market ID', this.createdMarketId);
|
||||
cy.validate_element_from_table('Trading Mode', 'Opening auction');
|
||||
cy.validate_element_from_table('Market Decimal Places', '5');
|
||||
cy.validate_element_from_table('Position Decimal Places', '5');
|
||||
|
||||
@@ -537,10 +537,10 @@ describe(
|
||||
cy.VegaWalletSubmitProposal(createGovernanceTransferProposalTxBody());
|
||||
cy.reload();
|
||||
getProposalFromTitle('Governance transfer proposal').within(() => {
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'NewTransfer');
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'New transfer');
|
||||
cy.getByTestId(viewProposalButton).click();
|
||||
});
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'NewTransfer');
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'New transfer');
|
||||
cy.getByTestId(governanceTransferToggle).click();
|
||||
cy.getByTestId('proposal-transfer-details-table').within(() => {
|
||||
getProposalInformationFromTable('Source Type')
|
||||
@@ -590,7 +590,7 @@ describe(
|
||||
);
|
||||
cy.getByTestId(viewProposalButton).click();
|
||||
});
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'CancelTransfer');
|
||||
cy.getByTestId(marketProposalType).should('have.text', 'Cancel transfer');
|
||||
getProposalInformationFromTable('Error details')
|
||||
.invoke('text')
|
||||
.and('eq', 'Governance transfer invalid transfer id not found');
|
||||
|
||||
@@ -28,9 +28,10 @@ const headers = [
|
||||
'Adjusted stake share',
|
||||
'Share',
|
||||
'Live supplied liquidity',
|
||||
'Live time fraction on book',
|
||||
'Fees accrued this epoch',
|
||||
'Live time on book',
|
||||
'Live liquidity quality score (%)',
|
||||
'Last time fraction on the book',
|
||||
'Last time on the book',
|
||||
'Last fee penalty',
|
||||
'Last bond penalty',
|
||||
'Status',
|
||||
|
||||
+4
-8
@@ -3,17 +3,16 @@ NX_ETHERSCAN_URL=https://sepolia.etherscan.io
|
||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||
NX_HOSTED_WALLET_URL=https://wallet.testnet.vega.xyz
|
||||
NX_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.sentry.io/6300613
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/fairground/vegawallet-fairground.toml
|
||||
NX_VEGA_ENV=TESTNET
|
||||
NX_VEGA_EXPLORER_URL=https://explorer.fairground.wtf
|
||||
NX_VEGA_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/networks-internal/main/stagnet1/vegawallet-stagnet1.toml
|
||||
NX_VEGA_ENV=STAGNET1
|
||||
NX_VEGA_EXPLORER_URL=https://explorer.stagnet1.vega.rocks
|
||||
NX_VEGA_NETWORKS={\"MAINNET\":\"https://console.vega.xyz\",\"TESTNET\":\"https://console.fairground.wtf\",\"STAGNET1\":\"https://trading.stagnet1.vega.rocks\"}
|
||||
NX_VEGA_TOKEN_URL=https://governance.fairground.wtf
|
||||
NX_VEGA_TOKEN_URL=https://governance.stagnet1.vega.rocks
|
||||
NX_VEGA_WALLET_URL=http://localhost:1789
|
||||
NX_VEGA_DOCS_URL=https://docs.vega.xyz/testnet
|
||||
NX_VEGA_REPO_URL=https://github.com/vegaprotocol/vega/releases
|
||||
NX_ANNOUNCEMENTS_CONFIG_URL=https://raw.githubusercontent.com/vegaprotocol/announcements/fairground/announcements.json
|
||||
NX_VEGA_INCIDENT_URL=https://blog.vega.xyz/tagged/vega-incident-reports
|
||||
NX_VEGA_CONSOLE_URL=https://console.fairground.wtf
|
||||
NX_CHROME_EXTENSION_URL=https://chrome.google.com/webstore/detail/vega-wallet-fairground/nmmjkiafpmphlikhefgjbblebfgclikn
|
||||
NX_MOZILLA_EXTENSION_URL=https://addons.mozilla.org/firefox/addon/vega-wallet-fairground
|
||||
NX_ORACLE_PROOFS_URL=https://raw.githubusercontent.com/vegaprotocol/well-known/main/__generated__/oracle-proofs.json
|
||||
@@ -26,6 +25,3 @@ NX_ICEBERG_ORDERS=true
|
||||
# NX_PRODUCT_PERPETUALS
|
||||
NX_METAMASK_SNAPS=true
|
||||
NX_REFERRALS=true
|
||||
|
||||
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
|
||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { Fees } from './fees';
|
||||
@@ -32,7 +32,7 @@ const WithdrawalsIndicator = () => {
|
||||
return null;
|
||||
}
|
||||
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}
|
||||
</span>
|
||||
);
|
||||
@@ -128,7 +128,7 @@ interface PortfolioGridChildProps {
|
||||
const PortfolioGridChild = ({ children }: PortfolioGridChildProps) => {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -27,6 +27,7 @@ import { useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import maxBy from 'lodash/maxBy';
|
||||
|
||||
export const ReferralStatistics = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
@@ -102,9 +103,9 @@ export const Statistics = ({
|
||||
!isNaN(t.discountFactor) &&
|
||||
t.discountFactor === discountFactorValue
|
||||
);
|
||||
const nextBenefitTierValue =
|
||||
currentBenefitTierValue &&
|
||||
benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier - 1);
|
||||
const nextBenefitTierValue = currentBenefitTierValue
|
||||
? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier - 1)
|
||||
: maxBy(benefitTiers, (bt) => bt.tier); // max tier number is lowest tier
|
||||
const epochsValue =
|
||||
!isNaN(currentEpoch) && refereeInfo?.atEpoch
|
||||
? currentEpoch - refereeInfo?.atEpoch
|
||||
@@ -189,7 +190,7 @@ export const Statistics = ({
|
||||
|
||||
const currentBenefitTierTile = (
|
||||
<StatTile title={t('Current tier')}>
|
||||
{currentBenefitTierValue?.tier || '-'}
|
||||
{currentBenefitTierValue?.tier || 'None'}
|
||||
</StatTile>
|
||||
);
|
||||
const discountFactorTile = (
|
||||
@@ -204,14 +205,24 @@ export const Statistics = ({
|
||||
<StatTile title={t('Epochs in set')}>{epochsValue}</StatTile>
|
||||
);
|
||||
const nextTierVolumeTile = (
|
||||
<StatTile title={t('Volume to next tier')}>
|
||||
<StatTile
|
||||
title={t(
|
||||
'Volume to next tier %s',
|
||||
nextBenefitTierValue?.tier ? `(${nextBenefitTierValue.tier})` : ''
|
||||
)}
|
||||
>
|
||||
{nextBenefitTierVolumeValue <= 0
|
||||
? '0'
|
||||
: compactNumFormat.format(nextBenefitTierVolumeValue)}
|
||||
</StatTile>
|
||||
);
|
||||
const nextTierEpochsTile = (
|
||||
<StatTile title={t('Epochs to next tier')}>
|
||||
<StatTile
|
||||
title={t(
|
||||
'Epochs to next tier %s',
|
||||
nextBenefitTierValue?.tier ? `(${nextBenefitTierValue.tier})` : ''
|
||||
)}
|
||||
>
|
||||
{nextBenefitTierEpochsValue <= 0 ? '0' : nextBenefitTierEpochsValue}
|
||||
</StatTile>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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, formatNumberRounded } 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={formatNumberRounded(new BigNumber(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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { FeesContainer } from './fees-container';
|
||||
@@ -0,0 +1,104 @@
|
||||
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';
|
||||
import { useNavigateWithMeta } from '../../lib/hooks/use-market-click-handler';
|
||||
import { Links } from '../../lib/links';
|
||||
|
||||
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 navigateWithMeta = useNavigateWithMeta();
|
||||
|
||||
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 {
|
||||
id: m.id,
|
||||
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}
|
||||
getRowId={({ data }) => data.id}
|
||||
defaultColDef={feesTableDefaultColDef}
|
||||
domLayout="autoHeight"
|
||||
components={components}
|
||||
rowHeight={45}
|
||||
rowClass="cursor-pointer"
|
||||
onRowClicked={({ data, event }) => {
|
||||
navigateWithMeta(
|
||||
Links.MARKET(data.id),
|
||||
// @ts-ignore metaKey and ctrlKey exist
|
||||
event.metaKey || event.ctrlKey
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { getAdjustedFee } from './utils';
|
||||
|
||||
describe('getAdjustedFee', () => {
|
||||
it('simple', () => {
|
||||
const volumeDiscount = 0.5;
|
||||
const referralDiscount = 0.5;
|
||||
|
||||
const infraFee = 0.1;
|
||||
const makerFee = 0.1;
|
||||
const liqFee = 0.1;
|
||||
|
||||
const fees = [
|
||||
new BigNumber(infraFee),
|
||||
new BigNumber(makerFee),
|
||||
new BigNumber(liqFee),
|
||||
];
|
||||
|
||||
const discounts = [
|
||||
new BigNumber(volumeDiscount),
|
||||
new BigNumber(referralDiscount),
|
||||
];
|
||||
|
||||
// 1 - 0.5 - 0.5
|
||||
const v = new BigNumber(1).minus(new BigNumber(volumeDiscount));
|
||||
|
||||
// 1 - 0.5 = 0.5
|
||||
const r = new BigNumber(1).minus(new BigNumber(referralDiscount));
|
||||
|
||||
// 0.5 * 0.5 = 0.25
|
||||
// 1 - 0.25 = 0.75
|
||||
const factor = new BigNumber(1).minus(v.times(r));
|
||||
|
||||
// 0.1 + 0.1 + 0.1 = 0.3
|
||||
const totalFees = fees.reduce((sum, x) => sum.plus(x), new BigNumber(0));
|
||||
|
||||
// 0.3 * 0.75 = 0.225
|
||||
const expected = new BigNumber(totalFees).times(factor).toNumber();
|
||||
|
||||
expect(getAdjustedFee(fees, discounts)).toBe(expected);
|
||||
});
|
||||
|
||||
it('combines discount factors multiplicativly', () => {
|
||||
const volumeDiscount = 0.4;
|
||||
const referralDiscount = 0.1;
|
||||
|
||||
const infraFee = 0.0005;
|
||||
const makerFee = 0.0002;
|
||||
const liqFee = 0.01;
|
||||
|
||||
const fees = [
|
||||
new BigNumber(infraFee),
|
||||
new BigNumber(makerFee),
|
||||
new BigNumber(liqFee),
|
||||
];
|
||||
|
||||
const discounts = [
|
||||
new BigNumber(volumeDiscount),
|
||||
new BigNumber(referralDiscount),
|
||||
];
|
||||
|
||||
// formula for calculating adjusted fees
|
||||
const v = new BigNumber(1).minus(new BigNumber(volumeDiscount));
|
||||
const r = new BigNumber(1).minus(new BigNumber(referralDiscount));
|
||||
const factor = new BigNumber(1).minus(v.times(r));
|
||||
|
||||
// summed fees
|
||||
const totalFees = fees.reduce((sum, x) => sum.plus(x), new BigNumber(0));
|
||||
|
||||
const expected = new BigNumber(totalFees).times(factor).toNumber();
|
||||
|
||||
expect(getAdjustedFee(fees, discounts)).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
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
|
||||
*
|
||||
* Formula for calculating the adjusted fees
|
||||
* total_discount_factor = 1 - (1 - volumeDiscount) * (1 - referralDiscount)
|
||||
*/
|
||||
export const getAdjustedFee = (fees: BigNumber[], discounts: BigNumber[]) => {
|
||||
const totalFee = fees.reduce((sum, f) => sum.plus(f), new BigNumber(0));
|
||||
|
||||
const combinedFactors = discounts.reduce((acc, d) => {
|
||||
return acc.times(new BigNumber(1).minus(d));
|
||||
}, new BigNumber(1));
|
||||
|
||||
const totalFactor = new BigNumber(1).minus(combinedFactors);
|
||||
|
||||
return totalFee.times(BigNumber.max(0, totalFactor)).toNumber();
|
||||
};
|
||||
@@ -27,7 +27,7 @@ export const LayoutWithSidebar = ({
|
||||
<div className={gridClasses}>
|
||||
<div className="col-span-full">{header}</div>
|
||||
<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,
|
||||
'hidden lg:block lg:col-end-2': sidebarOpen,
|
||||
})}
|
||||
|
||||
@@ -67,6 +67,7 @@ describe('Navbar', () => {
|
||||
[`/markets/${marketId}`, 'Trading'],
|
||||
['/portfolio', 'Portfolio'],
|
||||
['/referrals', 'Referrals'],
|
||||
['/fees', 'Fees'],
|
||||
[expect.stringContaining('governance'), 'Governance'],
|
||||
];
|
||||
|
||||
@@ -100,6 +101,7 @@ describe('Navbar', () => {
|
||||
[`/markets/${marketId}`, 'Trading'],
|
||||
['/portfolio', 'Portfolio'],
|
||||
['/referrals', 'Referrals'],
|
||||
['/fees', 'Fees'],
|
||||
[expect.stringContaining('governance'), 'Governance'],
|
||||
];
|
||||
const links = menu.getAllByRole('link');
|
||||
|
||||
@@ -187,6 +187,11 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
|
||||
</NavbarLink>
|
||||
</NavbarItem>
|
||||
)}
|
||||
<NavbarItem>
|
||||
<NavbarLink to={Links.FEES()} onClick={onClick}>
|
||||
{t('Fees')}
|
||||
</NavbarLink>
|
||||
</NavbarItem>
|
||||
<NavbarItem>
|
||||
<NavbarLinkExternal to={useLinks(DApp.Governance)()}>
|
||||
{t('Governance')}
|
||||
|
||||
@@ -17,6 +17,7 @@ export const Routes = {
|
||||
REFERRALS_APPLY_CODE: '/referrals/apply-code',
|
||||
REFERRALS_CREATE_CODE: '/referrals/create-code',
|
||||
TEAMS: '/teams',
|
||||
FEES: '/fees',
|
||||
} as const;
|
||||
|
||||
type ConsoleLinks = {
|
||||
@@ -40,4 +41,5 @@ export const Links: ConsoleLinks = {
|
||||
REFERRALS_APPLY_CODE: () => Routes.REFERRALS_APPLY_CODE,
|
||||
REFERRALS_CREATE_CODE: () => Routes.REFERRALS_CREATE_CODE,
|
||||
TEAMS: () => Routes.TEAMS,
|
||||
FEES: () => Routes.FEES,
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Assets } from '../client-pages/assets';
|
||||
import { Deposit } from '../client-pages/deposit';
|
||||
import { Withdraw } from '../client-pages/withdraw';
|
||||
import { Transfer } from '../client-pages/transfer';
|
||||
import { Fees } from '../client-pages/fees';
|
||||
import { Routes as AppRoutes } from '../lib/links';
|
||||
import { LayoutWithSky } from '../client-pages/referrals/layout';
|
||||
import { Referrals } from '../client-pages/referrals/referrals';
|
||||
@@ -85,6 +86,16 @@ export const routerConfig: RouteObject[] = compact([
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
{
|
||||
path: 'fees/*',
|
||||
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Fees />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'markets/*',
|
||||
element: (
|
||||
|
||||
@@ -184,10 +184,10 @@ html [data-theme='dark'] {
|
||||
|
||||
/* Light variables */
|
||||
.ag-theme-balham {
|
||||
--ag-background-color: theme(colors.white);
|
||||
--ag-background-color: transparent;
|
||||
--ag-border-color: theme(colors.vega.clight.600);
|
||||
--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-row-border-color: theme(colors.vega.clight.600);
|
||||
--ag-row-hover-color: theme(colors.vega.clight.800);
|
||||
@@ -196,10 +196,10 @@ html [data-theme='dark'] {
|
||||
|
||||
/* Dark variables */
|
||||
.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-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-row-border-color: theme(colors.vega.cdark.600);
|
||||
--ag-row-hover-color: theme(colors.vega.cdark.800);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ToastsContainer, useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { useUpdateNetworkParametersToasts } from '@vegaprotocol/proposals';
|
||||
import { useProposalToasts } from '@vegaprotocol/proposals';
|
||||
import { useVegaTransactionToasts } from '@vegaprotocol/web3';
|
||||
import { useEthereumTransactionToasts } from '@vegaprotocol/web3';
|
||||
import { useEthereumWithdrawApprovalsToasts } from '@vegaprotocol/web3';
|
||||
@@ -7,7 +7,7 @@ import { useReadyToWithdrawalToasts } from '@vegaprotocol/withdraws';
|
||||
import { Links } from '../lib/links';
|
||||
|
||||
export const ToastsManager = () => {
|
||||
useUpdateNetworkParametersToasts();
|
||||
useProposalToasts();
|
||||
useVegaTransactionToasts();
|
||||
useEthereumTransactionToasts();
|
||||
useEthereumWithdrawApprovalsToasts();
|
||||
|
||||
@@ -3,7 +3,12 @@ import throttle from 'lodash/throttle';
|
||||
import isEqualWith from 'lodash/isEqualWith';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import type { OperationVariables } from '@apollo/client';
|
||||
import type { Subscribe, Load, UpdateCallback } from './generic-data-provider';
|
||||
import type {
|
||||
Subscribe,
|
||||
Load,
|
||||
UpdateCallback,
|
||||
PageInfo,
|
||||
} from './generic-data-provider';
|
||||
import { variablesIsEqualCustomizer } from './generic-data-provider';
|
||||
|
||||
export interface useDataProviderParams<
|
||||
@@ -12,13 +17,23 @@ export interface useDataProviderParams<
|
||||
Variables extends OperationVariables | undefined = undefined
|
||||
> {
|
||||
dataProvider: Subscribe<Data, Delta, Variables>;
|
||||
update?: ({ delta, data }: { delta?: Delta; data: Data | null }) => boolean;
|
||||
update?: ({
|
||||
delta,
|
||||
data,
|
||||
pageInfo,
|
||||
}: {
|
||||
delta?: Delta;
|
||||
data: Data | null;
|
||||
pageInfo: PageInfo | null;
|
||||
}) => boolean;
|
||||
insert?: ({
|
||||
insertionData,
|
||||
data,
|
||||
pageInfo,
|
||||
}: {
|
||||
insertionData?: Data | null;
|
||||
data: Data | null;
|
||||
pageInfo: PageInfo | null;
|
||||
}) => boolean;
|
||||
variables: Variables;
|
||||
skipUpdates?: boolean;
|
||||
@@ -30,7 +45,7 @@ export interface useDataProviderParams<
|
||||
* @param dataProvider subscribe function created by makeDataProvider
|
||||
* @param update optional function called on each delta received in subscription, if returns true updated data will be not passed from hook (component handles updates internally)
|
||||
* @param variables optional
|
||||
* @returns state: data, loading, error, methods: flush (pass updated data to update function without delta), restart: () => void}};
|
||||
* @returns state: data, loading, pageInfo, error, methods: flush (pass updated data to update function without delta), restart: () => void}};
|
||||
*/
|
||||
export const useDataProvider = <
|
||||
Data,
|
||||
@@ -48,6 +63,7 @@ export const useDataProvider = <
|
||||
const [data, setData] = useState<Data | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(!skip);
|
||||
const [error, setError] = useState<Error | undefined>(undefined);
|
||||
const [pageInfo, setPageInfo] = useState<PageInfo | null>(null);
|
||||
const flushRef = useRef<(() => void) | undefined>(undefined);
|
||||
const reloadRef = useRef<((force?: boolean) => void) | undefined>(undefined);
|
||||
const loadRef = useRef<Load<Data> | undefined>(undefined);
|
||||
@@ -93,6 +109,7 @@ export const useDataProvider = <
|
||||
isInsert,
|
||||
isUpdate,
|
||||
loaded,
|
||||
pageInfo,
|
||||
} = args;
|
||||
setError(error);
|
||||
setLoading(!loaded && loading);
|
||||
@@ -104,21 +121,22 @@ export const useDataProvider = <
|
||||
(skipUpdatesRef.current ||
|
||||
(!skipUpdatesRef.current &&
|
||||
updateRef.current &&
|
||||
updateRef.current({ delta, data })))
|
||||
updateRef.current({ delta, data, pageInfo })))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
isInsert &&
|
||||
insertRef.current &&
|
||||
insertRef.current({ insertionData, data })
|
||||
insertRef.current({ insertionData, data, pageInfo })
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
setData(data);
|
||||
setPageInfo(pageInfo);
|
||||
if (!loading && !isUpdate && updateRef.current) {
|
||||
updateRef.current({ data });
|
||||
updateRef.current({ data, pageInfo });
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -136,15 +154,13 @@ export const useDataProvider = <
|
||||
|
||||
useEffect(() => {
|
||||
setData(null);
|
||||
setPageInfo(null);
|
||||
setError(undefined);
|
||||
if (updateRef.current) {
|
||||
updateRef.current({ data: null });
|
||||
updateRef.current({ data: null, pageInfo: null });
|
||||
}
|
||||
if (skip) {
|
||||
setLoading(false);
|
||||
if (updateRef.current) {
|
||||
updateRef.current({ data: null });
|
||||
}
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
@@ -165,6 +181,7 @@ export const useDataProvider = <
|
||||
}, [client, dataProvider, callback, variables, skip]);
|
||||
return {
|
||||
data,
|
||||
pageInfo,
|
||||
loading,
|
||||
error,
|
||||
flush,
|
||||
|
||||
@@ -109,7 +109,7 @@ describe('DealTicket', () => {
|
||||
variables: {
|
||||
partyId: 'pubKey',
|
||||
filter: { liveOnly: true },
|
||||
pagination: { first: 5000 },
|
||||
pagination: { first: 1000 },
|
||||
},
|
||||
},
|
||||
result: {
|
||||
|
||||
@@ -42,8 +42,12 @@ fragment FillEdge on TradeEdge {
|
||||
cursor
|
||||
}
|
||||
|
||||
query Fills($filter: TradesFilter, $pagination: Pagination) {
|
||||
trades(filter: $filter, pagination: $pagination) {
|
||||
query Fills(
|
||||
$filter: TradesFilter
|
||||
$pagination: Pagination
|
||||
$dateRange: DateRange
|
||||
) {
|
||||
trades(filter: $filter, dateRange: $dateRange, pagination: $pagination) {
|
||||
edges {
|
||||
...FillEdge
|
||||
}
|
||||
|
||||
+4
-2
@@ -12,6 +12,7 @@ export type FillEdgeFragment = { __typename?: 'TradeEdge', cursor: string, node:
|
||||
export type FillsQueryVariables = Types.Exact<{
|
||||
filter?: Types.InputMaybe<Types.TradesFilter>;
|
||||
pagination?: Types.InputMaybe<Types.Pagination>;
|
||||
dateRange?: Types.InputMaybe<Types.DateRange>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -95,8 +96,8 @@ export const FillUpdateFieldsFragmentDoc = gql`
|
||||
}
|
||||
${TradeFeeFieldsFragmentDoc}`;
|
||||
export const FillsDocument = gql`
|
||||
query Fills($filter: TradesFilter, $pagination: Pagination) {
|
||||
trades(filter: $filter, pagination: $pagination) {
|
||||
query Fills($filter: TradesFilter, $pagination: Pagination, $dateRange: DateRange) {
|
||||
trades(filter: $filter, dateRange: $dateRange, pagination: $pagination) {
|
||||
edges {
|
||||
...FillEdge
|
||||
}
|
||||
@@ -124,6 +125,7 @@ export const FillsDocument = gql`
|
||||
* variables: {
|
||||
* filter: // value for 'filter'
|
||||
* pagination: // value for 'pagination'
|
||||
* dateRange: // value for 'dateRange'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { useRef } from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { FillsTable } from './fills-table';
|
||||
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import type * as Schema from '@vegaprotocol/types';
|
||||
import { fillsWithMarketProvider } from './fills-data-provider';
|
||||
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
interface FillsManagerProps {
|
||||
partyId: string;
|
||||
@@ -22,26 +23,60 @@ export const FillsManager = ({
|
||||
const filter: Schema.TradesFilter | Schema.TradesSubscriptionFilter = {
|
||||
partyIds: [partyId],
|
||||
};
|
||||
const { data, error } = useDataProvider({
|
||||
dataProvider: fillsWithMarketProvider,
|
||||
update: ({ data }) => {
|
||||
if (data?.length && gridRef.current?.api) {
|
||||
gridRef.current?.api.setRowData(data);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
const [hasDisplayedRow, setHasDisplayedRow] = useState<boolean | undefined>(
|
||||
undefined
|
||||
);
|
||||
const { onFilterChanged, ...props } = gridProps || {};
|
||||
const onRowDataUpdated = useCallback(
|
||||
({ api }: { api: AgGridReact['api'] }) => {
|
||||
setHasDisplayedRow(!!api.getDisplayedRowCount());
|
||||
},
|
||||
[]
|
||||
);
|
||||
const { data, error, load, pageInfo } = useDataProvider({
|
||||
dataProvider: fillsWithMarketProvider,
|
||||
variables: { filter },
|
||||
});
|
||||
|
||||
return (
|
||||
<FillsTable
|
||||
ref={gridRef}
|
||||
rowData={data}
|
||||
partyId={partyId}
|
||||
onMarketClick={onMarketClick}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No fills')}
|
||||
{...gridProps}
|
||||
/>
|
||||
<div className="flex flex-col h-full">
|
||||
<FillsTable
|
||||
ref={gridRef}
|
||||
rowData={data}
|
||||
onFilterChanged={(event) => {
|
||||
onRowDataUpdated(event);
|
||||
onFilterChanged(event);
|
||||
}}
|
||||
onRowDataUpdated={onRowDataUpdated}
|
||||
partyId={partyId}
|
||||
onMarketClick={onMarketClick}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No fills')}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<div className="flex justify-between border-t border-default p-1 items-center">
|
||||
<div className="text-xs">
|
||||
{t(
|
||||
'Depending on data node retention you may not be able see the "full" history'
|
||||
)}
|
||||
</div>
|
||||
<div className="flex text-xs items-center">
|
||||
{data?.length && !pageInfo?.hasNextPage
|
||||
? t('all %s items loaded', [data.length.toString()])
|
||||
: t('%s items loaded', [
|
||||
data?.length ? data.length.toString() : ' ',
|
||||
])}
|
||||
{pageInfo?.hasNextPage ? (
|
||||
<Button size="extra-small" className="ml-1" onClick={() => load()}>
|
||||
{t('Load more')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{data?.length && hasDisplayedRow === false ? (
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
|
||||
{t('No fills matching selected filters')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
negativeClassNames,
|
||||
MarketNameCell,
|
||||
COL_DEFS,
|
||||
DateRangeFilter,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import type {
|
||||
VegaValueFormatterParams,
|
||||
@@ -120,6 +121,7 @@ export const FillsTable = forwardRef<AgGridReact, Props>(
|
||||
},
|
||||
{
|
||||
headerName: t('Date'),
|
||||
filter: DateRangeFilter,
|
||||
field: 'createdAt',
|
||||
valueFormatter: ({
|
||||
value,
|
||||
|
||||
@@ -4,7 +4,7 @@ fragment LiquidityProvisionFields on LiquidityProvision {
|
||||
id
|
||||
party {
|
||||
id
|
||||
accountsConnection(marketId: $marketId, type: ACCOUNT_TYPE_BOND) {
|
||||
accountsConnection(marketId: $marketId) {
|
||||
edges {
|
||||
node {
|
||||
type
|
||||
@@ -22,10 +22,21 @@ fragment LiquidityProvisionFields on LiquidityProvision {
|
||||
|
||||
query LiquidityProvisions($marketId: ID!) {
|
||||
market(id: $marketId) {
|
||||
liquidityProvisionsConnection(live: true) {
|
||||
liquiditySLAParameters {
|
||||
priceRange
|
||||
commitmentMinTimeFraction
|
||||
performanceHysteresisEpochs
|
||||
slaCompetitionFactor
|
||||
}
|
||||
liquidityProvisions(live: true) {
|
||||
edges {
|
||||
node {
|
||||
...LiquidityProvisionFields
|
||||
current {
|
||||
...LiquidityProvisionFields
|
||||
}
|
||||
pending {
|
||||
...LiquidityProvisionFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
fragment MarketNode on Market {
|
||||
id
|
||||
liquidityProvisionsConnection(live: true) {
|
||||
liquidityProvisions(live: true) {
|
||||
edges {
|
||||
node {
|
||||
commitmentAmount
|
||||
fee
|
||||
current {
|
||||
commitmentAmount
|
||||
fee
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-4
@@ -10,7 +10,7 @@ export type LiquidityProvisionsQueryVariables = Types.Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type LiquidityProvisionsQuery = { __typename?: 'Query', market?: { __typename?: 'Market', liquidityProvisionsConnection?: { __typename?: 'LiquidityProvisionsConnection', edges?: Array<{ __typename?: 'LiquidityProvisionsEdge', node: { __typename?: 'LiquidityProvision', id: string, createdAt: any, updatedAt?: any | null, commitmentAmount: string, fee: string, status: Types.LiquidityProvisionStatus, party: { __typename?: 'Party', id: string, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string } } | null> | null } | null } } } | null> | null } | null } | null };
|
||||
export type LiquidityProvisionsQuery = { __typename?: 'Query', market?: { __typename?: 'Market', liquiditySLAParameters?: { __typename?: 'LiquiditySLAParameters', priceRange: string, commitmentMinTimeFraction: string, performanceHysteresisEpochs: number, slaCompetitionFactor: string } | null, liquidityProvisions?: { __typename?: 'LiquidityProvisionsWithPendingConnection', edges?: Array<{ __typename?: 'LiquidityProvisionWithPendingEdge', node: { __typename?: 'LiquidityProvisionWithPending', current: { __typename?: 'LiquidityProvision', id: string, createdAt: any, updatedAt?: any | null, commitmentAmount: string, fee: string, status: Types.LiquidityProvisionStatus, party: { __typename?: 'Party', id: string, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string } } | null> | null } | null } }, pending?: { __typename?: 'LiquidityProvision', id: string, createdAt: any, updatedAt?: any | null, commitmentAmount: string, fee: string, status: Types.LiquidityProvisionStatus, party: { __typename?: 'Party', id: string, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string } } | null> | null } | null } } | null } } | null> | null } | null } | null };
|
||||
|
||||
export type LiquidityProviderFeeShareFieldsFragment = { __typename?: 'LiquidityProviderFeeShare', equityLikeShare: string, averageEntryValuation: string, averageScore: string, virtualStake: string };
|
||||
|
||||
@@ -30,7 +30,7 @@ export const LiquidityProvisionFieldsFragmentDoc = gql`
|
||||
id
|
||||
party {
|
||||
id
|
||||
accountsConnection(marketId: $marketId, type: ACCOUNT_TYPE_BOND) {
|
||||
accountsConnection(marketId: $marketId) {
|
||||
edges {
|
||||
node {
|
||||
type
|
||||
@@ -82,10 +82,21 @@ ${LiquidityProviderSLAFieldsFragmentDoc}`;
|
||||
export const LiquidityProvisionsDocument = gql`
|
||||
query LiquidityProvisions($marketId: ID!) {
|
||||
market(id: $marketId) {
|
||||
liquidityProvisionsConnection(live: true) {
|
||||
liquiditySLAParameters {
|
||||
priceRange
|
||||
commitmentMinTimeFraction
|
||||
performanceHysteresisEpochs
|
||||
slaCompetitionFactor
|
||||
}
|
||||
liquidityProvisions(live: true) {
|
||||
edges {
|
||||
node {
|
||||
...LiquidityProvisionFields
|
||||
current {
|
||||
...LiquidityProvisionFields
|
||||
}
|
||||
pending {
|
||||
...LiquidityProvisionFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-5
@@ -3,21 +3,23 @@ import * as Types from '@vegaprotocol/types';
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type MarketNodeFragment = { __typename?: 'Market', id: string, liquidityProvisionsConnection?: { __typename?: 'LiquidityProvisionsConnection', edges?: Array<{ __typename?: 'LiquidityProvisionsEdge', node: { __typename?: 'LiquidityProvision', commitmentAmount: string, fee: string } } | null> | null } | null, data?: { __typename?: 'MarketData', targetStake?: string | null } | null };
|
||||
export type MarketNodeFragment = { __typename?: 'Market', id: string, liquidityProvisions?: { __typename?: 'LiquidityProvisionsWithPendingConnection', edges?: Array<{ __typename?: 'LiquidityProvisionWithPendingEdge', node: { __typename?: 'LiquidityProvisionWithPending', current: { __typename?: 'LiquidityProvision', commitmentAmount: string, fee: string } } } | null> | null } | null, data?: { __typename?: 'MarketData', targetStake?: string | null } | null };
|
||||
|
||||
export type LiquidityProvisionMarketsQueryVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type LiquidityProvisionMarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, liquidityProvisionsConnection?: { __typename?: 'LiquidityProvisionsConnection', edges?: Array<{ __typename?: 'LiquidityProvisionsEdge', node: { __typename?: 'LiquidityProvision', commitmentAmount: string, fee: string } } | null> | null } | null, data?: { __typename?: 'MarketData', targetStake?: string | null } | null } }> } | null };
|
||||
export type LiquidityProvisionMarketsQuery = { __typename?: 'Query', marketsConnection?: { __typename?: 'MarketConnection', edges: Array<{ __typename?: 'MarketEdge', node: { __typename?: 'Market', id: string, liquidityProvisions?: { __typename?: 'LiquidityProvisionsWithPendingConnection', edges?: Array<{ __typename?: 'LiquidityProvisionWithPendingEdge', node: { __typename?: 'LiquidityProvisionWithPending', current: { __typename?: 'LiquidityProvision', commitmentAmount: string, fee: string } } } | null> | null } | null, data?: { __typename?: 'MarketData', targetStake?: string | null } | null } }> } | null };
|
||||
|
||||
export const MarketNodeFragmentDoc = gql`
|
||||
fragment MarketNode on Market {
|
||||
id
|
||||
liquidityProvisionsConnection(live: true) {
|
||||
liquidityProvisions(live: true) {
|
||||
edges {
|
||||
node {
|
||||
commitmentAmount
|
||||
fee
|
||||
current {
|
||||
commitmentAmount
|
||||
fee
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import type { LiquidityProvisionFields } from './liquidity-data-provider';
|
||||
import { getLiquidityProvision } from './liquidity-data-provider';
|
||||
import type {
|
||||
LiquidityProviderFieldsFragment,
|
||||
LiquidityProvisionFieldsFragment,
|
||||
} from './__generated__/MarketLiquidity';
|
||||
import type { LiquidityProviderFieldsFragment } from './__generated__/MarketLiquidity';
|
||||
|
||||
const input = {
|
||||
liquidityProvisions: [
|
||||
{
|
||||
id: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
|
||||
party: {
|
||||
id: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
|
||||
accountsConnection: {
|
||||
@@ -31,7 +30,11 @@ const input = {
|
||||
fee: '0.001',
|
||||
status: 'STATUS_ACTIVE',
|
||||
__typename: 'LiquidityProvision',
|
||||
} as LiquidityProvisionFieldsFragment,
|
||||
priceRange: '0',
|
||||
commitmentMinTimeFraction: '0.5',
|
||||
performanceHysteresisEpochs: 5678,
|
||||
slaCompetitionFactor: '0',
|
||||
} as unknown as LiquidityProvisionFields,
|
||||
],
|
||||
liquidityProviders: [
|
||||
{
|
||||
@@ -49,9 +52,12 @@ const input = {
|
||||
const result = [
|
||||
{
|
||||
__typename: undefined,
|
||||
balance: '1.8003328918633596575e+22',
|
||||
balance: 1.8003328918633597e22,
|
||||
earmarkedFees: 0,
|
||||
commitmentAmount: '18003328918633596575000',
|
||||
createdAt: '2022-12-16T09:28:29.071781Z',
|
||||
commitmentMinTimeFraction: '0.5',
|
||||
id: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
|
||||
feeShare: {
|
||||
equityLikeShare: '1',
|
||||
__typename: 'LiquidityProviderFeeShare',
|
||||
@@ -59,6 +65,9 @@ const result = [
|
||||
},
|
||||
fee: '0.001',
|
||||
partyId: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
|
||||
performanceHysteresisEpochs: 5678,
|
||||
priceRange: '0',
|
||||
slaCompetitionFactor: '0',
|
||||
party: {
|
||||
__typename: 'Party',
|
||||
accountsConnection: {
|
||||
@@ -106,7 +115,9 @@ describe('getLiquidityProvision', () => {
|
||||
{
|
||||
__typename: 'LiquidityProvision',
|
||||
commitmentAmount: '18003328918633596575000',
|
||||
commitmentMinTimeFraction: '0.5',
|
||||
createdAt: '2022-12-16T09:28:29.071781Z',
|
||||
id: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
|
||||
fee: '0.001',
|
||||
party: {
|
||||
__typename: 'Party',
|
||||
@@ -125,6 +136,9 @@ describe('getLiquidityProvision', () => {
|
||||
},
|
||||
id: 'dde288688af2aeb5feb349dd72d3679a7a9be34c7375f6a4a48ef2f6140e7e59',
|
||||
},
|
||||
performanceHysteresisEpochs: 5678,
|
||||
priceRange: '0',
|
||||
slaCompetitionFactor: '0',
|
||||
status: 'STATUS_ACTIVE',
|
||||
updatedAt: '2023-01-04T22:13:27.761985Z',
|
||||
},
|
||||
|
||||
@@ -20,20 +20,45 @@ import type {
|
||||
LiquidityProvisionsQueryVariables,
|
||||
} from './__generated__/MarketLiquidity';
|
||||
|
||||
export type LiquidityProvisionFields = LiquidityProvisionFieldsFragment &
|
||||
Schema.LiquiditySLAParameters & {
|
||||
currentCommitmentAmount?: string;
|
||||
currentFee?: string;
|
||||
};
|
||||
|
||||
export const liquidityProvisionsDataProvider = makeDataProvider<
|
||||
LiquidityProvisionsQuery,
|
||||
LiquidityProvisionFieldsFragment[],
|
||||
LiquidityProvisionFields[],
|
||||
never,
|
||||
never,
|
||||
LiquidityProvisionsQueryVariables
|
||||
>({
|
||||
query: LiquidityProvisionsDocument,
|
||||
getData: (responseData: LiquidityProvisionsQuery | null) => {
|
||||
return (
|
||||
responseData?.market?.liquidityProvisionsConnection?.edges?.map(
|
||||
(e) => e?.node
|
||||
) ?? []
|
||||
).filter((n) => !!n) as LiquidityProvisionFieldsFragment[];
|
||||
return (responseData?.market?.liquidityProvisions?.edges
|
||||
?.filter((n) => !!n)
|
||||
.map((e) => {
|
||||
let node;
|
||||
if (!e?.node.pending && e?.node.current) {
|
||||
node = {
|
||||
...e?.node.current,
|
||||
...responseData.market?.liquiditySLAParameters,
|
||||
};
|
||||
} else if (!e?.node.current && e?.node.pending) {
|
||||
node = {
|
||||
...e?.node.pending,
|
||||
...responseData.market?.liquiditySLAParameters,
|
||||
};
|
||||
} else {
|
||||
node = {
|
||||
...e?.node.pending,
|
||||
currentCommitmentAmount: e?.node.current.commitmentAmount,
|
||||
currentFee: e?.node.current.fee,
|
||||
...responseData.market?.liquiditySLAParameters,
|
||||
};
|
||||
}
|
||||
return node;
|
||||
}) ?? []) as LiquidityProvisionFields[];
|
||||
},
|
||||
});
|
||||
|
||||
@@ -81,16 +106,14 @@ export const lpAggregatedDataProvider = makeDerivedDataProvider<
|
||||
}
|
||||
);
|
||||
|
||||
export const matchFilter = (
|
||||
filter: Filter,
|
||||
lp: LiquidityProvisionFieldsFragment
|
||||
) => {
|
||||
export const matchFilter = (filter: Filter, lp: LiquidityProvisionData) => {
|
||||
if (filter.partyId && lp.party.id !== filter.partyId) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
filter.active === true &&
|
||||
lp.status !== Schema.LiquidityProvisionStatus.STATUS_ACTIVE
|
||||
lp.status !== Schema.LiquidityProvisionStatus.STATUS_ACTIVE &&
|
||||
lp.status !== Schema.LiquidityProvisionStatus.STATUS_PENDING
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -104,16 +127,20 @@ export const matchFilter = (
|
||||
};
|
||||
|
||||
export interface LiquidityProvisionData
|
||||
extends Omit<LiquidityProvisionFieldsFragment, '__typename'> {
|
||||
extends Omit<LiquidityProvisionFields, '__typename'>,
|
||||
Partial<LiquidityProviderFieldsFragment>,
|
||||
Omit<Schema.LiquiditySLAParameters, '__typename'> {
|
||||
assetDecimalPlaces?: number;
|
||||
balance?: string;
|
||||
balance?: number;
|
||||
averageEntryValuation?: string;
|
||||
equityLikeShare?: string;
|
||||
earmarkedFees?: number;
|
||||
status: Schema.LiquidityProvisionStatus;
|
||||
}
|
||||
|
||||
export const getLiquidityProvision = (
|
||||
liquidityProvisions: LiquidityProvisionFieldsFragment[],
|
||||
liquidityProvider: LiquidityProviderFieldsFragment[],
|
||||
liquidityProvisions: LiquidityProvisionFields[],
|
||||
liquidityProviders: LiquidityProviderFieldsFragment[],
|
||||
filter?: Filter
|
||||
): LiquidityProvisionData[] => {
|
||||
return liquidityProvisions
|
||||
@@ -132,26 +159,40 @@ export const getLiquidityProvision = (
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((lp) => {
|
||||
const lpObj = liquidityProvider.find((f) => lp.party.id === f.partyId);
|
||||
if (!lpObj) return lp;
|
||||
const accounts = compact(lp.party.accountsConnection?.edges).map(
|
||||
(e) => e.node
|
||||
.map((liquidityProvision) => {
|
||||
const liquidityProvider = liquidityProviders.find(
|
||||
(f) => liquidityProvision.party.id === f.partyId
|
||||
);
|
||||
if (!liquidityProvider) return liquidityProvision;
|
||||
const accounts = compact(
|
||||
liquidityProvision.party.accountsConnection?.edges
|
||||
).map((e) => e.node);
|
||||
const bondAccounts = accounts?.filter(
|
||||
(a) => a?.type === Schema.AccountType.ACCOUNT_TYPE_BOND
|
||||
);
|
||||
const feeAccounts = accounts?.filter(
|
||||
(a) => a?.type === Schema.AccountType.ACCOUNT_TYPE_LP_LIQUIDITY_FEES
|
||||
);
|
||||
const balance =
|
||||
bondAccounts
|
||||
?.reduce(
|
||||
(acc, a) => acc.plus(new BigNumber(a.balance ?? 0)),
|
||||
new BigNumber(0)
|
||||
)
|
||||
.toString() || '0';
|
||||
.toNumber() ?? 0;
|
||||
|
||||
const earmarkedFees =
|
||||
feeAccounts
|
||||
?.reduce(
|
||||
(acc, a) => acc.plus(new BigNumber(a.balance ?? 0)),
|
||||
new BigNumber(0)
|
||||
)
|
||||
.toNumber() ?? 0;
|
||||
return {
|
||||
...lp,
|
||||
...lpObj,
|
||||
...liquidityProvision,
|
||||
...liquidityProvider,
|
||||
balance,
|
||||
earmarkedFees,
|
||||
__typename: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -55,9 +55,10 @@ describe('LiquidityTable', () => {
|
||||
'Adjusted stake share',
|
||||
'Share',
|
||||
'Live supplied liquidity',
|
||||
'Live time fraction on book',
|
||||
'Fees accrued this epoch',
|
||||
'Live time on book',
|
||||
'Live liquidity quality score (%)',
|
||||
'Last time fraction on the book',
|
||||
'Last time on the book',
|
||||
'Last fee penalty',
|
||||
'Last bond penalty',
|
||||
'Status',
|
||||
|
||||
@@ -6,7 +6,10 @@ import {
|
||||
getDateTimeFormat,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { TypedDataAgGrid } from '@vegaprotocol/datagrid';
|
||||
import type {
|
||||
TypedDataAgGrid,
|
||||
VegaValueFormatterParams,
|
||||
} from '@vegaprotocol/datagrid';
|
||||
import { AgGrid } from '@vegaprotocol/datagrid';
|
||||
import {
|
||||
CopyWithTooltip,
|
||||
@@ -22,7 +25,7 @@ import type {
|
||||
ValueFormatterParams,
|
||||
} from 'ag-grid-community';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { LiquidityProvisionStatus } from '@vegaprotocol/types';
|
||||
import { LiquidityProvisionStatus } from '@vegaprotocol/types';
|
||||
import { LiquidityProvisionStatusMapping } from '@vegaprotocol/types';
|
||||
import type { LiquidityProvisionData } from './liquidity-data-provider';
|
||||
|
||||
@@ -96,6 +99,58 @@ export const LiquidityTable = ({
|
||||
return `${addDecimalsFormatNumber(newValue, assetDecimalPlaces ?? 0)}`;
|
||||
};
|
||||
|
||||
const feesAccruedTooltip = ({ value, data }: ITooltipParams) => {
|
||||
if (!value) return '-';
|
||||
const newValue = new BigNumber(value)
|
||||
.times(Number(stakeToCcyVolume) || 1)
|
||||
.toString();
|
||||
let lessThanFull = false,
|
||||
lessThanMinimum = false;
|
||||
if (data.sla) {
|
||||
lessThanFull =
|
||||
data.sla &&
|
||||
new BigNumber(data.sla.currentEpochFractionOfTimeOnBook).isLessThan(
|
||||
1
|
||||
);
|
||||
lessThanMinimum =
|
||||
data.sla &&
|
||||
new BigNumber(data.sla.currentEpochFractionOfTimeOnBook).isLessThan(
|
||||
data.commitmentMinTimeFraction
|
||||
);
|
||||
}
|
||||
if (lessThanMinimum) {
|
||||
return t(
|
||||
`This LP's time on the book in the current epoch (%s) is less than the minimum required (%s), so they could lose all fee revenue for this epoch.`,
|
||||
[
|
||||
formatNumberPercentage(
|
||||
new BigNumber(data.sla.currentEpochFractionOfTimeOnBook).times(
|
||||
100
|
||||
),
|
||||
2
|
||||
),
|
||||
formatNumberPercentage(
|
||||
new BigNumber(data.commitmentMinTimeFraction).times(100),
|
||||
2
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
if (lessThanFull) {
|
||||
return t(
|
||||
`This LP's time on the book in the current epoch (%s) is less than 100%, so they could lose some fees to a better performing LP.`,
|
||||
[
|
||||
formatNumberPercentage(
|
||||
new BigNumber(data.sla.currentEpochFractionOfTimeOnBook).times(
|
||||
100
|
||||
),
|
||||
2
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
return addDecimalsFormatNumber(newValue, assetDecimalPlaces ?? 0);
|
||||
};
|
||||
|
||||
const stakeToCcyVolumeQuantumFormatter = ({
|
||||
value,
|
||||
}: ValueFormatterParams) => {
|
||||
@@ -135,7 +190,32 @@ export const LiquidityTable = ({
|
||||
headerTooltip: t(
|
||||
'The amount committed to the market by this liquidity provider.'
|
||||
),
|
||||
valueFormatter: assetDecimalsQuantumFormatter,
|
||||
valueFormatter: ({
|
||||
data,
|
||||
value,
|
||||
}: VegaValueFormatterParams<
|
||||
LiquidityProvisionData,
|
||||
'commitmentAmount'
|
||||
>) => {
|
||||
if (!value) return '-';
|
||||
const formattedCommitmentAmount = addDecimalsFormatNumberQuantum(
|
||||
value,
|
||||
assetDecimalPlaces ?? 0,
|
||||
quantum ?? 0
|
||||
);
|
||||
if (
|
||||
data?.currentCommitmentAmount &&
|
||||
data?.currentCommitmentAmount !== value
|
||||
) {
|
||||
return `${addDecimalsFormatNumberQuantum(
|
||||
data.currentCommitmentAmount,
|
||||
assetDecimalPlaces ?? 0,
|
||||
quantum ?? 0
|
||||
)}/${formattedCommitmentAmount}`;
|
||||
} else {
|
||||
return formattedCommitmentAmount;
|
||||
}
|
||||
},
|
||||
tooltipValueGetter: assetDecimalsFormatter,
|
||||
},
|
||||
{
|
||||
@@ -155,7 +235,22 @@ export const LiquidityTable = ({
|
||||
),
|
||||
field: 'fee',
|
||||
type: 'rightAligned',
|
||||
valueFormatter: percentageFormatter,
|
||||
valueFormatter: ({
|
||||
data,
|
||||
value,
|
||||
}: ValueFormatterParams<LiquidityProvisionData, 'fee'>) => {
|
||||
if (!value) return '-';
|
||||
const formattedValue =
|
||||
formatNumberPercentage(new BigNumber(value).times(100), 2) ||
|
||||
'-';
|
||||
if (data?.currentFee && data?.currentFee !== value) {
|
||||
return `${formatNumberPercentage(
|
||||
new BigNumber(data.currentFee).times(100),
|
||||
2
|
||||
)}/${formattedValue}`;
|
||||
}
|
||||
return formattedValue;
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t('Adjusted stake share'),
|
||||
@@ -178,7 +273,7 @@ export const LiquidityTable = ({
|
||||
],
|
||||
},
|
||||
{
|
||||
headerName: t('Live liquidity details'),
|
||||
headerName: t('Live liquidity data'),
|
||||
marryChildren: true,
|
||||
children: [
|
||||
{
|
||||
@@ -192,7 +287,41 @@ export const LiquidityTable = ({
|
||||
tooltipValueGetter: stakeToCcyVolumeFormatter,
|
||||
},
|
||||
{
|
||||
headerName: t(`Live time fraction on book`),
|
||||
headerName: t('Fees accrued this epoch'),
|
||||
field: 'earmarkedFees',
|
||||
type: 'rightAligned',
|
||||
headerTooltip: t(
|
||||
`The liquidity fees accrued by each provider, which will be distributed at the end of the epoch after applying any penalties.`
|
||||
),
|
||||
valueFormatter: stakeToCcyVolumeQuantumFormatter,
|
||||
tooltipValueGetter: feesAccruedTooltip,
|
||||
cellClassRules: {
|
||||
'text-warning': ({ data }: { data: LiquidityProvisionData }) => {
|
||||
if (!data.sla) return false;
|
||||
return (
|
||||
new BigNumber(
|
||||
data.sla.currentEpochFractionOfTimeOnBook
|
||||
).isLessThan(1) &&
|
||||
new BigNumber(
|
||||
data.sla.currentEpochFractionOfTimeOnBook
|
||||
).isGreaterThan(data.commitmentMinTimeFraction)
|
||||
);
|
||||
},
|
||||
'text-red-500': ({ data }: { data: LiquidityProvisionData }) => {
|
||||
if (!data.sla) return false;
|
||||
return (
|
||||
new BigNumber(
|
||||
data.sla.currentEpochFractionOfTimeOnBook
|
||||
).isLessThan(data.commitmentMinTimeFraction) &&
|
||||
new BigNumber(
|
||||
data.sla.currentEpochFractionOfTimeOnBook
|
||||
).isGreaterThan(0)
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
headerName: t(`Live time on book`),
|
||||
field: 'sla.currentEpochFractionOfTimeOnBook',
|
||||
type: 'rightAligned',
|
||||
headerTooltip: t('Current epoch fraction of time on the book.'),
|
||||
@@ -212,7 +341,7 @@ export const LiquidityTable = ({
|
||||
marryChildren: true,
|
||||
children: [
|
||||
{
|
||||
headerName: t(`Last time fraction on the book`),
|
||||
headerName: t(`Last time on the book`),
|
||||
field: 'sla.lastEpochFractionOfTimeOnBook',
|
||||
type: 'rightAligned',
|
||||
headerTooltip: t('Last epoch fraction of time on the book.'),
|
||||
@@ -242,8 +371,17 @@ export const LiquidityTable = ({
|
||||
headerName: t('Status'),
|
||||
headerTooltip: t('The current status of this liquidity provision.'),
|
||||
field: 'status',
|
||||
valueFormatter: ({ value }) => {
|
||||
valueFormatter: ({
|
||||
data,
|
||||
value,
|
||||
}: ValueFormatterParams<LiquidityProvisionData, 'status'>) => {
|
||||
if (!value) return value;
|
||||
if (
|
||||
data?.status === LiquidityProvisionStatus.STATUS_PENDING &&
|
||||
(data?.currentCommitmentAmount || data?.currentFee)
|
||||
) {
|
||||
return t('Updating next epoch');
|
||||
}
|
||||
return LiquidityProvisionStatusMapping[
|
||||
value as LiquidityProvisionStatus
|
||||
];
|
||||
@@ -275,7 +413,9 @@ export const LiquidityTable = ({
|
||||
return (
|
||||
<AgGrid
|
||||
overlayNoRowsTemplate={t('No liquidity provisions')}
|
||||
getRowId={({ data }: { data: LiquidityProvisionData }) => data.id || ''}
|
||||
getRowId={({ data }: { data: LiquidityProvisionData }) => {
|
||||
return data.id || '';
|
||||
}}
|
||||
tooltipShowDelay={500}
|
||||
defaultColDef={defaultColDef}
|
||||
{...props}
|
||||
|
||||
@@ -12,12 +12,14 @@ export const liquidityProvisionsQuery = (
|
||||
): LiquidityProvisionsQuery => {
|
||||
const defaultResult: LiquidityProvisionsQuery = {
|
||||
market: {
|
||||
liquidityProvisionsConnection: {
|
||||
__typename: 'LiquidityProvisionsConnection',
|
||||
liquidityProvisions: {
|
||||
__typename: 'LiquidityProvisionsWithPendingConnection',
|
||||
edges: liquidityFields.map((node) => {
|
||||
return {
|
||||
__typename: 'LiquidityProvisionsEdge',
|
||||
node,
|
||||
__typename: 'LiquidityProvisionWithPendingEdge',
|
||||
node: {
|
||||
current: node,
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -106,10 +106,9 @@ export const getLiquidityForMarket = (
|
||||
markets: LiquidityProvisionMarket[]
|
||||
) => {
|
||||
const liquidity =
|
||||
markets.find((m) => m.id === marketId)?.liquidityProvisionsConnection
|
||||
?.edges || [];
|
||||
markets.find((m) => m.id === marketId)?.liquidityProvisions?.edges || [];
|
||||
|
||||
return liquidity.map((l) => l?.node);
|
||||
return liquidity.map((l) => l?.node.current);
|
||||
};
|
||||
|
||||
export const getTargetStake = (
|
||||
|
||||
@@ -1001,7 +1001,7 @@ export const LiquiditySLAParametersInfoPanel = ({
|
||||
|
||||
const { params: networkParams } = useNetworkParams([
|
||||
NetworkParams.market_liquidity_bondPenaltyParameter,
|
||||
NetworkParams.market_liquidity_nonPerformanceBondPenaltySlope,
|
||||
NetworkParams.market_liquidity_sla_nonPerformanceBondPenaltySlope,
|
||||
NetworkParams.market_liquidity_sla_nonPerformanceBondPenaltyMax,
|
||||
NetworkParams.market_liquidity_maximumLiquidityFeeFactorLevel,
|
||||
NetworkParams.market_liquidity_stakeToCcyVolume,
|
||||
@@ -1017,7 +1017,7 @@ export const LiquiditySLAParametersInfoPanel = ({
|
||||
bondPenaltyParameter:
|
||||
networkParams['market_liquidity_bondPenaltyParameter'],
|
||||
nonPerformanceBondPenaltySlope:
|
||||
networkParams['market_liquidity_nonPerformanceBondPenaltySlope'],
|
||||
networkParams['market_liquidity_sla_nonPerformanceBondPenaltySlope'],
|
||||
nonPerformanceBondPenaltyMax:
|
||||
networkParams['market_liquidity_sla_nonPerformanceBondPenaltyMax'],
|
||||
maxLiquidityFeeFactorLevel:
|
||||
|
||||
@@ -153,12 +153,14 @@ export const NetworkParams = {
|
||||
'spam_protection_minimumWithdrawalQuantumMultiple',
|
||||
spam_protection_voting_min_tokens: 'spam_protection_voting_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_bondPenaltyParameter:
|
||||
'market_liquidity_bondPenaltyParameter',
|
||||
market_liquidity_nonPerformanceBondPenaltySlope:
|
||||
'market_liquidity_nonPerformanceBondPenaltySlope',
|
||||
market_liquidity_sla_nonPerformanceBondPenaltySlope:
|
||||
'market_liquidity_sla_nonPerformanceBondPenaltySlope',
|
||||
market_liquidity_sla_nonPerformanceBondPenaltyMax:
|
||||
'market_liquidity_sla_nonPerformanceBondPenaltyMax',
|
||||
market_liquidity_maximumLiquidityFeeFactorLevel:
|
||||
|
||||
@@ -166,7 +166,7 @@ export const ordersProvider = makeDataProvider<
|
||||
pagination: {
|
||||
getPageInfo,
|
||||
append,
|
||||
first: 5000,
|
||||
first: 1000,
|
||||
},
|
||||
resetDelay: 1000,
|
||||
additionalContext: { isEnlargedTimeout: true },
|
||||
|
||||
@@ -27,6 +27,7 @@ describe('OrderListManager', () => {
|
||||
flush: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
load: jest.fn(),
|
||||
pageInfo: null,
|
||||
});
|
||||
render(generateJsx());
|
||||
|
||||
@@ -45,6 +46,7 @@ describe('OrderListManager', () => {
|
||||
flush: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
load: jest.fn(),
|
||||
pageInfo: null,
|
||||
});
|
||||
|
||||
render(generateJsx());
|
||||
@@ -62,6 +64,7 @@ describe('OrderListManager', () => {
|
||||
flush: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
load: jest.fn(),
|
||||
pageInfo: null,
|
||||
});
|
||||
|
||||
render(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { useCallback, useRef, useState, useEffect } from 'react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import type { FilterChangedEvent } from 'ag-grid-community';
|
||||
import { OrderListTable } from '../order-list';
|
||||
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
@@ -12,7 +11,7 @@ import type { OrderTxUpdateFieldsFragment } from '@vegaprotocol/web3';
|
||||
import { OrderEditDialog } from '../order-list/order-edit-dialog';
|
||||
import type { Order } from '../order-data-provider';
|
||||
import { OrderViewDialog } from '../order-list/order-view-dialog';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { Splash, TradingButton as Button } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export enum Filter {
|
||||
'Open' = 'Open',
|
||||
@@ -48,33 +47,11 @@ export const OrderListManager = ({
|
||||
? { partyId, filter: { liveOnly: true } }
|
||||
: { partyId };
|
||||
|
||||
const { data, error } = useDataProvider({
|
||||
const { data, error, pageInfo, load } = useDataProvider({
|
||||
dataProvider: ordersWithMarketProvider,
|
||||
variables,
|
||||
update: ({ data }) => {
|
||||
if (data && gridRef.current?.api) {
|
||||
gridRef.current.api.setRowData(data);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
const onFilterChanged = useCallback(
|
||||
(event: FilterChangedEvent) => {
|
||||
gridProps?.onFilterChanged?.(event);
|
||||
if (event.api) {
|
||||
const isEmpty = event.api.getDisplayedRowCount() === 0;
|
||||
if (isEmpty) {
|
||||
event.api.showNoRowsOverlay();
|
||||
} else {
|
||||
event.api.hideOverlay();
|
||||
}
|
||||
}
|
||||
},
|
||||
[gridProps]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data || !data.length) {
|
||||
gridRef.current?.api?.showNoRowsOverlay();
|
||||
@@ -96,6 +73,17 @@ export const OrderListManager = ({
|
||||
[create]
|
||||
);
|
||||
|
||||
const [hasDisplayedRow, setHasDisplayedRow] = useState<boolean | undefined>(
|
||||
undefined
|
||||
);
|
||||
const { onFilterChanged, ...props } = gridProps || {};
|
||||
const onRowDataUpdated = useCallback(
|
||||
({ api }: { api: AgGridReact['api'] }) => {
|
||||
setHasDisplayedRow(!!api.getDisplayedRowCount());
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return <Splash>{t(`Something went wrong: ${error.message}`)}</Splash>;
|
||||
}
|
||||
@@ -103,20 +91,60 @@ export const OrderListManager = ({
|
||||
return (
|
||||
<>
|
||||
<div className="h-full relative">
|
||||
<OrderListTable
|
||||
rowData={data}
|
||||
ref={gridRef}
|
||||
filter={filter}
|
||||
onCancel={cancel}
|
||||
onEdit={setEditOrder}
|
||||
onView={setViewOrder}
|
||||
onMarketClick={onMarketClick}
|
||||
onOrderTypeClick={onOrderTypeClick}
|
||||
isReadOnly={isReadOnly}
|
||||
overlayNoRowsTemplate={noRowsMessage || t('No orders')}
|
||||
{...gridProps}
|
||||
onFilterChanged={onFilterChanged}
|
||||
/>
|
||||
<div className="flex flex-col h-full">
|
||||
<OrderListTable
|
||||
rowData={data}
|
||||
ref={gridRef}
|
||||
filter={filter}
|
||||
onCancel={cancel}
|
||||
onEdit={setEditOrder}
|
||||
onView={setViewOrder}
|
||||
onMarketClick={onMarketClick}
|
||||
onOrderTypeClick={onOrderTypeClick}
|
||||
onFilterChanged={(event) => {
|
||||
onRowDataUpdated(event);
|
||||
if (onFilterChanged) {
|
||||
onFilterChanged(event);
|
||||
}
|
||||
}}
|
||||
onRowDataUpdated={onRowDataUpdated}
|
||||
isReadOnly={isReadOnly}
|
||||
overlayNoRowsTemplate={noRowsMessage || t('No orders')}
|
||||
{...props}
|
||||
/>
|
||||
<div className="flex justify-between border-t border-default p-1 items-center">
|
||||
<div className="text-xs">
|
||||
{variables.filter?.liveOnly
|
||||
? null
|
||||
: t(
|
||||
'Depending on data node retention you may not be able see the "full" history'
|
||||
)}
|
||||
</div>
|
||||
{data ? (
|
||||
<div className="flex text-xs items-center">
|
||||
{data?.length && !pageInfo?.hasNextPage
|
||||
? t('all %s items loaded', [data.length.toString()])
|
||||
: t('%s items loaded', [
|
||||
data?.length ? data.length.toString() : ' ',
|
||||
])}
|
||||
{pageInfo?.hasNextPage ? (
|
||||
<Button
|
||||
size="extra-small"
|
||||
className="ml-1"
|
||||
onClick={() => load()}
|
||||
>
|
||||
{t('Load more')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{data?.length && hasDisplayedRow === false ? (
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
|
||||
{t('No orders matching selected filters')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{editOrder && (
|
||||
<OrderEditDialog
|
||||
|
||||
@@ -30,6 +30,7 @@ describe('StopOrdersManager', () => {
|
||||
flush: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
load: jest.fn(),
|
||||
pageInfo: null,
|
||||
});
|
||||
await act(async () => {
|
||||
render(generateJsx());
|
||||
|
||||
@@ -12,10 +12,15 @@ subscription ProposalEvent($partyId: ID!) {
|
||||
}
|
||||
}
|
||||
|
||||
fragment UpdateNetworkParameterProposal on Proposal {
|
||||
fragment OnProposalFragment on Proposal {
|
||||
id
|
||||
state
|
||||
datetime
|
||||
rationale {
|
||||
title
|
||||
description
|
||||
}
|
||||
rejectionReason
|
||||
terms {
|
||||
enactmentDatetime
|
||||
change {
|
||||
@@ -26,9 +31,9 @@ fragment UpdateNetworkParameterProposal on Proposal {
|
||||
}
|
||||
}
|
||||
|
||||
subscription OnUpdateNetworkParameters {
|
||||
subscription OnProposal {
|
||||
proposals {
|
||||
...UpdateNetworkParameterProposal
|
||||
...OnProposalFragment
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+22
-17
@@ -13,12 +13,12 @@ export type ProposalEventSubscriptionVariables = Types.Exact<{
|
||||
|
||||
export type ProposalEventSubscription = { __typename?: 'Subscription', proposals: { __typename?: 'Proposal', id?: string | null, reference: string, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null, errorDetails?: string | null } };
|
||||
|
||||
export type UpdateNetworkParameterProposalFragment = { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, datetime: any, terms: { __typename?: 'ProposalTerms', enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } };
|
||||
export type OnProposalFragmentFragment = { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, terms: { __typename?: 'ProposalTerms', enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } };
|
||||
|
||||
export type OnUpdateNetworkParametersSubscriptionVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
export type OnProposalSubscriptionVariables = Types.Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type OnUpdateNetworkParametersSubscription = { __typename?: 'Subscription', proposals: { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, datetime: any, terms: { __typename?: 'ProposalTerms', enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } } };
|
||||
export type OnProposalSubscription = { __typename?: 'Subscription', proposals: { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, datetime: any, rejectionReason?: Types.ProposalRejectionReason | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string }, terms: { __typename?: 'ProposalTerms', enactmentDatetime?: any | null, change: { __typename?: 'CancelTransfer' } | { __typename?: 'NewAsset' } | { __typename?: 'NewFreeform' } | { __typename?: 'NewMarket' } | { __typename?: 'NewSpotMarket' } | { __typename?: 'NewTransfer' } | { __typename?: 'UpdateAsset' } | { __typename?: 'UpdateMarket' } | { __typename?: 'UpdateMarketState' } | { __typename?: 'UpdateNetworkParameter', networkParameter: { __typename?: 'NetworkParameter', key: string, value: string } } | { __typename?: 'UpdateReferralProgram' } | { __typename?: 'UpdateSpotMarket' } | { __typename?: 'UpdateVolumeDiscountProgram' } } } };
|
||||
|
||||
export type ProposalOfMarketQueryVariables = Types.Exact<{
|
||||
marketId: Types.Scalars['ID'];
|
||||
@@ -64,11 +64,16 @@ export const ProposalEventFieldsFragmentDoc = gql`
|
||||
errorDetails
|
||||
}
|
||||
`;
|
||||
export const UpdateNetworkParameterProposalFragmentDoc = gql`
|
||||
fragment UpdateNetworkParameterProposal on Proposal {
|
||||
export const OnProposalFragmentFragmentDoc = gql`
|
||||
fragment OnProposalFragment on Proposal {
|
||||
id
|
||||
state
|
||||
datetime
|
||||
rationale {
|
||||
title
|
||||
description
|
||||
}
|
||||
rejectionReason
|
||||
terms {
|
||||
enactmentDatetime
|
||||
change {
|
||||
@@ -109,35 +114,35 @@ export function useProposalEventSubscription(baseOptions: Apollo.SubscriptionHoo
|
||||
}
|
||||
export type ProposalEventSubscriptionHookResult = ReturnType<typeof useProposalEventSubscription>;
|
||||
export type ProposalEventSubscriptionResult = Apollo.SubscriptionResult<ProposalEventSubscription>;
|
||||
export const OnUpdateNetworkParametersDocument = gql`
|
||||
subscription OnUpdateNetworkParameters {
|
||||
export const OnProposalDocument = gql`
|
||||
subscription OnProposal {
|
||||
proposals {
|
||||
...UpdateNetworkParameterProposal
|
||||
...OnProposalFragment
|
||||
}
|
||||
}
|
||||
${UpdateNetworkParameterProposalFragmentDoc}`;
|
||||
${OnProposalFragmentFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useOnUpdateNetworkParametersSubscription__
|
||||
* __useOnProposalSubscription__
|
||||
*
|
||||
* To run a query within a React component, call `useOnUpdateNetworkParametersSubscription` and pass it any options that fit your needs.
|
||||
* When your component renders, `useOnUpdateNetworkParametersSubscription` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* To run a query within a React component, call `useOnProposalSubscription` and pass it any options that fit your needs.
|
||||
* When your component renders, `useOnProposalSubscription` 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 subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useOnUpdateNetworkParametersSubscription({
|
||||
* const { data, loading, error } = useOnProposalSubscription({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useOnUpdateNetworkParametersSubscription(baseOptions?: Apollo.SubscriptionHookOptions<OnUpdateNetworkParametersSubscription, OnUpdateNetworkParametersSubscriptionVariables>) {
|
||||
export function useOnProposalSubscription(baseOptions?: Apollo.SubscriptionHookOptions<OnProposalSubscription, OnProposalSubscriptionVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useSubscription<OnUpdateNetworkParametersSubscription, OnUpdateNetworkParametersSubscriptionVariables>(OnUpdateNetworkParametersDocument, options);
|
||||
return Apollo.useSubscription<OnProposalSubscription, OnProposalSubscriptionVariables>(OnProposalDocument, options);
|
||||
}
|
||||
export type OnUpdateNetworkParametersSubscriptionHookResult = ReturnType<typeof useOnUpdateNetworkParametersSubscription>;
|
||||
export type OnUpdateNetworkParametersSubscriptionResult = Apollo.SubscriptionResult<OnUpdateNetworkParametersSubscription>;
|
||||
export type OnProposalSubscriptionHookResult = ReturnType<typeof useOnProposalSubscription>;
|
||||
export type OnProposalSubscriptionResult = Apollo.SubscriptionResult<OnProposalSubscription>;
|
||||
export const ProposalOfMarketDocument = gql`
|
||||
query ProposalOfMarket($marketId: ID!) {
|
||||
proposal(id: $marketId) {
|
||||
|
||||
@@ -3,7 +3,7 @@ export * from './use-proposal-event';
|
||||
export * from './use-vega-transaction';
|
||||
export * from './use-proposal-submit';
|
||||
export * from './use-update-proposal';
|
||||
export * from './use-update-network-paramaters-toasts';
|
||||
export * from './use-proposal-toasts';
|
||||
export * from './use-successor-market-proposal-details';
|
||||
export * from './use-new-transfer-proposal-details';
|
||||
export * from './use-cancel-transfer-proposal-details';
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import type { ProposalRejectionReason } from '@vegaprotocol/types';
|
||||
import {
|
||||
ProposalChangeMapping,
|
||||
ProposalState,
|
||||
ProposalStateMapping,
|
||||
} from '@vegaprotocol/types';
|
||||
import type { ReactNode } from 'react';
|
||||
import {
|
||||
PROPOSAL_STATES_TO_TOAST,
|
||||
ProposalToastContent,
|
||||
useProposalToasts,
|
||||
} from './use-proposal-toasts';
|
||||
import { useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { waitFor, renderHook, render } from '@testing-library/react';
|
||||
import {
|
||||
OnProposalDocument,
|
||||
type OnProposalFragmentFragment,
|
||||
type OnProposalSubscription,
|
||||
} from './__generated__/Proposal';
|
||||
import sample from 'lodash/sample';
|
||||
|
||||
const renderUseProposalToasts = (mocks?: MockedResponse[]) => {
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider mocks={mocks}>{children}</MockedProvider>
|
||||
);
|
||||
return renderHook(() => useProposalToasts(), { wrapper });
|
||||
};
|
||||
|
||||
type ProposalChange = OnProposalFragmentFragment['terms']['change'];
|
||||
|
||||
const NEW_MARKET_CHANGE: ProposalChange = { __typename: 'NewMarket' };
|
||||
const UPDATE_MARKET_CHANGE: ProposalChange = { __typename: 'UpdateMarket' };
|
||||
const UPDATE_NETWORK_PARAMETER_CHANGE: ProposalChange = {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
networkParameter: {
|
||||
__typename: 'NetworkParameter',
|
||||
key: 'abc.def',
|
||||
value: '123',
|
||||
},
|
||||
};
|
||||
const NEW_ASSET_CHANGE: ProposalChange = { __typename: 'NewAsset' };
|
||||
const UPDATE_ASSET_CHANGE: ProposalChange = { __typename: 'UpdateAsset' };
|
||||
const NEW_FREEFORM_CHANGE: ProposalChange = { __typename: 'NewFreeform' };
|
||||
const NEW_TRANSFER_CHANGE: ProposalChange = { __typename: 'NewTransfer' };
|
||||
const CANCEL_TRANSFER_CHANGE: ProposalChange = { __typename: 'CancelTransfer' };
|
||||
const UPDATE_MARKET_STATE_CHANGE: ProposalChange = {
|
||||
__typename: 'UpdateMarketState',
|
||||
};
|
||||
const NEW_SPOT_MARKET_CHANGE: ProposalChange = { __typename: 'NewSpotMarket' };
|
||||
const UPDATE_SPOT_MARKET_CHANGE: ProposalChange = {
|
||||
__typename: 'UpdateSpotMarket',
|
||||
};
|
||||
const UPDATE_VOLUME_DISCOUNT_PROGRAM_CHANGE: ProposalChange = {
|
||||
__typename: 'UpdateVolumeDiscountProgram',
|
||||
};
|
||||
const UPDATE_REFERRAL_PROGRAM_CHANGE: ProposalChange = {
|
||||
__typename: 'UpdateReferralProgram',
|
||||
};
|
||||
|
||||
const GenericToastProposals = [
|
||||
NEW_MARKET_CHANGE,
|
||||
UPDATE_MARKET_CHANGE,
|
||||
NEW_ASSET_CHANGE,
|
||||
UPDATE_ASSET_CHANGE,
|
||||
NEW_FREEFORM_CHANGE,
|
||||
NEW_TRANSFER_CHANGE,
|
||||
CANCEL_TRANSFER_CHANGE,
|
||||
UPDATE_MARKET_STATE_CHANGE,
|
||||
NEW_SPOT_MARKET_CHANGE,
|
||||
UPDATE_SPOT_MARKET_CHANGE,
|
||||
UPDATE_VOLUME_DISCOUNT_PROGRAM_CHANGE,
|
||||
UPDATE_REFERRAL_PROGRAM_CHANGE,
|
||||
];
|
||||
|
||||
const generateProposal = (
|
||||
title: string,
|
||||
state: ProposalState = ProposalState.STATE_OPEN,
|
||||
change: ProposalChange = { __typename: undefined },
|
||||
rejectionReason: ProposalRejectionReason | null = null
|
||||
): OnProposalFragmentFragment => ({
|
||||
__typename: 'Proposal',
|
||||
id: Math.random().toString(),
|
||||
datetime: Math.random().toString(),
|
||||
rationale: {
|
||||
title,
|
||||
description: '',
|
||||
},
|
||||
rejectionReason,
|
||||
state,
|
||||
terms: {
|
||||
__typename: 'ProposalTerms',
|
||||
enactmentDatetime: '2022-12-09T14:40:38Z',
|
||||
change,
|
||||
},
|
||||
});
|
||||
|
||||
const INITIAL = useToasts.getState();
|
||||
|
||||
const clear = () => {
|
||||
useToasts.setState(INITIAL);
|
||||
};
|
||||
|
||||
describe('useProposalToasts', () => {
|
||||
beforeEach(clear);
|
||||
afterAll(clear);
|
||||
|
||||
it.each(PROPOSAL_STATES_TO_TOAST)(
|
||||
'renders toast for %s proposal',
|
||||
async (state) => {
|
||||
const mockProposal: MockedResponse<OnProposalSubscription> = {
|
||||
request: {
|
||||
query: OnProposalDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
proposals: generateProposal(
|
||||
'Things to change',
|
||||
state,
|
||||
NEW_MARKET_CHANGE
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
const { result } = renderUseProposalToasts([mockProposal]);
|
||||
expect(result.current.loading).toBe(true);
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(useToasts.getState().count).toBe(1);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const IGNORE_STATES = Object.keys(ProposalState).filter((state) => {
|
||||
return !PROPOSAL_STATES_TO_TOAST.includes(state as ProposalState);
|
||||
}) as ProposalState[];
|
||||
it.each(IGNORE_STATES)(
|
||||
'does not render toast for %s proposal',
|
||||
async (state) => {
|
||||
const mockFailedProposal: MockedResponse<OnProposalSubscription> = {
|
||||
request: {
|
||||
query: OnProposalDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
proposals: generateProposal('Things to change but ignored', state),
|
||||
},
|
||||
},
|
||||
};
|
||||
const { result } = renderUseProposalToasts([mockFailedProposal]);
|
||||
expect(result.current.loading).toBe(true);
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(useToasts.getState().count).toBe(0);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
it('does not render toast for empty proposal', async () => {
|
||||
const error = console.error;
|
||||
console.error = () => {
|
||||
/* no op */
|
||||
};
|
||||
const mockEmptyProposal: MockedResponse<OnProposalSubscription> = {
|
||||
request: {
|
||||
query: OnProposalDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
proposals: undefined as unknown as OnProposalFragmentFragment,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { result } = renderUseProposalToasts([mockEmptyProposal]);
|
||||
expect(result.current.loading).toBe(true);
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(useToasts.getState().count).toBe(0);
|
||||
});
|
||||
console.error = error;
|
||||
});
|
||||
|
||||
const allTypes: [
|
||||
ProposalChange['__typename'],
|
||||
ProposalChange,
|
||||
ProposalState?
|
||||
][] = [...GenericToastProposals, UPDATE_NETWORK_PARAMETER_CHANGE].map(
|
||||
(ch) => [ch.__typename, ch, sample(PROPOSAL_STATES_TO_TOAST)]
|
||||
);
|
||||
it.each(allTypes)(
|
||||
'renders toast for %s proposal',
|
||||
async (_, change, state) => {
|
||||
const proposalData = generateProposal('Things to change', state, change);
|
||||
const mockProposal: MockedResponse<OnProposalSubscription> = {
|
||||
request: {
|
||||
query: OnProposalDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
proposals: proposalData,
|
||||
},
|
||||
},
|
||||
};
|
||||
const { result } = renderUseProposalToasts([mockProposal]);
|
||||
expect(result.current.loading).toBe(true);
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(useToasts.getState().count).toBe(1);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('ProposalToastContent', () => {
|
||||
const genericTypes: [
|
||||
ProposalChange['__typename'],
|
||||
ProposalChange,
|
||||
ProposalState?
|
||||
][] = GenericToastProposals.map((ch) => [
|
||||
ch.__typename,
|
||||
ch,
|
||||
sample(PROPOSAL_STATES_TO_TOAST),
|
||||
]);
|
||||
it.each(genericTypes)(
|
||||
'renders generic toast content for %s',
|
||||
async (_, change, state) => {
|
||||
const proposalData = generateProposal('Things to change', state, change);
|
||||
const { container } = render(
|
||||
<ProposalToastContent proposal={proposalData} />
|
||||
);
|
||||
const title = container.querySelector(
|
||||
'[data-testid="proposal-toast-title"]'
|
||||
);
|
||||
const rationale = container.querySelector(
|
||||
'[data-testid="proposal-toast-rationale-title"]'
|
||||
);
|
||||
const expectedChangeName = change.__typename
|
||||
? ProposalChangeMapping[change.__typename]
|
||||
: '';
|
||||
const expectedState =
|
||||
ProposalStateMapping[proposalData.state].toLocaleLowerCase();
|
||||
expect(title).toHaveTextContent(
|
||||
`${expectedChangeName} proposal ${expectedState}`
|
||||
);
|
||||
expect(rationale).toHaveTextContent('Things to change');
|
||||
}
|
||||
);
|
||||
|
||||
it('renders specific content for UpdateNetworkParameter proposal', () => {
|
||||
const proposalData = generateProposal(
|
||||
'Things to change',
|
||||
ProposalState.STATE_OPEN,
|
||||
UPDATE_NETWORK_PARAMETER_CHANGE
|
||||
);
|
||||
const { container } = render(
|
||||
<ProposalToastContent proposal={proposalData} />
|
||||
);
|
||||
const title = container.querySelector(
|
||||
'[data-testid="proposal-toast-title"]'
|
||||
);
|
||||
const rationale = container.querySelector(
|
||||
'[data-testid="proposal-toast-rationale-title"]'
|
||||
);
|
||||
const param = container.querySelector(
|
||||
'[data-testid="proposal-toast-network-param"]'
|
||||
);
|
||||
expect(title).toHaveTextContent('Update network parameter proposal open');
|
||||
expect(rationale).toBe(null);
|
||||
expect(param).toHaveTextContent('Update abc.def to 123');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,136 @@
|
||||
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
|
||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import {
|
||||
ProposalChangeMapping,
|
||||
ProposalRejectionReasonMapping,
|
||||
ProposalStateMapping,
|
||||
} from '@vegaprotocol/types';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import type { Toast } from '@vegaprotocol/ui-toolkit';
|
||||
import { ToastHeading } from '@vegaprotocol/ui-toolkit';
|
||||
import { useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { ExternalLink, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
useOnProposalSubscription,
|
||||
type OnProposalFragmentFragment,
|
||||
} from './__generated__/Proposal';
|
||||
|
||||
export const PROPOSAL_STATES_TO_TOAST = [
|
||||
ProposalState.STATE_DECLINED,
|
||||
ProposalState.STATE_ENACTED,
|
||||
ProposalState.STATE_OPEN,
|
||||
ProposalState.STATE_PASSED,
|
||||
];
|
||||
const CLOSE_AFTER = 0;
|
||||
type Proposal = OnProposalFragmentFragment;
|
||||
|
||||
const ProposalDetails = ({ proposal }: { proposal: Proposal }) => {
|
||||
const change = proposal.terms.change;
|
||||
switch (change.__typename) {
|
||||
case 'UpdateNetworkParameter':
|
||||
return <UpdateNetworkParameterDetails proposal={proposal} />;
|
||||
default:
|
||||
// generic details: rationale title and rejection reason if rejected
|
||||
return (
|
||||
<>
|
||||
{proposal.rationale.title ? (
|
||||
<p data-testid="proposal-toast-rationale-title" className="italic">
|
||||
{proposal.rationale.title}
|
||||
</p>
|
||||
) : null}
|
||||
{proposal.state === ProposalState.STATE_REJECTED &&
|
||||
proposal.rejectionReason ? (
|
||||
<p data-testid="proposal-toast-rejection-reason">
|
||||
{t('Rejection reason:')}{' '}
|
||||
{ProposalRejectionReasonMapping[proposal.rejectionReason]}
|
||||
</p>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const UpdateNetworkParameterDetails = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: Proposal;
|
||||
}) => {
|
||||
const change = proposal.terms.change;
|
||||
if (change.__typename !== 'UpdateNetworkParameter') return null;
|
||||
return (
|
||||
<p data-testid="proposal-toast-network-param" className="italic">
|
||||
'{t('Update ')}
|
||||
<span className="break-all">{change.networkParameter.key}</span>
|
||||
{t(' to ')}
|
||||
<span>{change.networkParameter.value}</span>'
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProposalToastContent = ({ proposal }: { proposal: Proposal }) => {
|
||||
const tokenLink = useLinks(DApp.Governance);
|
||||
const change = proposal.terms.change;
|
||||
|
||||
// Generates toast's title,
|
||||
// e.g. Update market proposal enacted, New transfer proposal open, ...
|
||||
const title = t('%s proposal %s', [
|
||||
change.__typename ? ProposalChangeMapping[change.__typename] : 'Unknown',
|
||||
ProposalStateMapping[proposal.state].toLowerCase(),
|
||||
]);
|
||||
|
||||
const enactment = Date.parse(proposal.terms.enactmentDatetime);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ToastHeading data-testid="proposal-toast-title">{title}</ToastHeading>
|
||||
<ProposalDetails proposal={proposal} />
|
||||
{!isNaN(enactment) && (
|
||||
<p>
|
||||
{t('Enactment date:')} {getDateTimeFormat().format(enactment)}
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
<ExternalLink
|
||||
href={tokenLink(TOKEN_PROPOSAL).replace(':id', proposal?.id || '')}
|
||||
>
|
||||
{t('View proposal details')}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const useProposalToasts = () => {
|
||||
const { setToast, remove } = useToasts((store) => ({
|
||||
setToast: store.setToast,
|
||||
remove: store.remove,
|
||||
}));
|
||||
|
||||
const fromProposal = useCallback(
|
||||
(proposal: Proposal): Toast => {
|
||||
const id = `proposal-toast-${proposal.id}`;
|
||||
return {
|
||||
id,
|
||||
intent: Intent.Warning,
|
||||
content: <ProposalToastContent proposal={proposal} />,
|
||||
onClose: () => {
|
||||
remove(id);
|
||||
},
|
||||
closeAfter: CLOSE_AFTER,
|
||||
};
|
||||
},
|
||||
[remove]
|
||||
);
|
||||
|
||||
return useOnProposalSubscription({
|
||||
onData: ({ data }) => {
|
||||
const proposal = data.data?.proposals;
|
||||
if (!proposal || !proposal.terms.change.__typename) return;
|
||||
if (PROPOSAL_STATES_TO_TOAST.includes(proposal.state)) {
|
||||
setToast(fromProposal(proposal));
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,96 +0,0 @@
|
||||
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
|
||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { UpdateNetworkParameter } from '@vegaprotocol/types';
|
||||
import { ProposalStateMapping } from '@vegaprotocol/types';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import type { Toast } from '@vegaprotocol/ui-toolkit';
|
||||
import { ToastHeading } from '@vegaprotocol/ui-toolkit';
|
||||
import { useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { ExternalLink, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { useCallback } from 'react';
|
||||
import type { UpdateNetworkParameterProposalFragment } from './__generated__/Proposal';
|
||||
import { useOnUpdateNetworkParametersSubscription } from './__generated__/Proposal';
|
||||
|
||||
export const PROPOSAL_STATES_TO_TOAST = [
|
||||
ProposalState.STATE_DECLINED,
|
||||
ProposalState.STATE_ENACTED,
|
||||
ProposalState.STATE_OPEN,
|
||||
ProposalState.STATE_PASSED,
|
||||
];
|
||||
const CLOSE_AFTER = 0;
|
||||
type Proposal = UpdateNetworkParameterProposalFragment;
|
||||
|
||||
const UpdateNetworkParameterToastContent = ({
|
||||
proposal,
|
||||
}: {
|
||||
proposal: Proposal;
|
||||
}) => {
|
||||
const tokenLink = useLinks(DApp.Governance);
|
||||
const change = proposal.terms.change as UpdateNetworkParameter;
|
||||
const title = t('Network change proposal %s').replace(
|
||||
'%s',
|
||||
ProposalStateMapping[proposal.state].toLowerCase()
|
||||
);
|
||||
const enactment = Date.parse(proposal.terms.enactmentDatetime);
|
||||
return (
|
||||
<div>
|
||||
<ToastHeading>{title}</ToastHeading>
|
||||
<p className="italic">
|
||||
'{t('Update ')}
|
||||
<span className="break-all">{change.networkParameter.key}</span>
|
||||
{t(' to ')}
|
||||
<span>{change.networkParameter.value}</span>'
|
||||
</p>
|
||||
{!isNaN(enactment) && (
|
||||
<p>
|
||||
{t('Enactment date:')} {getDateTimeFormat().format(enactment)}
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
<ExternalLink
|
||||
href={tokenLink(TOKEN_PROPOSAL).replace(':id', proposal?.id || '')}
|
||||
>
|
||||
{t('View proposal details')}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const useUpdateNetworkParametersToasts = () => {
|
||||
const { setToast, remove } = useToasts((store) => ({
|
||||
setToast: store.setToast,
|
||||
remove: store.remove,
|
||||
}));
|
||||
|
||||
const fromProposal = useCallback(
|
||||
(proposal: Proposal): Toast => {
|
||||
const id = `update-network-param-proposal-${proposal.id}`;
|
||||
return {
|
||||
id: `update-network-param-proposal-${proposal.id}`,
|
||||
intent: Intent.Warning,
|
||||
content: <UpdateNetworkParameterToastContent proposal={proposal} />,
|
||||
onClose: () => {
|
||||
remove(id);
|
||||
},
|
||||
closeAfter: CLOSE_AFTER,
|
||||
};
|
||||
},
|
||||
[remove]
|
||||
);
|
||||
|
||||
return useOnUpdateNetworkParametersSubscription({
|
||||
onData: ({ data }) => {
|
||||
// note proposals is poorly named, it is actually a single proposal
|
||||
const proposal = data.data?.proposals;
|
||||
if (!proposal) return;
|
||||
if (proposal.terms.change.__typename !== 'UpdateNetworkParameter') return;
|
||||
|
||||
// if one of the following states show a toast
|
||||
if (PROPOSAL_STATES_TO_TOAST.includes(proposal.state)) {
|
||||
setToast(fromProposal(proposal));
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,168 +0,0 @@
|
||||
import merge from 'lodash/merge';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import type { ReactNode } from 'react';
|
||||
import {
|
||||
PROPOSAL_STATES_TO_TOAST,
|
||||
useUpdateNetworkParametersToasts,
|
||||
} from './use-update-network-paramaters-toasts';
|
||||
import type {
|
||||
UpdateNetworkParameterProposalFragment,
|
||||
OnUpdateNetworkParametersSubscription,
|
||||
} from './__generated__/Proposal';
|
||||
import { OnUpdateNetworkParametersDocument } from './__generated__/Proposal';
|
||||
import { useToasts } from '@vegaprotocol/ui-toolkit';
|
||||
import { waitFor, renderHook } from '@testing-library/react';
|
||||
|
||||
const render = (mocks?: MockedResponse[]) => {
|
||||
const wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider mocks={mocks}>{children}</MockedProvider>
|
||||
);
|
||||
return renderHook(() => useUpdateNetworkParametersToasts(), { wrapper });
|
||||
};
|
||||
|
||||
const generateUpdateNetworkParametersProposal = (
|
||||
key: string,
|
||||
value: string,
|
||||
state: ProposalState = ProposalState.STATE_OPEN
|
||||
): UpdateNetworkParameterProposalFragment => ({
|
||||
__typename: 'Proposal',
|
||||
id: Math.random().toString(),
|
||||
datetime: Math.random().toString(),
|
||||
state,
|
||||
terms: {
|
||||
__typename: 'ProposalTerms',
|
||||
enactmentDatetime: '2022-12-09T14:40:38Z',
|
||||
change: {
|
||||
__typename: 'UpdateNetworkParameter',
|
||||
networkParameter: {
|
||||
__typename: 'NetworkParameter',
|
||||
key,
|
||||
value,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const INITIAL = useToasts.getState();
|
||||
|
||||
const clear = () => {
|
||||
useToasts.setState(INITIAL);
|
||||
};
|
||||
|
||||
describe('useUpdateNetworkParametersToasts', () => {
|
||||
beforeEach(clear);
|
||||
afterAll(clear);
|
||||
|
||||
it.each(PROPOSAL_STATES_TO_TOAST)(
|
||||
'toasts for %s network param proposals',
|
||||
async (state) => {
|
||||
const mockOpenProposal: MockedResponse<OnUpdateNetworkParametersSubscription> =
|
||||
{
|
||||
request: {
|
||||
query: OnUpdateNetworkParametersDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
proposals: generateUpdateNetworkParametersProposal(
|
||||
'abc.def',
|
||||
'123.456',
|
||||
state
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
const { result } = render([mockOpenProposal]);
|
||||
expect(result.current.loading).toBe(true);
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(useToasts.getState().count).toBe(1);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const IGNORE_STATES = Object.keys(ProposalState).filter((state) => {
|
||||
return !PROPOSAL_STATES_TO_TOAST.includes(state as ProposalState);
|
||||
}) as ProposalState[];
|
||||
it.each(IGNORE_STATES)('does not toast for %s proposals', async (state) => {
|
||||
const mockFailedProposal: MockedResponse<OnUpdateNetworkParametersSubscription> =
|
||||
{
|
||||
request: {
|
||||
query: OnUpdateNetworkParametersDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
proposals: generateUpdateNetworkParametersProposal(
|
||||
'abc.def',
|
||||
'123.456',
|
||||
state
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
const { result } = render([mockFailedProposal]);
|
||||
expect(result.current.loading).toBe(true);
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(useToasts.getState().count).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not return toast for empty propsal', async () => {
|
||||
const error = console.error;
|
||||
console.error = () => {
|
||||
/* no op */
|
||||
};
|
||||
const mockEmptyProposal: MockedResponse<OnUpdateNetworkParametersSubscription> =
|
||||
{
|
||||
request: {
|
||||
query: OnUpdateNetworkParametersDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
proposals:
|
||||
undefined as unknown as UpdateNetworkParameterProposalFragment,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { result } = render([mockEmptyProposal]);
|
||||
expect(result.current.loading).toBe(true);
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(useToasts.getState().count).toBe(0);
|
||||
});
|
||||
console.error = error;
|
||||
});
|
||||
|
||||
it('does not return toast for wrong proposal type', async () => {
|
||||
const wrongProposalType = merge(
|
||||
generateUpdateNetworkParametersProposal('a', 'b'),
|
||||
{
|
||||
terms: {
|
||||
change: {
|
||||
__typename: 'NewMarket',
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
const mockWrongProposalType: MockedResponse<OnUpdateNetworkParametersSubscription> =
|
||||
{
|
||||
request: {
|
||||
query: OnUpdateNetworkParametersDocument,
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
proposals: wrongProposalType,
|
||||
},
|
||||
},
|
||||
};
|
||||
const { result } = render([mockWrongProposalType]);
|
||||
expect(result.current.loading).toBe(true);
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(useToasts.getState().count).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,10 +4,13 @@ import { TradesTable } from './trades-table';
|
||||
import { useDealTicketFormValues } from '@vegaprotocol/deal-ticket';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import type { useDataGridEvents } from '@vegaprotocol/datagrid';
|
||||
import { useCallback, useState } from 'react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import { TradingButton as Button } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
interface TradesContainerProps {
|
||||
marketId: string;
|
||||
gridProps?: ReturnType<typeof useDataGridEvents>;
|
||||
gridProps: ReturnType<typeof useDataGridEvents>;
|
||||
}
|
||||
|
||||
export const TradesManager = ({
|
||||
@@ -16,19 +19,60 @@ export const TradesManager = ({
|
||||
}: TradesContainerProps) => {
|
||||
const update = useDealTicketFormValues((state) => state.updateAll);
|
||||
|
||||
const { data, error } = useDataProvider({
|
||||
const { data, error, load, pageInfo } = useDataProvider({
|
||||
dataProvider: tradesWithMarketProvider,
|
||||
variables: { marketId },
|
||||
});
|
||||
const [hasDisplayedRow, setHasDisplayedRow] = useState<boolean | undefined>(
|
||||
undefined
|
||||
);
|
||||
const { onFilterChanged, ...props } = gridProps || {};
|
||||
const onRowDataUpdated = useCallback(
|
||||
({ api }: { api: AgGridReact['api'] }) => {
|
||||
setHasDisplayedRow(!!api.getDisplayedRowCount());
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<TradesTable
|
||||
rowData={data}
|
||||
onClick={(price?: string) => {
|
||||
update(marketId, { price });
|
||||
}}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No trades')}
|
||||
{...gridProps}
|
||||
/>
|
||||
<div className="flex flex-col h-full">
|
||||
<TradesTable
|
||||
rowData={data}
|
||||
onClick={(price?: string) => {
|
||||
update(marketId, { price });
|
||||
}}
|
||||
onFilterChanged={(event) => {
|
||||
onRowDataUpdated(event);
|
||||
onFilterChanged(event);
|
||||
}}
|
||||
onRowDataUpdated={onRowDataUpdated}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No trades')}
|
||||
{...props}
|
||||
/>
|
||||
<div className="flex justify-between border-t border-default p-1 items-center">
|
||||
<div className="text-xs">
|
||||
{t(
|
||||
'Depending on data node retention you may not be able see the "full" history'
|
||||
)}
|
||||
</div>
|
||||
<div className="flex text-xs items-center">
|
||||
{data?.length && !pageInfo?.hasNextPage
|
||||
? t('all %s items loaded', [data.length.toString()])
|
||||
: t('%s items loaded', [
|
||||
data?.length ? data.length.toString() : ' ',
|
||||
])}
|
||||
{pageInfo?.hasNextPage ? (
|
||||
<Button size="extra-small" className="ml-1" onClick={() => load()}>
|
||||
{t('Load more')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{data?.length && hasDisplayedRow === false ? (
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-xs">
|
||||
{t('No trades matching selected filters')}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Generated
+36
-1
@@ -3387,6 +3387,8 @@ export type Party = {
|
||||
transfersConnection?: Maybe<TransferConnection>;
|
||||
/** The current reward vesting summary of the party for the last epoch */
|
||||
vestingBalancesSummary: PartyVestingBalancesSummary;
|
||||
/** The current statistics about a party's vesting rewards for the last epoch */
|
||||
vestingStats?: Maybe<PartyVestingStats>;
|
||||
/** All votes on proposals in the Vega network by the given party */
|
||||
votesConnection?: Maybe<ProposalVoteConnection>;
|
||||
/** The list of all withdrawals initiated by the party */
|
||||
@@ -3502,6 +3504,7 @@ export type PartytradesConnectionArgs = {
|
||||
/** Represents a party on Vega, could be an ethereum wallet address in the future */
|
||||
export type PartytransfersConnectionArgs = {
|
||||
direction?: InputMaybe<TransferDirection>;
|
||||
isReward?: InputMaybe<Scalars['Boolean']>;
|
||||
pagination?: InputMaybe<Pagination>;
|
||||
};
|
||||
|
||||
@@ -3616,6 +3619,15 @@ export type PartyVestingBalancesSummary = {
|
||||
vestingBalances?: Maybe<Array<PartyVestingBalance>>;
|
||||
};
|
||||
|
||||
/** Statistics about a party's vesting rewards */
|
||||
export type PartyVestingStats = {
|
||||
__typename?: 'PartyVestingStats';
|
||||
/** Epoch for which the statistics are valid */
|
||||
epochSeq: Scalars['Int'];
|
||||
/** The reward bonus multiplier */
|
||||
rewardBonusMultiplier: Scalars['String'];
|
||||
};
|
||||
|
||||
/** Create an order linked to an index rather than a price */
|
||||
export type PeggedOrder = {
|
||||
__typename?: 'PeggedOrder';
|
||||
@@ -4905,6 +4917,7 @@ export type QuerytransferArgs = {
|
||||
/** Queries allow a caller to read data and filter data via GraphQL. */
|
||||
export type QuerytransfersConnectionArgs = {
|
||||
direction?: InputMaybe<TransferDirection>;
|
||||
isReward?: InputMaybe<Scalars['Boolean']>;
|
||||
pagination?: InputMaybe<Pagination>;
|
||||
partyId?: InputMaybe<Scalars['ID']>;
|
||||
};
|
||||
@@ -5081,6 +5094,8 @@ export type ReferralSetStats = {
|
||||
rewardsFactorMultiplier: Scalars['String'];
|
||||
/** The multiplier applied to the referral reward factor when calculating referral rewards due to the referrer. */
|
||||
rewardsMultiplier: Scalars['String'];
|
||||
/** Indicates if the referral set was eligible to be part of the referral program. */
|
||||
wasEligible: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
/** Connection type for retrieving cursor-based paginated referral set statistics information */
|
||||
@@ -6102,11 +6117,31 @@ export enum TransferDirection {
|
||||
export type TransferEdge = {
|
||||
__typename?: 'TransferEdge';
|
||||
cursor: Scalars['String'];
|
||||
node: Transfer;
|
||||
node: TransferNode;
|
||||
};
|
||||
|
||||
/** A transfer fee record */
|
||||
export type TransferFee = {
|
||||
__typename?: 'TransferFee';
|
||||
/** The fee amount */
|
||||
amount: Scalars['String'];
|
||||
/** The epoch when this fee was paid */
|
||||
epoch: Scalars['Int'];
|
||||
/** Transfer ID of the transfer for which the fee was paid */
|
||||
transferId: Scalars['ID'];
|
||||
};
|
||||
|
||||
export type TransferKind = OneOffGovernanceTransfer | OneOffTransfer | RecurringGovernanceTransfer | RecurringTransfer;
|
||||
|
||||
/** A transfer record with the fee payments associated with the transfer */
|
||||
export type TransferNode = {
|
||||
__typename?: 'TransferNode';
|
||||
/** The list of fee payments made */
|
||||
fees?: Maybe<Array<Maybe<TransferFee>>>;
|
||||
/** The transfer record */
|
||||
transfer: Transfer;
|
||||
};
|
||||
|
||||
export type TransferResponse = {
|
||||
__typename?: 'TransferResponse';
|
||||
/** The balances of accounts involved in the transfer */
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
GovernanceTransferKind,
|
||||
GovernanceTransferType,
|
||||
PeggedReference,
|
||||
ProposalChange,
|
||||
} from './__generated__/types';
|
||||
import type { AccountType } from './__generated__/types';
|
||||
import type {
|
||||
@@ -299,6 +300,29 @@ export const OrderTypeMapping: {
|
||||
TYPE_NETWORK: 'Network',
|
||||
};
|
||||
|
||||
/**
|
||||
* Proposal change type mapping
|
||||
*/
|
||||
export const ProposalChangeMapping: Record<
|
||||
NonNullable<ProposalChange['__typename']>,
|
||||
string
|
||||
> = {
|
||||
NewMarket: 'New market',
|
||||
UpdateMarket: 'Update market',
|
||||
UpdateNetworkParameter: 'Update network parameter',
|
||||
NewAsset: 'New asset',
|
||||
UpdateAsset: 'Update asset',
|
||||
/* cspell:disable-next-line */
|
||||
NewFreeform: 'New free-form',
|
||||
NewTransfer: 'New transfer',
|
||||
CancelTransfer: 'Cancel transfer',
|
||||
UpdateMarketState: 'Update market state',
|
||||
NewSpotMarket: 'New spot market',
|
||||
UpdateSpotMarket: 'Update spot market',
|
||||
UpdateVolumeDiscountProgram: 'Update volume discount program',
|
||||
UpdateReferralProgram: 'Update referral program',
|
||||
};
|
||||
|
||||
/**
|
||||
* Reason for the proposal being rejected by the core node
|
||||
*/
|
||||
|
||||
@@ -1,77 +1,68 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { Sparkline } from './sparkline';
|
||||
import type { SparklineProps } from './sparkline';
|
||||
|
||||
const props = {
|
||||
data: [
|
||||
1, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 6, 7, 8, 9,
|
||||
10, 11, 12,
|
||||
],
|
||||
muted: true,
|
||||
};
|
||||
describe('Sparkline', () => {
|
||||
let props: SparklineProps = {
|
||||
data: [],
|
||||
};
|
||||
|
||||
it('Renders an svg with a single path', () => {
|
||||
render(<Sparkline {...props} />);
|
||||
expect(screen.getByTestId('sparkline-svg')).toBeInTheDocument();
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toBeInTheDocument();
|
||||
expect(path).toHaveAttribute('d', expect.any(String));
|
||||
expect(path).toHaveAttribute('stroke', expect.any(String));
|
||||
expect(path).toHaveAttribute('stroke-width', '1');
|
||||
expect(path).toHaveAttribute('fill', 'transparent');
|
||||
});
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
data: [
|
||||
1, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 6, 7, 8,
|
||||
9, 10, 11, 12,
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
it('Requires a data prop but width and height are optional', () => {
|
||||
render(<Sparkline {...props} />);
|
||||
const svg = screen.getByTestId('sparkline-svg');
|
||||
expect(svg).toHaveAttribute('width', '60');
|
||||
expect(svg).toHaveAttribute('height', '15');
|
||||
});
|
||||
it('Renders an svg with a single path', () => {
|
||||
render(<Sparkline {...props} />);
|
||||
expect(screen.getByTestId('sparkline-svg')).toBeInTheDocument();
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toBeInTheDocument();
|
||||
expect(path).toHaveAttribute('d', expect.any(String));
|
||||
expect(path).toHaveAttribute('stroke-width', '1');
|
||||
});
|
||||
|
||||
it('Renders a red line if the last value is less than the first', () => {
|
||||
props.data[0] = 10;
|
||||
props.data[props.data.length - 1] = 5;
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toHaveClass(
|
||||
'[vector-effect:non-scaling-stroke] stroke-market-red dark:stroke-market-red'
|
||||
);
|
||||
});
|
||||
it('Requires a data prop but width and height are optional', () => {
|
||||
render(<Sparkline {...props} />);
|
||||
const svg = screen.getByTestId('sparkline-svg');
|
||||
expect(svg).toHaveAttribute('width', '60');
|
||||
expect(svg).toHaveAttribute('height', '15');
|
||||
});
|
||||
|
||||
it('Renders a green line if the last value is greater than the first', () => {
|
||||
props.data[0] = 5;
|
||||
props.data[props.data.length - 1] = 10;
|
||||
props.muted = true;
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toHaveClass(
|
||||
'[vector-effect:non-scaling-stroke] stroke-market-green-600 dark:stroke-market-green'
|
||||
);
|
||||
});
|
||||
it('Renders a red line if the last value is less than the first', () => {
|
||||
props.data[0] = 10;
|
||||
props.data[props.data.length - 1] = 5;
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toHaveClass(
|
||||
'[vector-effect:non-scaling-stroke] stroke-market-red dark:stroke-market-red'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders a white line if the first and last values are equal', () => {
|
||||
props.data[0] = 5;
|
||||
props.data[props.data.length - 1] = 5;
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toHaveClass(
|
||||
'[vector-effect:non-scaling-stroke] stroke-black/40 dark:stroke-white/40'
|
||||
);
|
||||
});
|
||||
it('Renders a green line if the last value is greater than the first', () => {
|
||||
props.data[0] = 5;
|
||||
props.data[props.data.length - 1] = 10;
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toHaveClass(
|
||||
'[vector-effect:non-scaling-stroke] stroke-market-green-600 dark:stroke-market-green'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders a gray line if there are not 24 values', () => {
|
||||
props.data = [
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
|
||||
22, 23,
|
||||
];
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.queryAllByTestId('sparkline-path');
|
||||
expect(paths).toHaveLength(2);
|
||||
expect(paths[0]).toHaveClass(
|
||||
'[vector-effect:non-scaling-stroke] stroke-black/40 dark:stroke-white/40'
|
||||
);
|
||||
it('Renders a white line if the first and last values are equal', () => {
|
||||
props.data[0] = 5;
|
||||
props.data[props.data.length - 1] = 5;
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toHaveClass(
|
||||
'[vector-effect:non-scaling-stroke] stroke-black/40 dark:stroke-white/40'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,39 +8,49 @@ export default {
|
||||
|
||||
const Template: Story = (args) => <Sparkline data={args['data']} {...args} />;
|
||||
|
||||
export const Grey = Template.bind({});
|
||||
Grey.args = {
|
||||
data: [
|
||||
1, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 6, 7, 8,
|
||||
],
|
||||
width: 60,
|
||||
height: 30,
|
||||
points: 25,
|
||||
className: 'w-[113px]',
|
||||
};
|
||||
|
||||
export const Equal = Template.bind({});
|
||||
Equal.args = {
|
||||
data: [
|
||||
12, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 6, 7, 8, 9,
|
||||
10, 11, 12,
|
||||
],
|
||||
width: 60,
|
||||
width: 110,
|
||||
height: 30,
|
||||
points: 25,
|
||||
className: 'w-[113px]',
|
||||
};
|
||||
|
||||
export const Increase = Template.bind({});
|
||||
Increase.args = {
|
||||
data: [
|
||||
1, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 6, 7, 8, 9,
|
||||
10, 11, 12,
|
||||
22,
|
||||
22,
|
||||
22, // extra values should be ignored, this should still render an increase
|
||||
0,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
],
|
||||
width: 60,
|
||||
width: 110,
|
||||
height: 30,
|
||||
points: 25,
|
||||
className: 'w-[113px]',
|
||||
};
|
||||
|
||||
export const Decrease = Template.bind({});
|
||||
@@ -49,8 +59,27 @@ Decrease.args = {
|
||||
12, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 6, 7, 8, 9,
|
||||
10, 11, 1,
|
||||
],
|
||||
width: 60,
|
||||
width: 110,
|
||||
height: 30,
|
||||
};
|
||||
|
||||
export const LessThan24HoursIncrease = Template.bind({});
|
||||
LessThan24HoursIncrease.args = {
|
||||
data: [20, 21, 22, 25, 24, 24, 22, 19, 20, 22, 23, 27],
|
||||
width: 110,
|
||||
height: 30,
|
||||
};
|
||||
|
||||
export const LessThan24HoursDecrease = Template.bind({});
|
||||
LessThan24HoursDecrease.args = {
|
||||
data: [20, 21, 22, 23, 24, 6, 7, 9, 11, 13, 11, 9],
|
||||
width: 110,
|
||||
height: 30,
|
||||
};
|
||||
|
||||
export const NoData = Template.bind({});
|
||||
NoData.args = {
|
||||
data: [],
|
||||
width: 110,
|
||||
height: 30,
|
||||
points: 25,
|
||||
className: 'w-[113px]',
|
||||
};
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
import { extent } from 'd3-array';
|
||||
import { scaleLinear } from 'd3-scale';
|
||||
import { line } from 'd3-shape';
|
||||
import { area, line } from 'd3-shape';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import React from 'react';
|
||||
|
||||
function colorByChange(a: number, b: number) {
|
||||
return a === b
|
||||
? 'stroke-black/40 dark:stroke-white/40'
|
||||
: a < b
|
||||
? 'stroke-market-green-600 dark:stroke-market-green'
|
||||
: 'stroke-market-red dark:stroke-market-red';
|
||||
if (a < b) {
|
||||
return 'stroke-market-green-600 dark:stroke-market-green';
|
||||
} else if (a > b) {
|
||||
return 'stroke-market-red dark:stroke-market-red';
|
||||
}
|
||||
return 'stroke-black/40 dark:stroke-white/40';
|
||||
}
|
||||
|
||||
function shadedColor(a: number, b: number) {
|
||||
if (a < b) {
|
||||
return 'fill-market-green-600';
|
||||
} else if (a > b) {
|
||||
return 'fill-market-red';
|
||||
}
|
||||
return 'fill-black dark:fill-white';
|
||||
}
|
||||
|
||||
export interface SparklineProps {
|
||||
@@ -18,75 +28,60 @@ export interface SparklineProps {
|
||||
height?: number;
|
||||
points?: number;
|
||||
className?: string;
|
||||
muted?: boolean;
|
||||
}
|
||||
|
||||
export const SparklineView = ({
|
||||
data,
|
||||
width = 60,
|
||||
height = 15,
|
||||
points = 25,
|
||||
muted = false,
|
||||
points = 24,
|
||||
className,
|
||||
}: SparklineProps) => {
|
||||
// How many points are missing. If market is 12 hours old the 25 - 12
|
||||
const preMarketLength = points - data.length;
|
||||
|
||||
// Create two dimensional array for sparkline points [x, y]
|
||||
const marketData: [number, number][] = data.map((d, i) => [
|
||||
preMarketLength + i,
|
||||
d,
|
||||
]);
|
||||
// Empty two dimensional array for gray, 'no data' line
|
||||
let preMarketData: [number, number][] = [];
|
||||
|
||||
// Get the extent for our y value
|
||||
const [min, max] = extent(marketData, (d) => d[1]);
|
||||
const [min, max] = extent(data, (d) => d);
|
||||
|
||||
if (typeof min !== 'number' || typeof max !== 'number') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create a second set of data to render a gray line for any
|
||||
// missing points if the market is less than 24 hours old
|
||||
if (marketData.length < points) {
|
||||
// Populate preMarketData with the average of our extents
|
||||
// so that the line renders centered vertically
|
||||
const fillValue = (min + max) / 2;
|
||||
preMarketData = new Array(points - marketData.length)
|
||||
.fill(fillValue)
|
||||
.map((d: number, i) => [i, d] as [number, number]);
|
||||
const midValue = (min + max) / 2;
|
||||
|
||||
// Add the first point of or market data so that the two
|
||||
// lines join up
|
||||
preMarketData.push(marketData[0] as [number, number]);
|
||||
}
|
||||
// Market may be less than 24hr old so padd the data array
|
||||
// with values that is the mid value (avg of min and max).
|
||||
// This will rendera horizontal line until the real data shifts the line
|
||||
const padCount = data.length < points ? points - data.length : 0;
|
||||
const padArr = new Array(padCount).fill(midValue);
|
||||
const trimmedData = data.slice(-points);
|
||||
|
||||
const xScale = scaleLinear().domain([0, points]).range([0, 100]);
|
||||
const yScale = scaleLinear().domain([min, max]).range([100, 0]);
|
||||
// Get the last 24 values if data has more than needed
|
||||
const lineData: [number, number][] = [...padArr, ...trimmedData].map(
|
||||
(d, i) => {
|
||||
return [i, d];
|
||||
}
|
||||
);
|
||||
|
||||
const xScale = scaleLinear().domain([0, points]).range([0, width]);
|
||||
const yScale = scaleLinear().domain([min, max]).range([height, 0]);
|
||||
|
||||
const lineSeries = line()
|
||||
.x((d) => xScale(d[0]))
|
||||
.y((d) => yScale(d[1]));
|
||||
|
||||
// Get the color of the marketData line
|
||||
const [firstVal, lastVal] = [data[0], data[data.length - 1]];
|
||||
const strokeClassName = muted
|
||||
? data.length >= 24
|
||||
? colorByChange(firstVal, lastVal)
|
||||
: 'stroke-black/40 dark:stroke-white/40'
|
||||
: colorByChange(firstVal, lastVal);
|
||||
const areaSeries = area()
|
||||
.x((d) => xScale(d[0]))
|
||||
.y0(height)
|
||||
.y1((d) => yScale(d[1]));
|
||||
|
||||
const firstVal = trimmedData[0];
|
||||
const lastVal = trimmedData[trimmedData.length - 1];
|
||||
|
||||
// Get the color of the marketData line depending on market movement
|
||||
const strokeClassName = colorByChange(firstVal, lastVal);
|
||||
const areaClassName = shadedColor(firstVal, lastVal);
|
||||
|
||||
// Create paths
|
||||
const preMarketCreationPath = lineSeries(preMarketData);
|
||||
const mainPath = lineSeries(marketData);
|
||||
const pathProps = {
|
||||
'data-testid': 'sparkline-path',
|
||||
className: `[vector-effect:non-scaling-stroke] ${strokeClassName}`,
|
||||
stroke: 'strokeCurrent',
|
||||
strokeWidth: 1,
|
||||
fill: 'transparent',
|
||||
};
|
||||
const linePath = lineSeries(lineData);
|
||||
const areaPath = areaSeries(lineData);
|
||||
|
||||
return (
|
||||
<svg
|
||||
@@ -94,13 +89,25 @@ export const SparklineView = ({
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 100 100"
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
{preMarketCreationPath && (
|
||||
<path {...pathProps} d={preMarketCreationPath} />
|
||||
{linePath && (
|
||||
<path
|
||||
d={linePath}
|
||||
data-testid="sparkline-path"
|
||||
className={`[vector-effect:non-scaling-stroke] fill-transparent ${strokeClassName}`}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
)}
|
||||
{areaPath && (
|
||||
<path
|
||||
className={areaClassName}
|
||||
fillOpacity={0.2}
|
||||
stroke="none"
|
||||
d={areaPath}
|
||||
/>
|
||||
)}
|
||||
{mainPath && <path {...pathProps} d={mainPath} />}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -179,3 +179,33 @@ export const toNumberParts = (
|
||||
export const isNumeric = (
|
||||
value?: string | number | BigNumber | bigint | null
|
||||
): value is NonNullable<number | string> => /^-?\d*\.?\d+$/.test(String(value));
|
||||
|
||||
/**
|
||||
* Format a number greater than 1 million with m for million, b for billion
|
||||
* and t for trillion
|
||||
*/
|
||||
export const formatNumberRounded = (num: BigNumber) => {
|
||||
let value = '';
|
||||
|
||||
const format = (divisor: string) => {
|
||||
const result = num.dividedBy(divisor);
|
||||
return result.isInteger() ? result.toString() : result.toFixed(1);
|
||||
};
|
||||
|
||||
if (num.isGreaterThan(new BigNumber('1e14'))) {
|
||||
value = '>100t';
|
||||
} else if (num.isGreaterThanOrEqualTo(new BigNumber('1e12'))) {
|
||||
// Trillion
|
||||
value = `${format('1e12')}t`;
|
||||
} else if (num.isGreaterThanOrEqualTo(new BigNumber('1e9'))) {
|
||||
// Billion
|
||||
value = `${format('1e9')}b`;
|
||||
} else if (num.isGreaterThanOrEqualTo(new BigNumber('1e6'))) {
|
||||
// Million
|
||||
value = `${format('1e6')}m`;
|
||||
} else {
|
||||
value = formatNumber(num);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user