Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a3657a9b9 | ||
|
|
a59f7dfd29 | ||
|
|
61471228aa | ||
|
|
70d748fb15 | ||
|
|
2d926c0ce0 | ||
|
|
f57d6a7c7b | ||
|
|
15f905046f | ||
|
|
4f7918f64e | ||
|
|
52ab0562b0 | ||
|
|
4e2b0d1b1d | ||
|
|
eac26c1966 | ||
|
|
e5d4d2b0b8 | ||
|
|
d4e801cfc6 | ||
|
|
d9dc43b359 | ||
|
|
06a6fe6d67 |
@@ -52,3 +52,4 @@ cypress.env.json
|
||||
/apps/**/cypress/reports/
|
||||
/apps/**/cypress/downloads/
|
||||
/apps/**/fixtures/wallet/node**
|
||||
.nx/
|
||||
|
||||
+20
-1
@@ -8,7 +8,7 @@ import {
|
||||
networkParamsQueryMock,
|
||||
nextWeek,
|
||||
} from '../../test-helpers/mocks';
|
||||
import { VoteBreakdown } from './vote-breakdown';
|
||||
import { CompactVotes, VoteBreakdown } from './vote-breakdown';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import type { MockedResponse } from '@apollo/client/testing';
|
||||
import {
|
||||
@@ -346,3 +346,22 @@ describe('VoteBreakdown', () => {
|
||||
expect(style.width).toBe(`${expectedProgress}%`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CompactVotes', () => {
|
||||
it.each([
|
||||
[0, '0'],
|
||||
[1, '1'],
|
||||
[12, '12'],
|
||||
[123, '123'],
|
||||
[1234, '1.2K'],
|
||||
[12345, '12.3K'],
|
||||
[123456, '123.5K'],
|
||||
[1234567, '1.2M'],
|
||||
[12345678, '12.3M'],
|
||||
[123456789, '123.5M'],
|
||||
[1234567890, '1.2B'],
|
||||
])('compacts %s to %s', (input, output) => {
|
||||
const { getByTestId } = render(<CompactVotes number={BigNumber(input)} />);
|
||||
expect(getByTestId('compact-number').textContent).toBe(output);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,11 +3,21 @@ import BigNumber from 'bignumber.js';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useVoteInformation } from '../../hooks';
|
||||
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||
import { formatNumber, toBigNum } from '@vegaprotocol/utils';
|
||||
import { formatNumber } from '@vegaprotocol/utils';
|
||||
import { ProposalState } from '@vegaprotocol/types';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||
import { CompactNumber } from '@vegaprotocol/react-helpers';
|
||||
|
||||
export const CompactVotes = ({ number }: { number: BigNumber }) => (
|
||||
<CompactNumber
|
||||
number={number}
|
||||
decimals={number.isGreaterThan(1000) ? 1 : 0}
|
||||
compactAbove={1000}
|
||||
compactDisplay="short"
|
||||
/>
|
||||
);
|
||||
|
||||
interface VoteBreakdownProps {
|
||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||
@@ -198,10 +208,7 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
|
||||
)}
|
||||
>
|
||||
<button>
|
||||
{yesEquityLikeShareWeight
|
||||
.dividedBy(toBigNum(10 ** 6, 0))
|
||||
.toFixed(1)}
|
||||
M
|
||||
<CompactVotes number={yesEquityLikeShareWeight} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<span>
|
||||
@@ -226,10 +233,7 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
|
||||
)}
|
||||
>
|
||||
<button>
|
||||
{noEquityLikeShareWeight
|
||||
.dividedBy(toBigNum(10 ** 6, 0))
|
||||
.toFixed(1)}
|
||||
M
|
||||
<CompactVotes number={noEquityLikeShareWeight} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<span>
|
||||
@@ -279,10 +283,7 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
|
||||
)}
|
||||
>
|
||||
<button>
|
||||
{totalEquityLikeShareWeight
|
||||
.dividedBy(toBigNum(10 ** 6, 0))
|
||||
.toFixed(1)}
|
||||
M
|
||||
<CompactVotes number={totalEquityLikeShareWeight} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<span>
|
||||
@@ -321,7 +322,7 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
|
||||
<span>{t('tokenVotesFor')}:</span>
|
||||
<Tooltip description={formatNumber(yesTokens, defaultDP)}>
|
||||
<button data-testid="num-votes-for">
|
||||
{yesTokens.dividedBy(toBigNum(10 ** 6, 0)).toFixed(1)}M
|
||||
<CompactVotes number={yesTokens} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<span>
|
||||
@@ -341,7 +342,7 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
|
||||
<span>{t('tokenVotesAgainst')}:</span>
|
||||
<Tooltip description={formatNumber(noTokens, defaultDP)}>
|
||||
<button data-testid="num-votes-against">
|
||||
{noTokens.dividedBy(toBigNum(10 ** 6, 0)).toFixed(1)}M
|
||||
<CompactVotes number={noTokens} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<span>
|
||||
@@ -384,7 +385,7 @@ export const VoteBreakdown = ({ proposal }: VoteBreakdownProps) => {
|
||||
<span>{t('totalTokensVoted')}:</span>
|
||||
<Tooltip description={formatNumber(totalTokensVoted, defaultDP)}>
|
||||
<button data-testid="total-voted">
|
||||
{totalTokensVoted.dividedBy(toBigNum(10 ** 6, 0)).toFixed(1)}M
|
||||
<CompactVotes number={totalTokensVoted} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<span data-testid="total-voted-percentage">
|
||||
|
||||
@@ -2,8 +2,6 @@ import { aliasGQLQuery } from '@vegaprotocol/cypress';
|
||||
import { proposalListQuery, marketUpdateProposal } from '@vegaprotocol/mock';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
const marketSummaryBlock = 'header-summary';
|
||||
|
||||
describe('Market proposal notification', { tags: '@smoke' }, () => {
|
||||
before(() => {
|
||||
cy.setVegaWallet();
|
||||
@@ -25,12 +23,13 @@ describe('Market proposal notification', { tags: '@smoke' }, () => {
|
||||
});
|
||||
cy.mockSubscription();
|
||||
cy.visit('/#/markets/market-0');
|
||||
cy.getByTestId('Info').click();
|
||||
cy.wait('@MarketData');
|
||||
cy.getByTestId(marketSummaryBlock).should('be.visible');
|
||||
cy.getByTestId('sidebar-content').should('be.visible');
|
||||
});
|
||||
|
||||
it('should display market proposal notification if proposal found', () => {
|
||||
cy.getByTestId(marketSummaryBlock).within(() => {
|
||||
cy.getByTestId('sidebar-content').within(() => {
|
||||
cy.getByTestId('market-proposal-notification').should(
|
||||
'contain.text',
|
||||
'Changes have been proposed for this market'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||
import { DocsLinks, useEnvironment } from '@vegaprotocol/environment';
|
||||
import { ButtonLink, ExternalLink, Link } from '@vegaprotocol/ui-toolkit';
|
||||
import { MarketProposalNotification } from '@vegaprotocol/proposals';
|
||||
import type { Market } from '@vegaprotocol/markets';
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
@@ -144,7 +143,6 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
|
||||
/>
|
||||
</HeaderStat>
|
||||
)}
|
||||
<MarketProposalNotification marketId={market.id} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -75,22 +75,20 @@ export const useReferralProgram = () => {
|
||||
|
||||
const benefitTiers = sortBy(data.currentReferralProgram.benefitTiers, (t) =>
|
||||
Number(t.referralRewardFactor)
|
||||
)
|
||||
.reverse()
|
||||
.map((t, i) => {
|
||||
return {
|
||||
tier: i + 1,
|
||||
rewardFactor: Number(t.referralRewardFactor),
|
||||
commission: Number(t.referralRewardFactor) * 100 + '%',
|
||||
discountFactor: Number(t.referralDiscountFactor),
|
||||
discount: Number(t.referralDiscountFactor) * 100 + '%',
|
||||
minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
|
||||
volume: getNumberFormat(0).format(
|
||||
Number(t.minimumRunningNotionalTakerVolume)
|
||||
),
|
||||
epochs: Number(t.minimumEpochs),
|
||||
};
|
||||
});
|
||||
).map((t, i) => {
|
||||
return {
|
||||
tier: i + 1, // sorted in asc order, hence first is the lowest tier
|
||||
rewardFactor: Number(t.referralRewardFactor),
|
||||
commission: Number(t.referralRewardFactor) * 100 + '%',
|
||||
discountFactor: Number(t.referralDiscountFactor),
|
||||
discount: Number(t.referralDiscountFactor) * 100 + '%',
|
||||
minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
|
||||
volume: getNumberFormat(0).format(
|
||||
Number(t.minimumRunningNotionalTakerVolume)
|
||||
),
|
||||
epochs: Number(t.minimumEpochs),
|
||||
};
|
||||
});
|
||||
|
||||
const stakingTiers = sortBy(
|
||||
data.currentReferralProgram.stakingTiers,
|
||||
|
||||
@@ -29,8 +29,8 @@ 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';
|
||||
import { DocsLinks } from '@vegaprotocol/environment';
|
||||
import minBy from 'lodash/minBy';
|
||||
|
||||
export const ReferralStatistics = () => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
@@ -123,8 +123,8 @@ export const Statistics = ({
|
||||
t.discountFactor === discountFactorValue
|
||||
);
|
||||
const nextBenefitTierValue = currentBenefitTierValue
|
||||
? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier - 1)
|
||||
: maxBy(benefitTiers, (bt) => bt.tier); // max tier number is lowest tier
|
||||
? benefitTiers.find((t) => t.tier === currentBenefitTierValue.tier + 1)
|
||||
: minBy(benefitTiers, (bt) => bt.tier); // min tier number is lowest tier
|
||||
const epochsValue =
|
||||
!isNaN(currentEpoch) && refereeInfo?.atEpoch
|
||||
? currentEpoch - refereeInfo?.atEpoch
|
||||
|
||||
@@ -199,13 +199,13 @@ const TiersTable = ({
|
||||
...d,
|
||||
className: classNames({
|
||||
'from-vega-pink-400 dark:from-vega-pink-600 to-20% bg-highlight':
|
||||
d.tier === 1,
|
||||
d.tier >= 3,
|
||||
'from-vega-purple-400 dark:from-vega-purple-600 to-20% bg-highlight':
|
||||
d.tier === 2,
|
||||
'from-vega-blue-400 dark:from-vega-blue-600 to-20% bg-highlight':
|
||||
d.tier === 3,
|
||||
d.tier === 1,
|
||||
'from-vega-orange-400 dark:from-vega-orange-600 to-20% bg-highlight':
|
||||
d.tier > 3,
|
||||
d.tier == 0,
|
||||
}),
|
||||
}))}
|
||||
/>
|
||||
|
||||
@@ -16,18 +16,11 @@ query DiscountPrograms {
|
||||
}
|
||||
}
|
||||
|
||||
query Fees(
|
||||
$partyId: ID!
|
||||
$volumeDiscountEpochs: Int!
|
||||
$referralDiscountEpochs: Int!
|
||||
) {
|
||||
query Fees($partyId: ID!) {
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
volumeDiscountStats(
|
||||
partyId: $partyId
|
||||
pagination: { last: $volumeDiscountEpochs }
|
||||
) {
|
||||
volumeDiscountStats(partyId: $partyId, pagination: { last: 1 }) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
@@ -59,10 +52,7 @@ query Fees(
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetStats(
|
||||
partyId: $partyId
|
||||
pagination: { last: $referralDiscountEpochs }
|
||||
) {
|
||||
referralSetStats(partyId: $partyId, pagination: { last: 1 }) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
|
||||
+3
-10
@@ -10,8 +10,6 @@ export type DiscountProgramsQuery = { __typename?: 'Query', currentReferralProgr
|
||||
|
||||
export type FeesQueryVariables = Types.Exact<{
|
||||
partyId: Types.Scalars['ID'];
|
||||
volumeDiscountEpochs: Types.Scalars['Int'];
|
||||
referralDiscountEpochs: Types.Scalars['Int'];
|
||||
}>;
|
||||
|
||||
|
||||
@@ -65,14 +63,11 @@ export type DiscountProgramsQueryHookResult = ReturnType<typeof useDiscountProgr
|
||||
export type DiscountProgramsLazyQueryHookResult = ReturnType<typeof useDiscountProgramsLazyQuery>;
|
||||
export type DiscountProgramsQueryResult = Apollo.QueryResult<DiscountProgramsQuery, DiscountProgramsQueryVariables>;
|
||||
export const FeesDocument = gql`
|
||||
query Fees($partyId: ID!, $volumeDiscountEpochs: Int!, $referralDiscountEpochs: Int!) {
|
||||
query Fees($partyId: ID!) {
|
||||
epoch {
|
||||
id
|
||||
}
|
||||
volumeDiscountStats(
|
||||
partyId: $partyId
|
||||
pagination: {last: $volumeDiscountEpochs}
|
||||
) {
|
||||
volumeDiscountStats(partyId: $partyId, pagination: {last: 1}) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
@@ -104,7 +99,7 @@ export const FeesDocument = gql`
|
||||
}
|
||||
}
|
||||
}
|
||||
referralSetStats(partyId: $partyId, pagination: {last: $referralDiscountEpochs}) {
|
||||
referralSetStats(partyId: $partyId, pagination: {last: 1}) {
|
||||
edges {
|
||||
node {
|
||||
atEpoch
|
||||
@@ -129,8 +124,6 @@ export const FeesDocument = gql`
|
||||
* const { data, loading, error } = useFeesQuery({
|
||||
* variables: {
|
||||
* partyId: // value for 'partyId'
|
||||
* volumeDiscountEpochs: // value for 'volumeDiscountEpochs'
|
||||
* referralDiscountEpochs: // value for 'referralDiscountEpochs'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
|
||||
@@ -36,25 +36,25 @@ export const FeesContainer = () => {
|
||||
const { data: markets, loading: marketsLoading } = useMarketList();
|
||||
|
||||
const { data: programData, loading: programLoading } =
|
||||
useDiscountProgramsQuery();
|
||||
useDiscountProgramsQuery({ errorPolicy: 'ignore' });
|
||||
|
||||
const volumeDiscountWindowLength =
|
||||
programData?.currentVolumeDiscountProgram?.windowLength || 1;
|
||||
const referralDiscountWindowLength =
|
||||
programData?.currentReferralProgram?.windowLength || 1;
|
||||
|
||||
const { data: feesData, loading: feesLoading } = useFeesQuery({
|
||||
variables: {
|
||||
partyId: pubKey || '',
|
||||
volumeDiscountEpochs: volumeDiscountWindowLength,
|
||||
referralDiscountEpochs: referralDiscountWindowLength,
|
||||
},
|
||||
skip: !pubKey || !programData,
|
||||
skip: !pubKey,
|
||||
});
|
||||
|
||||
const previousEpoch = (Number(feesData?.epoch.id) || 0) - 1;
|
||||
|
||||
const { volumeDiscount, volumeTierIndex, volumeInWindow, volumeTiers } =
|
||||
useVolumeStats(
|
||||
feesData?.volumeDiscountStats,
|
||||
previousEpoch,
|
||||
feesData?.volumeDiscountStats.edges?.[0]?.node,
|
||||
programData?.currentVolumeDiscountProgram
|
||||
);
|
||||
|
||||
@@ -67,12 +67,12 @@ export const FeesContainer = () => {
|
||||
code,
|
||||
isReferrer,
|
||||
} = useReferralStats(
|
||||
feesData?.referralSetStats,
|
||||
feesData?.referralSetReferees,
|
||||
previousEpoch,
|
||||
feesData?.referralSetStats.edges?.[0]?.node,
|
||||
feesData?.referralSetReferees.edges?.[0]?.node,
|
||||
programData?.currentReferralProgram,
|
||||
feesData?.epoch,
|
||||
feesData?.referrer,
|
||||
feesData?.referee
|
||||
feesData?.referrer.edges?.[0]?.node,
|
||||
feesData?.referee.edges?.[0]?.node
|
||||
);
|
||||
|
||||
const loading = paramsLoading || feesLoading || programLoading;
|
||||
@@ -459,25 +459,19 @@ const VolumeTiers = ({
|
||||
</tr>
|
||||
</THead>
|
||||
<tbody>
|
||||
{Array.from(tiers)
|
||||
.reverse()
|
||||
.map((tier, i) => {
|
||||
const isUserTier = tiers.length - 1 - tierIndex === i;
|
||||
{Array.from(tiers).map((tier, i) => {
|
||||
const isUserTier = 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>
|
||||
);
|
||||
})}
|
||||
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>
|
||||
@@ -518,37 +512,33 @@ const ReferralTiers = ({
|
||||
</tr>
|
||||
</THead>
|
||||
<tbody>
|
||||
{Array.from(tiers)
|
||||
.reverse()
|
||||
.map((t, i) => {
|
||||
const isUserTier = tiers.length - 1 - tierIndex === i;
|
||||
{Array.from(tiers).map((t, i) => {
|
||||
const isUserTier = tierIndex === i;
|
||||
|
||||
const requiredVolume = Number(
|
||||
t.minimumRunningNotionalTakerVolume
|
||||
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>
|
||||
);
|
||||
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>
|
||||
);
|
||||
})}
|
||||
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>
|
||||
|
||||
@@ -2,46 +2,15 @@ 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 stat = {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 9,
|
||||
discountFactor: '0.01',
|
||||
referralSetRunningNotionalTakerVolume: '100',
|
||||
};
|
||||
|
||||
const sets = {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
atEpoch: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
node: {
|
||||
atEpoch: 4,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const epoch = {
|
||||
id: '10',
|
||||
const set = {
|
||||
atEpoch: 4,
|
||||
};
|
||||
|
||||
const program = {
|
||||
@@ -78,102 +47,36 @@ describe('useReferralStats', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns formatted data and tiers', () => {
|
||||
it('returns default values if set is not from previous epoch', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useReferralStats(setStats, sets, program, epoch)
|
||||
useReferralStats(10, stat, set, program)
|
||||
);
|
||||
|
||||
// should use stats from latest epoch
|
||||
const stats = setStats.edges[1].node;
|
||||
const set = sets.edges[1].node;
|
||||
|
||||
expect(result.current).toEqual({
|
||||
referralDiscount: Number(stats.discountFactor),
|
||||
referralVolumeInWindow: Number(
|
||||
stats.referralSetRunningNotionalTakerVolume
|
||||
),
|
||||
referralTierIndex: 1,
|
||||
referralDiscount: 0,
|
||||
referralVolumeInWindow: 0,
|
||||
referralTierIndex: -1,
|
||||
referralTiers: program.benefitTiers,
|
||||
epochsInSet: Number(epoch.id) - set.atEpoch,
|
||||
epochsInSet: 0,
|
||||
code: undefined,
|
||||
isReferrer: false,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ joinedAt: 2, index: -1 },
|
||||
{ joinedAt: 3, index: -1 },
|
||||
{ joinedAt: 4, index: 0 },
|
||||
{ joinedAt: 5, index: 0 },
|
||||
{ joinedAt: 6, index: 1 },
|
||||
{ joinedAt: 7, index: 1 },
|
||||
{ joinedAt: 8, index: 2 },
|
||||
{ joinedAt: 9, index: 2 },
|
||||
])('joined at epoch: $joinedAt should be index: $index', (obj) => {
|
||||
const statsA = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'ReferralSetStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'ReferralSetStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
referralSetRunningNotionalTakerVolume: '100000',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
const setsA = {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
atEpoch: Number(epoch.id) - obj.joinedAt,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
it('returns formatted data and tiers', () => {
|
||||
const { result } = renderHook(() =>
|
||||
useReferralStats(statsA, setsA, program, epoch)
|
||||
useReferralStats(9, stat, set, program)
|
||||
);
|
||||
|
||||
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);
|
||||
expect(result.current).toEqual({
|
||||
referralDiscount: Number(stat.discountFactor),
|
||||
referralVolumeInWindow: Number(
|
||||
stat.referralSetRunningNotionalTakerVolume
|
||||
),
|
||||
referralTierIndex: 0,
|
||||
referralTiers: program.benefitTiers,
|
||||
epochsInSet: stat.atEpoch - set.atEpoch,
|
||||
code: undefined,
|
||||
isReferrer: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import compact from 'lodash/compact';
|
||||
import maxBy from 'lodash/maxBy';
|
||||
import { getReferralBenefitTier } from './utils';
|
||||
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
|
||||
import { first } from 'lodash';
|
||||
|
||||
export const useReferralStats = (
|
||||
setStats?: FeesQuery['referralSetStats'],
|
||||
setReferees?: FeesQuery['referralSetReferees'],
|
||||
previousEpoch?: number,
|
||||
referralStats?: NonNullable<
|
||||
FeesQuery['referralSetStats']['edges']['0']
|
||||
>['node'],
|
||||
setReferees?: NonNullable<
|
||||
FeesQuery['referralSetReferees']['edges']['0']
|
||||
>['node'],
|
||||
program?: DiscountProgramsQuery['currentReferralProgram'],
|
||||
epoch?: FeesQuery['epoch'],
|
||||
setIfReferrer?: FeesQuery['referrer'],
|
||||
setIfReferee?: FeesQuery['referee']
|
||||
setIfReferrer?: NonNullable<FeesQuery['referrer']['edges']['0']>['node'],
|
||||
setIfReferee?: NonNullable<FeesQuery['referee']['edges']['0']>['node']
|
||||
) => {
|
||||
const referralTiers = program?.benefitTiers || [];
|
||||
|
||||
if (!setStats || !setReferees || !program || !epoch) {
|
||||
if (
|
||||
!previousEpoch ||
|
||||
referralStats?.atEpoch !== previousEpoch ||
|
||||
!program ||
|
||||
!setReferees
|
||||
) {
|
||||
return {
|
||||
referralDiscount: 0,
|
||||
referralVolumeInWindow: 0,
|
||||
@@ -26,41 +30,22 @@ export const useReferralStats = (
|
||||
};
|
||||
}
|
||||
|
||||
const setIfReferrerData = first(
|
||||
compact(setIfReferrer?.edges).map((e) => e.node)
|
||||
);
|
||||
const setIfRefereeData = first(
|
||||
compact(setIfReferee?.edges).map((e) => e.node)
|
||||
);
|
||||
|
||||
const referralSetsStats = compact(setStats.edges).map((e) => e.node);
|
||||
const referralSets = compact(setReferees.edges).map((e) => e.node);
|
||||
|
||||
const referralSet = maxBy(referralSets, (s) => s.atEpoch);
|
||||
const referralStats = maxBy(referralSetsStats, (s) => s.atEpoch);
|
||||
|
||||
const epochsInSet = referralSet ? Number(epoch.id) - referralSet.atEpoch : 0;
|
||||
|
||||
const referralDiscount = Number(referralStats?.discountFactor || 0);
|
||||
const referralVolumeInWindow = Number(
|
||||
referralStats?.referralSetRunningNotionalTakerVolume || 0
|
||||
);
|
||||
|
||||
const referralTierIndex = referralStats
|
||||
? getReferralBenefitTier(
|
||||
epochsInSet,
|
||||
Number(referralStats.referralSetRunningNotionalTakerVolume),
|
||||
referralTiers
|
||||
)
|
||||
: -1;
|
||||
const referralTierIndex = referralTiers.findIndex(
|
||||
(tier) => tier.referralDiscountFactor === referralStats?.discountFactor
|
||||
);
|
||||
|
||||
return {
|
||||
referralDiscount,
|
||||
referralVolumeInWindow,
|
||||
referralTierIndex,
|
||||
referralTiers,
|
||||
epochsInSet,
|
||||
code: (setIfReferrerData || setIfRefereeData)?.id,
|
||||
isReferrer: Boolean(setIfReferrerData),
|
||||
epochsInSet: referralStats.atEpoch - setReferees.atEpoch,
|
||||
code: (setIfReferrer || setIfReferee)?.id,
|
||||
isReferrer: Boolean(setIfReferrer),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,27 +2,11 @@ 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 stats = {
|
||||
__typename: 'VolumeDiscountStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.05',
|
||||
runningVolume: '200',
|
||||
};
|
||||
|
||||
const program = {
|
||||
@@ -44,7 +28,7 @@ describe('useReferralStats', () => {
|
||||
};
|
||||
|
||||
it('returns correct default values', () => {
|
||||
const { result } = renderHook(() => useVolumeStats());
|
||||
const { result } = renderHook(() => useVolumeStats(10));
|
||||
expect(result.current).toEqual({
|
||||
volumeDiscount: 0,
|
||||
volumeInWindow: 0,
|
||||
@@ -53,11 +37,18 @@ describe('useReferralStats', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns formatted data and tiers', () => {
|
||||
const { result } = renderHook(() => useVolumeStats(statsList, program));
|
||||
it('returns default values if no stat is not from previous epoch', () => {
|
||||
const { result } = renderHook(() => useVolumeStats(11, stats, program));
|
||||
expect(result.current).toEqual({
|
||||
volumeDiscount: 0,
|
||||
volumeInWindow: 0,
|
||||
volumeTierIndex: -1,
|
||||
volumeTiers: program.benefitTiers,
|
||||
});
|
||||
});
|
||||
|
||||
// should use stats from latest epoch
|
||||
const stats = statsList.edges[1].node;
|
||||
it('returns formatted data and tiers', () => {
|
||||
const { result } = renderHook(() => useVolumeStats(10, stats, program));
|
||||
|
||||
expect(result.current).toEqual({
|
||||
volumeDiscount: Number(stats.discountFactor),
|
||||
@@ -66,30 +57,4 @@ describe('useReferralStats', () => {
|
||||
volumeTiers: program.benefitTiers,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ volume: '100', index: 0 },
|
||||
{ volume: '150', index: 0 },
|
||||
{ volume: '200', index: 1 },
|
||||
{ volume: '250', index: 1 },
|
||||
{ volume: '300', index: 2 },
|
||||
{ volume: '350', index: 2 },
|
||||
])('returns index: $index for the running volume: $volume', (obj) => {
|
||||
const statsA = {
|
||||
edges: [
|
||||
{
|
||||
__typename: 'VolumeDiscountStatsEdge' as const,
|
||||
node: {
|
||||
__typename: 'VolumeDiscountStats' as const,
|
||||
atEpoch: 10,
|
||||
discountFactor: '0.3',
|
||||
runningVolume: obj.volume,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useVolumeStats(statsA, program));
|
||||
expect(result.current.volumeTierIndex).toBe(obj.index);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import compact from 'lodash/compact';
|
||||
import maxBy from 'lodash/maxBy';
|
||||
import { getVolumeTier } from './utils';
|
||||
import type { DiscountProgramsQuery, FeesQuery } from './__generated__/Fees';
|
||||
|
||||
export const useVolumeStats = (
|
||||
stats?: FeesQuery['volumeDiscountStats'],
|
||||
previousEpoch: number,
|
||||
lastEpochStats?: NonNullable<
|
||||
FeesQuery['volumeDiscountStats']['edges']['0']
|
||||
>['node'],
|
||||
program?: DiscountProgramsQuery['currentVolumeDiscountProgram']
|
||||
) => {
|
||||
const volumeTiers = program?.benefitTiers || [];
|
||||
|
||||
if (!stats || !program) {
|
||||
if (!lastEpochStats || lastEpochStats.atEpoch !== previousEpoch || !program) {
|
||||
return {
|
||||
volumeDiscount: 0,
|
||||
volumeTierIndex: -1,
|
||||
@@ -18,11 +18,11 @@ export const useVolumeStats = (
|
||||
};
|
||||
}
|
||||
|
||||
const volumeStats = compact(stats.edges).map((e) => e.node);
|
||||
const lastEpochStats = maxBy(volumeStats, (s) => s.atEpoch);
|
||||
const volumeDiscount = Number(lastEpochStats?.discountFactor || 0);
|
||||
const volumeInWindow = Number(lastEpochStats?.runningVolume || 0);
|
||||
const volumeTierIndex = getVolumeTier(volumeInWindow, volumeTiers);
|
||||
const volumeTierIndex = volumeTiers.findIndex(
|
||||
(tier) => tier.volumeDiscountFactor === lastEpochStats?.discountFactor
|
||||
);
|
||||
|
||||
return {
|
||||
volumeDiscount,
|
||||
|
||||
@@ -20,73 +20,6 @@ export const formatPercentage = (num: number) => {
|
||||
return formatter.format(parseFloat(pct.toFixed(5)));
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the index of the benefit tier for volume discounts. A user
|
||||
* only needs to fulfill a minimum volume requirement for the tier
|
||||
*/
|
||||
export const getVolumeTier = (
|
||||
volume: number,
|
||||
tiers: Array<{
|
||||
minimumRunningNotionalTakerVolume: string;
|
||||
}>
|
||||
) => {
|
||||
return tiers.findIndex((tier, i) => {
|
||||
const nextTier = tiers[i + 1];
|
||||
const validVolume =
|
||||
volume >= Number(tier.minimumRunningNotionalTakerVolume);
|
||||
|
||||
if (nextTier) {
|
||||
return (
|
||||
validVolume &&
|
||||
volume < Number(nextTier.minimumRunningNotionalTakerVolume)
|
||||
);
|
||||
}
|
||||
|
||||
return validVolume;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the index of the benefit tiers for referrals. A user must
|
||||
* fulfill both the minimum epochs in the referral set, and the set
|
||||
* must reach the combined total volume
|
||||
*/
|
||||
export const getReferralBenefitTier = (
|
||||
epochsInSet: number,
|
||||
volume: number,
|
||||
tiers: Array<{
|
||||
minimumRunningNotionalTakerVolume: string;
|
||||
minimumEpochs: number;
|
||||
}>
|
||||
) => {
|
||||
const indexByEpoch = tiers.findIndex((tier, i) => {
|
||||
const nextTier = tiers[i + 1];
|
||||
const validEpochs = epochsInSet >= tier.minimumEpochs;
|
||||
|
||||
if (nextTier) {
|
||||
return validEpochs && epochsInSet < nextTier.minimumEpochs;
|
||||
}
|
||||
|
||||
return validEpochs;
|
||||
});
|
||||
const indexByVolume = tiers.findIndex((tier, i) => {
|
||||
const nextTier = tiers[i + 1];
|
||||
const validVolume =
|
||||
volume >= Number(tier.minimumRunningNotionalTakerVolume);
|
||||
|
||||
if (nextTier) {
|
||||
return (
|
||||
validVolume &&
|
||||
volume < Number(nextTier.minimumRunningNotionalTakerVolume)
|
||||
);
|
||||
}
|
||||
|
||||
return validVolume;
|
||||
});
|
||||
|
||||
return Math.min(indexByEpoch, indexByVolume);
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a set of fees and a set of discounts return
|
||||
* the adjusted fee factor
|
||||
|
||||
@@ -15,15 +15,12 @@ export const LedgerContainer = () => {
|
||||
});
|
||||
|
||||
const assets = (data?.party?.accountsConnection?.edges ?? [])
|
||||
.map<PartyAssetFieldsFragment>(
|
||||
(item) => item?.node?.asset ?? ({} as PartyAssetFieldsFragment)
|
||||
)
|
||||
.reduce((aggr, item) => {
|
||||
if ('id' in item && 'symbol' in item) {
|
||||
aggr[item.id as string] = item.symbol as string;
|
||||
}
|
||||
return aggr;
|
||||
}, {} as Record<string, string>);
|
||||
.map((item) => item?.node?.asset)
|
||||
.filter((asset): asset is PartyAssetFieldsFragment => !!asset?.id)
|
||||
.reduce(
|
||||
(aggr, item) => Object.assign(aggr, { [item.id]: item.symbol }),
|
||||
{} as Record<string, string>
|
||||
);
|
||||
|
||||
if (!pubKey) {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Settings } from './settings';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
describe('Settings', () => {
|
||||
it('should the settings component with all the options', () => {
|
||||
render(<Settings />);
|
||||
expect(screen.getByText('Dark mode')).toBeInTheDocument();
|
||||
expect(screen.getByText('Share usage data')).toBeInTheDocument();
|
||||
expect(screen.getByText('Toast location')).toBeInTheDocument();
|
||||
expect(screen.getByText('Reset to default')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,19 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Switch, ToastPositionSetter } from '@vegaprotocol/ui-toolkit';
|
||||
import {
|
||||
Dialog,
|
||||
Intent,
|
||||
Switch,
|
||||
ToastPositionSetter,
|
||||
TradingButton,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
|
||||
import { useTelemetryApproval } from '../../lib/hooks/use-telemetry-approval';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useState, type ReactNode } from 'react';
|
||||
|
||||
export const Settings = () => {
|
||||
const { theme, setTheme } = useThemeSwitcher();
|
||||
const [isApproved, setIsApproved] = useTelemetryApproval();
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<div>
|
||||
<SettingsGroup label={t('Dark mode')}>
|
||||
@@ -31,6 +38,52 @@ export const Settings = () => {
|
||||
<SettingsGroup label={t('Toast location')}>
|
||||
<ToastPositionSetter />
|
||||
</SettingsGroup>
|
||||
<SettingsGroup label={t('Reset to default')}>
|
||||
<TradingButton
|
||||
name="reset-to-defaults"
|
||||
size="small"
|
||||
intent={Intent.None}
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{t('Reset')}
|
||||
</TradingButton>
|
||||
<Dialog open={open} title={t('Reset')}>
|
||||
<div className="mb-4">
|
||||
<p>
|
||||
{t(
|
||||
'You will lose all persisted settings and you will be logged out.'
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{t('Are you sure you want to reset all settings to default?')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<TradingButton
|
||||
name="reset-to-defaults-cancel"
|
||||
intent={Intent.Primary}
|
||||
onClick={() => {
|
||||
localStorage.clear();
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
{t('Yes, clear cache and refresh')}
|
||||
</TradingButton>
|
||||
<TradingButton
|
||||
name="reset-to-defaults-cancel"
|
||||
intent={Intent.None}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{t('No, keep settings')}
|
||||
</TradingButton>
|
||||
</div>
|
||||
</Dialog>
|
||||
</SettingsGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -170,7 +170,7 @@ html [data-theme='dark'] {
|
||||
|
||||
.ag-theme-balham,
|
||||
.ag-theme-balham-dark {
|
||||
--ag-grid-size: 2px; /* Used for compactness */
|
||||
--ag-grid-size: 3px; /* Used for compactness */
|
||||
--ag-row-height: 36px;
|
||||
--ag-header-height: 28px;
|
||||
}
|
||||
@@ -184,7 +184,7 @@ html [data-theme='dark'] {
|
||||
|
||||
/* Light variables */
|
||||
.ag-theme-balham {
|
||||
--ag-background-color: transparent;
|
||||
--ag-background-color: theme(colors.vega.clight.900);
|
||||
--ag-border-color: theme(colors.vega.clight.600);
|
||||
--ag-header-background-color: theme(colors.vega.clight.700);
|
||||
--ag-odd-row-background-color: transparent;
|
||||
@@ -196,7 +196,7 @@ html [data-theme='dark'] {
|
||||
|
||||
/* Dark variables */
|
||||
.ag-theme-balham-dark {
|
||||
--ag-background-color: transparent;
|
||||
--ag-background-color: theme(colors.vega.cdark.900);
|
||||
--ag-border-color: theme(colors.vega.cdark.600);
|
||||
--ag-header-background-color: theme(colors.vega.cdark.700);
|
||||
--ag-odd-row-background-color: transparent;
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
import { act, render, screen } from '@testing-library/react';
|
||||
import { act, render, screen, within } from '@testing-library/react';
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
import type { AccountFields } from './accounts-data-provider';
|
||||
import { getAccountData } from './accounts-data-provider';
|
||||
import { AccountTable } from './accounts-table';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
const asset1 = {
|
||||
__typename: 'Asset',
|
||||
id: 'asset-1',
|
||||
symbol: 'tBTC',
|
||||
decimals: 5,
|
||||
name: 'T BTC',
|
||||
};
|
||||
const asset2 = {
|
||||
__typename: 'Asset',
|
||||
id: 'asset-2',
|
||||
symbol: 'aBTC',
|
||||
decimals: 5,
|
||||
name: 'A BTC',
|
||||
};
|
||||
const singleRow = {
|
||||
__typename: 'AccountBalance',
|
||||
type: Types.AccountType.ACCOUNT_TYPE_MARGIN,
|
||||
@@ -13,12 +26,7 @@ const singleRow = {
|
||||
__typename: 'Market',
|
||||
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
symbol: 'tBTC',
|
||||
decimals: 5,
|
||||
},
|
||||
asset: asset1,
|
||||
available: '125600000',
|
||||
used: '125600000',
|
||||
total: '251200000',
|
||||
@@ -33,12 +41,7 @@ const secondRow = {
|
||||
__typename: 'Market',
|
||||
id: '10cd0a793ad2887b340940337fa6d97a212e0e517fe8e9eab2b5ef3a38633f35',
|
||||
},
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
symbol: 'aBTC',
|
||||
decimals: 5,
|
||||
},
|
||||
asset: asset2,
|
||||
available: '125600001',
|
||||
used: '125600001',
|
||||
total: '251200002',
|
||||
@@ -134,7 +137,7 @@ describe('AccountsTable', () => {
|
||||
|
||||
it('should sort assets', async () => {
|
||||
// 7001-COLL-010
|
||||
const { container } = render(
|
||||
render(
|
||||
<AccountTable
|
||||
rowData={multiRowData}
|
||||
onClickAsset={() => null}
|
||||
@@ -142,13 +145,13 @@ describe('AccountsTable', () => {
|
||||
/>
|
||||
);
|
||||
|
||||
const headerCell = screen.getByText('Asset');
|
||||
await userEvent.click(headerCell);
|
||||
const rows = container.querySelectorAll(
|
||||
'.ag-center-cols-container .ag-row'
|
||||
);
|
||||
expect(rows[0].textContent).toContain('aBTC');
|
||||
expect(rows[1].textContent).toContain('tBTC');
|
||||
const headerCell = screen
|
||||
.getAllByRole('columnheader')
|
||||
.find((h) => h?.getAttribute('col-id') === 'asset.symbol') as HTMLElement;
|
||||
|
||||
await userEvent.click(within(headerCell).getByText(/asset/i));
|
||||
|
||||
expect(headerCell).toHaveAttribute('aria-sort', 'ascending');
|
||||
});
|
||||
|
||||
it('should apply correct formatting in view as user mode', async () => {
|
||||
@@ -176,12 +179,7 @@ describe('AccountsTable', () => {
|
||||
rowData={singleRowData}
|
||||
onClickAsset={() => null}
|
||||
isReadOnly={false}
|
||||
pinnedAsset={{
|
||||
decimals: 5,
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
symbol: 'tBTC',
|
||||
name: 'tBTC',
|
||||
}}
|
||||
pinnedAsset={asset1}
|
||||
/>
|
||||
);
|
||||
await screen.findAllByRole('rowgroup');
|
||||
@@ -213,23 +211,13 @@ describe('AccountsTable', () => {
|
||||
const result = getAccountData([singleRow]);
|
||||
const expected = [
|
||||
{
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
decimals: 5,
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
symbol: 'tBTC',
|
||||
},
|
||||
asset: asset1,
|
||||
available: '0',
|
||||
balance: '0',
|
||||
breakdown: [
|
||||
{
|
||||
__typename: 'AccountBalance',
|
||||
asset: {
|
||||
__typename: 'Asset',
|
||||
decimals: 5,
|
||||
id: '5cfa87844724df6069b94e4c8a6f03af21907d7bc251593d08e4251043ee9f7c',
|
||||
symbol: 'tBTC',
|
||||
},
|
||||
asset: asset1,
|
||||
available: '0',
|
||||
balance: '125600000',
|
||||
total: '125600000',
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import {
|
||||
@@ -7,7 +13,7 @@ import {
|
||||
TransferForm,
|
||||
type TransferFormProps,
|
||||
} from './transfer-form';
|
||||
import { AccountType } from '@vegaprotocol/types';
|
||||
import { AccountType, AccountTypeMapping } from '@vegaprotocol/types';
|
||||
import { removeDecimal } from '@vegaprotocol/utils';
|
||||
|
||||
describe('TransferForm', () => {
|
||||
@@ -39,8 +45,7 @@ describe('TransferForm', () => {
|
||||
};
|
||||
|
||||
const amount = '100';
|
||||
const pubKey =
|
||||
'70d14a321e02e71992fd115563df765000ccc4775cbe71a0e2f9ff5a3b9dc680';
|
||||
const pubKey = '1'.repeat(64);
|
||||
const asset = {
|
||||
id: 'eur',
|
||||
symbol: '€',
|
||||
@@ -50,10 +55,7 @@ describe('TransferForm', () => {
|
||||
};
|
||||
const props = {
|
||||
pubKey,
|
||||
pubKeys: [
|
||||
pubKey,
|
||||
'a4b6e3de5d7ef4e31ae1b090be49d1a2ef7bcefff60cccf7658a0d4922651cce',
|
||||
],
|
||||
pubKeys: [pubKey, '2'.repeat(64)],
|
||||
feeFactor: '0.001',
|
||||
submitTransfer: jest.fn(),
|
||||
accounts: [
|
||||
@@ -182,7 +184,7 @@ describe('TransferForm', () => {
|
||||
|
||||
// Test use max button
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
|
||||
expect(amountInput).toHaveValue('1000');
|
||||
expect(amountInput).toHaveValue('1000.00');
|
||||
|
||||
// Test amount validation
|
||||
await userEvent.clear(amountInput);
|
||||
@@ -267,7 +269,7 @@ describe('TransferForm', () => {
|
||||
|
||||
// Test use max button
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
|
||||
expect(amountInput).toHaveValue('100');
|
||||
expect(amountInput).toHaveValue('100.00');
|
||||
|
||||
// If transfering from a vested account 'include fees' checkbox should
|
||||
// be disabled and fees should be 0
|
||||
@@ -296,6 +298,88 @@ describe('TransferForm', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('handles lots of decimal places', async () => {
|
||||
const balance = '904195168829277777';
|
||||
const expectedBalance = '0.904195168829277777';
|
||||
|
||||
const longDecimalAsset = {
|
||||
id: 'assetId',
|
||||
symbol: 'VEGA',
|
||||
name: 'VEGA',
|
||||
decimals: 18,
|
||||
quantum: '1',
|
||||
};
|
||||
|
||||
const account = {
|
||||
type: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
asset: longDecimalAsset,
|
||||
balance,
|
||||
};
|
||||
|
||||
const mockSubmit = jest.fn();
|
||||
|
||||
renderComponent({
|
||||
...props,
|
||||
accounts: [account],
|
||||
submitTransfer: mockSubmit,
|
||||
minQuantumMultiple: '100000',
|
||||
});
|
||||
|
||||
// Select a pubkey
|
||||
await userEvent.selectOptions(
|
||||
screen.getByLabelText('To Vega key'),
|
||||
props.pubKeys[1] // Use not current pubkey so we can check it switches to current pubkey later
|
||||
);
|
||||
|
||||
// Select asset
|
||||
await selectAsset(longDecimalAsset);
|
||||
|
||||
const accountSelect = screen.getByLabelText('From account');
|
||||
const option = within(accountSelect)
|
||||
.getAllByRole('option')
|
||||
.find(
|
||||
(o) => o.getAttribute('value') === `${account.type}-${account.asset.id}`
|
||||
);
|
||||
// plus one for disabled 'please select' option
|
||||
|
||||
expect(option).toHaveTextContent(
|
||||
`${AccountTypeMapping[account.type]} (${expectedBalance} ${
|
||||
account.asset.symbol
|
||||
})`
|
||||
);
|
||||
|
||||
await userEvent.selectOptions(
|
||||
accountSelect,
|
||||
`${AccountType.ACCOUNT_TYPE_VESTED_REWARDS}-${longDecimalAsset.id}`
|
||||
);
|
||||
|
||||
expect(accountSelect).toHaveValue(
|
||||
`${AccountType.ACCOUNT_TYPE_VESTED_REWARDS}-${longDecimalAsset.id}`
|
||||
);
|
||||
|
||||
// Check switch back to connected key
|
||||
const amountInput = screen.getByLabelText('Amount');
|
||||
|
||||
// Test use max button
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Use max' }));
|
||||
expect(amountInput).toHaveValue(expectedBalance);
|
||||
|
||||
await submit();
|
||||
|
||||
await waitFor(() => {
|
||||
// 1003-TRAN-023
|
||||
expect(mockSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(mockSubmit).toHaveBeenCalledWith({
|
||||
fromAccountType: AccountType.ACCOUNT_TYPE_VESTED_REWARDS,
|
||||
toAccountType: AccountType.ACCOUNT_TYPE_GENERAL,
|
||||
to: props.pubKey,
|
||||
asset: longDecimalAsset.id,
|
||||
amount: balance,
|
||||
oneOff: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('IncludeFeesCheckbox', () => {
|
||||
it('validates fields and submits when checkbox is checked', async () => {
|
||||
const mockSubmit = jest.fn();
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
vegaPublicKey,
|
||||
addDecimal,
|
||||
formatNumber,
|
||||
addDecimalsFormatNumber,
|
||||
toBigNum,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
@@ -214,12 +213,7 @@ export const TransferForm = ({
|
||||
<AssetOption
|
||||
key={a.key}
|
||||
asset={a}
|
||||
balance={
|
||||
<Balance
|
||||
balance={formatNumber(a.balance, a.decimals)}
|
||||
symbol={a.symbol}
|
||||
/>
|
||||
}
|
||||
balance={<Balance balance={a.balance} symbol={a.symbol} />}
|
||||
/>
|
||||
))}
|
||||
</TradingRichSelect>
|
||||
@@ -286,8 +280,8 @@ export const TransferForm = ({
|
||||
return (
|
||||
<option value={id} key={id}>
|
||||
{AccountTypeMapping[a.type]} (
|
||||
{addDecimalsFormatNumber(a.balance, a.asset.decimals)}{' '}
|
||||
{a.asset.symbol})
|
||||
{addDecimal(a.balance, a.asset.decimals)} {a.asset.symbol}
|
||||
)
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
@@ -413,7 +407,7 @@ export const TransferForm = ({
|
||||
type="button"
|
||||
className="absolute top-0 right-0 ml-auto text-xs underline"
|
||||
onClick={() =>
|
||||
setValue('amount', parseFloat(accountBalance).toString(), {
|
||||
setValue('amount', accountBalance, {
|
||||
shouldValidate: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -37,6 +37,9 @@ export const AgGridThemed = ({
|
||||
<div className={wrapperClasses}>
|
||||
<AgGridReact
|
||||
defaultColDef={defaultColDef}
|
||||
overlayLoadingTemplate={t('Loading...')}
|
||||
overlayNoRowsTemplate={t('No data')}
|
||||
suppressDragLeaveHidesColumns
|
||||
ref={gridRef}
|
||||
{...defaultProps}
|
||||
{...props}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { getAsset, getProductType, getQuoteName } from '@vegaprotocol/markets';
|
||||
import { getAsset, getQuoteName } from '@vegaprotocol/markets';
|
||||
import type { OrderSubmissionBody } from '@vegaprotocol/wallet';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
|
||||
@@ -151,6 +151,7 @@ export const DealTicketMarginDetails = ({
|
||||
const { decimals: assetDecimals, quantum } = asset;
|
||||
let marginRequiredBestCase: string | undefined = undefined;
|
||||
let marginRequiredWorstCase: string | undefined = undefined;
|
||||
|
||||
if (marginEstimate) {
|
||||
if (currentMargins) {
|
||||
marginRequiredBestCase = (
|
||||
@@ -286,115 +287,99 @@ export const DealTicketMarginDetails = ({
|
||||
);
|
||||
|
||||
const quoteName = getQuoteName(market);
|
||||
const productType = getProductType(market);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col w-full gap-2 pt-2">
|
||||
{/*
|
||||
TODO: remove this conditional check once the following PRs are deployed
|
||||
and the estimatePosition query is working for perps
|
||||
|
||||
- https://github.com/vegaprotocol/vega/pull/10119
|
||||
- https://github.com/vegaprotocol/vega/pull/10122
|
||||
*/}
|
||||
{productType === 'Future' && (
|
||||
<>
|
||||
<Accordion>
|
||||
<AccordionPanel
|
||||
itemId="margin"
|
||||
trigger={
|
||||
<AccordionPrimitive.Trigger
|
||||
data-testid="accordion-toggle"
|
||||
className={classNames(
|
||||
'w-full',
|
||||
'flex items-center gap-2 text-xs',
|
||||
'group'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
data-testid={`deal-ticket-fee-margin-required`}
|
||||
key={'value-dropdown'}
|
||||
className="flex items-center justify-between w-full gap-2"
|
||||
>
|
||||
<div className="flex items-center text-left gap-1">
|
||||
<Tooltip
|
||||
description={MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol)}
|
||||
>
|
||||
<span className="text-muted">
|
||||
{t('Margin required')}
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
<AccordionChevron size={10} />
|
||||
</div>
|
||||
<Tooltip
|
||||
description={
|
||||
formatRange(
|
||||
marginRequiredBestCase,
|
||||
marginRequiredWorstCase,
|
||||
assetDecimals
|
||||
) ?? '-'
|
||||
}
|
||||
>
|
||||
<div className="font-mono text-right">
|
||||
{formatValue(
|
||||
marginRequiredWorstCase,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}{' '}
|
||||
{assetSymbol || ''}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</AccordionPrimitive.Trigger>
|
||||
}
|
||||
<Accordion>
|
||||
<AccordionPanel
|
||||
itemId="margin"
|
||||
trigger={
|
||||
<AccordionPrimitive.Trigger
|
||||
data-testid="accordion-toggle"
|
||||
className={classNames(
|
||||
'w-full',
|
||||
'flex items-center gap-2 text-xs',
|
||||
'group'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col w-full gap-2">
|
||||
<KeyValue
|
||||
label={t('Total margin available')}
|
||||
indent
|
||||
value={formatValue(totalMarginAvailable, assetDecimals)}
|
||||
formattedValue={formatValue(
|
||||
totalMarginAvailable,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={TOTAL_MARGIN_AVAILABLE(
|
||||
formatValue(generalAccountBalance, assetDecimals, quantum),
|
||||
formatValue(marginAccountBalance, assetDecimals, quantum),
|
||||
formatValue(
|
||||
currentMargins?.maintenanceLevel,
|
||||
<div
|
||||
data-testid={`deal-ticket-fee-margin-required`}
|
||||
key={'value-dropdown'}
|
||||
className="flex items-center justify-between w-full gap-2"
|
||||
>
|
||||
<div className="flex items-center text-left gap-1">
|
||||
<Tooltip description={MARGIN_DIFF_TOOLTIP_TEXT(assetSymbol)}>
|
||||
<span className="text-muted">{t('Margin required')}</span>
|
||||
</Tooltip>
|
||||
|
||||
<AccordionChevron size={10} />
|
||||
</div>
|
||||
<Tooltip
|
||||
description={
|
||||
formatRange(
|
||||
marginRequiredBestCase,
|
||||
marginRequiredWorstCase,
|
||||
assetDecimals
|
||||
) ?? '-'
|
||||
}
|
||||
>
|
||||
<div className="font-mono text-right">
|
||||
{formatValue(
|
||||
marginRequiredWorstCase,
|
||||
assetDecimals,
|
||||
quantum
|
||||
),
|
||||
assetSymbol
|
||||
)}
|
||||
/>
|
||||
{deductionFromCollateral}
|
||||
<KeyValue
|
||||
label={t('Current margin allocation')}
|
||||
indent
|
||||
onClick={
|
||||
generalAccountBalance
|
||||
? () => setBreakdownDialog(true)
|
||||
: undefined
|
||||
}
|
||||
value={formatValue(marginAccountBalance, assetDecimals)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={MARGIN_ACCOUNT_TOOLTIP_TEXT}
|
||||
formattedValue={formatValue(
|
||||
marginAccountBalance,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
/>
|
||||
)}{' '}
|
||||
{assetSymbol || ''}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</Accordion>
|
||||
{projectedMargin}
|
||||
</>
|
||||
)}
|
||||
</AccordionPrimitive.Trigger>
|
||||
}
|
||||
>
|
||||
<div className="flex flex-col w-full gap-2">
|
||||
<KeyValue
|
||||
label={t('Total margin available')}
|
||||
indent
|
||||
value={formatValue(totalMarginAvailable, assetDecimals)}
|
||||
formattedValue={formatValue(
|
||||
totalMarginAvailable,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={TOTAL_MARGIN_AVAILABLE(
|
||||
formatValue(generalAccountBalance, assetDecimals, quantum),
|
||||
formatValue(marginAccountBalance, assetDecimals, quantum),
|
||||
formatValue(
|
||||
currentMargins?.maintenanceLevel,
|
||||
assetDecimals,
|
||||
quantum
|
||||
),
|
||||
assetSymbol
|
||||
)}
|
||||
/>
|
||||
{deductionFromCollateral}
|
||||
<KeyValue
|
||||
label={t('Current margin allocation')}
|
||||
indent
|
||||
onClick={
|
||||
generalAccountBalance
|
||||
? () => setBreakdownDialog(true)
|
||||
: undefined
|
||||
}
|
||||
value={formatValue(marginAccountBalance, assetDecimals)}
|
||||
symbol={assetSymbol}
|
||||
labelDescription={MARGIN_ACCOUNT_TOOLTIP_TEXT}
|
||||
formattedValue={formatValue(
|
||||
marginAccountBalance,
|
||||
assetDecimals,
|
||||
quantum
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</AccordionPanel>
|
||||
</Accordion>
|
||||
{projectedMargin}
|
||||
<KeyValue
|
||||
label={t('Liquidation')}
|
||||
value={liquidationPriceEstimateRange}
|
||||
|
||||
@@ -4,14 +4,8 @@ import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
import type { PartialDeep } from 'type-fest';
|
||||
import type { Trade } from './fills-data-provider';
|
||||
import {
|
||||
FeesDiscountBreakdownTooltip,
|
||||
FillsTable,
|
||||
getFeesBreakdown,
|
||||
getTotalFeesDiscounts,
|
||||
} from './fills-table';
|
||||
import { FeesDiscountBreakdownTooltip, FillsTable } from './fills-table';
|
||||
import { generateFill } from './test-helpers';
|
||||
import type { TradeFeeFieldsFragment } from './__generated__/Fills';
|
||||
|
||||
const partyId = 'party-id';
|
||||
const defaultFill: PartialDeep<Trade> = {
|
||||
@@ -35,6 +29,7 @@ const defaultFill: PartialDeep<Trade> = {
|
||||
},
|
||||
createdAt: new Date('2022-02-02T14:00:00').toISOString(),
|
||||
};
|
||||
|
||||
describe('FillsTable', () => {
|
||||
it('correct columns are rendered', async () => {
|
||||
// 7005-FILL-001
|
||||
@@ -65,7 +60,7 @@ describe('FillsTable', () => {
|
||||
expect(headers.map((h) => h.textContent?.trim())).toEqual(expectedHeaders);
|
||||
});
|
||||
|
||||
it('formats cells correctly for buyer fill', async () => {
|
||||
it('formats cells correctly for buyer fill for maker', async () => {
|
||||
const buyerFill = generateFill({
|
||||
...defaultFill,
|
||||
buyer: {
|
||||
@@ -89,7 +84,7 @@ describe('FillsTable', () => {
|
||||
'3.00 BTC',
|
||||
'Maker',
|
||||
'2.00 BTC',
|
||||
'0.27 BTC',
|
||||
'0.09 BTC',
|
||||
getDateTimeFormat().format(new Date(buyerFill.createdAt)),
|
||||
'', // action column
|
||||
];
|
||||
@@ -271,96 +266,48 @@ describe('FillsTable', () => {
|
||||
.find((c) => c.getAttribute('col-id') === 'size');
|
||||
expect(sizeCell).toHaveTextContent('3,000,000,000');
|
||||
});
|
||||
});
|
||||
|
||||
describe('FeesDiscountBreakdownTooltip', () => {
|
||||
it('shows all discounts', () => {
|
||||
const data = generateFill({
|
||||
...defaultFill,
|
||||
buyer: {
|
||||
id: partyId,
|
||||
},
|
||||
});
|
||||
const props = {
|
||||
data,
|
||||
partyId,
|
||||
value: data.market,
|
||||
} as Parameters<typeof FeesDiscountBreakdownTooltip>['0'];
|
||||
const { container } = render(<FeesDiscountBreakdownTooltip {...props} />);
|
||||
const dt = container.querySelectorAll('dt');
|
||||
const dd = container.querySelectorAll('dd');
|
||||
const expectedDt = [
|
||||
'Infrastructure Fee',
|
||||
'Referral Discount',
|
||||
'Volume Discount',
|
||||
'Liquidity Fee',
|
||||
'Referral Discount',
|
||||
'Volume Discount',
|
||||
'Maker Fee',
|
||||
'Referral Discount',
|
||||
'Volume Discount',
|
||||
];
|
||||
const expectedDD = [
|
||||
'0.05 BTC',
|
||||
'0.06 BTC',
|
||||
'0.01 BTC',
|
||||
'0.02 BTC',
|
||||
'0.03 BTC',
|
||||
'0.04 BTC',
|
||||
];
|
||||
expectedDt.forEach((label, i) => {
|
||||
expect(dt[i]).toHaveTextContent(label);
|
||||
});
|
||||
expectedDD.forEach((label, i) => {
|
||||
expect(dd[i]).toHaveTextContent(label);
|
||||
describe('FeesDiscountBreakdownTooltip', () => {
|
||||
it('shows all discounts', () => {
|
||||
const data = generateFill({
|
||||
...defaultFill,
|
||||
buyer: {
|
||||
id: partyId,
|
||||
},
|
||||
});
|
||||
const props = {
|
||||
data,
|
||||
partyId,
|
||||
value: data.market,
|
||||
} as Parameters<typeof FeesDiscountBreakdownTooltip>['0'];
|
||||
const { container } = render(<FeesDiscountBreakdownTooltip {...props} />);
|
||||
const dt = container.querySelectorAll('dt');
|
||||
const dd = container.querySelectorAll('dd');
|
||||
const expectedDt = [
|
||||
'Infrastructure Fee',
|
||||
'Referral Discount',
|
||||
'Volume Discount',
|
||||
'Liquidity Fee',
|
||||
'Referral Discount',
|
||||
'Volume Discount',
|
||||
'Maker Fee',
|
||||
'Referral Discount',
|
||||
'Volume Discount',
|
||||
];
|
||||
const expectedDD = [
|
||||
'0.05 BTC',
|
||||
'0.06 BTC',
|
||||
'0.01 BTC',
|
||||
'0.02 BTC',
|
||||
'0.03 BTC',
|
||||
'0.04 BTC',
|
||||
];
|
||||
expectedDt.forEach((label, i) => {
|
||||
expect(dt[i]).toHaveTextContent(label);
|
||||
});
|
||||
expectedDD.forEach((label, i) => {
|
||||
expect(dd[i]).toHaveTextContent(label);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFeesBreakdown', () => {
|
||||
it('should return correct fees breakdown for a taker', () => {
|
||||
const fees = {
|
||||
makerFee: '1000',
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
};
|
||||
const expectedBreakdown = {
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
makerFee: '1000',
|
||||
totalFee: '6000',
|
||||
};
|
||||
expect(getFeesBreakdown('Taker', fees)).toEqual(expectedBreakdown);
|
||||
});
|
||||
|
||||
it('should return correct fees breakdown for a maker', () => {
|
||||
const fees = {
|
||||
makerFee: '1000',
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
};
|
||||
const expectedBreakdown = {
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
makerFee: '-1000',
|
||||
totalFee: '4000',
|
||||
};
|
||||
expect(getFeesBreakdown('Maker', fees)).toEqual(expectedBreakdown);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTotalFeesDiscounts', () => {
|
||||
it('should return correct total value', () => {
|
||||
const fees = {
|
||||
infrastructureFeeReferralDiscount: '1',
|
||||
infrastructureFeeVolumeDiscount: '2',
|
||||
liquidityFeeReferralDiscount: '3',
|
||||
liquidityFeeVolumeDiscount: '4',
|
||||
makerFeeReferralDiscount: '5',
|
||||
makerFeeVolumeDiscount: '6',
|
||||
};
|
||||
expect(getTotalFeesDiscounts(fees as TradeFeeFieldsFragment)).toEqual(
|
||||
(1 + 2 + 3 + 4 + 5 + 6).toString()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useMemo } from 'react';
|
||||
import type {
|
||||
AgGridReact,
|
||||
AgGridReactProps,
|
||||
AgReactUiProps,
|
||||
import {
|
||||
type AgGridReact,
|
||||
type AgGridReactProps,
|
||||
type AgReactUiProps,
|
||||
} from 'ag-grid-react';
|
||||
import type { ITooltipParams, ColDef } from 'ag-grid-community';
|
||||
import type { ColDef } from 'ag-grid-community';
|
||||
import type { ITooltipParams } from 'ag-grid-community';
|
||||
import {
|
||||
addDecimal,
|
||||
addDecimalsFormatNumber,
|
||||
@@ -29,19 +30,11 @@ import type {
|
||||
import { forwardRef } from 'react';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type { Trade } from './fills-data-provider';
|
||||
import type {
|
||||
FillFieldsFragment,
|
||||
TradeFeeFieldsFragment,
|
||||
} from './__generated__/Fills';
|
||||
import { FillActionsDropdown } from './fill-actions-dropdown';
|
||||
import { getAsset } from '@vegaprotocol/markets';
|
||||
import { MAKER, TAKER, getFeesBreakdown, getRoleAndFees } from './fills-utils';
|
||||
|
||||
const TAKER = 'Taker';
|
||||
const MAKER = 'Maker';
|
||||
|
||||
export type Role = typeof TAKER | typeof MAKER | '-';
|
||||
|
||||
export type Props = (AgGridReactProps | AgReactUiProps) & {
|
||||
type Props = (AgGridReactProps | AgReactUiProps) & {
|
||||
partyId: string;
|
||||
onMarketClick?: (marketId: string, metaKey?: boolean) => void;
|
||||
};
|
||||
@@ -261,63 +254,13 @@ const formatFeeDiscount = (partyId: string) => {
|
||||
}: VegaValueFormatterParams<Trade, 'market'>) => {
|
||||
if (!market || !data) return '-';
|
||||
const asset = getAsset(market);
|
||||
const { fees } = getRoleAndFees({ data, partyId });
|
||||
if (!fees) return '-';
|
||||
|
||||
const total = getTotalFeesDiscounts(fees);
|
||||
return addDecimalsFormatNumber(total, asset.decimals);
|
||||
const { fees: roleFees, role } = getRoleAndFees({ data, partyId });
|
||||
if (!roleFees) return '-';
|
||||
const { totalFeeDiscount } = getFeesBreakdown(role, roleFees);
|
||||
return addDecimalsFormatNumber(totalFeeDiscount, asset.decimals);
|
||||
};
|
||||
};
|
||||
|
||||
export const isEmptyFeeObj = (feeObj: Schema.TradeFee) => {
|
||||
if (!feeObj) return true;
|
||||
return (
|
||||
feeObj.liquidityFee === '0' &&
|
||||
feeObj.makerFee === '0' &&
|
||||
feeObj.infrastructureFee === '0'
|
||||
);
|
||||
};
|
||||
|
||||
export const getRoleAndFees = ({
|
||||
data,
|
||||
partyId,
|
||||
}: {
|
||||
data: Pick<
|
||||
FillFieldsFragment,
|
||||
'buyerFee' | 'sellerFee' | 'buyer' | 'seller' | 'aggressor'
|
||||
>;
|
||||
partyId?: string;
|
||||
}) => {
|
||||
let role: Role;
|
||||
let fees;
|
||||
if (data?.buyer.id === partyId) {
|
||||
if (data.aggressor === Schema.Side.SIDE_BUY) {
|
||||
role = TAKER;
|
||||
fees = data?.buyerFee;
|
||||
} else if (data.aggressor === Schema.Side.SIDE_SELL) {
|
||||
role = MAKER;
|
||||
fees = data?.sellerFee;
|
||||
} else {
|
||||
role = '-';
|
||||
fees = !isEmptyFeeObj(data?.buyerFee) ? data.buyerFee : data.sellerFee;
|
||||
}
|
||||
} else if (data?.seller.id === partyId) {
|
||||
if (data.aggressor === Schema.Side.SIDE_SELL) {
|
||||
role = TAKER;
|
||||
fees = data?.sellerFee;
|
||||
} else if (data.aggressor === Schema.Side.SIDE_BUY) {
|
||||
role = MAKER;
|
||||
fees = data?.buyerFee;
|
||||
} else {
|
||||
role = '-';
|
||||
fees = !isEmptyFeeObj(data.sellerFee) ? data.sellerFee : data.buyerFee;
|
||||
}
|
||||
} else {
|
||||
return { role: '-', fees: undefined };
|
||||
}
|
||||
return { role, fees };
|
||||
};
|
||||
|
||||
const FeesBreakdownTooltip = ({
|
||||
data,
|
||||
value: market,
|
||||
@@ -329,16 +272,23 @@ const FeesBreakdownTooltip = ({
|
||||
|
||||
const asset = getAsset(market);
|
||||
|
||||
const { role, fees } = getRoleAndFees({ data, partyId }) ?? {};
|
||||
const { role, fees, marketState } = getRoleAndFees({ data, partyId }) ?? {};
|
||||
if (!fees) return null;
|
||||
const { infrastructureFee, liquidityFee, makerFee, totalFee } =
|
||||
getFeesBreakdown(role, fees);
|
||||
getFeesBreakdown(role, fees, marketState);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="fee-breakdown-tooltip"
|
||||
className="z-20 max-w-sm px-4 py-2 text-sm text-black border rounded bg-vega-light-100 dark:bg-vega-dark-100 border-vega-light-200 dark:border-vega-dark-200 break-word dark:text-white"
|
||||
className="z-20 max-w-sm px-4 py-2 text-xs text-black border rounded bg-vega-light-100 dark:bg-vega-dark-100 border-vega-light-200 dark:border-vega-dark-200 break-word dark:text-white"
|
||||
>
|
||||
{marketState && (
|
||||
<p className="mb-1 italic">
|
||||
{t('If the market was %s', [
|
||||
Schema.MarketStateMapping[marketState].toLowerCase(),
|
||||
])}
|
||||
</p>
|
||||
)}
|
||||
{role === MAKER && (
|
||||
<>
|
||||
<p className="mb-1">{t('The maker will receive the maker fee.')}</p>
|
||||
@@ -352,7 +302,7 @@ const FeesBreakdownTooltip = ({
|
||||
{role === TAKER && (
|
||||
<p className="mb-1">{t('Fees to be paid by the taker.')}</p>
|
||||
)}
|
||||
{role === '-' && (
|
||||
{(role === '-' || marketState === Schema.MarketState.STATE_SUSPENDED) && (
|
||||
<p className="mb-1">
|
||||
{t(
|
||||
'If the market is in monitoring auction, half of the infrastructure and liquidity fees will be paid.'
|
||||
@@ -393,8 +343,8 @@ const FeesDiscountBreakdownTooltipItem = ({
|
||||
}) =>
|
||||
value && value !== '0' ? (
|
||||
<>
|
||||
<dt className="col-span-1">{label}</dt>
|
||||
<dd className="text-right col-span-1">
|
||||
<dt className="col-span-2">{label}</dt>
|
||||
<dd className="text-right col-span-2">
|
||||
{addDecimalsFormatNumber(value, asset.decimals)} {asset.symbol}
|
||||
</dd>
|
||||
</>
|
||||
@@ -409,15 +359,19 @@ export const FeesDiscountBreakdownTooltip = ({
|
||||
}
|
||||
const asset = getAsset(data.market);
|
||||
|
||||
const { fees } = getRoleAndFees({ data, partyId }) ?? {};
|
||||
if (!fees) return null;
|
||||
|
||||
const {
|
||||
fees: roleFees,
|
||||
marketState,
|
||||
role,
|
||||
} = getRoleAndFees({ data, partyId }) ?? {};
|
||||
if (!roleFees) return null;
|
||||
const fees = getFeesBreakdown(role, roleFees, marketState);
|
||||
return (
|
||||
<div
|
||||
data-testid="fee-discount-breakdown-tooltip"
|
||||
className="max-w-sm bg-vega-light-100 dark:bg-vega-dark-100 border border-vega-light-200 dark:border-vega-dark-200 px-4 py-2 z-20 rounded text-sm break-word text-black dark:text-white"
|
||||
>
|
||||
<dl className="grid grid-cols-2 gap-x-1">
|
||||
<dl className="grid grid-cols-6 gap-x-1 text-xs">
|
||||
{(fees.infrastructureFeeReferralDiscount || '0') !== '0' ||
|
||||
(fees.infrastructureFeeVolumeDiscount || '0') !== '0' ? (
|
||||
<dt className="col-span-2">{t('Infrastructure Fee')}</dt>
|
||||
@@ -461,42 +415,14 @@ export const FeesDiscountBreakdownTooltip = ({
|
||||
label={t('Volume Discount')}
|
||||
asset={asset}
|
||||
/>
|
||||
|
||||
<dt className="col-span-2">{t('Total Fee Discount')}</dt>
|
||||
<FeesDiscountBreakdownTooltipItem
|
||||
value={fees.totalFeeDiscount}
|
||||
label={''}
|
||||
asset={asset}
|
||||
/>
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const getTotalFeesDiscounts = (fees: TradeFeeFieldsFragment) => {
|
||||
return (
|
||||
BigInt(fees.infrastructureFeeReferralDiscount || '0') +
|
||||
BigInt(fees.infrastructureFeeVolumeDiscount || '0') +
|
||||
BigInt(fees.liquidityFeeReferralDiscount || '0') +
|
||||
BigInt(fees.liquidityFeeVolumeDiscount || '0') +
|
||||
BigInt(fees.makerFeeReferralDiscount || '0') +
|
||||
BigInt(fees.makerFeeVolumeDiscount || '0')
|
||||
).toString();
|
||||
};
|
||||
|
||||
export const getFeesBreakdown = (
|
||||
role: Role,
|
||||
feesObj: TradeFeeFieldsFragment
|
||||
) => {
|
||||
const makerFee =
|
||||
role === MAKER
|
||||
? new BigNumber(feesObj.makerFee).times(-1).toString()
|
||||
: feesObj.makerFee;
|
||||
|
||||
const infrastructureFee = feesObj.infrastructureFee;
|
||||
const liquidityFee = feesObj.liquidityFee;
|
||||
|
||||
const totalFee = new BigNumber(infrastructureFee)
|
||||
.plus(makerFee)
|
||||
.plus(liquidityFee)
|
||||
.toString();
|
||||
return {
|
||||
infrastructureFee,
|
||||
liquidityFee,
|
||||
makerFee,
|
||||
totalFee,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
import { getFeesBreakdown } from './fills-utils';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
describe('getFeesBreakdown', () => {
|
||||
it('should return correct fees breakdown for a taker', () => {
|
||||
const fees = {
|
||||
makerFee: '1000',
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
};
|
||||
const expectedBreakdown = {
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
makerFee: '1000',
|
||||
totalFee: '6000',
|
||||
totalFeeDiscount: '0',
|
||||
};
|
||||
expect(getFeesBreakdown('Taker', fees)).toEqual(expectedBreakdown);
|
||||
});
|
||||
|
||||
it('should return correct fees breakdown for a maker if market is active', () => {
|
||||
const fees = {
|
||||
makerFee: '1000',
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
};
|
||||
const expectedBreakdown = {
|
||||
infrastructureFee: '0',
|
||||
liquidityFee: '0',
|
||||
makerFee: '-1000',
|
||||
totalFee: '-1000',
|
||||
totalFeeDiscount: '0',
|
||||
};
|
||||
expect(
|
||||
getFeesBreakdown('Maker', fees, Schema.MarketState.STATE_ACTIVE)
|
||||
).toEqual(expectedBreakdown);
|
||||
});
|
||||
|
||||
it('should return correct fees breakdown for a maker if the market is suspended', () => {
|
||||
const fees = {
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
makerFee: '0',
|
||||
};
|
||||
const expectedBreakdown = {
|
||||
infrastructureFee: '1000',
|
||||
liquidityFee: '1500',
|
||||
makerFee: '0',
|
||||
totalFee: '2500',
|
||||
totalFeeDiscount: '0',
|
||||
};
|
||||
expect(
|
||||
getFeesBreakdown('Maker', fees, Schema.MarketState.STATE_SUSPENDED)
|
||||
).toEqual(expectedBreakdown);
|
||||
});
|
||||
|
||||
it('should return correct fees breakdown for a taker if the market is suspended', () => {
|
||||
const fees = {
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
makerFee: '0',
|
||||
};
|
||||
const expectedBreakdown = {
|
||||
infrastructureFee: '1000',
|
||||
liquidityFee: '1500',
|
||||
makerFee: '0',
|
||||
totalFee: '2500',
|
||||
totalFeeDiscount: '0',
|
||||
};
|
||||
expect(
|
||||
getFeesBreakdown('Taker', fees, Schema.MarketState.STATE_SUSPENDED)
|
||||
).toEqual(expectedBreakdown);
|
||||
});
|
||||
|
||||
it('should return correct fees breakdown for a taker if market is active', () => {
|
||||
const fees = {
|
||||
makerFee: '1000',
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
};
|
||||
const expectedBreakdown = {
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
makerFee: '1000',
|
||||
totalFee: '6000',
|
||||
totalFeeDiscount: '0',
|
||||
};
|
||||
expect(
|
||||
getFeesBreakdown('Taker', fees, Schema.MarketState.STATE_ACTIVE)
|
||||
).toEqual(expectedBreakdown);
|
||||
});
|
||||
|
||||
it('should return correct fees breakdown for a maker', () => {
|
||||
const fees = {
|
||||
makerFee: '1000',
|
||||
infrastructureFee: '2000',
|
||||
liquidityFee: '3000',
|
||||
};
|
||||
const expectedBreakdown = {
|
||||
infrastructureFee: '0',
|
||||
liquidityFee: '0',
|
||||
makerFee: '-1000',
|
||||
totalFee: '-1000',
|
||||
totalFeeDiscount: '0',
|
||||
};
|
||||
expect(getFeesBreakdown('Maker', fees)).toEqual(expectedBreakdown);
|
||||
});
|
||||
|
||||
it('should return correct total fees discount value for a taker (if the market is active - default)', () => {
|
||||
const fees = {
|
||||
infrastructureFeeReferralDiscount: '1',
|
||||
infrastructureFeeVolumeDiscount: '2',
|
||||
liquidityFeeReferralDiscount: '3',
|
||||
liquidityFeeVolumeDiscount: '4',
|
||||
makerFeeReferralDiscount: '5',
|
||||
makerFeeVolumeDiscount: '6',
|
||||
infrastructureFee: '1000',
|
||||
liquidityFee: '2000',
|
||||
makerFee: '3000',
|
||||
};
|
||||
const { totalFeeDiscount } = getFeesBreakdown('Taker', fees);
|
||||
expect(totalFeeDiscount).toEqual((1 + 2 + 3 + 4 + 5 + 6).toString());
|
||||
});
|
||||
|
||||
it('should return correct total fees discount value for a maker (if the market is active - default)', () => {
|
||||
const fees = {
|
||||
infrastructureFeeReferralDiscount: '1',
|
||||
infrastructureFeeVolumeDiscount: '2',
|
||||
liquidityFeeReferralDiscount: '3',
|
||||
liquidityFeeVolumeDiscount: '4',
|
||||
makerFeeReferralDiscount: '5',
|
||||
makerFeeVolumeDiscount: '6',
|
||||
infrastructureFee: '1000',
|
||||
liquidityFee: '2000',
|
||||
makerFee: '3000',
|
||||
};
|
||||
const { totalFeeDiscount } = getFeesBreakdown('Maker', fees);
|
||||
// makerFeeReferralDiscount and makerFeeVolumeDiscount are added, infra and liq. fees are zeroed
|
||||
expect(totalFeeDiscount).toEqual((5 + 6).toString());
|
||||
});
|
||||
|
||||
it('should return correct total fees discount value for a maker (if the market is suspended)', () => {
|
||||
const fees = {
|
||||
infrastructureFeeReferralDiscount: '1',
|
||||
infrastructureFeeVolumeDiscount: '2',
|
||||
liquidityFeeReferralDiscount: '3',
|
||||
liquidityFeeVolumeDiscount: '4',
|
||||
makerFeeReferralDiscount: '5',
|
||||
makerFeeVolumeDiscount: '6',
|
||||
infrastructureFee: '1000',
|
||||
liquidityFee: '2000',
|
||||
makerFee: '3000',
|
||||
};
|
||||
const { totalFeeDiscount } = getFeesBreakdown(
|
||||
'Maker',
|
||||
fees,
|
||||
Schema.MarketState.STATE_SUSPENDED
|
||||
);
|
||||
// makerFeeReferralDiscount and makerFeeVolumeDiscount are zeroed, infra and liq. fees are halved
|
||||
expect(totalFeeDiscount).toEqual(((1 + 2 + 3 + 4) / 2).toString());
|
||||
});
|
||||
|
||||
it('should return correct total fees discount value for a taker (if the market is suspended)', () => {
|
||||
const fees = {
|
||||
infrastructureFeeReferralDiscount: '1',
|
||||
infrastructureFeeVolumeDiscount: '2',
|
||||
liquidityFeeReferralDiscount: '3',
|
||||
liquidityFeeVolumeDiscount: '4',
|
||||
makerFeeReferralDiscount: '5',
|
||||
makerFeeVolumeDiscount: '6',
|
||||
infrastructureFee: '1000',
|
||||
liquidityFee: '2000',
|
||||
makerFee: '3000',
|
||||
};
|
||||
const { totalFeeDiscount } = getFeesBreakdown(
|
||||
'Taker',
|
||||
fees,
|
||||
Schema.MarketState.STATE_SUSPENDED
|
||||
);
|
||||
// makerFeeReferralDiscount and makerFeeVolumeDiscount are zeroed, infra and liq. fees are halved
|
||||
expect(totalFeeDiscount).toEqual(((1 + 2 + 3 + 4) / 2).toString());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,164 @@
|
||||
import BigNumber from 'bignumber.js';
|
||||
import type {
|
||||
FillFieldsFragment,
|
||||
TradeFeeFieldsFragment,
|
||||
} from './__generated__/Fills';
|
||||
import * as Schema from '@vegaprotocol/types';
|
||||
|
||||
export const TAKER = 'Taker';
|
||||
export const MAKER = 'Maker';
|
||||
|
||||
export type Role = typeof TAKER | typeof MAKER | '-';
|
||||
|
||||
export const getRoleAndFees = ({
|
||||
data,
|
||||
partyId,
|
||||
}: {
|
||||
data: Pick<
|
||||
FillFieldsFragment,
|
||||
'buyerFee' | 'sellerFee' | 'buyer' | 'seller' | 'aggressor'
|
||||
>;
|
||||
partyId?: string;
|
||||
}): {
|
||||
role: Role;
|
||||
fees?: TradeFeeFieldsFragment;
|
||||
marketState?: Schema.MarketState;
|
||||
} => {
|
||||
let role: Role;
|
||||
let fees;
|
||||
|
||||
if (data?.buyer.id === partyId) {
|
||||
if (data.aggressor === Schema.Side.SIDE_BUY) {
|
||||
role = TAKER;
|
||||
fees = data?.buyerFee;
|
||||
} else if (data.aggressor === Schema.Side.SIDE_SELL) {
|
||||
role = MAKER;
|
||||
fees = data?.sellerFee;
|
||||
} else {
|
||||
role = '-';
|
||||
fees = !isEmptyFeeObj(data?.buyerFee) ? data.buyerFee : data.sellerFee;
|
||||
}
|
||||
} else if (data?.seller.id === partyId) {
|
||||
if (data.aggressor === Schema.Side.SIDE_SELL) {
|
||||
role = TAKER;
|
||||
fees = data?.sellerFee;
|
||||
} else if (data.aggressor === Schema.Side.SIDE_BUY) {
|
||||
role = MAKER;
|
||||
fees = data?.buyerFee;
|
||||
} else {
|
||||
role = '-';
|
||||
fees = !isEmptyFeeObj(data.sellerFee) ? data.sellerFee : data.buyerFee;
|
||||
}
|
||||
} else {
|
||||
return { role: '-', fees: undefined };
|
||||
}
|
||||
|
||||
// We make the assumption that the market state is active if the maker fee is zero on both sides
|
||||
// This needs to be updated when we have a way to get the correct market state when that fill happened from the API
|
||||
// because the maker fee factor can be set to 0 via governance
|
||||
const marketState =
|
||||
data?.buyerFee.makerFee === data.sellerFee.makerFee &&
|
||||
new BigNumber(data?.buyerFee.makerFee).isZero()
|
||||
? Schema.MarketState.STATE_SUSPENDED
|
||||
: Schema.MarketState.STATE_ACTIVE;
|
||||
return { role, fees, marketState };
|
||||
};
|
||||
|
||||
export const getFeesBreakdown = (
|
||||
role: Role,
|
||||
fees: TradeFeeFieldsFragment,
|
||||
marketState: Schema.MarketState = Schema.MarketState.STATE_ACTIVE
|
||||
) => {
|
||||
// If market is in auction we assume maker fee is zero
|
||||
const isMarketActive = marketState === Schema.MarketState.STATE_ACTIVE;
|
||||
|
||||
// If role is taker, then these are the fees to be paid
|
||||
let { makerFee, infrastructureFee, liquidityFee } = fees;
|
||||
// If role is taker, then these are the fees discounts to be applied
|
||||
let {
|
||||
makerFeeVolumeDiscount,
|
||||
makerFeeReferralDiscount,
|
||||
infrastructureFeeVolumeDiscount,
|
||||
infrastructureFeeReferralDiscount,
|
||||
liquidityFeeVolumeDiscount,
|
||||
liquidityFeeReferralDiscount,
|
||||
} = fees;
|
||||
|
||||
if (isMarketActive) {
|
||||
if (role === MAKER) {
|
||||
makerFee = new BigNumber(fees.makerFee).times(-1).toString();
|
||||
infrastructureFee = '0';
|
||||
liquidityFee = '0';
|
||||
|
||||
// discounts are also zero or we can leave them undefined
|
||||
infrastructureFeeReferralDiscount =
|
||||
infrastructureFeeReferralDiscount && '0';
|
||||
infrastructureFeeVolumeDiscount = infrastructureFeeVolumeDiscount && '0';
|
||||
liquidityFeeReferralDiscount = liquidityFeeReferralDiscount && '0';
|
||||
liquidityFeeVolumeDiscount = liquidityFeeVolumeDiscount && '0';
|
||||
|
||||
// we leave maker discount fees as they are defined
|
||||
}
|
||||
} else {
|
||||
// If market is suspended (in monitoring auction), then half of the fees are paid
|
||||
infrastructureFee = new BigNumber(infrastructureFee)
|
||||
.dividedBy(2)
|
||||
.toString();
|
||||
liquidityFee = new BigNumber(liquidityFee).dividedBy(2).toString();
|
||||
// maker fee is already zero
|
||||
makerFee = '0';
|
||||
|
||||
// discounts are also halved
|
||||
infrastructureFeeReferralDiscount =
|
||||
infrastructureFeeReferralDiscount &&
|
||||
new BigNumber(infrastructureFeeReferralDiscount).dividedBy(2).toString();
|
||||
infrastructureFeeVolumeDiscount =
|
||||
infrastructureFeeVolumeDiscount &&
|
||||
new BigNumber(infrastructureFeeVolumeDiscount).dividedBy(2).toString();
|
||||
liquidityFeeReferralDiscount =
|
||||
liquidityFeeReferralDiscount &&
|
||||
new BigNumber(liquidityFeeReferralDiscount).dividedBy(2).toString();
|
||||
liquidityFeeVolumeDiscount =
|
||||
liquidityFeeVolumeDiscount &&
|
||||
new BigNumber(liquidityFeeVolumeDiscount).dividedBy(2).toString();
|
||||
// maker discount fees should already be zero
|
||||
makerFeeReferralDiscount = makerFeeReferralDiscount && '0';
|
||||
makerFeeVolumeDiscount = makerFeeVolumeDiscount && '0';
|
||||
}
|
||||
|
||||
const totalFee = new BigNumber(infrastructureFee)
|
||||
.plus(makerFee)
|
||||
.plus(liquidityFee)
|
||||
.toString();
|
||||
|
||||
const totalFeeDiscount = new BigNumber(makerFeeVolumeDiscount || '0')
|
||||
.plus(makerFeeReferralDiscount || '0')
|
||||
.plus(infrastructureFeeReferralDiscount || '0')
|
||||
.plus(infrastructureFeeVolumeDiscount || '0')
|
||||
.plus(liquidityFeeReferralDiscount || '0')
|
||||
.plus(liquidityFeeVolumeDiscount || '0')
|
||||
.toString();
|
||||
|
||||
return {
|
||||
infrastructureFee,
|
||||
infrastructureFeeReferralDiscount,
|
||||
infrastructureFeeVolumeDiscount,
|
||||
liquidityFee,
|
||||
liquidityFeeReferralDiscount,
|
||||
liquidityFeeVolumeDiscount,
|
||||
makerFee,
|
||||
makerFeeReferralDiscount,
|
||||
makerFeeVolumeDiscount,
|
||||
totalFee,
|
||||
totalFeeDiscount,
|
||||
};
|
||||
};
|
||||
|
||||
export const isEmptyFeeObj = (feeObj: Schema.TradeFee) => {
|
||||
if (!feeObj) return true;
|
||||
return (
|
||||
feeObj.liquidityFee === '0' &&
|
||||
feeObj.makerFee === '0' &&
|
||||
feeObj.infrastructureFee === '0'
|
||||
);
|
||||
};
|
||||
@@ -278,18 +278,4 @@ describe('createDownloadUrl', () => {
|
||||
)}&dateRange.endTimestamp=${toNanoSeconds(dateTo)}`
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw if invalid args are provided', () => {
|
||||
// invalid url
|
||||
expect(() => {
|
||||
// @ts-ignore override z.infer type
|
||||
createDownloadUrl({ ...args, protohost: 'foo' });
|
||||
}).toThrow();
|
||||
|
||||
// invalid partyId
|
||||
expect(() => {
|
||||
// @ts-ignore override z.infer type
|
||||
createDownloadUrl({ ...args, partyId: 'z'.repeat(64) });
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { useRef, useState } from 'react';
|
||||
import { format, subDays } from 'date-fns';
|
||||
import { useRef, useCallback } from 'react';
|
||||
import { subDays } from 'date-fns';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import {
|
||||
InputError,
|
||||
Intent,
|
||||
Loader,
|
||||
TradingButton,
|
||||
TradingFormGroup,
|
||||
TradingInput,
|
||||
TradingSelect,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
formatForInput,
|
||||
getDateTimeFormat,
|
||||
toNanoSeconds,
|
||||
VEGA_ID_REGEX,
|
||||
} from '@vegaprotocol/utils';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { localLoggerFactory } from '@vegaprotocol/logger';
|
||||
@@ -35,18 +35,18 @@ const getProtoHost = (vegaurl: string) => {
|
||||
return `${loc.protocol}//${loc.host}`;
|
||||
};
|
||||
|
||||
const downloadSchema = z.object({
|
||||
protohost: z.string().url().nonempty(),
|
||||
partyId: z.string().regex(VEGA_ID_REGEX).nonempty(),
|
||||
assetId: z.string().regex(VEGA_ID_REGEX).nonempty(),
|
||||
dateFrom: z.string().nonempty(),
|
||||
dateTo: z.string().optional(),
|
||||
});
|
||||
|
||||
export const createDownloadUrl = (args: z.infer<typeof downloadSchema>) => {
|
||||
// check args from form inputs
|
||||
downloadSchema.parse(args);
|
||||
type LedgerFormValues = {
|
||||
assetId: string;
|
||||
dateFrom: string;
|
||||
dateTo?: string;
|
||||
};
|
||||
|
||||
export const createDownloadUrl = (
|
||||
args: LedgerFormValues & {
|
||||
partyId: string;
|
||||
protohost: string;
|
||||
}
|
||||
) => {
|
||||
const params = new URLSearchParams();
|
||||
params.append('partyId', args.partyId);
|
||||
params.append('assetId', args.assetId);
|
||||
@@ -71,114 +71,101 @@ interface Props {
|
||||
|
||||
export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
|
||||
const now = useRef(new Date());
|
||||
const [dateFrom, setDateFrom] = useState(() => {
|
||||
return formatForInput(subDays(now.current, 7));
|
||||
const { control, handleSubmit, watch } = useForm<LedgerFormValues>({
|
||||
defaultValues: {
|
||||
dateFrom: formatForInput(subDays(now.current, 7)),
|
||||
dateTo: '',
|
||||
assetId: Object.keys(assets)[0],
|
||||
},
|
||||
});
|
||||
const [dateTo, setDateTo] = useState('');
|
||||
const dateTo = watch('dateTo');
|
||||
const maxFromDate = formatForInput(new Date(dateTo || now.current));
|
||||
const maxToDate = formatForInput(now.current);
|
||||
|
||||
const [assetId, setAssetId] = useState(Object.keys(assets)[0]);
|
||||
const protohost = getProtoHost(vegaUrl);
|
||||
const disabled = Boolean(!assetId);
|
||||
|
||||
const hasItem = useLedgerDownloadFile((store) => store.hasItem);
|
||||
const updateDownloadQueue = useLedgerDownloadFile(
|
||||
(store) => store.updateQueue
|
||||
);
|
||||
|
||||
const assetDropDown = (
|
||||
<TradingSelect
|
||||
id="select-ledger-asset"
|
||||
value={assetId}
|
||||
onChange={(e) => {
|
||||
setAssetId(e.target.value);
|
||||
}}
|
||||
className="w-full"
|
||||
data-testid="select-ledger-asset"
|
||||
>
|
||||
{Object.keys(assets).map((assetKey) => (
|
||||
<option key={assetKey} value={assetKey}>
|
||||
{assets[assetKey]}
|
||||
</option>
|
||||
))}
|
||||
</TradingSelect>
|
||||
);
|
||||
const startDownload = useCallback(
|
||||
async (formValues: LedgerFormValues) => {
|
||||
const link = createDownloadUrl({
|
||||
protohost,
|
||||
partyId,
|
||||
...formValues,
|
||||
});
|
||||
|
||||
const link = createDownloadUrl({
|
||||
protohost,
|
||||
partyId,
|
||||
assetId,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
});
|
||||
const dateTimeFormatter = getDateTimeFormat();
|
||||
const title = t('Downloading for %s from %s till %s', [
|
||||
assets[formValues.assetId],
|
||||
dateTimeFormatter.format(new Date(formValues.dateFrom)),
|
||||
dateTimeFormatter.format(new Date(formValues.dateTo || Date.now())),
|
||||
]);
|
||||
|
||||
const startDownload = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
const title = t('Downloading for %s from %s till %s', [
|
||||
assets[assetId],
|
||||
format(new Date(dateFrom), 'dd MMMM yyyy HH:mm'),
|
||||
format(new Date(dateTo || Date.now()), 'dd MMMM yyyy HH:mm'),
|
||||
]);
|
||||
|
||||
const downloadStoreItem = {
|
||||
title,
|
||||
link,
|
||||
isChanged: true,
|
||||
};
|
||||
if (hasItem(link)) {
|
||||
updateDownloadQueue(downloadStoreItem);
|
||||
return;
|
||||
}
|
||||
const ts = setTimeout(() => {
|
||||
updateDownloadQueue({
|
||||
...downloadStoreItem,
|
||||
intent: Intent.Warning,
|
||||
isDelayed: true,
|
||||
const downloadStoreItem = {
|
||||
title,
|
||||
link,
|
||||
isChanged: true,
|
||||
});
|
||||
}, 1000 * 30);
|
||||
|
||||
try {
|
||||
updateDownloadQueue(downloadStoreItem);
|
||||
const resp = await fetch(link);
|
||||
if (!resp?.ok) {
|
||||
if (resp?.status === 429) {
|
||||
throw new Error('Too many requests. Try again later.');
|
||||
}
|
||||
throw new Error('Download of ledger entries failed');
|
||||
};
|
||||
if (hasItem(link)) {
|
||||
updateDownloadQueue(downloadStoreItem);
|
||||
return;
|
||||
}
|
||||
const { headers } = resp;
|
||||
const nameHeader = headers.get('content-disposition');
|
||||
const filename = nameHeader?.split('=').pop() ?? DEFAULT_EXPORT_FILE_NAME;
|
||||
updateDownloadQueue({
|
||||
...downloadStoreItem,
|
||||
filename,
|
||||
});
|
||||
const blob = await resp.blob();
|
||||
if (blob) {
|
||||
const ts = setTimeout(() => {
|
||||
updateDownloadQueue({
|
||||
...downloadStoreItem,
|
||||
blob,
|
||||
isDownloaded: true,
|
||||
intent: Intent.Warning,
|
||||
isDelayed: true,
|
||||
isChanged: true,
|
||||
intent: Intent.Success,
|
||||
});
|
||||
}, 1000 * 30);
|
||||
|
||||
try {
|
||||
updateDownloadQueue(downloadStoreItem);
|
||||
const resp = await fetch(link);
|
||||
if (!resp?.ok) {
|
||||
if (resp?.status === 429) {
|
||||
throw new Error('Too many requests. Try again later.');
|
||||
}
|
||||
throw new Error('Download of ledger entries failed');
|
||||
}
|
||||
const { headers } = resp;
|
||||
const nameHeader = headers.get('content-disposition');
|
||||
const filename =
|
||||
nameHeader?.split('=').pop() ?? DEFAULT_EXPORT_FILE_NAME;
|
||||
updateDownloadQueue({
|
||||
...downloadStoreItem,
|
||||
filename,
|
||||
});
|
||||
const blob = await resp.blob();
|
||||
if (blob) {
|
||||
updateDownloadQueue({
|
||||
...downloadStoreItem,
|
||||
blob,
|
||||
isDownloaded: true,
|
||||
isChanged: true,
|
||||
intent: Intent.Success,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
localLoggerFactory({ application: 'ledger' }).error(
|
||||
'Download file',
|
||||
err
|
||||
);
|
||||
updateDownloadQueue({
|
||||
...downloadStoreItem,
|
||||
intent: Intent.Danger,
|
||||
isError: true,
|
||||
isChanged: true,
|
||||
errorMessage: (err as Error).message || undefined,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(ts);
|
||||
}
|
||||
} catch (err) {
|
||||
localLoggerFactory({ application: 'ledger' }).error('Download file', err);
|
||||
updateDownloadQueue({
|
||||
...downloadStoreItem,
|
||||
intent: Intent.Danger,
|
||||
isError: true,
|
||||
isChanged: true,
|
||||
errorMessage: (err as Error).message || undefined,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(ts);
|
||||
}
|
||||
};
|
||||
},
|
||||
[assets, hasItem, partyId, protohost, updateDownloadQueue]
|
||||
);
|
||||
|
||||
if (!protohost || Object.keys(assets).length === 0) {
|
||||
return null;
|
||||
@@ -187,49 +174,117 @@ export const LedgerExportForm = ({ partyId, vegaUrl, assets }: Props) => {
|
||||
const offset = new Date().getTimezoneOffset();
|
||||
|
||||
return (
|
||||
<form onSubmit={startDownload} className="p-4 w-[350px]">
|
||||
<form
|
||||
onSubmit={handleSubmit(startDownload)}
|
||||
className="p-4 w-[350px]"
|
||||
noValidate
|
||||
>
|
||||
<h2 className="mb-4">{t('Export ledger entries')}</h2>
|
||||
<TradingFormGroup label={t('Select asset')} labelFor="asset">
|
||||
{assetDropDown}
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup label={t('Date from')} labelFor="date-from">
|
||||
<TradingInput
|
||||
type="datetime-local"
|
||||
data-testid="date-from"
|
||||
id="date-from"
|
||||
value={dateFrom}
|
||||
onChange={(e) => setDateFrom(e.target.value)}
|
||||
max={maxFromDate}
|
||||
/>
|
||||
</TradingFormGroup>
|
||||
<TradingFormGroup label={t('Date to')} labelFor="date-to">
|
||||
<TradingInput
|
||||
type="datetime-local"
|
||||
data-testid="date-to"
|
||||
id="date-to"
|
||||
value={dateTo}
|
||||
onChange={(e) => setDateTo(e.target.value)}
|
||||
max={maxToDate}
|
||||
/>
|
||||
</TradingFormGroup>
|
||||
<Controller
|
||||
name="assetId"
|
||||
control={control}
|
||||
rules={{
|
||||
required: t('You need to select an asset'),
|
||||
}}
|
||||
render={({ field, fieldState }) => (
|
||||
<div className="mb-2">
|
||||
<TradingFormGroup
|
||||
label={t('Select asset')}
|
||||
labelFor="asset"
|
||||
compact
|
||||
>
|
||||
<TradingSelect
|
||||
{...field}
|
||||
id="select-ledger-asset"
|
||||
className="w-full"
|
||||
data-testid="select-ledger-asset"
|
||||
>
|
||||
{Object.keys(assets).map((assetKey) => (
|
||||
<option key={assetKey} value={assetKey}>
|
||||
{assets[assetKey]}
|
||||
</option>
|
||||
))}
|
||||
</TradingSelect>
|
||||
</TradingFormGroup>
|
||||
{fieldState.error && (
|
||||
<InputError>{fieldState.error.message}</InputError>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="dateFrom"
|
||||
control={control}
|
||||
rules={{
|
||||
required: t('You need to provide a date from'),
|
||||
max: {
|
||||
value: maxFromDate,
|
||||
message: dateTo
|
||||
? t('Date from cannot be greater than date to')
|
||||
: t('Date from cannot be in the future'),
|
||||
},
|
||||
deps: ['dateTo'],
|
||||
}}
|
||||
render={({ field, fieldState }) => (
|
||||
<div className="mb-2">
|
||||
<TradingFormGroup
|
||||
label={t('Date from')}
|
||||
labelFor="date-from"
|
||||
compact
|
||||
>
|
||||
<TradingInput
|
||||
{...field}
|
||||
type="datetime-local"
|
||||
data-testid="date-from"
|
||||
id="date-from"
|
||||
max={maxFromDate}
|
||||
/>
|
||||
</TradingFormGroup>
|
||||
{fieldState.error && (
|
||||
<InputError>{fieldState.error.message}</InputError>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="dateTo"
|
||||
control={control}
|
||||
rules={{
|
||||
max: {
|
||||
value: maxToDate,
|
||||
message: t('Date to cannot be in the future'),
|
||||
},
|
||||
}}
|
||||
render={({ field, fieldState }) => (
|
||||
<div className="mb-2">
|
||||
<TradingFormGroup label={t('Date to')} labelFor="date-to" compact>
|
||||
<TradingInput
|
||||
{...field}
|
||||
type="datetime-local"
|
||||
data-testid="date-to"
|
||||
id="date-to"
|
||||
max={maxToDate}
|
||||
/>
|
||||
</TradingFormGroup>
|
||||
{fieldState.error && (
|
||||
<InputError>{fieldState.error.message}</InputError>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<div className="relative text-sm" title={t('Download all to .csv file')}>
|
||||
<TradingButton
|
||||
fill
|
||||
disabled={disabled}
|
||||
type="submit"
|
||||
data-testid="ledger-download-button"
|
||||
>
|
||||
<TradingButton fill type="submit" data-testid="ledger-download-button">
|
||||
{t('Download')}
|
||||
</TradingButton>
|
||||
</div>
|
||||
{offset && (
|
||||
{offset ? (
|
||||
<p className="text-xs text-neutral-400 mt-1">
|
||||
{t(
|
||||
'The downloaded file uses the UTC time zone for all listed times. Your time zone is UTC%s.',
|
||||
[toHoursAndMinutes(offset)]
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
) : null}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -106,9 +106,6 @@ export const LiquidityTable = ({
|
||||
|
||||
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) {
|
||||
@@ -153,7 +150,7 @@ export const LiquidityTable = ({
|
||||
]
|
||||
);
|
||||
}
|
||||
return addDecimalsFormatNumber(newValue, assetDecimalPlaces ?? 0);
|
||||
return addDecimalsFormatNumber(value, assetDecimalPlaces ?? 0);
|
||||
};
|
||||
|
||||
const stakeToCcyVolumeQuantumFormatter = ({
|
||||
@@ -415,14 +412,9 @@ export const LiquidityTable = ({
|
||||
},
|
||||
'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)
|
||||
);
|
||||
return new BigNumber(
|
||||
data.sla.currentEpochFractionOfTimeOnBook
|
||||
).isLessThan(data.commitmentMinTimeFraction);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -27,7 +27,9 @@ jest.mock('@vegaprotocol/data-provider', () => ({
|
||||
}));
|
||||
|
||||
describe('PositionsManager', () => {
|
||||
it('should close position with max uint64', async () => {
|
||||
// TODO: temporarily disable close position
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('should close position with max uint64', async () => {
|
||||
render(<PositionsManager partyIds={['partyId']} isReadOnly={false} />, {
|
||||
wrapper: MockedProvider,
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ export const PositionsManager = ({
|
||||
}: PositionsManagerProps) => {
|
||||
const { pubKeys, pubKey } = useVegaWallet();
|
||||
const create = useVegaTransactionStore((store) => store.create);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const onClose = useCallback(
|
||||
({ marketId, openVolume }: { marketId: string; openVolume: string }) =>
|
||||
create({
|
||||
@@ -73,7 +74,8 @@ export const PositionsManager = ({
|
||||
pubKeys={pubKeys}
|
||||
rowData={data}
|
||||
onMarketClick={onMarketClick}
|
||||
onClose={onClose}
|
||||
// TODO: temporarily disable close position
|
||||
// onClose={onClose}
|
||||
isReadOnly={isReadOnly}
|
||||
multipleKeys={partyIds.length > 1}
|
||||
overlayNoRowsTemplate={error ? error.message : t('No positions')}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const MarketProposalNotification = ({
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="border-l border-default pl-1 pr-1 pb-1 min-w-min whitespace-nowrap">
|
||||
<div className="border-default min-w-min border-l pb-1 pl-1 pr-1">
|
||||
<Notification
|
||||
intent={Intent.Warning}
|
||||
message={message}
|
||||
|
||||
@@ -22,11 +22,13 @@ export const CompactNumber = ({
|
||||
decimals = 'infer',
|
||||
compactDisplay = 'short',
|
||||
testId = 'compact-number',
|
||||
compactAbove = DEFAULT_COMPACT_ABOVE,
|
||||
}: {
|
||||
number: BigNumber;
|
||||
decimals?: number | 'infer';
|
||||
compactDisplay?: 'short' | 'long';
|
||||
testId?: string;
|
||||
compactAbove?: number;
|
||||
}) => {
|
||||
if (!number.isFinite()) {
|
||||
return (
|
||||
@@ -44,7 +46,7 @@ export const CompactNumber = ({
|
||||
return <span data-testid={testId}>{INFINITY}</span>;
|
||||
}
|
||||
|
||||
if (number.isLessThan(DEFAULT_COMPACT_ABOVE)) {
|
||||
if (number.isLessThan(compactAbove)) {
|
||||
return (
|
||||
<span data-testid={testId}>{formatNumber(number, decimalPlaces)}</span>
|
||||
);
|
||||
|
||||
Generated
+13
-1
@@ -4376,7 +4376,19 @@ export type Query = {
|
||||
/** The last block process by the blockchain */
|
||||
lastBlockHeight: Scalars['String'];
|
||||
/**
|
||||
* Get ledger entries by asset, market, party, account type, transfer type within the given date range.
|
||||
* Get a list of ledger entries within the given date range. The date range is restricted to a maximum of 5 days.
|
||||
* This query requests and sums the number of ledger entries from a given subset of accounts, specified via the 'filter' argument.
|
||||
* It returns a time series - implemented as a list of AggregateLedgerEntry structs - with a row for every time
|
||||
* the summed ledger entries of the set of specified accounts changes.
|
||||
* Each account filter must contain no more than one party ID.
|
||||
* At least one party ID must be specified in the from or to account filter.
|
||||
*
|
||||
* Entries can be filtered by:
|
||||
* - the sending account (market ID, asset ID, account type)
|
||||
* - receiving account (market ID, asset ID, account type)
|
||||
* - sending AND receiving account
|
||||
* - transfer type either in addition to the above filters or as a standalone option
|
||||
*
|
||||
* Note: The date range is restricted to any 5 days.
|
||||
* If no start or end date is provided, only ledger entries from the last 5 days will be returned.
|
||||
* If a start and end date are provided, but the end date is more than 5 days after the start date, only data up to 5 days after the start date will be returned.
|
||||
|
||||
@@ -93,7 +93,7 @@ export const Notification = ({
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-col items-start overflow-hidden gap-0 mt-1',
|
||||
'flex flex-col items-start overflow-hidden gap-0',
|
||||
'text-vega-clight-50 dark:text-vega-cdark-50',
|
||||
'font-alpha',
|
||||
{ 'text-sm': size === 'small', 'text-base': size === 'medium' }
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { useVegaWallet } from '../use-vega-wallet';
|
||||
import { useChainId } from './use-chain-id';
|
||||
|
||||
global.fetch = jest.fn();
|
||||
const mockFetch = global.fetch as jest.Mock;
|
||||
mockFetch.mockImplementation((url: string) => {
|
||||
return Promise.resolve({ ok: true });
|
||||
});
|
||||
|
||||
jest.mock('../use-vega-wallet', () => {
|
||||
const original = jest.requireActual('../use-vega-wallet');
|
||||
return {
|
||||
...original,
|
||||
useVegaWallet: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('useChainId', () => {
|
||||
it('does not call fetch when statistics url could not be determined', async () => {
|
||||
(useVegaWallet as jest.Mock).mockReturnValue({
|
||||
vegaUrl: '',
|
||||
});
|
||||
renderHook(() => useChainId());
|
||||
await waitFor(() => {
|
||||
expect(mockFetch).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls fetch with correct statistics url', async () => {
|
||||
(useVegaWallet as jest.Mock).mockReturnValue({
|
||||
vegaUrl: 'http://localhost:1234/graphql',
|
||||
});
|
||||
renderHook(() => useChainId());
|
||||
await waitFor(() => {
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
'http://localhost:1234/statistics'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not return chain id when chain id is not present in response', async () => {
|
||||
(useVegaWallet as jest.Mock).mockReturnValue({
|
||||
vegaUrl: 'http://localhost:1234/graphql',
|
||||
});
|
||||
const { result } = renderHook(() => useChainId());
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('returns chain id when chain id is present in response', async () => {
|
||||
(useVegaWallet as jest.Mock).mockReturnValue({
|
||||
vegaUrl: 'http://localhost:1234/graphql',
|
||||
});
|
||||
mockFetch.mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: () => ({
|
||||
statistics: {
|
||||
chainId: '1234',
|
||||
},
|
||||
}),
|
||||
});
|
||||
});
|
||||
const { result } = renderHook(() => useChainId());
|
||||
await waitFor(() => {
|
||||
expect(result.current).not.toBeUndefined();
|
||||
expect(result.current).toEqual('1234');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,17 +3,37 @@ import { useVegaWallet } from '../use-vega-wallet';
|
||||
|
||||
const cache: Record<string, string> = {};
|
||||
|
||||
/**
|
||||
* Gets the statistics url for a given vega url.
|
||||
* Example:
|
||||
* https://graphql.example.com/graphql -> https://graphql.example.com/statistics
|
||||
*/
|
||||
const getNodeStatisticsUrl = (vegaUrl: string) => {
|
||||
try {
|
||||
const url = new URL(vegaUrl);
|
||||
url.pathname = 'statistics';
|
||||
return url.toString();
|
||||
} catch (err) {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const useChainId = () => {
|
||||
const { vegaUrl } = useVegaWallet();
|
||||
const [chainId, setChainId] = useState<undefined | string>(cache[vegaUrl]);
|
||||
const [fetchAttempt, setFetchAttempt] = useState(1);
|
||||
|
||||
const statisticsUrl = getNodeStatisticsUrl(vegaUrl);
|
||||
|
||||
useEffect(() => {
|
||||
// abort when `/statistics` URL could not be determined
|
||||
if (!statisticsUrl) return;
|
||||
let isCancelled = false;
|
||||
if (cache[vegaUrl]) {
|
||||
setChainId(cache[vegaUrl]);
|
||||
return;
|
||||
}
|
||||
fetch(vegaUrl.replace('graphql', 'statistics'))
|
||||
fetch(statisticsUrl)
|
||||
.then((response) => response.json())
|
||||
.then((response) => {
|
||||
if (isCancelled) {
|
||||
@@ -32,6 +52,6 @@ export const useChainId = () => {
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, [fetchAttempt, vegaUrl]);
|
||||
}, [fetchAttempt, statisticsUrl, vegaUrl]);
|
||||
return chainId;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user